Guest User

Untitled

a guest
Mar 1st, 2026
32
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 16.03 KB | None | 0 0
  1. def scan_prefix(prefix_obj, dns_servers=None, perform_dns=True, max_workers=100, ping_timeout=1, dns_settings=None, job_logger=None, skip_reserved=False):
  2.     """
  3.    Ping all existing IPs in a prefix. Creates/updates PingResult and SubnetScanResult.
  4.  
  5.    Pings run in a thread pool for speed; DB writes happen on the main thread
  6.    to avoid exhausting PostgreSQL connections.
  7.  
  8.    Returns dict: {'total': int, 'up': int, 'down': int, 'skipped': int}
  9.    """
  10.     from .models import PingResult, PingHistory, SubnetScanResult, DnsHistory, ScanEvent
  11.  
  12.     log = job_logger or logger
  13.     ip_addresses = list(prefix_obj.get_child_ips().select_related())
  14.  
  15.     # Partition: skip reserved IPs if setting is enabled
  16.     if skip_reserved:
  17.         pingable_ips = [ip for ip in ip_addresses if ip.status != 'reserved']
  18.         skipped_ips = [ip for ip in ip_addresses if ip.status == 'reserved']
  19.     else:
  20.         pingable_ips = ip_addresses
  21.         skipped_ips = []
  22.  
  23.     if skipped_ips:
  24.         msg = f'Skipping {len(skipped_ips)} reserved IPs'
  25.         log.info(msg)
  26.         print(f'[Scan] {msg}', flush=True)
  27.  
  28.     # Phase 1: ping + DNS in parallel (no DB access)
  29.     def _ping_ip(ip_obj):
  30.         ip_str = str(ip_obj.address.ip)
  31.         ping_data = ping_host(ip_str, timeout=ping_timeout)
  32.         dns_name = ''
  33.         dns_attempted = False
  34.         if perform_dns and ping_data['is_reachable']:
  35.             dns_attempted = True
  36.             dns_name = resolve_dns(ip_str, dns_servers)
  37.         return ip_obj, ping_data, dns_name, dns_attempted
  38.  
  39.     ping_results = []
  40.     total_ips = len(pingable_ips)
  41.     last_log_time = time.monotonic()
  42.     with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
  43.         futures = {executor.submit(_ping_ip, ip_obj): ip_obj for ip_obj in pingable_ips}
  44.         for future in concurrent.futures.as_completed(futures):
  45.             try:
  46.                 ping_results.append(future.result())
  47.             except Exception as e:
  48.                 log.error(f'Error pinging IP: {e}')
  49.             now_mono = time.monotonic()
  50.             if now_mono - last_log_time >= 60 or len(ping_results) == total_ips:
  51.                 up_so_far = sum(1 for _, r, _, _ in ping_results if r['is_reachable'])
  52.                 msg = f'Ping progress: {len(ping_results)}/{total_ips} ({up_so_far} up)'
  53.                 log.info(msg)
  54.                 print(f'[Scan] {msg}', flush=True)
  55.                 last_log_time = now_mono
  56.  
  57.     # Phase 2: bulk DB writes on the main thread
  58.     now = timezone.now()
  59.  
  60.     # Pre-fetch all existing PingResults for all IPs (pingable + skipped)
  61.     all_ip_ids = [ip_obj.pk for ip_obj in ip_addresses]
  62.     existing_results = {}
  63.     for i in range(0, len(all_ip_ids), 5000):
  64.         batch = all_ip_ids[i:i + 5000]
  65.         for pr in PingResult.objects.filter(ip_address_id__in=batch):
  66.             existing_results[pr.ip_address_id] = pr
  67.  
  68.     to_update = []
  69.     to_create = []
  70.     history_records = []
  71.     ips_to_dns_update = []
  72.     dns_history_records = []
  73.     scan_events = []
  74.  
  75.     for ip_obj, ping_data, dns_name, dns_attempted in ping_results:
  76.         existing = existing_results.get(ip_obj.pk)
  77.  
  78.         if existing:
  79.             # Capture state change before overwriting
  80.             was_reachable = existing.is_reachable
  81.             is_now_reachable = ping_data['is_reachable']
  82.             if not existing.is_skipped:
  83.                 if was_reachable and not is_now_reachable:
  84.                     scan_events.append(ScanEvent(
  85.                         event_type='ip_went_down',
  86.                         prefix=prefix_obj,
  87.                         ip_address=ip_obj,
  88.                         detail={
  89.                             'dns_name': existing.dns_name or '',
  90.                             'last_response_ms': existing.response_time_ms,
  91.                         },
  92.                     ))
  93.                 elif not was_reachable and is_now_reachable:
  94.                     scan_events.append(ScanEvent(
  95.                         event_type='ip_came_up',
  96.                         prefix=prefix_obj,
  97.                         ip_address=ip_obj,
  98.                         detail={
  99.                             'dns_name': dns_name or existing.dns_name or '',
  100.                             'response_time_ms': ping_data['response_time_ms'],
  101.                         },
  102.                     ))
  103.  
  104.             existing.is_reachable = ping_data['is_reachable']
  105.             existing.is_skipped = False
  106.             existing.response_time_ms = ping_data['response_time_ms']
  107.             existing.dns_name = dns_name if dns_name else (existing.dns_name or '')
  108.             existing.last_checked = now
  109.             if ping_data['is_reachable']:
  110.                 existing.last_seen = now
  111.                 existing.consecutive_down_count = 0
  112.                 existing.is_stale = False
  113.             else:
  114.                 existing.consecutive_down_count += 1
  115.             to_update.append(existing)
  116.         else:
  117.             to_create.append(PingResult(
  118.                 ip_address=ip_obj,
  119.                 is_reachable=ping_data['is_reachable'],
  120.                 is_skipped=False,
  121.                 response_time_ms=ping_data['response_time_ms'],
  122.                 dns_name=dns_name,
  123.                 last_checked=now,
  124.                 last_seen=now if ping_data['is_reachable'] else None,
  125.                 consecutive_down_count=0 if ping_data['is_reachable'] else 1,
  126.             ))
  127.  
  128.         history_records.append(PingHistory(
  129.             ip_address=ip_obj,
  130.             is_reachable=ping_data['is_reachable'],
  131.             response_time_ms=ping_data['response_time_ms'],
  132.             dns_name=dns_name,
  133.             checked_at=now,
  134.         ))
  135.  
  136.         # DNS sync logic
  137.         if dns_settings and dns_settings.dns_sync_to_netbox:
  138.             current_netbox_dns = ip_obj.dns_name or ''
  139.             should_update, new_value = _compute_dns_sync(
  140.                 dns_name, ping_data['is_reachable'], dns_attempted,
  141.                 current_netbox_dns, dns_settings,
  142.             )
  143.             if should_update:
  144.                 ip_obj.dns_name = new_value
  145.                 ips_to_dns_update.append(ip_obj)
  146.                 dns_history_records.append(DnsHistory(
  147.                     ip_address=ip_obj,
  148.                     old_dns_name=current_netbox_dns,
  149.                     new_dns_name=new_value,
  150.                     changed_at=now,
  151.                 ))
  152.                 scan_events.append(ScanEvent(
  153.                     event_type='dns_changed',
  154.                     prefix=prefix_obj,
  155.                     ip_address=ip_obj,
  156.                     detail={
  157.                         'old_dns': current_netbox_dns,
  158.                         'new_dns': new_value,
  159.                     },
  160.                 ))
  161.  
  162.     # Phase 2b: create/update PingResult for skipped IPs (no history)
  163.     for ip_obj in skipped_ips:
  164.         existing = existing_results.get(ip_obj.pk)
  165.         if existing:
  166.             existing.is_reachable = False
  167.             existing.is_skipped = True
  168.             existing.response_time_ms = None
  169.             existing.last_checked = now
  170.             to_update.append(existing)
  171.         else:
  172.             to_create.append(PingResult(
  173.                 ip_address=ip_obj,
  174.                 is_reachable=False,
  175.                 is_skipped=True,
  176.                 response_time_ms=None,
  177.                 dns_name='',
  178.                 last_checked=now,
  179.                 last_seen=None,
  180.             ))
  181.  
  182.     BATCH = 1000
  183.     if to_update:
  184.         update_fields = ['is_reachable', 'is_skipped', 'response_time_ms', 'dns_name', 'last_checked', 'last_seen', 'consecutive_down_count', 'is_stale']
  185.         for i in range(0, len(to_update), BATCH):
  186.             PingResult.objects.bulk_update(to_update[i:i + BATCH], update_fields)
  187.         msg = f'Bulk updated {len(to_update)} existing results'
  188.         log.info(msg)
  189.         print(f'[Scan] {msg}', flush=True)
  190.  
  191.     if to_create:
  192.         for i in range(0, len(to_create), BATCH):
  193.             PingResult.objects.bulk_create(to_create[i:i + BATCH])
  194.         msg = f'Bulk created {len(to_create)} new results'
  195.         log.info(msg)
  196.         print(f'[Scan] {msg}', flush=True)
  197.  
  198.     for i in range(0, len(history_records), BATCH):
  199.         PingHistory.objects.bulk_create(history_records[i:i + BATCH])
  200.     msg = f'Bulk created {len(history_records)} history records'
  201.     log.info(msg)
  202.     print(f'[Scan] {msg}', flush=True)
  203.  
  204.     # DNS sync: bulk update IPAddress.dns_name and create DnsHistory
  205.     if ips_to_dns_update:
  206.         from ipam.models import IPAddress as IPAddressModel
  207.         for i in range(0, len(ips_to_dns_update), BATCH):
  208.             IPAddressModel.objects.bulk_update(ips_to_dns_update[i:i + BATCH], ['dns_name'])
  209.         msg = f'DNS sync: updated {len(ips_to_dns_update)} IPAddress dns_name fields'
  210.         log.info(msg)
  211.         print(f'[Scan] {msg}', flush=True)
  212.  
  213.     if dns_history_records:
  214.         for i in range(0, len(dns_history_records), BATCH):
  215.             DnsHistory.objects.bulk_create(dns_history_records[i:i + BATCH])
  216.         msg = f'DNS sync: created {len(dns_history_records)} DNS history records'
  217.         log.info(msg)
  218.         print(f'[Scan] {msg}', flush=True)
  219.  
  220.     # Phase 3: create ScanEvent records for digest notifications
  221.     if scan_events:
  222.         for i in range(0, len(scan_events), BATCH):
  223.             ScanEvent.objects.bulk_create(scan_events[i:i + BATCH])
  224.         msg = f'Created {len(scan_events)} scan event(s) for digest'
  225.         log.info(msg)
  226.         print(f'[Scan] {msg}', flush=True)
  227.  
  228.     # Phase 4: Stale IP detection
  229.     from .models import PrefixSchedule
  230.     stale_settings = dns_settings  # reuse the PluginSettings object passed in
  231.     stale_excluded = False
  232.     if stale_settings:
  233.         try:
  234.             prefix_schedule = PrefixSchedule.objects.get(prefix=prefix_obj)
  235.             if prefix_schedule.stale_mode == 'exclude':
  236.                 stale_excluded = True
  237.         except PrefixSchedule.DoesNotExist:
  238.             pass
  239.  
  240.     stale_events = []
  241.     stale_to_update = []
  242.     ips_to_remove = []
  243.  
  244.     if stale_settings and not stale_excluded:
  245.         # Phase 4a: Tag stale IPs
  246.         if stale_settings.stale_enabled:
  247.             scans_thresh = stale_settings.stale_scans_threshold
  248.             days_thresh = stale_settings.stale_days_threshold
  249.  
  250.             if scans_thresh > 0 or days_thresh > 0:
  251.                 # Get all down, non-stale PingResults for this prefix
  252.                 candidate_prs = PingResult.objects.filter(
  253.                     ip_address__in=[ip.pk for ip in ip_addresses],
  254.                     is_reachable=False,
  255.                     is_skipped=False,
  256.                     is_stale=False,
  257.                 ).select_related('ip_address')
  258.  
  259.                 for pr in candidate_prs:
  260.                     is_stale_now = False
  261.                     if scans_thresh > 0 and pr.consecutive_down_count >= scans_thresh:
  262.                         is_stale_now = True
  263.                     if days_thresh > 0:
  264.                         if pr.last_seen is not None:
  265.                             if (now - pr.last_seen) >= timedelta(days=days_thresh):
  266.                                 is_stale_now = True
  267.                         elif pr.last_checked is not None:
  268.                             # Never seen online — use last_checked as reference
  269.                             if (now - pr.last_checked) >= timedelta(days=days_thresh):
  270.                                 is_stale_now = True
  271.  
  272.                     if is_stale_now:
  273.                         pr.is_stale = True
  274.                         stale_to_update.append(pr)
  275.                         stale_events.append(ScanEvent(
  276.                             event_type='ip_went_stale',
  277.                             prefix=prefix_obj,
  278.                             ip_address=pr.ip_address,
  279.                             detail={
  280.                                 'dns_name': pr.dns_name or '',
  281.                                 'consecutive_down_count': pr.consecutive_down_count,
  282.                                 'last_seen': str(pr.last_seen) if pr.last_seen else None,
  283.                             },
  284.                         ))
  285.  
  286.                 if stale_to_update:
  287.                     for i in range(0, len(stale_to_update), BATCH):
  288.                         PingResult.objects.bulk_update(stale_to_update[i:i + BATCH], ['is_stale'])
  289.                     msg = f'Tagged {len(stale_to_update)} IP(s) as stale'
  290.                     log.info(msg)
  291.                     print(f'[Scan] {msg}', flush=True)
  292.  
  293.         # Phase 4b: Auto-remove stale IPs
  294.         if stale_settings.stale_remove_enabled and stale_settings.stale_remove_days > 0:
  295.             remove_cutoff = now - timedelta(days=stale_settings.stale_remove_days)
  296.             from django.db.models import Q
  297.             removable_prs = PingResult.objects.filter(
  298.                 ip_address__in=[ip.pk for ip in ip_addresses],
  299.                 is_reachable=False,
  300.                 is_skipped=False,
  301.                 is_stale=True,
  302.             ).filter(
  303.                 Q(last_seen__lte=remove_cutoff) | Q(last_seen__isnull=True)
  304.             ).select_related('ip_address')
  305.  
  306.             for pr in removable_prs:
  307.                 stale_events.append(ScanEvent(
  308.                     event_type='ip_removed_stale',
  309.                     prefix=prefix_obj,
  310.                     ip_address=pr.ip_address,
  311.                     detail={
  312.                         'dns_name': pr.dns_name or '',
  313.                         'ip_address': str(pr.ip_address.address),
  314.                         'last_seen': str(pr.last_seen) if pr.last_seen else None,
  315.                     },
  316.                 ))
  317.                 ips_to_remove.append(pr.ip_address_id)
  318.  
  319.             if ips_to_remove:
  320.                 # Create events before deleting (ip_address FK will cascade)
  321.                 for i in range(0, len(stale_events), BATCH):
  322.                     ScanEvent.objects.bulk_create(stale_events[i:i + BATCH])
  323.                 stale_events = []  # already saved
  324.                 from ipam.models import IPAddress as IPAddressModel
  325.                 deleted_count = IPAddressModel.objects.filter(pk__in=ips_to_remove).delete()[0]
  326.                 msg = f'Auto-removed {deleted_count} stale IP(s) from NetBox'
  327.                 log.info(msg)
  328.                 print(f'[Scan] {msg}', flush=True)
  329.  
  330.     # Save any remaining stale events
  331.     if stale_events:
  332.         for i in range(0, len(stale_events), BATCH):
  333.             ScanEvent.objects.bulk_create(stale_events[i:i + BATCH])
  334.  
  335.     # Calculate actual subnet size for correct utilization
  336.     network = ip_network(prefix_obj.prefix, strict=False)
  337.     subnet_size = max(network.num_addresses - 2, 1) if network.prefixlen < 31 else network.num_addresses
  338.  
  339.     up = sum(1 for _, r, _, _ in ping_results if r['is_reachable'])
  340.     skipped = len(skipped_ips)
  341.     removed = len(ips_to_remove)
  342.     tracked = len(ip_addresses)
  343.     down = tracked - up - skipped - removed
  344.  
  345.     # Count stale IPs (remaining after removal)
  346.     remaining_ip_pks = [ip.pk for ip in ip_addresses if ip.pk not in ips_to_remove] if ips_to_remove else [ip.pk for ip in ip_addresses]
  347.     stale_count = PingResult.objects.filter(
  348.         ip_address__in=remaining_ip_pks,
  349.         is_stale=True,
  350.     ).count()
  351.     new_count = PingResult.objects.filter(
  352.         ip_address__in=remaining_ip_pks,
  353.         is_new=True,
  354.     ).count()
  355.  
  356.     SubnetScanResult.objects.update_or_create(
  357.         prefix=prefix_obj,
  358.         defaults={
  359.             'total_hosts': subnet_size,
  360.             'hosts_up': up,
  361.             'hosts_down': down,
  362.             'hosts_skipped': skipped,
  363.             'hosts_stale': stale_count,
  364.             'hosts_new': new_count,
  365.             'last_scanned': timezone.now(),
  366.         },
  367.     )
  368.  
  369.     state_changes = {}
  370.     for event in scan_events:
  371.         state_changes[event.event_type] = state_changes.get(event.event_type, 0) + 1
  372.     if stale_to_update:
  373.         state_changes['ip_went_stale'] = len(stale_to_update)
  374.     if ips_to_remove:
  375.         state_changes['ip_removed_stale'] = len(ips_to_remove)
  376.  
  377.     return {'total': tracked, 'up': up, 'down': down, 'skipped': skipped, 'stale': stale_count, 'removed': removed, 'state_changes': state_changes}
  378.  
Advertisement
Add Comment
Please, Sign In to add comment