├── .gitignore ├── README.md ├── contrib ├── linux │ ├── fusioninventory-agent.service │ ├── fusioninventory-agent.timer │ └── install.sh └── macos │ ├── install.sh │ └── org.fusioninventory.agent.company.plist ├── netbox_fusioninventory_plugin ├── __init__.py ├── urls.py ├── utils.py └── views.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | build 3 | *.egg-info 4 | dist 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # netbox-fusioninventory-plugin 2 | 3 | This plugin can add devices from fusioninventory-agent or ocsinventory-agent. 4 | 5 | ``` 6 | fusioninventory-agent -s http://netbox.local/plugins/fusion-inventory/ 7 | ``` 8 | 9 | ### what working: 10 | 11 | * Device creating/updating 12 | * Serial Number (UUID) 13 | * Asset Tag 14 | * Components creating/updating 15 | * Interfaces creating/updating 16 | * IP addresses creating/updating 17 | * Automatic tracking of everything 18 | * History preserving of everything 19 | * Full logging (Journal + Change Log) 20 | * "Lazy" and "computed" variables 21 | 22 | ## Installation 23 | 24 | First, add netbox-fusioninventory-plugin to your `/opt/netbox/local_requirements.txt` file. Create it if it doesn't exist. 25 | 26 | Then enable the plugin in `/opt/netbox/netbox/netbox/configuration.py`, like: 27 | 28 | ```python 29 | 30 | PLUGINS = [ 31 | 'netbox_fusioninventory_plugin', 32 | ] 33 | ``` 34 | 35 | And finally run `/opt/netbox/upgrade.sh`. This will download and install the plugin and update the database when necessary. Don't forget to run sudo systemctl restart netbox netbox-rq like upgrade.sh tells you! 36 | 37 | ## configuration 38 | 39 | You can overwrite fields with xml content or use internal objects. 40 | 41 | ```python 42 | PLUGINS_CONFIG = { 43 | 'netbox_fusioninventory_plugin':{ 44 | "name":"xml:request.content.hardware.name", 45 | "device_role":"object:DeviceRole:unknown", 46 | "tenant":None, 47 | "manufacturer":"xml:request.content.bios.mmanufacturer", 48 | "device_type":"xml:request.content.bios.mmodel", 49 | "platform":"xml:request.content.hardware.osname", 50 | "serial":"xml:request.content.hardware.uuid", 51 | "asset_tag":"lazy:'WKS-'+device['serial']", 52 | "status":"str:active", 53 | "site":"object:Site:unknown", 54 | "location":None, 55 | "rack":None, 56 | "position":None, 57 | "face":None, 58 | "virtual_chassis":None, 59 | "vc_position":None, 60 | "vc_priority":None, 61 | "cluster":None, 62 | "comments":None, 63 | } 64 | } 65 | ``` 66 | -------------------------------------------------------------------------------- /contrib/linux/fusioninventory-agent.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=fusioninventory-agent 3 | 4 | [Service] 5 | Type=oneshot 6 | ExecStart=/usr/bin/fusioninventory -s https://netbox.example.com/plugins/fusion-inventory/ --no-ssl-check 7 | WorkingDirectory=/usr/share/fusioninventory/ 8 | 9 | -------------------------------------------------------------------------------- /contrib/linux/fusioninventory-agent.timer: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=fusioninventory-agent 3 | 4 | [Timer] 5 | OnCalendar=*-*-* 11:00:00 6 | AccuracySec=1h 7 | OnBootSec=5m 8 | Persistent=true 9 | 10 | [Install] 11 | WantedBy=timers.target 12 | 13 | -------------------------------------------------------------------------------- /contrib/linux/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ "$EUID" -ne 0 ] 4 | then echo "Please run as root" 5 | exit 6 | fi 7 | 8 | echo "Installing packages" 9 | # Install fusioninventory-agent and useful deps 10 | pacman -S hdparm dmidecode pciutils fusioninventory-agent perl-parse-edid 11 | 12 | echo "Disabling service and enabling systemd timer" 13 | # disable fusioninventory-agent service because we will use systemd timer (like cron): daily at 11:00:00 and 5m after each boot 14 | systemctl disable fusioninventory-agent && systemctl stop fusioninventory-agent.service 15 | 16 | # add fusioninventory-agent service and its timer to run once per day 17 | cp ./fusioninventory-agent.service ./fusioninventory-agent.timer /etc/systemd/system/ 18 | systemctl enable fusioninventory-agent.timer && systemctl start fusioninventory-agent.timer 19 | 20 | systemctl status fusioninventory-agent.timer 21 | 22 | echo "First run" 23 | # run once 24 | systemctl start fusioninventory-agent.service 25 | 26 | -------------------------------------------------------------------------------- /contrib/macos/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ "$EUID" -ne 0 ] 4 | then echo "Please run as root" 5 | exit 6 | fi 7 | 8 | echo "Downloading package" 9 | curl -O -J -k https://netbox.company.com/warehouse/fusioninventory/macos/FusionInventory-Agent-2.6-2.pkg.tar.gz 10 | curl -O -J -k https://netbox.company.com/warehouse/fusioninventory/macos/org.fusioninventory.agent.company.plist 11 | 12 | echo "Installing package" 13 | # Install fusioninventory-agent 14 | tar -xzf ./FusionInventory-Agent-2.6-2.pkg.tar.gz 15 | installer -pkg ./FusionInventory-Agent-2.6-2.pkg -target / 16 | 17 | echo "Disabling service and enabling launchd task" 18 | # disable org.fusioninventory.agent service because we will use launchd task (like cron): every 86400 seconds 19 | launchctl stop -w org.fusioninventory.agent 20 | launchctl remove -w org.fusioninventory.agent 21 | 22 | launchctl unload -w /Library/LaunchAgents/org.fusioninventory.agent.plist 23 | launchctl unload -w /Library/LaunchDaemons/org.fusioninventory.agent.plist 24 | launchctl unload -w /Library/LaunchDaemons/org.fusioninventory.agent.company.plist 25 | rm /Library/LaunchAgents/org.fusioninventory.agent.plist 26 | rm /Library/LaunchDaemons/org.fusioninventory.agent.company.plist 27 | 28 | # add org.fusioninventory.agent.plist to the launchd 29 | cp ./org.fusioninventory.agent.company.plist /Library/LaunchDaemons/ 30 | chown root /Library/LaunchDaemons/org.fusioninventory.agent.company.plist 31 | launchctl load -w /Library/LaunchDaemons/org.fusioninventory.agent.company.plist 32 | launchctl list org.fusioninventory.agent.company 33 | 34 | #echo "First run" 35 | # run once 36 | #/opt/fusioninventory-agent/bin/fusioninventory-agent -s https://netbox.company.com/plugins/fusion-inventory/ --no-ssl-check 37 | 38 | -------------------------------------------------------------------------------- /contrib/macos/org.fusioninventory.agent.company.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Label 6 | org.fusioninventory.agent.company 7 | 8 | EnvironmentVariables 9 | 10 | PATH 11 | /usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin 12 | 13 | 14 | ProgramArguments 15 | 16 | /opt/fusioninventory-agent/bin/fusioninventory-agent 17 | -s 18 | https://netbox.company.com/plugins/fusion-inventory/ 19 | --no-ssl-check 20 | --no-fork 21 | --logger=stderr 22 | 23 | 24 | StartInterval 25 | 86400 26 | 27 | KeepAlive 28 | 29 | 30 | RunAtLoad 31 | 32 | 33 | StandardErrorPath 34 | /tmp/org.fusioninventory.agent.company.err 35 | 36 | StandardOutPath 37 | /tmp/org.fusioninventory.agent.company.out 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /netbox_fusioninventory_plugin/__init__.py: -------------------------------------------------------------------------------- 1 | from extras.plugins import PluginConfig 2 | 3 | 4 | class FusionInventoryConfig(PluginConfig): 5 | """ 6 | This class defines attributes for the NetBox FI Gateway plugin. 7 | """ 8 | # Plugin package name 9 | name = 'netbox_fusioninventory_plugin' 10 | 11 | # Human-friendly name and description 12 | verbose_name = 'Fusion inventory plugin' 13 | description = 'A Plugin for import devices and their components from fusion inventory agent' 14 | 15 | # Plugin version 16 | version = '0.6' 17 | 18 | # Plugin author 19 | author = 'Michaël Ricart' 20 | author_email = 'michael.ricart@0w.tf' 21 | 22 | # Configuration parameters that MUST be defined by the user (if any) 23 | required_settings = [] 24 | 25 | # Default configuration parameter values, if not set by the user 26 | default_settings = { 27 | "name":"xml:request.content.hardware.name", 28 | "device_role":"object:DeviceRole:unknown", 29 | "tenant":None, 30 | "manufacturer":"xml:request.content.bios.mmanufacturer", 31 | "device_type":"xml:request.content.bios.mmodel", 32 | "platform":"xml:request.content.hardware.osname", 33 | "serial":"xml:request.content.hardware.uuid", 34 | "asset_tag":"lazy:'WKS-'+device['serial']", 35 | "status":"str:active", 36 | "site":"object:Site:unknown", 37 | "location":None, 38 | "rack":None, 39 | "position":None, 40 | "face":None, 41 | "virtual_chassis":None, 42 | "vc_position":None, 43 | "vc_priority":None, 44 | "cluster":None, 45 | "comments":None, 46 | } 47 | 48 | 49 | 50 | # Base URL path. If not set, the plugin name will be used. 51 | base_url = 'fusion-inventory' 52 | 53 | # Caching config 54 | caching_config = {} 55 | 56 | 57 | config = FusionInventoryConfig 58 | 59 | -------------------------------------------------------------------------------- /netbox_fusioninventory_plugin/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from django.views.decorators.csrf import csrf_exempt 3 | 4 | from . import views 5 | 6 | 7 | # Define a list of URL patterns to be imported by NetBox. Each pattern maps a URL to 8 | # a specific view so that it can be accessed by users. 9 | urlpatterns = ( 10 | path('', csrf_exempt(views.PostXMLView.as_view()), name='post_xml'), 11 | path('/', csrf_exempt(views.PostXMLView.as_view()), name='post_xml'), 12 | ) 13 | 14 | -------------------------------------------------------------------------------- /netbox_fusioninventory_plugin/utils.py: -------------------------------------------------------------------------------- 1 | import bs4 2 | from dcim.models import * 3 | from ipam.models import IPAddress 4 | from tenancy.models import * 5 | from pprint import * 6 | from slugify import slugify, SLUG_OK 7 | from django.conf import settings 8 | from extras.models import Tag 9 | 10 | import hashlib 11 | import uuid 12 | import logging 13 | 14 | from django.contrib.auth.models import Group, User 15 | from extras.models import JournalEntry, ObjectChange 16 | from extras.choices import JournalEntryKindChoices, ObjectChangeActionChoices 17 | 18 | action_update = ObjectChangeActionChoices.ACTION_UPDATE 19 | action_create = ObjectChangeActionChoices.ACTION_CREATE 20 | action_delete = ObjectChangeActionChoices.ACTION_DELETE 21 | 22 | user = User.objects.get_or_create(username="FusionInventory")[0] 23 | _uuid = uuid.uuid4() 24 | 25 | # Use default Django logger. Settings are in the Django settings. 26 | logger = logging.getLogger('django') 27 | 28 | PLUGIN_SETTINGS = settings.PLUGINS_CONFIG["netbox_fusioninventory_plugin"] 29 | 30 | # Map netbox object property with XML 31 | inventory_settings = { 32 | "cpus": { 33 | "name": "xml_value(xml, 'name')", 34 | "manufacturer": "xml_or_unknown(xml, 'manufacturer')", 35 | "label": "'CPU'", 36 | "serial": "lazy:hashlib.md5((item['description']).encode('utf-8')).hexdigest().upper()", 37 | "asset_tag": "lazy:item['label'] + '-' + item['serial']", 38 | "tag": "{'name': 'hw:cpu', 'slug': 'hw-cpu'}", 39 | "description": """ 40 | 'Device: ' + device['serial'] + '. ' + \ 41 | 'Manufacturer: ' + xml_or_unknown(xml, 'manufacturer') + '. ' + \ 42 | 'ID: ' + xml_or_unknown(xml, 'id') + '. ' + \ 43 | 'Core: ' + xml_or_unknown(xml, 'core') + '. ' + \ 44 | 'Thread: ' + xml_or_unknown(xml, 'thread') + '. ' + \ 45 | 'Model: ' + xml_or_unknown(xml, 'model') + '. ' + \ 46 | 'Speed: ' + xml_or_unknown(xml, 'speed') + '. ' + \ 47 | 'Stepping: ' + xml_or_unknown(xml, 'stepping') + '. ' 48 | """ 49 | }, 50 | "controllers": { 51 | "name": "xml_value(xml, 'name')", 52 | "manufacturer": "xml_or_unknown(xml, 'manufacturer')", 53 | "part_id": "xml_or_none(xml, 'productid')", 54 | "label": "'OTHER'", 55 | "serial": "lazy:hashlib.md5((item['description']).encode('utf-8')).hexdigest().upper()", 56 | "asset_tag": "lazy:item['label'] + '-' + item['serial']", 57 | "tag": "{'name': 'hw:other', 'slug': 'hw-other'}", 58 | "description": """ 59 | 'Dev: ' + device['serial'] + '. ' + \ 60 | 'Manufacturer: ' + xml_or_unknown(xml, 'manufacturer') + '. ' + \ 61 | 'Product ID: ' + xml_or_unknown(xml, 'productid') + '. ' + \ 62 | 'Vendor ID: ' + xml_or_unknown(xml, 'vendorid') + '. ' + \ 63 | 'PCI class: ' + xml_or_unknown(xml, 'pciclass') + '. ' + \ 64 | 'PCI slot: ' + xml_or_unknown(xml, 'pcislot') + '. ' + \ 65 | 'PCI subsystem ID: ' + xml_or_unknown(xml, 'pcisubsystemid') + '. ' 66 | """ 67 | }, 68 | # TODO: Do we need USB devices? They are temporary. And sometimes they are equal and throw a duplicate error 69 | # "usbdevices": { 70 | # }, 71 | "memories": { 72 | "name": "xml_value(xml, 'caption')", 73 | "serial": "lazy:hashlib.md5((item['description']).encode('utf-8')).hexdigest().upper() if is_xml_value_zero(xml, 'serialnumber') else xml_value(xml, 'serialnumber').upper()", 74 | "manufacturer": "xml_or_unknown(xml, 'manufacturer')", 75 | "part_id": "xml_or_none(xml, 'model')", 76 | "label": "'MEMORY'", 77 | "asset_tag": "lazy:item['label'] +'-' + item['serial']", 78 | "tag": "{'name': 'hw:memory', 'slug': 'hw-memory'}", 79 | "description": """ 80 | 'Device: ' + device['serial'] + '. ' + \ 81 | 'Manufacturer: ' + xml_or_unknown(xml, 'manufacturer') + '. ' + \ 82 | 'Model: ' + xml_or_unknown(xml, 'model') + '. ' + \ 83 | 'Type: ' + xml_or_unknown(xml, 'type') + '. ' + \ 84 | 'Speed: ' + xml_or_unknown(xml, 'speed') + '. ' + \ 85 | 'Capacity: ' + xml_or_unknown(xml, 'capacity') + '. ' + \ 86 | 'Slot: ' + xml_or_unknown(xml, 'numslots') + '. ' + \ 87 | 'Serial: ' + xml_or_unknown(xml, 'serialnumber') + '. ' + \ 88 | 'Description: ' + xml_or_unknown(xml, 'description') + '. ' 89 | """ 90 | }, 91 | "monitors": { 92 | "name": "xml_or_unknown(xml, 'caption')", 93 | "manufacturer": "xml_or_unknown(xml, 'manufacturer')", 94 | "serial": "lazy:xml_value(xml, 'serial').upper() if (not is_xml_value_zero(xml, 'serial') and (is_xml_value_zero(xml, 'altserial') or len(xml_value(xml, 'serial')) > len(xml_value(xml, 'altserial')))) else xml_value(xml, 'altserial').upper() if (not is_xml_value_zero(xml, 'altserial')) else hashlib.md5((item['description']).encode('utf-8')).hexdigest().upper()", 95 | "label": "'MONITOR'", 96 | "asset_tag": "lazy:item['label'] +'-' + item['serial']", 97 | "tag": "{'name': 'hw:monitor', 'slug': 'hw-monitor'}", 98 | "description": """ 99 | 'Device: ' + device['serial'] + '. ' + \ 100 | 'Manufacturer: ' + xml_or_unknown(xml, 'manufacturer') + '. ' + \ 101 | 'Serial: ' + xml_or_unknown(xml, 'serial') + '. ' + \ 102 | 'Description: ' + xml_or_unknown(xml, 'description') + '. ' 103 | """ 104 | }, 105 | "videos": { 106 | "name": "xml_value(xml, 'name')", 107 | "manufacturer": "xml_or_unknown(xml, 'manufacturer')", 108 | "label": "'GPU'", 109 | "serial": "lazy:hashlib.md5((item['description']).encode('utf-8')).hexdigest().upper()", 110 | "asset_tag": "lazy:item['label'] +'-' + item['serial']", 111 | "tag": "{'name': 'hw:gpu', 'slug': 'hw-gpu'}", 112 | "description": """ 113 | 'Device: ' + device['serial'] + '. ' + \ 114 | 'Manufacturer: ' + xml_or_unknown(xml, 'manufacturer') + '. ' + \ 115 | 'Chipset: ' + xml_or_unknown(xml, 'chipset') + '. ' + \ 116 | 'Memory: ' + xml_or_unknown(xml, 'memory') + '. ' + \ 117 | 'Resolution: ' + xml_or_unknown(xml, 'resolution') + '. ' + \ 118 | 'PCI slot: ' + xml_or_unknown(xml, 'pcislot') + '. ' 119 | """ 120 | }, 121 | 122 | "storages": { 123 | "name": "xml_value(xml, 'name')", 124 | "manufacturer": "xml_or_unknown(xml, 'manufacturer')", 125 | "part_id": "xml_or_none(xml, 'model')", 126 | "serial": "lazy:hashlib.md5((item['description']).encode('utf-8')).hexdigest().upper() if is_xml_value_zero(xml, 'serialnumber') else xml_value(xml, 'serialnumber').upper()", 127 | "label": "'STORAGE'", 128 | "asset_tag": "lazy:item['label'] +'-' + item['serial']", 129 | "tag": "{'name': 'hw:storage', 'slug': 'hw-storage'}", 130 | "description": """ 131 | 'Device: ' + device['serial'] + '. ' + \ 132 | 'Manufacturer: ' + xml_or_unknown(xml, 'manufacturer') + '. ' + \ 133 | 'Model: ' + xml_or_unknown(xml, 'model') + '. ' + \ 134 | 'Firmware: ' + xml_or_unknown(xml, 'firmware') + '. ' + \ 135 | 'Serial: ' + xml_or_unknown(xml, 'serialnumber') + '. ' + \ 136 | 'Disk size: ' + xml_or_unknown(xml, 'disksize') + '. ' + \ 137 | 'Interface: ' + xml_or_unknown(xml, 'interface') + '. ' + \ 138 | 'Type: ' + xml_or_unknown(xml, 'type') + '. ' + \ 139 | 'WWN: ' + xml_or_unknown(xml, 'wwn') + '. ' + \ 140 | 'Description: ' + xml_or_unknown(xml, 'description') + '. ' 141 | """ 142 | }, 143 | "networks": { 144 | "mac_address": "xml_or_none(xml, 'macaddr')", 145 | "name": "xml_value(xml, 'description')", 146 | "ipaddress": "xml_or_none(xml, 'ipaddress')", 147 | "ipmask": "xml_or_none(xml, 'ipmask')", 148 | "ipaddress6": "xml_or_none(xml, 'ipaddress6')", 149 | "ipmask6": "xml_or_none(xml, 'ipmask6')" 150 | } 151 | } 152 | 153 | 154 | def xml_value(xml, key): 155 | return xml.find(key).get_text(strip=True) 156 | 157 | 158 | def xml_or_unknown(xml, key): 159 | return xml.find(key).get_text(strip=True) if (xml.find(key) and xml.find(key).get_text(strip=True) != '') else 'UNKNOWN' 160 | 161 | def xmlpath_or_unknown(xml, path, key): 162 | # FIXME: Dirty workaround 163 | xml = eval('xml.' + path) 164 | return xml.find(key).get_text(strip=True) if (xml.find(key) and xml.find(key).get_text(strip=True) != '') else 'UNKNOWN' 165 | 166 | 167 | def xml_or_none(xml, key): 168 | return xml.find(key).get_text(strip=True) if (xml.find(key) and xml.find(key).get_text(strip=True) != '') else None 169 | 170 | def value_or_none(item, key): 171 | return item[key] if (key in item and item[key].strip() != '') else None 172 | 173 | 174 | def is_xml_value_zero(xml, key): 175 | # FIXME: Dirty workarounds to exclude bad serials 176 | if ( 177 | (not xml.find(key)) or 178 | ( 179 | # Zeroes 180 | ((xml.find(key).get_text(strip=True)).upper().startswith('00000000') or 181 | (xml.find(key).get_text(strip=True)).upper().startswith('0X000000') or 182 | # Western Digital buggy serial 183 | (xml.find(key).get_text(strip=True)).upper() == 'WD' or 184 | # Too short serial 185 | len(xml.find(key).get_text(strip=True)) <= 5 or 186 | # Empty serial 187 | (xml.find(key).get_text(strip=True)) == '') 188 | ) 189 | ): 190 | return True 191 | else: 192 | return False 193 | 194 | 195 | def created_or_update_device(device_dict, items_array): 196 | related_objects = [ 197 | "manufacturer", 198 | "device_role", 199 | "tenant", 200 | "device_type", 201 | "platform", 202 | "site", 203 | "location", 204 | "rack", 205 | "face", 206 | "virtual_chassis", 207 | "vc_position", 208 | "vc_priority", 209 | "cluster", 210 | ] 211 | device_update_objects = [ 212 | "name", 213 | "manufacturer", 214 | "device_type", 215 | "platform", 216 | "serial", 217 | "asset_tag" 218 | ] 219 | 220 | # FIXME: Now we ignore mask 221 | excluded_ip_addresses = [ 222 | "127.0.0.1" 223 | ] 224 | excluded_ip_addresses_tuple = tuple(excluded_ip_addresses) 225 | 226 | # FIXME: Now we ignore mask 227 | excluded_ip_addresses6 = [ 228 | "fe80::1", 229 | "::1" 230 | ] 231 | excluded_ip_addresses6_tuple = tuple(excluded_ip_addresses6) 232 | 233 | # FIXME: Now we ignore mask, so use first octets 234 | included_ip_networks = [ 235 | "192.168.106.", 236 | "192.168.108.", 237 | "192.168.110." 238 | ] 239 | included_ip_networks_tuple = tuple(included_ip_networks) 240 | 241 | # FIXME: Now we ignore mask, so use first octets 242 | included_ip_networks6 = [ 243 | ] 244 | included_ip_networks6_tuple = tuple(included_ip_networks6) 245 | 246 | 247 | for key in related_objects: 248 | if device_dict[key]: 249 | if isinstance(device_dict[key], str): 250 | if key == "device_role": 251 | device_dict[key] = DeviceRole.objects.get_or_create( 252 | slug=slugify(device_dict[key], only_ascii=True), 253 | defaults={"name": device_dict[key], "slug": slugify(device_dict[key], only_ascii=True)} 254 | )[0] 255 | elif key == "manufacturer": 256 | device_dict[key] = Manufacturer.objects.get_or_create( 257 | slug=slugify(device_dict[key], only_ascii=True), 258 | defaults={"name": device_dict[key], "slug": slugify(device_dict[key], only_ascii=True)} 259 | )[0] 260 | elif key == "tenant": 261 | device_dict[key] = Tenant.objects.get_or_create( 262 | slug=slugify(device_dict[key], only_ascii=True), 263 | defaults={"name": device_dict[key], "slug": slugify(device_dict[key], only_ascii=True)} 264 | )[0] 265 | elif key == "device_type": 266 | # FIXME: Will throw an error if manufacturer wasn't get_or_created() before this key 267 | device_dict[key] = DeviceType.objects.get_or_create( 268 | slug=slugify(device_dict[key], only_ascii=True), 269 | defaults={"model": device_dict[key], "manufacturer": device_dict["manufacturer"], "slug": slugify(device_dict[key], only_ascii=True)} 270 | 271 | )[0] 272 | del device_dict["manufacturer"] 273 | elif key == "platform": 274 | device_dict[key] = Platform.objects.get_or_create( 275 | slug=slugify(device_dict[key], only_ascii=True), 276 | defaults={"name": device_dict[key], "slug": slugify(device_dict[key], only_ascii=True)} 277 | )[0] 278 | elif key == "site": 279 | device_dict[key] = Site.objects.get_or_create( 280 | slug=slugify(device_dict[key], only_ascii=True), 281 | defaults={"name": device_dict[key], "slug": slugify(device_dict[key], only_ascii=True)} 282 | )[0] 283 | elif key == "location": 284 | device_dict[key] = Location.objects.get_or_create( 285 | slug=slugify(device_dict[key], only_ascii=True), 286 | defaults={"name": device_dict[key], "slug": slugify(device_dict[key], only_ascii=True)} 287 | )[0] 288 | elif key == "rack": 289 | device_dict[key] = Rack.objects.get_or_create( 290 | slug=slugify(device_dict[key], only_ascii=True), 291 | defaults={"name": device_dict[key], "slug": slugify(device_dict[key], only_ascii=True)} 292 | )[0] 293 | else: 294 | del device_dict[key] 295 | to_del = [] 296 | for k, v in device_dict.items(): 297 | if not v: 298 | to_del.append(k) 299 | for key in to_del: 300 | del device_dict[key] 301 | 302 | journal_entries = [] 303 | if ('serial' in device_dict): 304 | device_serial = device_dict['serial'] 305 | devices = Device.objects.filter(serial=device_serial).all() 306 | if (devices.count() > 1): 307 | # TODO: We must report such devices via email 308 | logger.error( 309 | f'There are more than 1 device with this serial ({device_serial}): {devices}') 310 | else: 311 | try: 312 | device, device_created = Device.objects.get_or_create( 313 | serial=device_dict['serial'], defaults=device_dict) 314 | except Exception as e: 315 | logger.error(e) 316 | return 317 | 318 | if (device_created): 319 | logger.info( 320 | f'Creating a new device {device.name}, serial {device.serial} ({device_dict})') 321 | device.save() 322 | log = device.to_objectchange(action_create) 323 | log.user = user 324 | log.request_id = _uuid 325 | log.save() 326 | journal_entries.append( 327 | JournalEntry( 328 | assigned_object=device, 329 | created_by=user, 330 | kind=JournalEntryKindChoices.KIND_INFO, 331 | comments=f'Created a new device {device.name}, serial {device.serial}' 332 | ), 333 | ) 334 | 335 | # Iterate all the items from the FusionInventory 336 | 337 | # Collect all network interfaces: 338 | # 1. When the new device is being created, we just get_or_create() all IP addresses and interfaces 339 | # 2. In case of updating the interface we should get all already existing IP addresses/interfaces first via get()->true->update, 340 | # then unassociate not existed IP addresses via get()-> true -> del from interface, but from the netbox? 341 | # 3. What about interfaces? How do we delete non-existent? 342 | 343 | # FIXME: Didn't work with nested list comprehension [ [print(v2['asset_tag']) for v2 in v1 if 'asset_tag' in v2] for v1 in items_array.values()] 344 | # asset_tags_list = [] 345 | # for item1 in items_array.values(): 346 | # for item2 in item1: 347 | # if ('asset_tag' in item2 and item2['asset_tag'] != ''): 348 | # asset_tags_list.append(item2['asset_tag']) 349 | 350 | for key, value in items_array.items(): 351 | if (key == 'networks'): 352 | for item in value: 353 | logger.info(f'Creating network') 354 | ip_address = None 355 | ip_address6 = None 356 | if ('ipaddress' in item and 'ipmask' in item): 357 | if (not item['ipaddress'] in excluded_ip_addresses and item['ipaddress'].startswith(included_ip_networks_tuple)): 358 | try: 359 | ip_address, ip_created = IPAddress.objects.get_or_create( 360 | address=f'{item["ipaddress"]}/{item["ipmask"]}' 361 | ) 362 | except Exception as e: 363 | logger.error(e) 364 | journal_entries.append( 365 | JournalEntry( 366 | assigned_object=device, 367 | created_by=user, 368 | kind=JournalEntryKindChoices.KIND_WARNING, 369 | comments=f'IPv4 address {item["ipaddress"]}/{item["ipmask"]} was not created due to the error {e}' 370 | ), 371 | ) 372 | else: 373 | if (not ip_created): 374 | logger.warning( 375 | f'IP address {ip_address.address} already exists.') 376 | else: 377 | logger.warning( 378 | f'Excluded an IP address {item["ipaddress"]} due to the rules.') 379 | # journal_entries.append( 380 | # JournalEntry( 381 | # assigned_object=device, 382 | # created_by=user, 383 | # kind=JournalEntryKindChoices.KIND_WARNING, 384 | # comments=f'Excluded an IP address {item["ipaddress"]} due to the rules.' 385 | # ), 386 | # ) 387 | elif ('ipaddress' in item): 388 | if (not item['ipaddress'] in excluded_ip_addresses and item['ipaddress'].startswith(included_ip_networks_tuple)): 389 | try: 390 | ip_address, ip_created = IPAddress.objects.get_or_create( 391 | address=f'{item["ipaddress"]}' 392 | ) 393 | except Exception as e: 394 | logger.error(e) 395 | journal_entries.append( 396 | JournalEntry( 397 | assigned_object=device, 398 | created_by=user, 399 | kind=JournalEntryKindChoices.KIND_WARNING, 400 | comments=f'IPv4 address {item["ipaddress"]} was not created due to the error {e}' 401 | ), 402 | ) 403 | else: 404 | if (not ip_created): 405 | logger.warning( 406 | f'IP address {ip_address.address} already exists.') 407 | else: 408 | logger.warning( 409 | f'Excluded an IP address {item["ipaddress"]} due to the rules.') 410 | # journal_entries.append( 411 | # JournalEntry( 412 | # assigned_object=device, 413 | # created_by=user, 414 | # kind=JournalEntryKindChoices.KIND_WARNING, 415 | # comments=f'Excluded an IP address {item["ipaddress"]} due to the rules.' 416 | # ), 417 | # ) 418 | 419 | if ('ipaddress6' in item and 'ipmask6' in item): 420 | if (not item['ipaddress6'] in excluded_ip_addresses6 and item['ipaddress6'].startswith(included_ip_networks6_tuple)): 421 | try: 422 | ip_address6, ip_created6 = IPAddress.objects.get_or_create( 423 | address=f'{item["ipaddress6"]}/{item["ipmask6"]}' 424 | ) 425 | except Exception as e: 426 | logger.error(e) 427 | journal_entries.append( 428 | JournalEntry( 429 | assigned_object=device, 430 | created_by=user, 431 | kind=JournalEntryKindChoices.KIND_WARNING, 432 | comments=f'IPv6 address {item["ipaddress6"]}/{item["ipmask6"]} was not created due to the error {e}' 433 | ), 434 | ) 435 | else: 436 | if (not ip_created6): 437 | logger.warning( 438 | f'IP address {ip_address6.address} already exists.') 439 | else: 440 | logger.warning( 441 | f'Excluded an IP address {item["ipaddress6"]} due to the rules.') 442 | # journal_entries.append( 443 | # JournalEntry( 444 | # assigned_object=device, 445 | # created_by=user, 446 | # kind=JournalEntryKindChoices.KIND_WARNING, 447 | # comments=f'Excluded an IP address {item["ipaddress6"]} due to the rules.' 448 | # ), 449 | # ) 450 | elif ('ipaddress6' in item): 451 | if (not item['ipaddress6'] in excluded_ip_addresses6 and item['ipaddress6'].startswith(included_ip_networks6_tuple)): 452 | try: 453 | ip_address6, ip_created6 = IPAddress.objects.get_or_create( 454 | address=f'{item["ipaddress6"]}' 455 | ) 456 | except Exception as e: 457 | logger.error(e) 458 | journal_entries.append( 459 | JournalEntry( 460 | assigned_object=device, 461 | created_by=user, 462 | kind=JournalEntryKindChoices.KIND_WARNING, 463 | comments=f'IPv6 address {item["ipaddress6"]} was not created due to the error {e}' 464 | ), 465 | ) 466 | else: 467 | if (not ip_created6): 468 | logger.warning( 469 | f'IP address {ip_address6.address} already exists.') 470 | else: 471 | logger.warning( 472 | f'Excluded an IP address {item["ipaddress6"]} due to the rules.') 473 | # journal_entries.append( 474 | # JournalEntry( 475 | # assigned_object=device, 476 | # created_by=user, 477 | # kind=JournalEntryKindChoices.KIND_WARNING, 478 | # comments=f'Excluded an IP address {item["ipaddress6"]} due to the rules.' 479 | # ), 480 | # ) 481 | 482 | logger.info(f'Creating/updating interface {item}') 483 | try: 484 | interface, interface_created = device.interfaces.get_or_create( 485 | name=item['name'][:64]) 486 | except Exception as e: 487 | logger.error(e) 488 | journal_entries.append( 489 | JournalEntry( 490 | assigned_object=device, 491 | created_by=user, 492 | kind=JournalEntryKindChoices.KIND_WARNING, 493 | comments=f'Interface {item["name"]} was not created/updated due to the error {e}' 494 | ), 495 | ) 496 | continue 497 | 498 | mac_address = value_or_none(item,'mac_address') 499 | 500 | if (not interface_created): 501 | if (interface.mac_address != mac_address): 502 | logger.warning( 503 | f'Got iface:Set a new MAC address {mac_address} to the existing interface {interface.name}') 504 | interface.snapshot() 505 | interface.mac_address = mac_address 506 | interface.save() 507 | log = interface.to_objectchange( 508 | action_update) 509 | log.user = user 510 | log.request_id = _uuid 511 | log.save() 512 | journal_entries.append( 513 | JournalEntry( 514 | assigned_object=device, 515 | created_by=user, 516 | kind=JournalEntryKindChoices.KIND_WARNING, 517 | comments=f'Set a new MAC address {mac_address} to the existing interface {interface.name}' 518 | ), 519 | ) 520 | 521 | if (ip_address is not None): 522 | interface.snapshot() 523 | if (ip_created): 524 | logger.info( 525 | f'Got iface:Adding a new IPv4 address {ip_address.address} to the existing interface {interface.name}') 526 | else: 527 | logger.warning( 528 | f'Got iface:Moving the already existing IPv4 address {ip_address.address} to the existing interface {interface.name}') 529 | ip_address.snapshot() 530 | try: 531 | interface.ip_addresses.add(ip_address) 532 | except Exception as e: 533 | logger.error(e) 534 | journal_entries.append( 535 | JournalEntry( 536 | assigned_object=device, 537 | created_by=user, 538 | kind=JournalEntryKindChoices.KIND_WARNING, 539 | comments=f'IPv4 address {ip_address.address} was not added to the interface {interface.name} due to the error {e}' 540 | ), 541 | ) 542 | else: 543 | interface.save() 544 | ip_address.save() 545 | log = interface.to_objectchange( 546 | action_update) 547 | log.user = user 548 | log.request_id = _uuid 549 | log.save() 550 | if (ip_created): 551 | log = ip_address.to_objectchange( 552 | action_create) 553 | log.user = user 554 | log.request_id = _uuid 555 | log.save() 556 | journal_entries.append( 557 | JournalEntry( 558 | assigned_object=device, 559 | created_by=user, 560 | kind=JournalEntryKindChoices.KIND_INFO, 561 | comments=f'Added a new IPv4 address {ip_address.address} to the existing interface {interface.name}' 562 | ), 563 | ) 564 | journal_entries.append( 565 | JournalEntry( 566 | assigned_object=ip_address, 567 | created_by=user, 568 | kind=JournalEntryKindChoices.KIND_INFO, 569 | comments=f'Added a new IPv4 address {ip_address.address} to the existing interface {interface.name}' 570 | ), 571 | ) 572 | else: 573 | log = ip_address.to_objectchange( 574 | action_update) 575 | log.user = user 576 | log.request_id = _uuid 577 | log.save() 578 | journal_entries.append( 579 | JournalEntry( 580 | assigned_object=device, 581 | created_by=user, 582 | kind=JournalEntryKindChoices.KIND_WARNING, 583 | comments=f'Moved the already existing IPv4 address {ip_address.address} to the existing interface {interface.name}' 584 | ), 585 | ) 586 | journal_entries.append( 587 | JournalEntry( 588 | assigned_object=ip_address, 589 | created_by=user, 590 | kind=JournalEntryKindChoices.KIND_WARNING, 591 | comments=f'Moved the already existing IPv4 address {ip_address.address} to the existing interface {interface.name}' 592 | ), 593 | ) 594 | 595 | if (ip_address6 is not None): 596 | interface.snapshot() 597 | if (ip_created6): 598 | logger.info( 599 | f'Got iface:Adding a new IPv6 address {ip_address6.address} to the existing interface {interface.name}') 600 | else: 601 | logger.warning( 602 | f'Got iface:Moving the already existing IPv6 address {ip_address6.address} to the existing interface {interface.name}') 603 | ip_address6.snapshot() 604 | try: 605 | interface.ip_addresses.add(ip_address6) 606 | except Exception as e: 607 | logger.error(e) 608 | journal_entries.append( 609 | JournalEntry( 610 | assigned_object=device, 611 | created_by=user, 612 | kind=JournalEntryKindChoices.KIND_WARNING, 613 | comments=f'IPv6 address {ip_address6.address} was not added to the interface {interface.name} due to the error {e}' 614 | ), 615 | ) 616 | else: 617 | interface.save() 618 | ip_address6.save() 619 | log = interface.to_objectchange( 620 | action_update) 621 | log.user = user 622 | log.request_id = _uuid 623 | log.save() 624 | if (ip_created6): 625 | log = ip_address6.to_objectchange( 626 | action_create) 627 | log.user = user 628 | log.request_id = _uuid 629 | log.save() 630 | journal_entries.append( 631 | JournalEntry( 632 | assigned_object=device, 633 | created_by=user, 634 | kind=JournalEntryKindChoices.KIND_INFO, 635 | comments=f'Added a new IPv6 address {ip_address6.address} to the existing interface {interface.name}' 636 | ), 637 | ) 638 | journal_entries.append( 639 | JournalEntry( 640 | assigned_object=ip_address6, 641 | created_by=user, 642 | kind=JournalEntryKindChoices.KIND_INFO, 643 | comments=f'Added a new IPv6 address {ip_address6.address} to the existing interface {interface.name}' 644 | ), 645 | ) 646 | else: 647 | log = ip_address6.to_objectchange( 648 | action_update) 649 | log.user = user 650 | log.request_id = _uuid 651 | log.save() 652 | journal_entries.append( 653 | JournalEntry( 654 | assigned_object=device, 655 | created_by=user, 656 | kind=JournalEntryKindChoices.KIND_WARNING, 657 | comments=f'Moved the already existing IPv6 address {ip_address6.address} to the existing interface {interface.name}' 658 | ), 659 | ) 660 | journal_entries.append( 661 | JournalEntry( 662 | assigned_object=ip_address6, 663 | created_by=user, 664 | kind=JournalEntryKindChoices.KIND_WARNING, 665 | comments=f'Moved the already existing IPv6 address {ip_address6.address} to the existing interface {interface.name}' 666 | ), 667 | ) 668 | 669 | else: 670 | mac_address = value_or_none(item,'mac_address') 671 | interface.mac_address = mac_address 672 | interface.save() 673 | 674 | if (ip_address is not None): 675 | if (ip_created): 676 | logger.info( 677 | f'Adding a new IPv4 address {ip_address.address} to a new interface {interface.name}') 678 | else: 679 | logger.warning( 680 | f'Moving the already existing IPv4 address {ip_address.address} to a new interface {interface.name}') 681 | ip_address.snapshot() 682 | try: 683 | interface.ip_addresses.add(ip_address) 684 | except Exception as e: 685 | logger.error(e) 686 | journal_entries.append( 687 | JournalEntry( 688 | assigned_object=device, 689 | created_by=user, 690 | kind=JournalEntryKindChoices.KIND_WARNING, 691 | comments=f'IPv4 address {ip_address.address} was not added to the interface {interface.name} due to the error {e}' 692 | ), 693 | ) 694 | else: 695 | interface.save() 696 | ip_address.save() 697 | log = interface.to_objectchange( 698 | action_create) 699 | log.user = user 700 | log.request_id = _uuid 701 | log.save() 702 | if (ip_created): 703 | log = ip_address.to_objectchange( 704 | action_create) 705 | log.user = user 706 | log.request_id = _uuid 707 | log.save() 708 | journal_entries.append( 709 | JournalEntry( 710 | assigned_object=device, 711 | created_by=user, 712 | kind=JournalEntryKindChoices.KIND_INFO, 713 | comments=f'Added a new IPv4 address {ip_address.address} to a new interface {interface.name}' 714 | ), 715 | ) 716 | 717 | journal_entries.append( 718 | JournalEntry( 719 | assigned_object=ip_address, 720 | created_by=user, 721 | kind=JournalEntryKindChoices.KIND_INFO, 722 | comments=f'Added a new IPv4 address {ip_address.address} to a new interface {interface.name}' 723 | ), 724 | ) 725 | else: 726 | log = ip_address.to_objectchange( 727 | action_update) 728 | log.user = user 729 | log.request_id = _uuid 730 | log.save() 731 | journal_entries.append( 732 | JournalEntry( 733 | assigned_object=device, 734 | created_by=user, 735 | kind=JournalEntryKindChoices.KIND_WARNING, 736 | comments=f'Moved the already existing IPv4 address {ip_address.address} to a new interface {interface.name}' 737 | ), 738 | ) 739 | 740 | journal_entries.append( 741 | JournalEntry( 742 | assigned_object=ip_address, 743 | created_by=user, 744 | kind=JournalEntryKindChoices.KIND_WARNING, 745 | comments=f'Moved the already existing IPv4 address {ip_address.address} to a new interface {interface.name}' 746 | ), 747 | ) 748 | 749 | if (ip_address6 is not None): 750 | if (ip_created6): 751 | logger.info( 752 | f'Adding a new IPv6 address {ip_address6.address} to a new interface {interface.name}') 753 | else: 754 | logger.warning( 755 | f'Moving the already existing IPv6 address {ip_address6.address} to a new interface {interface.name}') 756 | ip_address6.snapshot() 757 | try: 758 | interface.ip_addresses.add(ip_address6) 759 | except Exception as e: 760 | logger.error(e) 761 | journal_entries.append( 762 | JournalEntry( 763 | assigned_object=device, 764 | created_by=user, 765 | kind=JournalEntryKindChoices.KIND_WARNING, 766 | comments=f'IPv6 address {ip_address6.address} was not added to the interface {interface.name} due to the error {e}' 767 | ), 768 | ) 769 | else: 770 | interface.save() 771 | ip_address6.save() 772 | log = interface.to_objectchange( 773 | action_create) 774 | log.user = user 775 | log.request_id = _uuid 776 | log.save() 777 | if (ip_created6): 778 | log = ip_address6.to_objectchange( 779 | action_create) 780 | log.user = user 781 | log.request_id = _uuid 782 | log.save() 783 | journal_entries.append( 784 | JournalEntry( 785 | assigned_object=device, 786 | created_by=user, 787 | kind=JournalEntryKindChoices.KIND_INFO, 788 | comments=f'Added a new IPv6 address {ip_address6.address} to a new interface {interface.name}' 789 | ), 790 | ) 791 | journal_entries.append( 792 | JournalEntry( 793 | assigned_object=ip_address6, 794 | created_by=user, 795 | kind=JournalEntryKindChoices.KIND_INFO, 796 | comments=f'Added a new IPv6 address {ip_address6.address} to a new interface {interface.name}' 797 | ), 798 | ) 799 | else: 800 | log = ip_address6.to_objectchange( 801 | action_update) 802 | log.user = user 803 | log.request_id = _uuid 804 | log.save() 805 | journal_entries.append( 806 | JournalEntry( 807 | assigned_object=device, 808 | created_by=user, 809 | kind=JournalEntryKindChoices.KIND_WARNING, 810 | comments=f'Moved the already existing IPv6 address {ip_address6.address} to a new interface {interface.name}' 811 | ), 812 | ) 813 | journal_entries.append( 814 | JournalEntry( 815 | assigned_object=ip_address6, 816 | created_by=user, 817 | kind=JournalEntryKindChoices.KIND_WARNING, 818 | comments=f'Moved the already existing IPv6 address {ip_address6.address} to a new interface {interface.name}' 819 | ), 820 | ) 821 | 822 | else: 823 | for item in value: 824 | tag = None 825 | tag_created = None 826 | # FIXME: We do list() to avoid RuntimeError: dictionary changed size during iteration (see del() below) 827 | for k, v in list(item.items()): 828 | if k == "manufacturer": 829 | try: 830 | item[k] = Manufacturer.objects.get_or_create( 831 | slug=slugify(item[k], only_ascii=True), 832 | defaults={"name": item[k], "slug": slugify(item[k], only_ascii=True)} 833 | )[0] 834 | except Exception as e: 835 | logger.error(e) 836 | continue 837 | elif k == "tag": 838 | try: 839 | tag, tag_created = Tag.objects.get_or_create( 840 | name=item[k]['name'], 841 | slug=item[k]['slug'], 842 | ) 843 | except Exception as e: 844 | logger.error(e) 845 | continue 846 | else: 847 | # Delete our special key, InventoryItem does not have this key in its model 848 | # FIXME: Should we delete this? 849 | del item[k] 850 | elif k == "name": 851 | # Shorten string 852 | item[k] = item[k][:64] 853 | elif (k == "serial" or k == "part_id" or k == "asset_tag"): 854 | # Shorten string 855 | item[k] = item[k][:50] 856 | elif k == "description": 857 | # Shorten string 858 | item[k] = item[k][:200] 859 | 860 | item['discovered'] = True 861 | logger.info( 862 | f'Creating item {item} for the device {device.name}') 863 | 864 | # Continue the loop if we couldn't add an item. Report an error and add Journal entry 865 | try: 866 | inventory_item, item_created = InventoryItem.objects.get_or_create( 867 | asset_tag=item['asset_tag'], defaults={'device': device, **item}) 868 | except Exception as e: 869 | logger.error(e) 870 | journal_entries.append( 871 | JournalEntry( 872 | assigned_object=device, 873 | created_by=user, 874 | kind=JournalEntryKindChoices.KIND_WARNING, 875 | comments=f'Item {item} was not added to the device {device.name} due to the error {e}' 876 | ), 877 | ) 878 | continue 879 | 880 | if (not item_created): 881 | logger.warning( 882 | f'Item with the asset_tag {inventory_item.asset_tag} previously was related to the device {inventory_item.device.name}. Change its device to the {device.name}') 883 | old_device = inventory_item.device 884 | inventory_item.snapshot() 885 | for k, v in item.items(): 886 | setattr(inventory_item, k, v) 887 | inventory_item.device = device 888 | # Add tag. Do not skip if we could not add the tag, but report the error and add a Journal entry. 889 | if (tag is not None): 890 | try: 891 | inventory_item.tags.add(tag) 892 | except Exception as e: 893 | logger.error(e) 894 | journal_entries.append( 895 | JournalEntry( 896 | assigned_object=device, 897 | created_by=user, 898 | kind=JournalEntryKindChoices.KIND_WARNING, 899 | comments=f'Tag {tag} was not created for the inventory item {inventory_item.name} due to the error {e}' 900 | ), 901 | ) 902 | 903 | inventory_item.save() 904 | log = inventory_item.to_objectchange( 905 | action_update) 906 | log.user = user 907 | log.request_id = _uuid 908 | log.save() 909 | journal_entries.append( 910 | JournalEntry( 911 | assigned_object=device, 912 | created_by=user, 913 | kind=JournalEntryKindChoices.KIND_WARNING, 914 | comments=f'Item with the asset_tag {inventory_item.asset_tag} previously was related to the device {inventory_item.device.name}. Change its device to the {device.name}' 915 | ), 916 | ) 917 | 918 | journal_entries.append( 919 | JournalEntry( 920 | assigned_object=old_device, 921 | created_by=user, 922 | kind=JournalEntryKindChoices.KIND_WARNING, 923 | comments=f'Item with the asset_tag {inventory_item.asset_tag} previously was related to the device {inventory_item.device.name}. Change its device to the {device.name}' 924 | ), 925 | ) 926 | 927 | journal_entries.append( 928 | JournalEntry( 929 | assigned_object=inventory_item, 930 | created_by=user, 931 | kind=JournalEntryKindChoices.KIND_WARNING, 932 | comments=f'Item with the asset_tag {inventory_item.asset_tag} previously was related to the device {inventory_item.device.name}. Change its device to the {device.name}' 933 | ), 934 | ) 935 | 936 | else: 937 | # Add tag. Do not skip if we could not add the tag, but report the error and add a Journal entry. 938 | if (tag is not None): 939 | try: 940 | inventory_item.tags.add(tag) 941 | except Exception as e: 942 | logger.error(e) 943 | journal_entries.append( 944 | JournalEntry( 945 | assigned_object=device, 946 | created_by=user, 947 | kind=JournalEntryKindChoices.KIND_WARNING, 948 | comments=f'Tag {tag} was not created for the inventory item {inventory_item.name} due to the error {e}' 949 | ), 950 | ) 951 | 952 | # Report about just created inventory item 953 | inventory_item.save() 954 | log = inventory_item.to_objectchange( 955 | action_create) 956 | log.user = user 957 | log.request_id = _uuid 958 | log.save() 959 | journal_entries.append( 960 | JournalEntry( 961 | assigned_object=device, 962 | created_by=user, 963 | kind=JournalEntryKindChoices.KIND_INFO, 964 | comments=f'Added a new inventory item {inventory_item.name} to the device {device.name}' 965 | ), 966 | ) 967 | journal_entries.append( 968 | JournalEntry( 969 | assigned_object=inventory_item, 970 | created_by=user, 971 | kind=JournalEntryKindChoices.KIND_INFO, 972 | comments=f'Added a new inventory item {inventory_item.name} to the device {device.name}' 973 | ), 974 | ) 975 | 976 | else: 977 | logger.info( 978 | f'Updating the existing device {device.name}, serial {device.serial} ({device_dict})') 979 | 980 | # Remove keys which we are not going to update 981 | for k in list(device_dict.keys()): 982 | if (not k in device_update_objects): 983 | del device_dict[k] 984 | 985 | device_updated = False 986 | device.snapshot() 987 | for k, v in device_dict.items(): 988 | if (getattr(device, k) != device_dict[k]): 989 | setattr(device, k, v) 990 | device_updated = True 991 | if (device_updated): 992 | device.save() 993 | log = device.to_objectchange(action_update) 994 | log.user = user 995 | log.request_id = _uuid 996 | log.save() 997 | journal_entries.append( 998 | JournalEntry( 999 | assigned_object=device, 1000 | created_by=user, 1001 | kind=JournalEntryKindChoices.KIND_INFO, 1002 | comments=f'Updated the existing device {device.name}, serial {device.serial}' 1003 | ), 1004 | ) 1005 | 1006 | # Updating the items 1007 | # FIXME: We must refactor this and merge two huge functions in a smaller one 1008 | 1009 | # Find all current items of the current device with asset_tags which do not exist in the new dataset, mark them and discovered = False and change asset_tag to 'LOST-'+asset_tag 1010 | 1011 | # FIXME: Didn't work with nested list comprehension [ [print(v2['asset_tag']) for v2 in v1 if 'asset_tag' in v2] for v1 in items_array.values()] 1012 | asset_tags_list = [] 1013 | for item1 in items_array.values(): 1014 | for item2 in item1: 1015 | if ('asset_tag' in item2 and item2['asset_tag'].strip() != ''): 1016 | asset_tags_list.append(item2['asset_tag'].strip()) 1017 | 1018 | # FIXME: Ugly and redundant solution. As much ugly as the above solution 1019 | # Collect all current interfaces and mark disabled those which do not exist 1020 | # Collect all current IP-addresses and delete those which do not exist. Use lower() for addresses 1021 | interfaces_list = [] 1022 | addresses_list = [] 1023 | if ('networks' in items_array.keys()): 1024 | for item1 in items_array['networks']: 1025 | if ('name' in item1 and item1['name'].strip() != ''): 1026 | interfaces_list.append(item1['name'].strip()[:64]) 1027 | if ('ipaddress' in item1 and item1['ipaddress'].strip() != ''): 1028 | addresses_list.append(item1['ipaddress'].strip().lower()) 1029 | if ('ipaddress6' in item1 and item1['ipaddress6'].strip() != ''): 1030 | addresses_list.append(item1['ipaddress6'].strip().lower()) 1031 | 1032 | # Tuple for istartswith() 1033 | addresses_list_tuple = tuple(addresses_list) 1034 | 1035 | # Mark as undiscovered when it's lost 1036 | for lost_item in (InventoryItem.objects.filter(device=device).filter(discovered=True).exclude(asset_tag__in=asset_tags_list)): 1037 | logger.warning( 1038 | f'Item with the asset_tag {lost_item.asset_tag} does not exist in the new inventory data of the device {device.name}. Mark it as not discovered') 1039 | lost_item.snapshot() 1040 | lost_item.discovered = False 1041 | lost_item.save() 1042 | log = lost_item.to_objectchange(action_update) 1043 | log.user = user 1044 | log.request_id = _uuid 1045 | log.save() 1046 | journal_entries.append( 1047 | JournalEntry( 1048 | assigned_object=device, 1049 | created_by=user, 1050 | kind=JournalEntryKindChoices.KIND_WARNING, 1051 | comments=f'Item with the asset_tag {lost_item.asset_tag} does not exist in the new inventory data of the device {device.name}. Mark it as not discovered' 1052 | ), 1053 | ) 1054 | journal_entries.append( 1055 | JournalEntry( 1056 | assigned_object=lost_item, 1057 | created_by=user, 1058 | kind=JournalEntryKindChoices.KIND_WARNING, 1059 | comments=f'Item with the asset_tag {lost_item.asset_tag} does not exist in the new inventory data of the device {device.name}. Mark it as not discovered' 1060 | ), 1061 | ) 1062 | 1063 | # Mark as discovered (even if it's not marked as undiscovered) and update when it's found in our device and was related to another one. 1064 | # Other item data will be updated later in the main loop 1065 | for found_item in (InventoryItem.objects.filter(asset_tag__in=asset_tags_list).exclude(device=device)): 1066 | logger.warning( 1067 | f'Item with the asset_tag {found_item.asset_tag} previously was related to the device {found_item.device.name}. Mark it as discovered again and change its device to the {device.name}') 1068 | old_device = found_item.device 1069 | found_item.snapshot() 1070 | found_item.discovered = True 1071 | found_item.device = device 1072 | found_item.save() 1073 | log = found_item.to_objectchange(action_update) 1074 | log.user = user 1075 | log.request_id = _uuid 1076 | log.save() 1077 | journal_entries.append( 1078 | JournalEntry( 1079 | assigned_object=device, 1080 | created_by=user, 1081 | kind=JournalEntryKindChoices.KIND_WARNING, 1082 | comments=f'Item with the asset_tag {found_item.asset_tag} previously was related to the device {old_device.name}. Mark it as discovered again and change its device to the {device.name}' 1083 | ), 1084 | ) 1085 | journal_entries.append( 1086 | JournalEntry( 1087 | assigned_object=found_item, 1088 | created_by=user, 1089 | kind=JournalEntryKindChoices.KIND_WARNING, 1090 | comments=f'Item with the asset_tag {found_item.asset_tag} previously was related to the device {old_device.name}. Mark it as discovered again and change its device to the {device.name}' 1091 | ), 1092 | ) 1093 | if (old_device != device): 1094 | journal_entries.append( 1095 | JournalEntry( 1096 | assigned_object=old_device, 1097 | created_by=user, 1098 | kind=JournalEntryKindChoices.KIND_WARNING, 1099 | comments=f'Item with the asset_tag {found_item.asset_tag} previously was related to the device {old_device.name}. Mark it as discovered again and change its device to the {device.name}' 1100 | ), 1101 | ) 1102 | 1103 | # Report about an item that was undiscovered in our device and now discovered again 1104 | # Other item data will be updated later in the main loop 1105 | for found_item in (InventoryItem.objects.filter(device=device).filter(discovered=False).filter(asset_tag__in=asset_tags_list)): 1106 | logger.warning( 1107 | f'Item with the asset_tag {found_item.asset_tag} previously was removed from the device {device.name} and now discovered back. Mark it as discovered again') 1108 | found_item.snapshot() 1109 | found_item.discovered = True 1110 | found_item.save() 1111 | log = found_item.to_objectchange(action_update) 1112 | log.user = user 1113 | log.request_id = _uuid 1114 | log.save() 1115 | journal_entries.append( 1116 | JournalEntry( 1117 | assigned_object=device, 1118 | created_by=user, 1119 | kind=JournalEntryKindChoices.KIND_WARNING, 1120 | comments=f'Item with the asset_tag {found_item.asset_tag} previously was removed from the device {device.name} and now discovered back. Mark it as discovered again' 1121 | ), 1122 | ) 1123 | journal_entries.append( 1124 | JournalEntry( 1125 | assigned_object=found_item, 1126 | created_by=user, 1127 | kind=JournalEntryKindChoices.KIND_WARNING, 1128 | comments=f'Item with the asset_tag {found_item.asset_tag} previously was removed from the device {device.name} and now discovered back. Mark it as discovered again' 1129 | ), 1130 | ) 1131 | 1132 | # Mark interfaces as disabled when it's lost 1133 | # FIXME: We have another loop to remove the lost IP-addresses below. Beware. 1134 | for lost_interface in (device.interfaces.filter(enabled=True).exclude(name__in=interfaces_list)): 1135 | logger.warning( 1136 | f'Interface with the name {lost_interface.name} does not exist in the new inventory data of the device {device.name}. Mark it as disabled') 1137 | lost_interface.snapshot() 1138 | lost_interface.enabled = False 1139 | lost_interface.save() 1140 | log = lost_interface.to_objectchange(action_update) 1141 | log.user = user 1142 | log.request_id = _uuid 1143 | log.save() 1144 | journal_entries.append( 1145 | JournalEntry( 1146 | assigned_object=device, 1147 | created_by=user, 1148 | kind=JournalEntryKindChoices.KIND_WARNING, 1149 | comments=f'Interface with the name {lost_interface.name} does not exist in the new inventory data of the device {device.name}. Mark it as disabled' 1150 | ), 1151 | ) 1152 | 1153 | # Mark interfaces as enabled again when it's found 1154 | for found_interface in (device.interfaces.filter(enabled=False).filter(name__in=interfaces_list)): 1155 | logger.warning( 1156 | f'Disabled interface with the name {found_interface.name} found in the new inventory data of the device {device.name}. Mark it as enabled') 1157 | found_interface.snapshot() 1158 | found_interface.enabled = True 1159 | found_interface.save() 1160 | log = found_interface.to_objectchange(action_update) 1161 | log.user = user 1162 | log.request_id = _uuid 1163 | log.save() 1164 | journal_entries.append( 1165 | JournalEntry( 1166 | assigned_object=device, 1167 | created_by=user, 1168 | kind=JournalEntryKindChoices.KIND_WARNING, 1169 | comments=f'Disabled interface with the name {found_interface.name} found in the new inventory data of the device {device.name}. Mark it as enabled' 1170 | ), 1171 | ) 1172 | 1173 | # Remove lost IP-addresses. Take into consideration excluded addresses and included networks 1174 | # FIXME: We do not take into consideration the mask! 1175 | # FIXME: Looks like Django doesn't support tuples for istartswith(), so we have to iterate 1176 | # FIXME: We already lowered tuples above 1177 | # FIXME: BUG: will startswith(excluded_ip_addresses_tuple) match 192.168.108.8 when there is address 192.168.108.80? 1178 | for interface in device.interfaces.all(): 1179 | addresses_unlinked = False 1180 | for address in interface.ip_addresses.all(): 1181 | if (not str(address.address.ip).lower().startswith(excluded_ip_addresses_tuple) and not str(address.address.ip).lower().startswith(excluded_ip_addresses6_tuple) and (str(address.address.ip).lower().startswith(included_ip_networks_tuple) or str(address.address.ip).lower().startswith(included_ip_networks6_tuple)) and not str(address.address.ip).lower().startswith(addresses_list_tuple)): 1182 | logger.warning( 1183 | f'IP address {address.address} does not exist in the new inventory data of the device {device.name}. Unlink it.') 1184 | # Make a snapshot for the first time 1185 | if (not addresses_unlinked): 1186 | interface.snapshot() 1187 | addresses_unlinked = True 1188 | 1189 | address.snapshot() 1190 | journal_entries.append( 1191 | JournalEntry( 1192 | assigned_object=device, 1193 | created_by=user, 1194 | kind=JournalEntryKindChoices.KIND_WARNING, 1195 | comments=f'IP address {address.address} does not exist in the new inventory data of the device {device.name}. Unlink it.' 1196 | ), 1197 | ) 1198 | journal_entries.append( 1199 | JournalEntry( 1200 | assigned_object=address, 1201 | created_by=user, 1202 | kind=JournalEntryKindChoices.KIND_WARNING, 1203 | comments=f'IP address {address.address} does not exist in the new inventory data of the device {device.name}. Unlink it.' 1204 | ), 1205 | ) 1206 | # Unlink it 1207 | address.assigned_object_type = None 1208 | address.assigned_object_id = 0 1209 | address.save() 1210 | 1211 | log = address.to_objectchange(action_update) 1212 | log.user = user 1213 | log.request_id = _uuid 1214 | log.save() 1215 | # Save and report changes 1216 | if (addresses_unlinked): 1217 | interface.save() 1218 | log = interface.to_objectchange(action_update) 1219 | log.user = user 1220 | log.request_id = _uuid 1221 | log.save() 1222 | # FIXME: Redundant but to be sure. 1223 | addresses_unlinked = False 1224 | 1225 | # FIXME: Should we run device.save() here and in all other cases above and below? 1226 | # device.save() 1227 | 1228 | # FIXME: Mostly the same code as for creating. We must refactor this! 1229 | for key, value in items_array.items(): 1230 | if (key == 'networks'): 1231 | for item in value: 1232 | logger.info(f'Updating network') 1233 | ip_address = None 1234 | ip_address6 = None 1235 | interface_updated = False 1236 | if ('ipaddress' in item and 'ipmask' in item): 1237 | if (not item['ipaddress'] in excluded_ip_addresses and item['ipaddress'].startswith(included_ip_networks_tuple)): 1238 | try: 1239 | ip_address, ip_created = IPAddress.objects.get_or_create( 1240 | address=f'{item["ipaddress"]}/{item["ipmask"]}' 1241 | ) 1242 | except Exception as e: 1243 | logger.error(e) 1244 | journal_entries.append( 1245 | JournalEntry( 1246 | assigned_object=device, 1247 | created_by=user, 1248 | kind=JournalEntryKindChoices.KIND_WARNING, 1249 | comments=f'IPv4 address {item["ipaddress"]}/{item["ipmask"]} was not created due to the error {e}' 1250 | ), 1251 | ) 1252 | else: 1253 | if (not ip_created): 1254 | logger.info( 1255 | f'IP address {ip_address.address} already exists.') 1256 | else: 1257 | logger.warning( 1258 | f'Excluded an IP address {item["ipaddress"]} due to the rules.') 1259 | # journal_entries.append( 1260 | # JournalEntry( 1261 | # assigned_object=device, 1262 | # created_by=user, 1263 | # kind=JournalEntryKindChoices.KIND_WARNING, 1264 | # comments=f'Excluded an IP address {item["ipaddress"]} due to the rules.' 1265 | # ), 1266 | # ) 1267 | elif ('ipaddress' in item): 1268 | if (not item['ipaddress'] in excluded_ip_addresses and item['ipaddress'].startswith(included_ip_networks_tuple)): 1269 | try: 1270 | ip_address, ip_created = IPAddress.objects.get_or_create( 1271 | address=f'{item["ipaddress"]}' 1272 | ) 1273 | except Exception as e: 1274 | logger.error(e) 1275 | journal_entries.append( 1276 | JournalEntry( 1277 | assigned_object=device, 1278 | created_by=user, 1279 | kind=JournalEntryKindChoices.KIND_WARNING, 1280 | comments=f'IPv4 address {item["ipaddress"]} was not created due to the error {e}' 1281 | ), 1282 | ) 1283 | else: 1284 | if (not ip_created): 1285 | logger.info( 1286 | f'IP address {ip_address.address} already exists.') 1287 | else: 1288 | logger.warning( 1289 | f'Excluded an IP address {item["ipaddress"]} due to the rules.') 1290 | # journal_entries.append( 1291 | # JournalEntry( 1292 | # assigned_object=device, 1293 | # created_by=user, 1294 | # kind=JournalEntryKindChoices.KIND_WARNING, 1295 | # comments=f'Excluded an IP address {item["ipaddress"]} due to the rules.' 1296 | # ), 1297 | # ) 1298 | 1299 | if ('ipaddress6' in item and 'ipmask6' in item): 1300 | if (not item['ipaddress6'] in excluded_ip_addresses6 and item['ipaddress6'].startswith(included_ip_networks6_tuple)): 1301 | try: 1302 | ip_address6, ip_created6 = IPAddress.objects.get_or_create( 1303 | address=f'{item["ipaddress6"]}/{item["ipmask6"]}' 1304 | ) 1305 | except Exception as e: 1306 | logger.error(e) 1307 | journal_entries.append( 1308 | JournalEntry( 1309 | assigned_object=device, 1310 | created_by=user, 1311 | kind=JournalEntryKindChoices.KIND_WARNING, 1312 | comments=f'IPv6 address {item["ipaddress6"]}/{item["ipmask6"]} was not created due to the error {e}' 1313 | ), 1314 | ) 1315 | else: 1316 | if (not ip_created6): 1317 | logger.info( 1318 | f'IP address {ip_address6.address} already exists.') 1319 | else: 1320 | logger.warning( 1321 | f'Excluded an IP address {item["ipaddress6"]} due to the rules.') 1322 | # journal_entries.append( 1323 | # JournalEntry( 1324 | # assigned_object=device, 1325 | # created_by=user, 1326 | # kind=JournalEntryKindChoices.KIND_WARNING, 1327 | # comments=f'Excluded an IP address {item["ipaddress6"]} due to the rules.' 1328 | # ), 1329 | # ) 1330 | elif ('ipaddress6' in item): 1331 | if (not item['ipaddress6'] in excluded_ip_addresses6 and item['ipaddress6'].startswith(included_ip_networks6_tuple)): 1332 | try: 1333 | ip_address6, ip_created6 = IPAddress.objects.get_or_create( 1334 | address=f'{item["ipaddress6"]}' 1335 | ) 1336 | except Exception as e: 1337 | logger.error(e) 1338 | journal_entries.append( 1339 | JournalEntry( 1340 | assigned_object=device, 1341 | created_by=user, 1342 | kind=JournalEntryKindChoices.KIND_WARNING, 1343 | comments=f'IPv6 address {item["ipaddress6"]} was not created due to the error {e}' 1344 | ), 1345 | ) 1346 | else: 1347 | if (not ip_created6): 1348 | logger.info( 1349 | f'IP address {ip_address6.address} already exists.') 1350 | else: 1351 | logger.warning( 1352 | f'Excluded an IP address {item["ipaddress6"]} due to the rules.') 1353 | # journal_entries.append( 1354 | # JournalEntry( 1355 | # assigned_object=device, 1356 | # created_by=user, 1357 | # kind=JournalEntryKindChoices.KIND_WARNING, 1358 | # comments=f'Excluded an IP address {item["ipaddress6"]} due to the rules.' 1359 | # ), 1360 | # ) 1361 | 1362 | logger.info(f'Updating/creating interface {item["name"]}. Received data: {item}') 1363 | try: 1364 | interface, interface_created = device.interfaces.get_or_create( 1365 | name=item['name'][:64]) 1366 | except Exception as e: 1367 | logger.error(e) 1368 | journal_entries.append( 1369 | JournalEntry( 1370 | assigned_object=device, 1371 | created_by=user, 1372 | kind=JournalEntryKindChoices.KIND_WARNING, 1373 | comments=f'Interface {item["name"]} was not created due to the error {e}' 1374 | ), 1375 | ) 1376 | continue 1377 | 1378 | mac_address = value_or_none(item,'mac_address') 1379 | 1380 | if (not interface_created): 1381 | if (interface.mac_address != mac_address): 1382 | logger.warning( 1383 | f'Got iface:Set a new MAC address {mac_address} to the existing interface {interface.name}') 1384 | interface.snapshot() 1385 | interface.mac_address = mac_address 1386 | interface.save() 1387 | log = interface.to_objectchange( 1388 | action_update) 1389 | log.user = user 1390 | log.request_id = _uuid 1391 | log.save() 1392 | journal_entries.append( 1393 | JournalEntry( 1394 | assigned_object=device, 1395 | created_by=user, 1396 | kind=JournalEntryKindChoices.KIND_WARNING, 1397 | comments=f'Set a new MAC address {mac_address} to the existing interface {interface.name}' 1398 | ), 1399 | ) 1400 | interface.snapshot() 1401 | if (ip_address is not None): 1402 | if (ip_created): 1403 | logger.info( 1404 | f'Got iface:Adding a new IPv4 address {ip_address.address} to the existing interface {interface.name}') 1405 | else: 1406 | logger.info( 1407 | f'Got iface:Updating the already existing IPv4 address {ip_address.address} for the existing interface {interface.name}') 1408 | ip_address.snapshot() 1409 | 1410 | if (not ip_address in interface.ip_addresses.all()): 1411 | try: 1412 | interface.ip_addresses.add( 1413 | ip_address) 1414 | except Exception as e: 1415 | logger.error(e) 1416 | journal_entries.append( 1417 | JournalEntry( 1418 | assigned_object=device, 1419 | created_by=user, 1420 | kind=JournalEntryKindChoices.KIND_WARNING, 1421 | comments=f'IPv4 address {ip_address.address} was not added to the interface {interface.name} due to the error {e}' 1422 | ), 1423 | ) 1424 | else: 1425 | interface_updated = True 1426 | 1427 | if (interface_updated): 1428 | interface.save() 1429 | ip_address.save() 1430 | log = interface.to_objectchange( 1431 | action_update) 1432 | log.user = user 1433 | log.request_id = _uuid 1434 | log.save() 1435 | if (ip_created): 1436 | log = ip_address.to_objectchange( 1437 | action_create) 1438 | log.user = user 1439 | log.request_id = _uuid 1440 | log.save() 1441 | journal_entries.append( 1442 | JournalEntry( 1443 | assigned_object=device, 1444 | created_by=user, 1445 | kind=JournalEntryKindChoices.KIND_INFO, 1446 | comments=f'Added a new IPv4 address {ip_address.address} to the existing interface {interface.name}' 1447 | ), 1448 | ) 1449 | journal_entries.append( 1450 | JournalEntry( 1451 | assigned_object=ip_address, 1452 | created_by=user, 1453 | kind=JournalEntryKindChoices.KIND_INFO, 1454 | comments=f'Added a new IPv4 address {ip_address.address} to the existing interface {interface.name}' 1455 | ), 1456 | ) 1457 | else: 1458 | log = ip_address.to_objectchange( 1459 | action_update) 1460 | log.user = user 1461 | log.request_id = _uuid 1462 | log.save() 1463 | journal_entries.append( 1464 | JournalEntry( 1465 | assigned_object=device, 1466 | created_by=user, 1467 | kind=JournalEntryKindChoices.KIND_WARNING, 1468 | comments=f'Updated the already existing IPv4 address {ip_address.address} for the existing interface {interface.name}' 1469 | ), 1470 | ) 1471 | journal_entries.append( 1472 | JournalEntry( 1473 | assigned_object=ip_address, 1474 | created_by=user, 1475 | kind=JournalEntryKindChoices.KIND_WARNING, 1476 | comments=f'Updated the already existing IPv4 address {ip_address.address} to the existing interface {interface.name}' 1477 | ), 1478 | ) 1479 | # Clear the interface_updated boolean at the end to allow next block 1480 | # FIXME: May be we should use separate boolean for IPv4/IPv6? 1481 | interface_updated = False 1482 | 1483 | if (ip_address6 is not None): 1484 | if (ip_created6): 1485 | logger.info( 1486 | f'Got iface:Adding a new IPv6 address {ip_address6.address} to the existing interface {interface.name}') 1487 | else: 1488 | logger.info( 1489 | f'Got iface:Updating the already existing IPv6 address {ip_address6.address} for the existing interface {interface.name}') 1490 | ip_address6.snapshot() 1491 | 1492 | if (not ip_address6 in interface.ip_addresses.all()): 1493 | try: 1494 | interface.ip_addresses.add( 1495 | ip_address6) 1496 | except Exception as e: 1497 | logger.error(e) 1498 | journal_entries.append( 1499 | JournalEntry( 1500 | assigned_object=device, 1501 | created_by=user, 1502 | kind=JournalEntryKindChoices.KIND_WARNING, 1503 | comments=f'IPv6 address {ip_address6.address} was not added to the interface {interface.name} due to the error {e}' 1504 | ), 1505 | ) 1506 | else: 1507 | interface_updated = True 1508 | 1509 | if (interface_updated): 1510 | interface.save() 1511 | ip_address6.save() 1512 | log = interface.to_objectchange( 1513 | action_update) 1514 | log.user = user 1515 | log.request_id = _uuid 1516 | log.save() 1517 | if (ip_created6): 1518 | log = ip_address6.to_objectchange( 1519 | action_create) 1520 | log.user = user 1521 | log.request_id = _uuid 1522 | log.save() 1523 | journal_entries.append( 1524 | JournalEntry( 1525 | assigned_object=device, 1526 | created_by=user, 1527 | kind=JournalEntryKindChoices.KIND_INFO, 1528 | comments=f'Added a new IPv6 address {ip_address6.address} to the existing interface {interface.name}' 1529 | ), 1530 | ) 1531 | journal_entries.append( 1532 | JournalEntry( 1533 | assigned_object=ip_address6, 1534 | created_by=user, 1535 | kind=JournalEntryKindChoices.KIND_INFO, 1536 | comments=f'Added a new IPv6 address {ip_address6.address} to the existing interface {interface.name}' 1537 | ), 1538 | ) 1539 | else: 1540 | log = ip_address6.to_objectchange( 1541 | action_update) 1542 | log.user = user 1543 | log.request_id = _uuid 1544 | log.save() 1545 | journal_entries.append( 1546 | JournalEntry( 1547 | assigned_object=device, 1548 | created_by=user, 1549 | kind=JournalEntryKindChoices.KIND_WARNING, 1550 | comments=f'Updated the already existing IPv6 address {ip_address6.address} for the existing interface {interface.name}' 1551 | ), 1552 | ) 1553 | journal_entries.append( 1554 | JournalEntry( 1555 | assigned_object=ip_address6, 1556 | created_by=user, 1557 | kind=JournalEntryKindChoices.KIND_WARNING, 1558 | comments=f'Updated the already existing IPv6 address {ip_address6.address} for the existing interface {interface.name}' 1559 | ), 1560 | ) 1561 | 1562 | else: 1563 | # Interface has been just created 1564 | logger.info(f'Created new interface {item["name"]}') 1565 | mac_address = value_or_none(item,'mac_address') 1566 | interface.mac_address = mac_address 1567 | interface.save() 1568 | 1569 | if (ip_address is not None): 1570 | if (ip_created): 1571 | logger.info( 1572 | f'Adding a new IPv4 address {ip_address.address} to a new interface {interface.name}') 1573 | else: 1574 | logger.warning( 1575 | f'Moving the already existing IPv4 address {ip_address.address} to a new interface {interface.name}') 1576 | ip_address.snapshot() 1577 | try: 1578 | interface.ip_addresses.add(ip_address) 1579 | except Exception as e: 1580 | logger.error(e) 1581 | journal_entries.append( 1582 | JournalEntry( 1583 | assigned_object=device, 1584 | created_by=user, 1585 | kind=JournalEntryKindChoices.KIND_WARNING, 1586 | comments=f'IPv4 address {ip_address.address} was not added to the interface {interface.name} due to the error {e}' 1587 | ), 1588 | ) 1589 | else: 1590 | # Uncondidionally report, because the IP addresss was just added to the interface 1591 | interface.save() 1592 | ip_address.save() 1593 | log = interface.to_objectchange( 1594 | action_create) 1595 | log.user = user 1596 | log.request_id = _uuid 1597 | log.save() 1598 | if (ip_created): 1599 | log = ip_address.to_objectchange( 1600 | action_create) 1601 | log.user = user 1602 | log.request_id = _uuid 1603 | log.save() 1604 | journal_entries.append( 1605 | JournalEntry( 1606 | assigned_object=device, 1607 | created_by=user, 1608 | kind=JournalEntryKindChoices.KIND_INFO, 1609 | comments=f'Added a new IPv4 address {ip_address.address} to a new interface {interface.name}' 1610 | ), 1611 | ) 1612 | 1613 | journal_entries.append( 1614 | JournalEntry( 1615 | assigned_object=ip_address, 1616 | created_by=user, 1617 | kind=JournalEntryKindChoices.KIND_INFO, 1618 | comments=f'Added a new IPv4 address {ip_address.address} to a new interface {interface.name}' 1619 | ), 1620 | ) 1621 | else: 1622 | log = ip_address.to_objectchange( 1623 | action_update) 1624 | log.user = user 1625 | log.request_id = _uuid 1626 | log.save() 1627 | journal_entries.append( 1628 | JournalEntry( 1629 | assigned_object=device, 1630 | created_by=user, 1631 | kind=JournalEntryKindChoices.KIND_WARNING, 1632 | comments=f'Moved the already existing IPv4 address {ip_address.address} to a new interface {interface.name}' 1633 | ), 1634 | ) 1635 | 1636 | journal_entries.append( 1637 | JournalEntry( 1638 | assigned_object=ip_address, 1639 | created_by=user, 1640 | kind=JournalEntryKindChoices.KIND_WARNING, 1641 | comments=f'Moved the already existing IPv4 address {ip_address.address} to a new interface {interface.name}' 1642 | ), 1643 | ) 1644 | 1645 | if (ip_address6 is not None): 1646 | if (ip_created6): 1647 | logger.info( 1648 | f'Adding a new IPv6 address {ip_address6.address} to a new interface {interface.name}') 1649 | else: 1650 | logger.warning( 1651 | f'Moving the already existing IPv6 address {ip_address6.address} to a new interface {interface.name}') 1652 | ip_address6.snapshot() 1653 | try: 1654 | interface.ip_addresses.add(ip_address6) 1655 | except Exception as e: 1656 | logger.error(e) 1657 | journal_entries.append( 1658 | JournalEntry( 1659 | assigned_object=device, 1660 | created_by=user, 1661 | kind=JournalEntryKindChoices.KIND_WARNING, 1662 | comments=f'IPv6 address {ip_address6.address} was not added to the interface {interface.name} due to the error {e}' 1663 | ), 1664 | ) 1665 | else: 1666 | # Uncondidionally report, because the IP addresss was just added to the interface 1667 | interface.save() 1668 | ip_address6.save() 1669 | log = interface.to_objectchange( 1670 | action_create) 1671 | log.user = user 1672 | log.request_id = _uuid 1673 | log.save() 1674 | if (ip_created6): 1675 | log = ip_address6.to_objectchange( 1676 | action_create) 1677 | log.user = user 1678 | log.request_id = _uuid 1679 | log.save() 1680 | journal_entries.append( 1681 | JournalEntry( 1682 | assigned_object=device, 1683 | created_by=user, 1684 | kind=JournalEntryKindChoices.KIND_INFO, 1685 | comments=f'Added a new IPv6 address {ip_address6.address} to a new interface {interface.name}' 1686 | ), 1687 | ) 1688 | journal_entries.append( 1689 | JournalEntry( 1690 | assigned_object=ip_address6, 1691 | created_by=user, 1692 | kind=JournalEntryKindChoices.KIND_INFO, 1693 | comments=f'Added a new IPv6 address {ip_address6.address} to a new interface {interface.name}' 1694 | ), 1695 | ) 1696 | else: 1697 | log = ip_address6.to_objectchange( 1698 | action_update) 1699 | log.user = user 1700 | log.request_id = _uuid 1701 | log.save() 1702 | journal_entries.append( 1703 | JournalEntry( 1704 | assigned_object=device, 1705 | created_by=user, 1706 | kind=JournalEntryKindChoices.KIND_WARNING, 1707 | comments=f'Moved the already existing IPv6 address {ip_address6.address} to a new interface {interface.name}' 1708 | ), 1709 | ) 1710 | journal_entries.append( 1711 | JournalEntry( 1712 | assigned_object=ip_address6, 1713 | created_by=user, 1714 | kind=JournalEntryKindChoices.KIND_WARNING, 1715 | comments=f'Moved the already existing IPv6 address {ip_address6.address} to a new interface {interface.name}' 1716 | ), 1717 | ) 1718 | 1719 | else: 1720 | for item in value: 1721 | tag = None 1722 | tag_created = None 1723 | item_updated = False 1724 | # FIXME: We do list() to avoid RuntimeError: dictionary changed size during iteration (see del() below) 1725 | for k, v in list(item.items()): 1726 | if k == "manufacturer": 1727 | try: 1728 | item[k] = Manufacturer.objects.get_or_create( 1729 | slug=slugify(item[k], only_ascii=True), 1730 | defaults={"name": item[k], "slug": slugify(item[k], only_ascii=True)} 1731 | )[0] 1732 | except Exception as e: 1733 | logger.error(e) 1734 | continue 1735 | elif k == "tag": 1736 | try: 1737 | tag, tag_created = Tag.objects.get_or_create( 1738 | name=item[k]['name'], 1739 | slug=item[k]['slug'], 1740 | ) 1741 | except Exception as e: 1742 | logger.error(e) 1743 | continue 1744 | else: 1745 | # Delete our special key, InventoryItem does not have this key in its model 1746 | # FIXME: Should we delete this? 1747 | del item[k] 1748 | elif k == "name": 1749 | # Shorten string 1750 | item[k] = item[k][:64] 1751 | elif (k == "serial" or k == "part_id" or k == "asset_tag"): 1752 | # Shorten string 1753 | item[k] = item[k][:50] 1754 | elif k == "description": 1755 | # Shorten string 1756 | item[k] = item[k][:200] 1757 | 1758 | item['discovered'] = True 1759 | logger.info( 1760 | f'Updating item {item} for the device {device.name}') 1761 | 1762 | # Continue the loop if we couldn't add an item. Report an error and add Journal entry 1763 | try: 1764 | inventory_item, item_created = InventoryItem.objects.get_or_create( 1765 | device=device, asset_tag=item['asset_tag'], defaults={**item}) 1766 | except Exception as e: 1767 | logger.error(e) 1768 | journal_entries.append( 1769 | JournalEntry( 1770 | assigned_object=device, 1771 | created_by=user, 1772 | kind=JournalEntryKindChoices.KIND_WARNING, 1773 | comments=f'Item {item} was not updated for the device {device.name} due to the error {e}' 1774 | ), 1775 | ) 1776 | continue 1777 | 1778 | if (not item_created): 1779 | logger.info( 1780 | f'Updating item with the asset_tag {inventory_item.asset_tag} for the device {device.name}') 1781 | inventory_item.snapshot() 1782 | # FIXME: Will it work with tags if they differ or if there are more than one? 1783 | # FIXME: Actually, we do not check for tags here, the 'tag' key has been deleted already! See below the tags.add() 1784 | for k, v in item.items(): 1785 | if (getattr(inventory_item, k) != item[k]): 1786 | setattr(inventory_item, k, v) 1787 | item_updated = True 1788 | # Add tag. Do not skip if we could not add the tag, but report the error and add a Journal entry. 1789 | if (tag is not None): 1790 | if (not tag in inventory_item.tags.all()): 1791 | try: 1792 | # FIXME: What if this tag already exists? Will it be duplicated? 1793 | inventory_item.tags.add(tag) 1794 | except Exception as e: 1795 | logger.error(e) 1796 | journal_entries.append( 1797 | JournalEntry( 1798 | assigned_object=device, 1799 | created_by=user, 1800 | kind=JournalEntryKindChoices.KIND_WARNING, 1801 | comments=f'Tag {tag} was not updated for the inventory item {inventory_item.name} due to the error {e}' 1802 | ), 1803 | ) 1804 | else: 1805 | item_updated = True 1806 | 1807 | if (item_updated): 1808 | inventory_item.save() 1809 | log = inventory_item.to_objectchange( 1810 | action_update) 1811 | log.user = user 1812 | log.request_id = _uuid 1813 | log.save() 1814 | journal_entries.append( 1815 | JournalEntry( 1816 | assigned_object=device, 1817 | created_by=user, 1818 | kind=JournalEntryKindChoices.KIND_INFO, 1819 | comments=f'Updated item with the asset_tag {inventory_item.asset_tag} for the device {device.name}' 1820 | ), 1821 | ) 1822 | 1823 | journal_entries.append( 1824 | JournalEntry( 1825 | assigned_object=inventory_item, 1826 | created_by=user, 1827 | kind=JournalEntryKindChoices.KIND_INFO, 1828 | comments=f'Updated item with the asset_tag {inventory_item.asset_tag} for the device {device.name}' 1829 | ), 1830 | ) 1831 | else: 1832 | logger.info( 1833 | f'Creating new item with the asset_tag {inventory_item.asset_tag} for the device {device.name}') 1834 | # Add tag. Do not skip if we could not add the tag, but report the error and add a Journal entry. 1835 | if (tag is not None): 1836 | try: 1837 | inventory_item.tags.add(tag) 1838 | except Exception as e: 1839 | logger.error(e) 1840 | journal_entries.append( 1841 | JournalEntry( 1842 | assigned_object=device, 1843 | created_by=user, 1844 | kind=JournalEntryKindChoices.KIND_WARNING, 1845 | comments=f'Tag {tag} was not updated for the inventory item {inventory_item.name} due to the error {e}' 1846 | ), 1847 | ) 1848 | 1849 | # Report about just created inventory item 1850 | inventory_item.save() 1851 | log = inventory_item.to_objectchange( 1852 | action_create) 1853 | log.user = user 1854 | log.request_id = _uuid 1855 | log.save() 1856 | journal_entries.append( 1857 | JournalEntry( 1858 | assigned_object=device, 1859 | created_by=user, 1860 | kind=JournalEntryKindChoices.KIND_INFO, 1861 | comments=f'Added a new inventory item {inventory_item.name} to the device {device.name}' 1862 | ), 1863 | ) 1864 | journal_entries.append( 1865 | JournalEntry( 1866 | assigned_object=inventory_item, 1867 | created_by=user, 1868 | kind=JournalEntryKindChoices.KIND_INFO, 1869 | comments=f'Added a new inventory item {inventory_item.name} to the device {device.name}' 1870 | ), 1871 | ) 1872 | 1873 | # Post journal entries in a bulk 1874 | JournalEntry.objects.bulk_create(journal_entries) 1875 | 1876 | else: 1877 | # TODO: We must report such devices via email 1878 | logger.error( 1879 | f'Device {device_dict} does not contain a serial. Skipping.') 1880 | 1881 | 1882 | def soup_to_dict(soup): 1883 | config = PLUGIN_SETTINGS 1884 | device = {} 1885 | items = {} 1886 | for k, v in config.items(): 1887 | if v: 1888 | value_type, content = v.split(':', 1) 1889 | if value_type == "xml": 1890 | path, tag = content.rsplit('.', 1) 1891 | device[k] = xmlpath_or_unknown(soup, path, tag) 1892 | elif value_type == "object": 1893 | obj_type, value = content.split(':', 1) 1894 | if value.isdigit(): 1895 | device[k] = eval( 1896 | obj_type + ".objects.filter(id=" + value + ")[0]") 1897 | else: 1898 | device[k] = eval(obj_type + ".objects.get_or_create(name='" + 1899 | value + "', slug=slugify('" + value + "', only_ascii=True))")[0] 1900 | elif value_type == "lazy": 1901 | # Leave lazy for the second loop 1902 | # FIXME: Too simple, we should support nested variables. 1903 | # FIXME: Requires two loops, not optimal 1904 | device[k] = v 1905 | else: 1906 | device[k] = v 1907 | 1908 | # Second loop for "lazy:" variables and serial->upper() 1909 | # FIXME: Not optimal and dirty 1910 | for k, v in device.items(): 1911 | # FIXME: Dirty upcase for device serial. We can't do other way right now. 1912 | if (k == 'serial' and v and isinstance(v, str)): 1913 | device[k] = v.upper() 1914 | if (v and isinstance(v, str) and ':' in v): 1915 | value_type, content = v.split(':', 1) 1916 | if value_type == "lazy": 1917 | # FIXME: Not all fields must be <= 50 length 1918 | try: 1919 | device[k] = eval(content) 1920 | except Exception as e: 1921 | logger.error(e) 1922 | continue 1923 | 1924 | for k, v in inventory_settings.items(): 1925 | items[k] = [] 1926 | for xml in soup.find_all(k): 1927 | item = {} 1928 | for k1, v1 in v.items(): 1929 | if (v1): 1930 | if not (isinstance(v1, str) and v1.startswith('lazy:')): 1931 | # print(f"First loop parse key is: {k1}, value is {v1}") 1932 | try: 1933 | item[k1] = eval(v1) 1934 | except Exception as e: 1935 | item[k1] = "ERROR" 1936 | logger.error(e) 1937 | continue 1938 | else: 1939 | item[k1] = v1 1940 | # Second loop for "lazy:" variables and deleting None vars, iterate over already parsed "item" items 1941 | # FIXME: Not optimal and dirty 1942 | for k1, v1 in list(item.items()): 1943 | if (v1): 1944 | if (isinstance(v1, str) and v1.startswith('lazy:')): 1945 | # print(f"Second loop parse key is: {k1}, value is {v1}") 1946 | value_type, content = v1.split(':', 1) 1947 | item[k1] = eval(content) 1948 | elif (v1 is None): 1949 | # print(f"Deleting key {k1} because its value is None") 1950 | del item[k1] 1951 | 1952 | items[k].append(item) 1953 | return device, items 1954 | -------------------------------------------------------------------------------- /netbox_fusioninventory_plugin/views.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponse 2 | from django.shortcuts import get_object_or_404, render 3 | from django.views.generic import View 4 | from django.views.decorators.csrf import csrf_exempt 5 | from django.contrib.auth.models import User 6 | from . import utils 7 | 8 | import zlib 9 | import bs4 10 | 11 | class PostXMLView(View): 12 | 13 | def post(self, request): 14 | request.user = User.objects.get_or_create(username="FusionInventory")[0] 15 | decompressed = zlib.decompress(request.body) 16 | if len(decompressed) > 200: 17 | xml_soup = bs4.BeautifulSoup(decompressed,features = "lxml") 18 | parsed_device,items = utils.soup_to_dict(xml_soup) 19 | utils.created_or_update_device(parsed_device,items) 20 | return HttpResponse() 21 | else: 22 | return HttpResponse('\n\n \nSEND\n 1\n\n') 23 | 24 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | with open("README.md", "r", encoding="utf-8") as fh: 4 | long_description = fh.read() 5 | 6 | setup( 7 | name='netbox_fusioninventory_plugin', 8 | version='0.6', 9 | description='A Plugin for import devices from fusion inventory agent', 10 | long_description=long_description, 11 | long_description_content_type="text/markdown", 12 | url='https://gitlab.com/Milka64/netbox-fusioninventory-plugin', 13 | author='Michael Ricart', 14 | license='BSD License', 15 | install_requires=[ 16 | 'beautifulsoup4', 17 | 'lxml', 18 | 'unicode-slugify', 19 | ], 20 | packages=find_packages(), 21 | include_package_data=True, 22 | ) 23 | 24 | --------------------------------------------------------------------------------