├── LICENSE ├── README.md └── netbox_ipscanner.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 bbird81 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Netbox-ipscanner 2 | ip scan script for populating IPAM module in Netbox 3 | 4 | # Requirement 5 | add the following packages to `local_requirements.txt` file in `/opt/netbox` 6 | 7 | ``` 8 | ipcalc 9 | pynetbox 10 | networkscan 11 | ``` 12 | 13 | # Usage 14 | add required modules in netbox environment and then copy the script in netbox script directory (usually `/opt/netbox/netbox/scripts/`)... you are ready to go :) 15 | 16 | # What it does exactly? 17 | Reads the prefixes in IPAM module and for each subnet makes a ping scan. Every responding address is added into the ip address IPAM module with DNS resolution. If an address exists in Netbox but is not pingable, it is marked as "Deprecated"; if DNS resolution is changed then it's updated. 18 | Subnets marked as "Reserved" are not scanned. 19 | -------------------------------------------------------------------------------- /netbox_ipscanner.py: -------------------------------------------------------------------------------- 1 | import pynetbox, urllib3, networkscan, socket, ipaddress 2 | from extras.scripts import Script 3 | 4 | TOKEN='xxx' 5 | NETBOXURL='https://netbox.eample.com' 6 | 7 | urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) #disable safety warnings 8 | 9 | class IpScan(Script): 10 | #optional variables in UI here! 11 | TagBasedScanning = BooleanVar( 12 | label="Tag Based Scanning?", 13 | default=False, 14 | description="enable Tag Based Scanning, to scan only Subnets with specified Tag.", 15 | ) 16 | tag = StringVar( 17 | max_length=20, 18 | label="Scan Tag?", 19 | default="scan", 20 | description="specify the Tag to filter Subnets to be scanned", 21 | required=True, 22 | ) 23 | 24 | 25 | class Meta: 26 | name = "IP Scanner" 27 | description = "Scans available prefixes and updates ip addresses in IPAM Module" 28 | 29 | 30 | def run(self, data, commit): 31 | 32 | def reverse_lookup(ip): 33 | ''' 34 | Mini function that does DNS reverse lookup with controlled failure 35 | ''' 36 | try: 37 | data = socket.gethostbyaddr(ip) 38 | except Exception: 39 | return '' #fails gracefully 40 | if data[0] == '': #if there is no name 41 | return '' 42 | else: 43 | return data[0] 44 | 45 | nb = pynetbox.api(NETBOXURL, token=TOKEN) 46 | nb.http_session.verify = False #disable certificate checking 47 | 48 | subnets = nb.ipam.prefixes.all() #extracts all prefixes, in format x.x.x.x/yy 49 | 50 | for subnet in subnets: 51 | if data['TagBasedScanning'] and data['tag'] not in str(subnet.tags): # Only scan subnets with the Tag 52 | self.log_debug(f'checking {subnet}...Tag is {subnet.tags}') 53 | self.log_warning(f"Scan of {subnet.prefix} NOT done (missing '{data['tag']}' tag)") 54 | continue 55 | if str(subnet.status) == 'Reserved': #Do not scan reserved subnets 56 | self.log_warning(f"Scan of {subnet.prefix} NOT done (is Reserved)") 57 | continue 58 | self.log_debug(f'checking {subnet}...Tag is {subnet.tags}') 59 | IPv4network = ipaddress.IPv4Network(subnet) 60 | mask = '/'+str(IPv4network.prefixlen) 61 | scan = networkscan.Networkscan(subnet) 62 | scan.run() 63 | self.log_info(f'Scan of {subnet} done.') 64 | 65 | #Routine to mark as DEPRECATED each Netbox entry that does not respond to ping 66 | for address in IPv4network.hosts(): #for each address of the prefix x.x.x.x/yy... 67 | #self.log_debug(f'checking {address}...') 68 | netbox_address = nb.ipam.ip_addresses.get(address=address) #extract address info from Netbox 69 | if netbox_address != None: #if the ip exists in netbox // if none exists, leave it to discover 70 | if str(netbox_address).rpartition('/')[0] in scan.list_of_hosts_found: #if he is in the list of "alive" 71 | pass #do nothing: It exists in NB and is in the pinged list: ok continue, you will see it later when you cycle the ip addresses that have responded whether to update something 72 | #self.log_success(f"The host {str(netbox_address).rpartition('/')[0]} exists in netbox and has been pinged") 73 | else: #if it exists in netbox but is NOT in the list, mark it as deprecated 74 | if str(netbox_address.status) == 'Deprecated' or str(netbox_address.status) == 'Reserved': #check the ip address to be Deprecated or Reserved 75 | pass # leave it as is 76 | else: 77 | self.log_warning(f"Host {str(netbox_address)} exists in netbox but not responding --> DEPRECATED") 78 | nb.ipam.ip_addresses.update([{'id':netbox_address.id, 'status':'deprecated'},]) 79 | #### 80 | 81 | if scan.list_of_hosts_found == []: 82 | self.log_warning(f'No host found in network {subnet}') 83 | else: 84 | self.log_success(f'IPs found: {scan.list_of_hosts_found}') 85 | for address1 in scan.list_of_hosts_found: #for each ip in the ping list... 86 | ip_mask=str(address1)+mask 87 | current_in_netbox = nb.ipam.ip_addresses.get(address=ip_mask) #extract current data in Netbox related to ip 88 | #self.log_debug(f'pinged ip: {address1} mask: {mask} --> {ip_mask} // extracted ip from netbox: {current_in_netbox}') 89 | if current_in_netbox != None: #the pinged address is already present in the Netbox, mark it as Active and check the name if it has changed 90 | nb.ipam.ip_addresses.update([{'id':current_in_netbox.id, 'status':'active'},]) 91 | name = reverse_lookup(address1) #name resolution from DNS 92 | if current_in_netbox.dns_name == name: #the names in Netbox and DNS match, do nothing 93 | pass 94 | else: #the names in Netbox and in DNS *DO NOT* match --> update Netbox with DNS name 95 | self.log_success(f'Name for {address1} updated to {name}') 96 | nb.ipam.ip_addresses.update([{'id':current_in_netbox.id, 'dns_name':name},]) 97 | else: #the pinged address is NOT present in Netbox, I have to add it 98 | name = reverse_lookup(address1) #name resolution from DNS 99 | res = nb.ipam.ip_addresses.create(address=ip_mask, status='active', dns_name=name) 100 | if res: 101 | self.log_success(f'Added {address1} - {name}') 102 | else: 103 | self.log_error(f'Adding {address1} - {name} FAILED') 104 | --------------------------------------------------------------------------------