From 29d2bff805a77faaaae149be3d732ee973f1cc23 Mon Sep 17 00:00:00 2001 From: Solvik Blum Date: Fri, 22 May 2020 13:23:28 +0200 Subject: [PATCH 001/148] fix flake8 errors --- netbox_agent/inventory.py | 2 +- netbox_agent/lshw.py | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/netbox_agent/inventory.py b/netbox_agent/inventory.py index 8321bba..d571e6a 100644 --- a/netbox_agent/inventory.py +++ b/netbox_agent/inventory.py @@ -139,7 +139,7 @@ class Inventory(): for nb_motherboard in nb_motherboards: if nb_motherboard.serial not in [x['serial'] for x in motherboards]: - logging.info('Deleting unknown motherboard {vendor} {motherboard}/{serial}'.format( + logging.info('Deleting unknown motherboard {motherboard}/{serial}'.format( motherboard=self.lshw.motherboard, serial=nb_motherboard.serial, )) diff --git a/netbox_agent/lshw.py b/netbox_agent/lshw.py index 3387c41..6e11fb7 100644 --- a/netbox_agent/lshw.py +++ b/netbox_agent/lshw.py @@ -86,8 +86,10 @@ class LSHW(): elif "nvme" in obj["configuration"]["driver"]: nvme = json.loads( - subprocess.check_output(["nvme", '-list', '-o', 'json'], - encoding='utf8')) # noqa: E128 + subprocess.check_output( + ["nvme", '-list', '-o', 'json'], + encoding='utf8') + ) d = {} d["vendor"] = obj["vendor"] From 28fc87c9e206700130674977c0410b2eb007a672 Mon Sep 17 00:00:00 2001 From: Solvik Date: Sat, 23 May 2020 13:23:51 +0200 Subject: [PATCH 002/148] handle netbox 2.8 deprecation of _choices method (#127) --- netbox_agent/network.py | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/netbox_agent/network.py b/netbox_agent/network.py index 4005f98..c635236 100644 --- a/netbox_agent/network.py +++ b/netbox_agent/network.py @@ -21,20 +21,20 @@ class Network(object): self.nics = self.scan() self.ipmi = None self.dcim_choices = {} - dcim_c = nb.dcim.choices() - - for choice in dcim_c: - self.dcim_choices[choice] = {} - for c in dcim_c[choice]: - self.dcim_choices[choice][c['label']] = c['value'] + dcim_c = nb.dcim.interfaces.choices() + for _choice_type in dcim_c: + key = 'interface:{}'.format(_choice_type) + self.dcim_choices[key] = {} + for choice in dcim_c[_choice_type]: + self.dcim_choices[key][choice['display_name']] = choice['value'] self.ipam_choices = {} - ipam_c = nb.ipam.choices() - - for choice in ipam_c: - self.ipam_choices[choice] = {} - for c in ipam_c[choice]: - self.ipam_choices[choice][c['label']] = c['value'] + ipam_c = nb.ipam.ip_addresses.choices() + for _choice_type in ipam_c: + key = 'ip-address:{}'.format(_choice_type) + self.ipam_choices[key] = {} + for choice in ipam_c[_choice_type]: + self.ipam_choices[key][choice['display_name']] = choice['value'] def get_network_type(): return NotImplementedError @@ -561,12 +561,12 @@ class VirtualNetwork(Network): self.custom_arg = {'virtual_machine': self.device.id} self.custom_arg_id = {'virtual_machine_id': self.device.id} - dcim_c = nb.virtualization.choices() - - for choice in dcim_c: - self.dcim_choices[choice] = {} - for c in dcim_c[choice]: - self.dcim_choices[choice][c['label']] = c['value'] + dcim_c = nb.virtualization.interfaces.choices() + for _choice_type in dcim_c: + key = 'interface:{}'.format(_choice_type) + self.dcim_choices[key] = {} + for choice in dcim_c[_choice_type]: + self.dcim_choices[key][choice['display_name']] = choice['value'] def get_network_type(self): return 'virtual' From 9dedbea47a1f88ca53498f3bc78f677ce501020a Mon Sep 17 00:00:00 2001 From: Cyrinux Date: Mon, 8 Jun 2020 14:16:50 +0200 Subject: [PATCH 003/148] Some fixes that prevent crash on register and update (#128) --- netbox_agent/network.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/netbox_agent/network.py b/netbox_agent/network.py index c635236..8659ee2 100644 --- a/netbox_agent/network.py +++ b/netbox_agent/network.py @@ -438,8 +438,8 @@ class ServerNetwork(Network): self.server = server self.device = self.server.get_netbox_server() self.nb_net = nb.dcim - self.custom_arg = {'device': self.device.id} - self.custom_arg_id = {'device_id': self.device.id} + self.custom_arg = {'device': getattr(self.device, "id", None)} + self.custom_arg_id = {'device_id': getattr(self.device, "id", None)} def get_network_type(self): return 'server' @@ -558,8 +558,8 @@ class VirtualNetwork(Network): self.server = server self.device = self.server.get_netbox_vm() self.nb_net = nb.virtualization - self.custom_arg = {'virtual_machine': self.device.id} - self.custom_arg_id = {'virtual_machine_id': self.device.id} + self.custom_arg = {'virtual_machine': getattr(self.device, "id", None)} + self.custom_arg_id = {'virtual_machine_id': getattr(self.device, "id", None)} dcim_c = nb.virtualization.interfaces.choices() for _choice_type in dcim_c: From 41f0b04d007188539d581bb98db9a9c0c8c32851 Mon Sep 17 00:00:00 2001 From: Cyrinux Date: Tue, 9 Jun 2020 09:05:35 +0200 Subject: [PATCH 004/148] Add missing N/A fallback on DIMM memory (#129) --- netbox_agent/lshw.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox_agent/lshw.py b/netbox_agent/lshw.py index 6e11fb7..c7841a1 100644 --- a/netbox_agent/lshw.py +++ b/netbox_agent/lshw.py @@ -129,7 +129,7 @@ class LSHW(): d["id"] = dimm.get("id") d["serial"] = dimm.get("serial", 'N/A') d["vendor"] = dimm.get("vendor", 'N/A') - d["product"] = dimm.get("product") + d["product"] = dimm.get("product", 'N/A') d["size"] = dimm.get("size", 0) / 2 ** 20 / 1024 self.memories.append(d) From 683e6cacb188f901ef1aa06e840457a31d802cbf Mon Sep 17 00:00:00 2001 From: Cyrinux Date: Wed, 10 Jun 2020 15:24:43 +0200 Subject: [PATCH 005/148] Fix HP raid controller parsing (#131) * Some messages about the cache for example, with indentation level 0 break the parsing. I ignore line indentation if indentation level and line dont match REGEXP_CONTROLLER_HP. ```bash DC1|server-01:~# hpacucli ctrl all show detail Smart Array P244br in Slot 0 (Embedded) A cache backup failure has occurred. Please execute the "reenablecache" command to enable the cache. Your controller may require a reboot after the operation to complete the cache recovery process. Bus Interface: PCI Slot: 0 Serial Number: PDZVU0WLM241FP Cache Serial Number: PDZVU0WLM241FP RAID 6 (ADG) Status: Enabled Controller Status: OK Hardware Revision: B Firmware Version: 7.00-0 ... ``` * Remove whitespace --- netbox_agent/raid/hp.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/netbox_agent/raid/hp.py b/netbox_agent/raid/hp.py index 40a9a27..144e73b 100644 --- a/netbox_agent/raid/hp.py +++ b/netbox_agent/raid/hp.py @@ -54,6 +54,13 @@ def _get_dict(lines, start_index, indentation): continue current_line_indentation = _get_indentation(current_line) + # This check ignore some useless information that make + # crash the parsing + product_name = REGEXP_CONTROLLER_HP.search(current_line) + if current_line_indentation == 0 and not product_name: + i = i + 1 + continue + if current_line_indentation == indentation: current_item = current_line.lstrip(' ') From 754a284fd1fefb08fd0e292892f4e8af68cf6469 Mon Sep 17 00:00:00 2001 From: Cyrinux Date: Sun, 14 Jun 2020 18:18:13 +0200 Subject: [PATCH 006/148] Add openvpn TUN and TAP interfaces support, change type to Virtual and remove mac address (#132) * Add Tun and Tap support, change type to Virtual and remove mac address * Simplify * Resimplify --- netbox_agent/network.py | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/netbox_agent/network.py b/netbox_agent/network.py index 8659ee2..cb163a3 100644 --- a/netbox_agent/network.py +++ b/netbox_agent/network.py @@ -90,6 +90,12 @@ class Network(object): bonding_slaves = open( '/sys/class/net/{}/bonding/slaves'.format(interface) ).read().split() + + # Tun and TAP support + virtual = os.path.isfile( + '/sys/class/net/{}/tun_flags'.format(interface) + ) + nic = { 'name': interface, 'mac': mac if mac != '00:00:00:00:00:00' else None, @@ -100,6 +106,7 @@ class Network(object): ) for x in ip_addr ] if ip_addr else None, # FIXME: handle IPv6 addresses 'ethtool': Ethtool(interface).parse(), + 'virtual': virtual, 'vlan': vlan, 'bonding': bonding, 'bonding_slaves': bonding_slaves, @@ -159,6 +166,10 @@ class Network(object): if nic.get('bonding'): return self.dcim_choices['interface:type']['Link Aggregation Group (LAG)'] + + if nic.get('virtual'): + return self.dcim_choices['interface:type']['Virtual'] + if nic.get('ethtool') is None: return self.dcim_choices['interface:type']['Other'] @@ -237,13 +248,18 @@ class Network(object): name=nic['name'], mac=nic['mac'], device=self.device.name)) nb_vlan = None - interface = self.nb_net.interfaces.create( - name=nic['name'], - mac_address=nic['mac'], - type=type, - mgmt_only=mgmt, + + params = { + 'name': nic['name'], + 'type': type, + 'mgmt_only': mgmt, **self.custom_arg, - ) + } + + if not nic.get('virtual', False): + params['mac_address'] = nic['mac'] + + interface = self.nb_net.interfaces.create(**params) if nic['vlan']: nb_vlan = self.get_or_create_vlan(nic['vlan']) From b94f2461966195f9036a44cd1ea4b4126e24a0cc Mon Sep 17 00:00:00 2001 From: Solvik Blum Date: Wed, 20 May 2020 16:12:47 +0200 Subject: [PATCH 007/148] some power fixes --- netbox_agent/power.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/netbox_agent/power.py b/netbox_agent/power.py index 8cd4eab..f825f5c 100644 --- a/netbox_agent/power.py +++ b/netbox_agent/power.py @@ -29,8 +29,15 @@ class PowerSupply(): psu.get('Manufacturer', 'No Manufacturer').strip(), psu.get('Name', 'No name').strip(), ) + + sn = psu.get('Serial Number', '').strip() + # Let's assume that if no serial and no power reported we skip it + if sn == '' and max_power == None: + continue + if sn == '': + sn = 'N/A' power_supply.append({ - 'name': psu.get('Serial Number', 'No S/N').strip(), + 'name': sn, 'description': desc, 'allocated_draw': None, 'maximum_draw': max_power, From d227f3596484ecaca61b411e41247343ae8b3a8a Mon Sep 17 00:00:00 2001 From: Solvik Blum Date: Sun, 14 Jun 2020 18:46:27 +0200 Subject: [PATCH 008/148] add long_description_content_type to setup.py to make twine works --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 8e63468..82e4edd 100644 --- a/setup.py +++ b/setup.py @@ -5,6 +5,7 @@ setup( version='0.6.0', description='NetBox agent for server', long_description=open('README.md', encoding="utf-8").read(), + long_description_content_type='text/markdown', url='https://github.com/solvik/netbox_agent', author='Solvik Blum', author_email='solvik@solvik.fr', From feec16c4fe659bf16a637ee7babe455c412c194c Mon Sep 17 00:00:00 2001 From: Solvik Date: Mon, 15 Jun 2020 15:46:09 +0200 Subject: [PATCH 009/148] fix latest flake8 error (#135) --- netbox_agent/power.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox_agent/power.py b/netbox_agent/power.py index f825f5c..9e763a8 100644 --- a/netbox_agent/power.py +++ b/netbox_agent/power.py @@ -32,7 +32,7 @@ class PowerSupply(): sn = psu.get('Serial Number', '').strip() # Let's assume that if no serial and no power reported we skip it - if sn == '' and max_power == None: + if sn == '' and max_power is None: continue if sn == '': sn = 'N/A' From b331bcb93442cba89a5bbf5f71fe789f7312525a Mon Sep 17 00:00:00 2001 From: Solvik Date: Wed, 17 Jun 2020 17:54:10 +0200 Subject: [PATCH 010/148] Better usage infos (#134) --- README.md | 88 ++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 80 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index e7ccd2a..3d0bc40 100644 --- a/README.md +++ b/README.md @@ -32,32 +32,81 @@ The goal is to generate an existing infrastructure on Netbox and have the abilit - lldpd - lshw -# Known limitations +## Inventory requirement +- hpassacli +- storcli +- omreport -* The project is only compatible with Linux. -Since it uses `ethtool` and parses `/sys/` directory, it's not compatible with *BSD distributions. -* Netbox `>=2.6.0,<=2.6.2` has a caching problem ; if the cache lifetime is too high, the script can get stale data after modification. -We advise to set `CACHE_TIME` to `0`. +# Installation + +``` +# pip3 install netbox-agent +``` + +# Usage + +The agent can be run from a shell and get its configuration from either the configuration file or environment variables. + +Configuration values are overridden based on the following precedence: command line arguments (might include config file) > environment variables > default config file > defaults. + +``` +# netbox_agent -c /etc/netbox_agent.yml --register +INFO:root:Creating chassis blade (serial: QTFCQ574502EF) +INFO:root:Creating blade (serial: QTFCQ574502D2) myserver on chassis QTFCQ574502EF +INFO:root:Setting device (QTFCQ574502D2) new slot on Slot 9 (Chassis QTFCQ574502EF).. +INFO:root:Interface a8:1e:84:f2:9e:6a not found, creating.. +INFO:root:Creating NIC enp1s0f1 (a8:1e:84:f2:9e:6a) on myserver +INFO:root:Interface 02:42:7a:89:cf:a4 not found, creating.. +INFO:root:Creating NIC br-07ea1e4a2f0e (02:42:7a:89:cf:a4) on myserver +INFO:root:Create new IP 172.19.0.1/16 on br-07ea1e4a2f0e +INFO:root:Interface a8:1e:84:f2:9e:69 not found, creating.. +INFO:root:Creating NIC enp1s0f0 (a8:1e:84:f2:9e:69) on myserver +INFO:root:Create new IP 42.42.42.42/24 on enp1s0f0 +INFO:root:Create new IP fe80::aa1e:84ff:fef2:9e69/64 on enp1s0f0 +INFO:root:Interface a8:1e:84:cd:9d:d6 not found, creating.. +INFO:root:Creating NIC IPMI (a8:1e:84:cd:9d:d6) on myserver +INFO:root:Create new IP 10.191.122.10/24 on IPMI +``` + +If you need, you can update only specific informations like: +* Network +* Inventory +* Location +* PSUs + +``` +# ip a add 42.42.42.43/24 dev enp1s0f1 +# netbox_agent -c /etc/netbox_agent.yaml --update-network +INFO:root:Create new IP 42.42.42.43/24 on enp1s0f1 +# netbox_agent --update-inventory +INFO:root:Creating Disk Samsung SSD 850 S2RBNX0K101698D +``` # Configuration ``` +# Netbox configuration netbox: url: 'http://netbox.internal.company.com' token: supersecrettoken +# Network configuration network: + # Regex to ignore interfaces ignore_interfaces: "(dummy.*|docker.*)" + # Regex to ignore IP addresses ignore_ips: (127\.0\.0\..*) - # enable auto-cabling + # enable auto-cabling by parsing LLDP answers lldp: true +## Enable virtual machine support # virtual: # # not mandatory, can be guessed # enabled: True # # see https://netbox.company.com/virtualization/clusters/ # cluster_name: my_vm_cluster +# Enable datacenter location feature in Netbox datacenter_location: driver: "cmd:cat /etc/qualification | tr [a-z] [A-Z]" regex: "DATACENTER: (?P[A-Za-z0-9]+)" @@ -67,6 +116,7 @@ datacenter_location: # driver: "file:/tmp/datacenter" # regex: "(.*)" +# Enable rack location feature in Netbox rack_location: # driver: 'cmd:lldpctl' # match SysName: sw-dist-a1.dc42 @@ -75,6 +125,7 @@ rack_location: # driver: "file:/tmp/datacenter" # regex: "(.*)" +# Enable local inventory reporting inventory: true ``` @@ -161,6 +212,27 @@ Feel free to send me a dmidecode output for Supermicro's blade! * Nothing ATM, feel free to send me a dmidecode or make a PR! -# TODO +# Known limitations -- [ ] `CustomFields` support with firmware versions for Device (BIOS), RAID Cards and disks +* The project is only compatible with Linux. +Since it uses `ethtool` and parses `/sys/` directory, it's not compatible with *BSD distributions. +* Netbox `>=2.6.0,<=2.6.2` has a caching problem ; if the cache lifetime is too high, the script can get stale data after modification. +We advise to set `CACHE_TIME` to `0`. + +# Developing + +If you want to run the agent while adding features or just for debugging purposes + +``` +# git clone https://github.com/Solvik/netbox-agent.git +# cd netbox-agent +# python3 -m netbox_agent.cli --register +``` + +On a personal note, I use the docker image from [netbox-community/netbox-docker](https://github.com/netbox-community/netbox-docker) +``` +# git clone https://github.com/netbox-community/netbox-docker +# cd netbox-docker +# docker-compose pull +# docker-compose up +``` From c6a1e48956ea2ada430e7ea7a2a905bf3d22f3ab Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 17 Jun 2020 17:57:41 +0200 Subject: [PATCH 011/148] Update dependency jsonargparse to v2.31.0 (#125) Co-authored-by: Renovate Bot --- requirements.txt | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 4632ed0..c514916 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,4 @@ pynetbox==4.3.1 netaddr==0.7.19 netifaces==0.10.9 pyyaml==5.3.1 -jsonargparse==2.27.0 +jsonargparse==2.31.0 diff --git a/setup.py b/setup.py index 82e4edd..728451f 100644 --- a/setup.py +++ b/setup.py @@ -18,7 +18,7 @@ setup( 'netaddr==0.7.19', 'netifaces==0.10.9', 'pyyaml==5.3.1', - 'jsonargparse==2.27.0', + 'jsonargparse==2.31.0', ], zip_safe=False, keywords=['netbox'], From a71d992a515685d5a178d41a23f1d73fd6fd0c3e Mon Sep 17 00:00:00 2001 From: Solvik Blum Date: Wed, 17 Jun 2020 17:58:18 +0200 Subject: [PATCH 012/148] prepare for release --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 728451f..35bbc3e 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import find_packages, setup setup( name='netbox_agent', - version='0.6.0', + version='0.6.1', description='NetBox agent for server', long_description=open('README.md', encoding="utf-8").read(), long_description_content_type='text/markdown', From a896d118d75999c6a1a6c4b1dbd79149ce0a8470 Mon Sep 17 00:00:00 2001 From: Cyrinux Date: Tue, 23 Jun 2020 21:01:32 +0200 Subject: [PATCH 013/148] * Loop on nvme disk (#138) * Remove vendor and product (not present, make crash) * Use firmware version for version --- netbox_agent/lshw.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/netbox_agent/lshw.py b/netbox_agent/lshw.py index c7841a1..dadc818 100644 --- a/netbox_agent/lshw.py +++ b/netbox_agent/lshw.py @@ -91,18 +91,16 @@ class LSHW(): encoding='utf8') ) - d = {} - d["vendor"] = obj["vendor"] - d["version"] = obj["version"] - d["product"] = obj["product"] + for device in nvme["Devices"]: + d = {} + d['logicalname'] = device["DevicePath"] + d['product'] = device["ModelNumber"] + d['serial'] = device["SerialNumber"] + d["version"] = device["Firmware"] + d['size'] = device["UsedSize"] + d['description'] = "NVME Disk" - d['description'] = "NVME Disk" - d['product'] = nvme["Devices"][0]["ModelNumber"] - d['size'] = nvme["Devices"][0]["PhysicalSize"] - d['serial'] = nvme["Devices"][0]["SerialNumber"] - d['logicalname'] = nvme["Devices"][0]["DevicePath"] - - self.disks.append(d) + self.disks.append(d) def find_cpus(self, obj): if "product" in obj: From f65418707412e93b2dc86893b9632502f8c87ab8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 29 Jun 2020 09:18:00 +0200 Subject: [PATCH 014/148] Update dependency netaddr to v0.7.20 (#136) Co-authored-by: Renovate Bot --- requirements.txt | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index c514916..82b9382 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ pynetbox==4.3.1 -netaddr==0.7.19 +netaddr==0.7.20 netifaces==0.10.9 pyyaml==5.3.1 jsonargparse==2.31.0 diff --git a/setup.py b/setup.py index 35bbc3e..cf6adae 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ setup( use_scm_version=True, install_requires=[ 'pynetbox==4.3.1', - 'netaddr==0.7.19', + 'netaddr==0.7.20', 'netifaces==0.10.9', 'pyyaml==5.3.1', 'jsonargparse==2.31.0', From 21ddf3f312d5566216fa3eebad7cdf8b56eafaa9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 1 Jul 2020 18:50:13 +0200 Subject: [PATCH 015/148] Update dependency pynetbox to v5 (#140) Co-authored-by: Renovate Bot --- requirements.txt | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 82b9382..ad9c1fd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -pynetbox==4.3.1 +pynetbox==5.0.1 netaddr==0.7.20 netifaces==0.10.9 pyyaml==5.3.1 diff --git a/setup.py b/setup.py index cf6adae..09ad5d8 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ setup( packages=find_packages(exclude=["*.tests", "*.tests.*", "tests.*", "tests"]), use_scm_version=True, install_requires=[ - 'pynetbox==4.3.1', + 'pynetbox==5.0.1', 'netaddr==0.7.20', 'netifaces==0.10.9', 'pyyaml==5.3.1', From 665531cb87a2c1fa541e58f05babb0fba7d39733 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 1 Jul 2020 18:50:34 +0200 Subject: [PATCH 016/148] Update dependency jsonargparse to v2.31.1 (#137) Co-authored-by: Renovate Bot --- requirements.txt | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index ad9c1fd..d705032 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,4 @@ pynetbox==5.0.1 netaddr==0.7.20 netifaces==0.10.9 pyyaml==5.3.1 -jsonargparse==2.31.0 +jsonargparse==2.31.1 diff --git a/setup.py b/setup.py index 09ad5d8..f23ac46 100644 --- a/setup.py +++ b/setup.py @@ -18,7 +18,7 @@ setup( 'netaddr==0.7.20', 'netifaces==0.10.9', 'pyyaml==5.3.1', - 'jsonargparse==2.31.0', + 'jsonargparse==2.31.1', ], zip_safe=False, keywords=['netbox'], From bd5037996b2c09366040b31ad9726dac12b7d52b Mon Sep 17 00:00:00 2001 From: Solvik Date: Wed, 1 Jul 2020 18:51:57 +0200 Subject: [PATCH 017/148] make the agent work if only a datacenter is specified (#143) --- netbox_agent/server.py | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/netbox_agent/server.py b/netbox_agent/server.py index 5db5513..687c964 100644 --- a/netbox_agent/server.py +++ b/netbox_agent/server.py @@ -1,6 +1,7 @@ import logging import socket import subprocess +import sys from pprint import pprint import netbox_agent.dmidecode as dmidecode @@ -49,10 +50,19 @@ class ServerBase(): return dc.get() def get_netbox_datacenter(self): - datacenter = nb.dcim.sites.get( - slug=self.get_datacenter() + dc = self.get_datacenter() + if dc is None: + logging.error("Specificing a datacenter (Site) is mandatory in Netbox") + sys.exit(1) + + nb_dc = nb.dcim.sites.get( + slug=dc, ) - return datacenter + if nb_dc is None: + logging.error("Site (slug: {}) has not been found".format(dc)) + sys.exit(1) + + return nb_dc def update_netbox_location(self, server): dc = self.get_datacenter() @@ -86,11 +96,18 @@ class ServerBase(): return rack.get() def get_netbox_rack(self): - rack = nb.dcim.racks.get( - name=self.get_rack(), - site_id=self.get_netbox_datacenter().id, + rack = self.get_rack() + datacenter = self.get_netbox_datacenter() + if not rack: + return None + if rack and not datacenter: + logging.error("Can't get rack if no datacenter is configured or found") + sys.exit(1) + + return nb.dcim.racks.get( + name=rack, + site_id=datacenter.id, ) - return rack def get_product_name(self): """ From a60c0cd70c8d45e7cb77b0efb319a2210e5be12d Mon Sep 17 00:00:00 2001 From: ThomasADavis Date: Wed, 1 Jul 2020 09:54:58 -0700 Subject: [PATCH 018/148] Add tags,device roles,tenants.. (#110) * Adds support for: * sets the tenant for IP addresses and Devices. * setting device tags * setting the blade, chassis, and server roles. Co-authored-by: Thomas Davis Co-authored-by: Solvik --- README.md | 17 +++++++++ netbox_agent.yaml.example | 16 +++++++++ netbox_agent/config.py | 14 ++++++++ netbox_agent/location.py | 11 ++++++ netbox_agent/misc.py | 32 +++++++++++++++++ netbox_agent/network.py | 4 +++ netbox_agent/server.py | 64 +++++++++++++++++++--------------- netbox_agent/virtualmachine.py | 42 ++++++++++++++++++---- 8 files changed, 165 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index 3d0bc40..7d46715 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,23 @@ network: # enable auto-cabling by parsing LLDP answers lldp: true +# +# You can use these to change the Netbox roles. +# These are the defaults. +# +#device: +# chassis_role: "Server Chassis" +# blade_role: "Blade" +# server_role: "Server" +# tags: server, blade, ,just a comma,delimited,list +# +# +# Can use this to set the tenant +# +#tenant: +# driver: "file:/tmp/tenant" +# regex: "(.*)" + ## Enable virtual machine support # virtual: # # not mandatory, can be guessed diff --git a/netbox_agent.yaml.example b/netbox_agent.yaml.example index 9c85d50..6c2f933 100644 --- a/netbox_agent.yaml.example +++ b/netbox_agent.yaml.example @@ -8,6 +8,22 @@ network: # enable auto-cabling lldp: true +# +# You can use these to change the roles. +# +#device: +# chassis_role: "Server Chassis" +# blade_role: "Blade" +# server_role: "Server" +# tags: server, blade, ,just a comma,delimited,list + +# +# Use this to set the tenant +# +#tenant: +# driver: "file:/tmp/tenant" +# regex: "(.*)" + datacenter_location: driver: "cmd:cat /etc/qualification | tr [a-z] [A-Z]" regex: "DATACENTER: (?P[A-Za-z0-9]+)" diff --git a/netbox_agent/config.py b/netbox_agent/config.py index 30d4675..72b0fbe 100644 --- a/netbox_agent/config.py +++ b/netbox_agent/config.py @@ -34,6 +34,20 @@ def get_config(): p.add_argument('--virtual.cluster_name', help='Cluster name of VM') p.add_argument('--hostname_cmd', default=None, help="Command to output hostname, used as Device's name in netbox") + p.add_argument('--device.tags', default=r'', + help='tags to use for a host') + p.add_argument('--device.blade_role', default=r'Blade', + help='role to use for a blade server') + p.add_argument('--device.chassis_role', default=r'Server Chassis', + help='role to use for a chassis') + p.add_argument('--device.server_role', default=r'Server', + help='role to use for a server') + p.add_argument('--tenant.driver', + help='tenant driver, ie cmd, file') + p.add_argument('--tenant.driver_file', + help='tenant driver custom driver file path') + p.add_argument('--tenant.regex', + help='tenant regex to extract Netbox tenant slug') p.add_argument('--datacenter_location.driver', help='Datacenter location driver, ie: cmd, file') p.add_argument('--datacenter_location.driver_file', diff --git a/netbox_agent/location.py b/netbox_agent/location.py index 20d2e9f..bb1fcfc 100644 --- a/netbox_agent/location.py +++ b/netbox_agent/location.py @@ -50,6 +50,17 @@ class LocationBase(): return getattr(self.driver, 'get')(self.driver_value, self.regex) +class Tenant(LocationBase): + def __init__(self): + driver = config.tenant.driver.split(':')[0] if \ + config.tenant.driver else None + driver_value = ':'.join(config.tenant.driver.split(':')[1:]) if \ + config.tenant.driver else None + driver_file = config.tenant.driver_file + regex = config.tenant.regex + super().__init__(driver, driver_value, driver_file, regex) + + class Datacenter(LocationBase): def __init__(self): driver = config.datacenter_location.driver.split(':')[0] if \ diff --git a/netbox_agent/misc.py b/netbox_agent/misc.py index cc1873c..7656f04 100644 --- a/netbox_agent/misc.py +++ b/netbox_agent/misc.py @@ -2,12 +2,32 @@ import socket import subprocess from shutil import which +from netbox_agent.config import netbox_instance as nb + def is_tool(name): '''Check whether `name` is on PATH and marked as executable.''' return which(name) is not None +def get_device_role(role): + device_role = nb.dcim.device_roles.get( + name=role + ) + if device_role is None: + raise Exception('DeviceRole "{}" does not exist, please create it'.format(role)) + return device_role + + +def get_device_type(type): + device_type = nb.dcim.device_types.get( + model=type + ) + if device_type is None: + raise Exception('DeviceType "{}" does not exist, please create it'.format(type)) + return device_type + + def get_vendor(name): vendors = { 'PERC': 'Dell', @@ -37,3 +57,15 @@ def get_hostname(config): if config.hostname_cmd is None: return '{}'.format(socket.gethostname()) return subprocess.getoutput(config.hostname_cmd) + + +def create_netbox_tags(tags): + for tag in tags: + nb_tag = nb.extras.tags.get( + name=tag + ) + if not nb_tag: + nb_tag = nb.extras.tags.create( + name=tag, + slug=tag, + ) diff --git a/netbox_agent/network.py b/netbox_agent/network.py index cb163a3..334ae01 100644 --- a/netbox_agent/network.py +++ b/netbox_agent/network.py @@ -17,6 +17,9 @@ class Network(object): def __init__(self, server, *args, **kwargs): self.nics = [] + self.server = server + self.tenant = self.server.get_netbox_tenant() + self.lldp = LLDP() if config.network.lldp else None self.nics = self.scan() self.ipmi = None @@ -337,6 +340,7 @@ class Network(object): interface=interface.id, status=1, role=self.ipam_choices['ip-address:role']['Anycast'], + tenant=self.tenant.id if self.tenant else None, ) return netbox_ip else: diff --git a/netbox_agent/server.py b/netbox_agent/server.py index 687c964..cf9de93 100644 --- a/netbox_agent/server.py +++ b/netbox_agent/server.py @@ -8,29 +8,12 @@ import netbox_agent.dmidecode as dmidecode from netbox_agent.config import config from netbox_agent.config import netbox_instance as nb from netbox_agent.inventory import Inventory -from netbox_agent.location import Datacenter, Rack +from netbox_agent.location import Datacenter, Rack, Tenant +from netbox_agent.misc import create_netbox_tags, get_device_role, get_device_type from netbox_agent.network import ServerNetwork from netbox_agent.power import PowerSupply -def get_device_role(role): - device_role = nb.dcim.device_roles.get( - name=role - ) - if device_role is None: - raise Exception('DeviceRole "{}" does not exist, please create it'.format(role)) - return device_role - - -def get_device_type(type): - device_type = nb.dcim.device_types.get( - model=type - ) - if device_type is None: - raise Exception('DeviceType "{}" does not exist, please create it'.format(type)) - return device_type - - class ServerBase(): def __init__(self, dmi=None): if dmi: @@ -45,6 +28,20 @@ class ServerBase(): self.network = None + self.tags = list(set(config.device.tags.split(','))) if config.device.tags else [] + if self.tags and len(self.tags): + create_netbox_tags(self.tags) + + def get_tenant(self): + tenant = Tenant() + return tenant.get() + + def get_netbox_tenant(self): + tenant = nb.tenancy.tenants.get( + slug=self.get_tenant() + ) + return tenant + def get_datacenter(self): dc = Datacenter() return dc.get() @@ -153,9 +150,9 @@ class ServerBase(): def get_power_consumption(self): raise NotImplementedError - def _netbox_create_chassis(self, datacenter, rack): + def _netbox_create_chassis(self, datacenter, tenant, rack): device_type = get_device_type(self.get_chassis()) - device_role = get_device_role('Server Chassis') + device_role = get_device_role(config.device.chassis_role) serial = self.get_chassis_service_tag() logging.info('Creating chassis blade (serial: {serial})'.format( serial=serial)) @@ -165,12 +162,14 @@ class ServerBase(): serial=serial, device_role=device_role.id, site=datacenter.id if datacenter else None, + tenant=tenant.id if tenant else None, rack=rack.id if rack else None, + tags=self.tags, ) return new_chassis - def _netbox_create_blade(self, chassis, datacenter, rack): - device_role = get_device_role('Blade') + def _netbox_create_blade(self, chassis, datacenter, tenant, rack): + device_role = get_device_role(config.device.blade_role) device_type = get_device_type(self.get_product_name()) serial = self.get_service_tag() hostname = self.get_hostname() @@ -185,12 +184,14 @@ class ServerBase(): device_type=device_type.id, parent_device=chassis.id, site=datacenter.id if datacenter else None, + tenant=tenant.id if tenant else None, rack=rack.id if rack else None, + tags=self.tags, ) return new_blade - def _netbox_create_server(self, datacenter, rack): - device_role = get_device_role('Server') + def _netbox_create_server(self, datacenter, tenant, rack): + device_role = get_device_role(config.device.server_role) device_type = get_device_type(self.get_product_name()) if not device_type: raise Exception('Chassis "{}" doesn\'t exist'.format(self.get_chassis())) @@ -204,7 +205,9 @@ class ServerBase(): device_role=device_role.id, device_type=device_type.id, site=datacenter.id if datacenter else None, + tenant=tenant.id if tenant else None, rack=rack.id if rack else None, + tags=self.tags, ) return new_server @@ -258,6 +261,7 @@ class ServerBase(): """ datacenter = self.get_netbox_datacenter() rack = self.get_netbox_rack() + tenant = self.get_netbox_tenant() if self.is_blade(): chassis = nb.dcim.devices.get( @@ -265,18 +269,18 @@ class ServerBase(): ) # Chassis does not exist if not chassis: - chassis = self._netbox_create_chassis(datacenter, rack) + chassis = self._netbox_create_chassis(datacenter, tenant, rack) server = nb.dcim.devices.get(serial=self.get_service_tag()) if not server: - server = self._netbox_create_blade(chassis, datacenter, rack) + server = self._netbox_create_blade(chassis, datacenter, tenant, rack) # Set slot for blade self._netbox_set_or_update_blade_slot(server, chassis, datacenter) else: server = nb.dcim.devices.get(serial=self.get_service_tag()) if not server: - self._netbox_create_server(datacenter, rack) + self._netbox_create_server(datacenter, tenant, rack) logging.debug('Updating Server...') # check network cards @@ -300,6 +304,10 @@ class ServerBase(): update += 1 server.name = self.get_hostname() + if sorted(set(server.tags)) != sorted(set(self.tags)): + server.tags = self.tags + update += 1 + if config.update_all or config.update_location: ret, server = self.update_netbox_location(server) update += ret diff --git a/netbox_agent/virtualmachine.py b/netbox_agent/virtualmachine.py index 1620ba0..1c917c5 100644 --- a/netbox_agent/virtualmachine.py +++ b/netbox_agent/virtualmachine.py @@ -3,8 +3,9 @@ import os import netbox_agent.dmidecode as dmidecode from netbox_agent.config import config from netbox_agent.config import netbox_instance as nb +from netbox_agent.location import Tenant from netbox_agent.logging import logging # NOQA -from netbox_agent.misc import get_hostname +from netbox_agent.misc import create_netbox_tags, get_hostname from netbox_agent.network import VirtualNetwork @@ -29,6 +30,10 @@ class VirtualMachine(object): self.dmi = dmidecode.parse() self.network = None + self.tags = list(set(config.device.tags.split(','))) if config.device.tags else [] + if self.tags and len(self.tags): + create_netbox_tags(self.tags) + def get_memory(self): mem_bytes = os.sysconf('SC_PAGE_SIZE') * os.sysconf('SC_PHYS_PAGES') # e.g. 4015976448 mem_gib = mem_bytes / (1024.**2) # e.g. 3.74 @@ -50,6 +55,22 @@ class VirtualMachine(object): ) return cluster + def get_netbox_datacenter(self, name): + cluster = self.get_netbox_cluster() + if cluster.datacenter: + return cluster.datacenter + return None + + def get_tenant(self): + tenant = Tenant() + return tenant.get() + + def get_netbox_tenant(self): + tenant = nb.tenancy.tenants.get( + slug=self.get_tenant() + ) + return tenant + def netbox_create_or_update(self, config): logging.debug('It\'s a virtual machine') created = False @@ -60,6 +81,7 @@ class VirtualMachine(object): vcpus = self.get_vcpus() memory = self.get_memory() + tenant = self.get_netbox_tenant() if not vm: logging.debug('Creating Virtual machine..') cluster = self.get_netbox_cluster(config.virtual.cluster_name) @@ -69,18 +91,24 @@ class VirtualMachine(object): cluster=cluster.id, vcpus=vcpus, memory=memory, + tenant=tenant.id if tenant else None, + tags=self.tags, ) created = True self.network = VirtualNetwork(server=self) self.network.create_or_update_netbox_network_cards() - if not created and vm.vcpus != vcpus: - vm.vcpus = vcpus - updated += 1 - elif not created and vm.memory != memory: - vm.memory = memory - updated += 1 + if not created: + if vm.vcpus != vcpus: + vm.vcpus = vcpus + updated += 1 + if vm.memory != memory: + vm.memory = memory + updated += 1 + if sorted(set(vm.tags)) != sorted(set(self.tags)): + vm.tags = self.tags + updated += 1 if updated: vm.save() From 9aedb46530fe4c69b0d831b94a00963b51a0239e Mon Sep 17 00:00:00 2001 From: Ryan Jones Date: Tue, 7 Jul 2020 14:08:35 -0400 Subject: [PATCH 019/148] small fix to add recognition of RHEV- Virtual machines (#150) --- netbox_agent/virtualmachine.py | 1 + 1 file changed, 1 insertion(+) diff --git a/netbox_agent/virtualmachine.py b/netbox_agent/virtualmachine.py index 1c917c5..0417545 100644 --- a/netbox_agent/virtualmachine.py +++ b/netbox_agent/virtualmachine.py @@ -16,6 +16,7 @@ def is_vm(dmi): if 'Hyper-V' in bios[0]['Version'] or \ 'Xen' in bios[0]['Version'] or \ 'Google Compute Engine' in system[0]['Product Name'] or \ + 'RHEV Hypervisor' in system[0]['Product Name'] or \ 'VirtualBox' in bios[0]['Version'] or \ 'VMware' in system[0]['Manufacturer']: return True From 275e1850c6effbd259490bc28704788c2c936a0b Mon Sep 17 00:00:00 2001 From: strus38 Date: Wed, 8 Jul 2020 09:36:13 +0200 Subject: [PATCH 020/148] Adding the --no_ssl_verify option to support Netbox connection with https and untrusted certificates --- netbox_agent/config.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/netbox_agent/config.py b/netbox_agent/config.py index 72b0fbe..6392995 100644 --- a/netbox_agent/config.py +++ b/netbox_agent/config.py @@ -1,5 +1,7 @@ import logging import sys +import requests +import urllib3 import jsonargparse import pynetbox @@ -67,6 +69,8 @@ def get_config(): p.add_argument('--network.lldp', help='Enable auto-cabling feature through LLDP infos') p.add_argument('--inventory', action='store_true', help='Enable HW inventory (CPU, Memory, RAID Cards, Disks) feature') + p.add_argument('--no_ssl_verify', default=False, action='store_true', + help='Disable SSL verification') options = p.parse_args() return options @@ -77,11 +81,18 @@ def get_netbox_instance(): if config.netbox.url is None or config.netbox.token is None: logging.error('Netbox URL and token are mandatory') sys.exit(1) - return pynetbox.api( + + nb = pynetbox.api( url=get_config().netbox.url, token=get_config().netbox.token, ) + if get_config().no_ssl_verify: + urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + session = requests.Session() + session.verify = False + nb.http_session = session + return nb config = get_config() netbox_instance = get_netbox_instance() From 95d2ae420e4be6612e6076f46f12f03953be987a Mon Sep 17 00:00:00 2001 From: strus38 Date: Wed, 8 Jul 2020 09:47:48 +0200 Subject: [PATCH 021/148] Fix indentation --- netbox_agent/config.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/netbox_agent/config.py b/netbox_agent/config.py index 6392995..00bd585 100644 --- a/netbox_agent/config.py +++ b/netbox_agent/config.py @@ -81,7 +81,7 @@ def get_netbox_instance(): if config.netbox.url is None or config.netbox.token is None: logging.error('Netbox URL and token are mandatory') sys.exit(1) - + nb = pynetbox.api( url=get_config().netbox.url, token=get_config().netbox.token, @@ -94,5 +94,6 @@ def get_netbox_instance(): return nb + config = get_config() netbox_instance = get_netbox_instance() From 0894e645a82efead82ef903d77c9e5bd950cf2a7 Mon Sep 17 00:00:00 2001 From: strus38 Date: Wed, 8 Jul 2020 10:02:45 +0200 Subject: [PATCH 022/148] Fix indentation --- netbox_agent/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox_agent/config.py b/netbox_agent/config.py index 00bd585..6058fdb 100644 --- a/netbox_agent/config.py +++ b/netbox_agent/config.py @@ -1,6 +1,6 @@ import logging -import sys import requests +import sys import urllib3 import jsonargparse From 67a564855b7aac4ff370a6bdd68a8599ff13358f Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Wed, 8 Jul 2020 16:06:09 +0000 Subject: [PATCH 023/148] Update dependency jsonargparse to v2.31.2 --- requirements.txt | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index d705032..92d8d1f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,4 @@ pynetbox==5.0.1 netaddr==0.7.20 netifaces==0.10.9 pyyaml==5.3.1 -jsonargparse==2.31.1 +jsonargparse==2.31.2 diff --git a/setup.py b/setup.py index f23ac46..98662b7 100644 --- a/setup.py +++ b/setup.py @@ -18,7 +18,7 @@ setup( 'netaddr==0.7.20', 'netifaces==0.10.9', 'pyyaml==5.3.1', - 'jsonargparse==2.31.1', + 'jsonargparse==2.31.2', ], zip_safe=False, keywords=['netbox'], From 70e5d407f58e2a501cf1b1495991206f16b8a3b8 Mon Sep 17 00:00:00 2001 From: strus38 Date: Wed, 8 Jul 2020 22:19:39 +0200 Subject: [PATCH 024/148] Fix indentation --- netbox_agent/config.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/netbox_agent/config.py b/netbox_agent/config.py index 6058fdb..65abefc 100644 --- a/netbox_agent/config.py +++ b/netbox_agent/config.py @@ -1,6 +1,7 @@ import logging -import requests import sys + +import requests import urllib3 import jsonargparse From 53cb29a6d47aceec401d798f84a806c365211321 Mon Sep 17 00:00:00 2001 From: Solvik Blum Date: Sat, 11 Jul 2020 15:14:33 +0200 Subject: [PATCH 025/148] fix import order --- netbox_agent/config.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/netbox_agent/config.py b/netbox_agent/config.py index 65abefc..b79fa00 100644 --- a/netbox_agent/config.py +++ b/netbox_agent/config.py @@ -1,11 +1,10 @@ import logging import sys -import requests -import urllib3 - import jsonargparse import pynetbox +import requests +import urllib3 def get_config(): From 9611400b22dbdb24b14cec989965df3ba583c704 Mon Sep 17 00:00:00 2001 From: Solvik Blum Date: Sat, 11 Jul 2020 15:27:32 +0200 Subject: [PATCH 026/148] move ssl_verify under 'netbox' config --- README.md | 2 ++ netbox_agent.yaml.example | 2 ++ netbox_agent/config.py | 6 +++--- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 7d46715..6a57ee8 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,8 @@ INFO:root:Creating Disk Samsung SSD 850 S2RBNX0K101698D netbox: url: 'http://netbox.internal.company.com' token: supersecrettoken + # uncomment to disable ssl verification + # ssl_verify: false # Network configuration network: diff --git a/netbox_agent.yaml.example b/netbox_agent.yaml.example index 6c2f933..9d6f3ac 100644 --- a/netbox_agent.yaml.example +++ b/netbox_agent.yaml.example @@ -1,6 +1,8 @@ netbox: url: 'http://netbox.internal.company.com' token: supersecrettoken + # uncomment to disable ssl verification + # ssl_verify: false network: ignore_interfaces: "(dummy.*|docker.*)" diff --git a/netbox_agent/config.py b/netbox_agent/config.py index b79fa00..cf88f69 100644 --- a/netbox_agent/config.py +++ b/netbox_agent/config.py @@ -32,6 +32,8 @@ def get_config(): p.add_argument('--log_level', default='debug') p.add_argument('--netbox.url', help='Netbox URL') p.add_argument('--netbox.token', help='Netbox API Token') + p.add_argument('--netbox.ssl_verify', default=True, action='store_true', + help='Disable SSL verification') p.add_argument('--virtual.enabled', action='store_true', help='Is a virtual machine or not') p.add_argument('--virtual.cluster_name', help='Cluster name of VM') p.add_argument('--hostname_cmd', default=None, @@ -69,8 +71,6 @@ def get_config(): p.add_argument('--network.lldp', help='Enable auto-cabling feature through LLDP infos') p.add_argument('--inventory', action='store_true', help='Enable HW inventory (CPU, Memory, RAID Cards, Disks) feature') - p.add_argument('--no_ssl_verify', default=False, action='store_true', - help='Disable SSL verification') options = p.parse_args() return options @@ -86,7 +86,7 @@ def get_netbox_instance(): url=get_config().netbox.url, token=get_config().netbox.token, ) - if get_config().no_ssl_verify: + if get_config().netbox.ssl_verify is False: urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) session = requests.Session() session.verify = False From 70146b8614d7f44199d6d6840a794f52a516a483 Mon Sep 17 00:00:00 2001 From: Solvik Blum Date: Sat, 11 Jul 2020 15:09:44 +0200 Subject: [PATCH 027/148] fix not passing tenant when tenants exist in Netbox --- netbox_agent/server.py | 7 +++++-- netbox_agent/virtualmachine.py | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/netbox_agent/server.py b/netbox_agent/server.py index cf9de93..df612fc 100644 --- a/netbox_agent/server.py +++ b/netbox_agent/server.py @@ -37,10 +37,13 @@ class ServerBase(): return tenant.get() def get_netbox_tenant(self): - tenant = nb.tenancy.tenants.get( + tenant = self.get_tenant() + if tenant is None: + return None + nb_tenant = nb.tenancy.tenants.get( slug=self.get_tenant() ) - return tenant + return nb_tenant def get_datacenter(self): dc = Datacenter() diff --git a/netbox_agent/virtualmachine.py b/netbox_agent/virtualmachine.py index 0417545..a58eb3d 100644 --- a/netbox_agent/virtualmachine.py +++ b/netbox_agent/virtualmachine.py @@ -67,10 +67,13 @@ class VirtualMachine(object): return tenant.get() def get_netbox_tenant(self): - tenant = nb.tenancy.tenants.get( + tenant = self.get_tenant() + if tenant is None: + return None + nb_tenant = nb.tenancy.tenants.get( slug=self.get_tenant() ) - return tenant + return nb_tenant def netbox_create_or_update(self, config): logging.debug('It\'s a virtual machine') From ebee42bb48bb5547b49b14ce1b32110c04dca492 Mon Sep 17 00:00:00 2001 From: Solvik Date: Sat, 11 Jul 2020 15:51:26 +0200 Subject: [PATCH 028/148] Update README.md --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 6a57ee8..2ee8ced 100644 --- a/README.md +++ b/README.md @@ -178,7 +178,7 @@ Tested on: * AWS * GCP -## Dell Inc. +## [Dell Inc.](https://github.com/Solvik/netbox-agent/blob/master/netbox_agent/vendors/dell.py) ### Blades @@ -194,7 +194,7 @@ Tested on: * DSS7500 -## HP / HPE +## [HP / HPE](https://github.com/Solvik/netbox-agent/blob/master/netbox_agent/vendors/hp.py) ### Blades @@ -210,18 +210,18 @@ Tested on: * ProLiant SL4540 Gen9 * ProLiant XL450 Gen10 -## Supermicro +## [Supermicro](https://github.com/Solvik/netbox-agent/blob/master/netbox_agent/vendors/supermicro.py) ### Blades -Feel free to send me a dmidecode output for Supermicro's blade! +* SBI-* and SBA-* should be supported, but I need dmidecode output example to support automatic blade location ### Pizzas * SSG-6028R * SYS-6018R -## QCT +## [QCT](https://github.com/Solvik/netbox-agent/blob/master/netbox_agent/vendors/qct.py) ### Blades From 839d9a908c4e7ae5f3055dd5fc4355985fd0484f Mon Sep 17 00:00:00 2001 From: Solvik Blum Date: Sat, 11 Jul 2020 15:56:10 +0200 Subject: [PATCH 029/148] better handling supermicro products (blade or not blade) --- netbox_agent/vendors/supermicro.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/netbox_agent/vendors/supermicro.py b/netbox_agent/vendors/supermicro.py index b10b7fd..c027409 100644 --- a/netbox_agent/vendors/supermicro.py +++ b/netbox_agent/vendors/supermicro.py @@ -23,8 +23,17 @@ class SupermicroHost(ServerBase): self.manufacturer = 'Supermicro' def is_blade(self): - blade = self.system[0]['Product Name'].startswith('SBI') - blade |= self.system[0]['Product Name'].startswith('SYS') + product_name = self.get_product_name() + # Blades + blade = product_name.startswith('SBI') + blade |= product_name.startswith('SBA') + # Twin + blade |= 'TR-' in product_name + # BigTwin + blade |= 'BT-' in product_name + # Microcloud + blade |= product_name.startswith('SYS-5039') + blade |= product_name.startswith('SYS-5038') return blade def get_blade_slot(self): From 18e34805745feabf9a1110b70bd545f7106419f2 Mon Sep 17 00:00:00 2001 From: Solvik Blum Date: Mon, 13 Jul 2020 20:34:04 +0200 Subject: [PATCH 030/148] fix supermicro dmidecode parsing --- netbox_agent/vendors/supermicro.py | 8 +- .../dmidecode/SYS-5039MS-H12TRF-OS012.txt | 536 ++++++++++++++++++ tests/server.py | 26 +- 3 files changed, 564 insertions(+), 6 deletions(-) create mode 100644 tests/fixtures/dmidecode/SYS-5039MS-H12TRF-OS012.txt diff --git a/netbox_agent/vendors/supermicro.py b/netbox_agent/vendors/supermicro.py index c027409..3e9fd6e 100644 --- a/netbox_agent/vendors/supermicro.py +++ b/netbox_agent/vendors/supermicro.py @@ -46,21 +46,19 @@ class SupermicroHost(ServerBase): return None def get_service_tag(self): - return self.baseboard[0]['Serial Number'].strip() + return self.system[0]['Serial Number'].strip() def get_product_name(self): - if self.is_blade(): - return self.baseboard[0]['Product Name'].strip() return self.system[0]['Product Name'].strip() def get_chassis(self): if self.is_blade(): - return self.system[0]['Product Name'].strip() + return self.chassis[0]['Product Name'].strip() return self.get_product_name() def get_chassis_service_tag(self): if self.is_blade(): - return self.system[0]['Serial Number'].strip() + return self.chassis[0]['Serial Number'].strip() return self.get_service_tag() def get_chassis_name(self): diff --git a/tests/fixtures/dmidecode/SYS-5039MS-H12TRF-OS012.txt b/tests/fixtures/dmidecode/SYS-5039MS-H12TRF-OS012.txt new file mode 100644 index 0000000..f1c4af7 --- /dev/null +++ b/tests/fixtures/dmidecode/SYS-5039MS-H12TRF-OS012.txt @@ -0,0 +1,536 @@ +# dmidecode 3.1 +Getting SMBIOS data from sysfs. +SMBIOS 3.0 present. +32 structures occupying 2366 bytes. +Table at 0x6FB76000. + +Handle 0x0000, DMI type 0, 24 bytes +BIOS Information + Vendor: American Megatrends Inc. + Version: 2.2 + Release Date: 05/16/2018 + Address: 0xF0000 + Runtime Size: 64 kB + ROM Size: 16 MB + Characteristics: + PCI is supported + BIOS is upgradeable + BIOS shadowing is allowed + Boot from CD is supported + Selectable boot is supported + BIOS ROM is socketed + EDD is supported + 5.25"/1.2 MB floppy services are supported (int 13h) + 3.5"/720 kB floppy services are supported (int 13h) + 3.5"/2.88 MB floppy services are supported (int 13h) + Print screen service is supported (int 5h) + Serial services are supported (int 14h) + Printer services are supported (int 17h) + ACPI is supported + USB legacy is supported + BIOS boot specification is supported + Targeted content distribution is supported + UEFI is supported + BIOS Revision: 5.11 + +Handle 0x0001, DMI type 1, 27 bytes +System Information + Manufacturer: Supermicro + Product Name: SYS-5039MS-H12TRF-OS012 + Version: 0123456789 + Serial Number: E235735X6B01665 + UUID: 00000000-0000-0000-0000-0CC47AE14338 + Wake-up Type: Power Switch + SKU Number: To be filled by O.E.M. + Family: To be filled by O.E.M. + +Handle 0x0002, DMI type 2, 15 bytes +Base Board Information + Manufacturer: Supermicro + Product Name: X11SSE-F + Version: 1.01 + Serial Number: ZM169S040205 + Asset Tag: To be filled by O.E.M. + Features: + Board is a hosting board + Board is replaceable + Location In Chassis: To be filled by O.E.M. + Chassis Handle: 0x0003 + Type: Motherboard + Contained Object Handles: 0 + +Handle 0x0003, DMI type 3, 22 bytes +Chassis Information + Manufacturer: Supermicro + Type: Other + Lock: Not Present + Version: 0123456789 + Serial Number: C9390AF40A20098 + Asset Tag: To be filled by O.E.M. + Boot-up State: Safe + Power Supply State: Safe + Thermal State: Safe + Security Status: None + OEM Information: 0x00000000 + Height: Unspecified + Number Of Power Cords: 1 + Contained Elements: 0 + SKU Number: To be filled by O.E.M. + +Handle 0x0004, DMI type 9, 17 bytes +System Slot Information + Designation: CPU MICRO-LP PCI-E 3.0 X8 + Type: x8 PCI Express 3 x8 + Current Usage: In Use + Length: Short + ID: 1 + Characteristics: + 3.3 V is provided + Opening is shared + PME signal is supported + Bus Address: 0000:01:00.0 + +Handle 0x0005, DMI type 11, 5 bytes +OEM Strings + String 1: Intel Skylake-S/Skylake-H/Greenlow + String 2: Supermicro motherboard-X11 Series + +Handle 0x0006, DMI type 32, 20 bytes +System Boot Information + Status: No errors detected + +Handle 0x0007, DMI type 39, 22 bytes +System Power Supply + Power Unit Group: 1 + Location: PSU1 + Name: PWS-2K04F-1R + Manufacturer: SUPERMICRO + Serial Number: P2K4FCG37KT0851 + Asset Tag: N/A + Model Part Number: PWS-2K04F-1R + Revision: 1.0 + Max Power Capacity: 2000 W + Status: Present, OK + Type: Switching + Input Voltage Range Switching: Auto-switch + Plugged: Yes + Hot Replaceable: No + +Handle 0x0008, DMI type 39, 22 bytes +System Power Supply + Power Unit Group: 2 + Location: PSU2 + Name: PWS-2K04F-1R + Manufacturer: SUPERMICRO + Serial Number: P2K4FCG37KT0852 + Asset Tag: N/A + Model Part Number: PWS-2K04F-1R + Revision: 1.0 + Max Power Capacity: 2000 W + Status: Present, OK + Type: Switching + Input Voltage Range Switching: Auto-switch + Plugged: Yes + Hot Replaceable: No + +Handle 0x0009, DMI type 41, 11 bytes +Onboard Device + Reference Designation: ASPEED Video AST2400 + Type: Video + Status: Enabled + Type Instance: 1 + Bus Address: 0000:04:00.0 + +Handle 0x000A, DMI type 38, 18 bytes +IPMI Device Information + Interface Type: KCS (Keyboard Control Style) + Specification Version: 2.0 + I2C Slave Address: 0x10 + NV Storage Device: Not Present + Base Address: 0x0000000000000CA2 (I/O) + Register Spacing: Successive Byte Boundaries + +Handle 0x000B, DMI type 7, 19 bytes +Cache Information + Socket Designation: L1 Cache + Configuration: Enabled, Not Socketed, Level 1 + Operational Mode: Write Back + Location: Internal + Installed Size: 128 kB + Maximum Size: 128 kB + Supported SRAM Types: + Synchronous + Installed SRAM Type: Synchronous + Speed: Unknown + Error Correction Type: Parity + System Type: Data + Associativity: 8-way Set-associative + +Handle 0x000C, DMI type 7, 19 bytes +Cache Information + Socket Designation: L1 Cache + Configuration: Enabled, Not Socketed, Level 1 + Operational Mode: Write Back + Location: Internal + Installed Size: 128 kB + Maximum Size: 128 kB + Supported SRAM Types: + Synchronous + Installed SRAM Type: Synchronous + Speed: Unknown + Error Correction Type: Parity + System Type: Instruction + Associativity: 8-way Set-associative + +Handle 0x000D, DMI type 7, 19 bytes +Cache Information + Socket Designation: L2 Cache + Configuration: Enabled, Not Socketed, Level 2 + Operational Mode: Write Back + Location: Internal + Installed Size: 1024 kB + Maximum Size: 1024 kB + Supported SRAM Types: + Synchronous + Installed SRAM Type: Synchronous + Speed: Unknown + Error Correction Type: Single-bit ECC + System Type: Unified + Associativity: 4-way Set-associative + +Handle 0x000E, DMI type 7, 19 bytes +Cache Information + Socket Designation: L3 Cache + Configuration: Enabled, Not Socketed, Level 3 + Operational Mode: Write Back + Location: Internal + Installed Size: 8192 kB + Maximum Size: 8192 kB + Supported SRAM Types: + Synchronous + Installed SRAM Type: Synchronous + Speed: Unknown + Error Correction Type: Multi-bit ECC + System Type: Unified + Associativity: 16-way Set-associative + +Handle 0x000F, DMI type 4, 48 bytes +Processor Information + Socket Designation: CPU + Type: Central Processor + Family: Xeon + Manufacturer: Intel(R) Corporation + ID: E3 06 05 00 FF FB EB BF + Signature: Type 0, Family 6, Model 94, Stepping 3 + Flags: + FPU (Floating-point unit on-chip) + VME (Virtual mode extension) + DE (Debugging extension) + PSE (Page size extension) + TSC (Time stamp counter) + MSR (Model specific registers) + PAE (Physical address extension) + MCE (Machine check exception) + CX8 (CMPXCHG8 instruction supported) + APIC (On-chip APIC hardware supported) + SEP (Fast system call) + MTRR (Memory type range registers) + PGE (Page global enable) + MCA (Machine check architecture) + CMOV (Conditional move instruction supported) + PAT (Page attribute table) + PSE-36 (36-bit page size extension) + CLFSH (CLFLUSH instruction supported) + DS (Debug store) + ACPI (ACPI supported) + MMX (MMX technology supported) + FXSR (FXSAVE and FXSTOR instructions supported) + SSE (Streaming SIMD extensions) + SSE2 (Streaming SIMD extensions 2) + SS (Self-snoop) + HTT (Multi-threading) + TM (Thermal monitor supported) + PBE (Pending break enabled) + Version: Intel(R) Xeon(R) CPU E3-1245 v5 @ 3.50GHz + Voltage: 1.1 V + External Clock: 100 MHz + Max Speed: 3900 MHz + Current Speed: 3500 MHz + Status: Populated, Enabled + Upgrade: Other + L1 Cache Handle: 0x000C + L2 Cache Handle: 0x000D + L3 Cache Handle: 0x000E + Serial Number: To Be Filled By O.E.M. + Asset Tag: To Be Filled By O.E.M. + Part Number: To Be Filled By O.E.M. + Core Count: 4 + Core Enabled: 4 + Thread Count: 8 + Characteristics: + 64-bit capable + Multi-Core + Hardware Thread + Execute Protection + Enhanced Virtualization + Power/Performance Control + +Handle 0x0010, DMI type 16, 23 bytes +Physical Memory Array + Location: System Board Or Motherboard + Use: System Memory + Error Correction Type: Single-bit ECC + Maximum Capacity: 64 GB + Error Information Handle: Not Provided + Number Of Devices: 4 + +Handle 0x0011, DMI type 17, 40 bytes +Memory Device + Array Handle: 0x0010 + Error Information Handle: Not Provided + Total Width: Unknown + Data Width: Unknown + Size: No Module Installed + Form Factor: Unknown + Set: None + Locator: DIMMA1 + Bank Locator: P0_Node0_Channel0_Dimm0 + Type: Unknown + Type Detail: None + Speed: Unknown + Manufacturer: Not Specified + Serial Number: Not Specified + Asset Tag: Not Specified + Part Number: Not Specified + Rank: Unknown + Configured Clock Speed: Unknown + Minimum Voltage: Unknown + Maximum Voltage: Unknown + Configured Voltage: Unknown + +Handle 0x0012, DMI type 17, 40 bytes +Memory Device + Array Handle: 0x0010 + Error Information Handle: Not Provided + Total Width: 72 bits + Data Width: 64 bits + Size: 16384 MB + Form Factor: DIMM + Set: None + Locator: DIMMA2 + Bank Locator: P0_Node0_Channel0_Dimm1 + Type: DDR4 + Type Detail: Synchronous + Speed: 2400 MT/s + Manufacturer: Micron + Serial Number: 13DA8BB7 + Asset Tag: DIMMA2_AssetTag(16/36) + Part Number: 18ADF2G72AZ-2G3B1 + Rank: 2 + Configured Clock Speed: 2133 MT/s + Minimum Voltage: 1.2 V + Maximum Voltage: 1.2 V + Configured Voltage: 1.2 V + +Handle 0x0013, DMI type 17, 40 bytes +Memory Device + Array Handle: 0x0010 + Error Information Handle: Not Provided + Total Width: Unknown + Data Width: Unknown + Size: No Module Installed + Form Factor: Unknown + Set: None + Locator: DIMMB1 + Bank Locator: P0_Node0_Channel1_Dimm0 + Type: Unknown + Type Detail: None + Speed: Unknown + Manufacturer: Not Specified + Serial Number: Not Specified + Asset Tag: Not Specified + Part Number: Not Specified + Rank: Unknown + Configured Clock Speed: Unknown + Minimum Voltage: Unknown + Maximum Voltage: Unknown + Configured Voltage: Unknown + +Handle 0x0014, DMI type 17, 40 bytes +Memory Device + Array Handle: 0x0010 + Error Information Handle: Not Provided + Total Width: 72 bits + Data Width: 64 bits + Size: 16384 MB + Form Factor: DIMM + Set: None + Locator: DIMMB2 + Bank Locator: P0_Node0_Channel1_Dimm1 + Type: DDR4 + Type Detail: Synchronous + Speed: 2400 MT/s + Manufacturer: Micron + Serial Number: 13DA8C08 + Asset Tag: DIMMB2_AssetTag(16/36) + Part Number: 18ADF2G72AZ-2G3B1 + Rank: 2 + Configured Clock Speed: 2133 MT/s + Minimum Voltage: 1.2 V + Maximum Voltage: 1.2 V + Configured Voltage: 1.2 V + +Handle 0x0015, DMI type 19, 31 bytes +Memory Array Mapped Address + Starting Address: 0x00000000000 + Ending Address: 0x007FFFFFFFF + Range Size: 32 GB + Physical Array Handle: 0x0010 + Partition Width: 2 + +Handle 0x0016, DMI type 15, 73 bytes +System Event Log + Area Length: 65535 bytes + Header Start Offset: 0x0000 + Header Length: 16 bytes + Data Start Offset: 0x0010 + Access Method: Memory-mapped physical 32-bit address + Access Address: 0xFF610000 + Status: Valid, Not Full + Change Token: 0x00000001 + Header Format: Type 1 + Supported Log Type Descriptors: 25 + Descriptor 1: Single-bit ECC memory error + Data Format 1: Multiple-event handle + Descriptor 2: Multi-bit ECC memory error + Data Format 2: Multiple-event handle + Descriptor 3: Parity memory error + Data Format 3: None + Descriptor 4: Bus timeout + Data Format 4: None + Descriptor 5: I/O channel block + Data Format 5: None + Descriptor 6: Software NMI + Data Format 6: None + Descriptor 7: POST memory resize + Data Format 7: None + Descriptor 8: POST error + Data Format 8: POST results bitmap + Descriptor 9: PCI parity error + Data Format 9: Multiple-event handle + Descriptor 10: PCI system error + Data Format 10: Multiple-event handle + Descriptor 11: CPU failure + Data Format 11: None + Descriptor 12: EISA failsafe timer timeout + Data Format 12: None + Descriptor 13: Correctable memory log disabled + Data Format 13: None + Descriptor 14: Logging disabled + Data Format 14: None + Descriptor 15: System limit exceeded + Data Format 15: None + Descriptor 16: Asynchronous hardware timer expired + Data Format 16: None + Descriptor 17: System configuration information + Data Format 17: None + Descriptor 18: Hard disk information + Data Format 18: None + Descriptor 19: System reconfigured + Data Format 19: None + Descriptor 20: Uncorrectable CPU-complex error + Data Format 20: None + Descriptor 21: Log area reset/cleared + Data Format 21: None + Descriptor 22: System boot + Data Format 22: None + Descriptor 23: End of log + Data Format 23: None + Descriptor 24: OEM-specific + Data Format 24: OEM-specific + Descriptor 25: OEM-specific + Data Format 25: OEM-specific + +Handle 0x0017, DMI type 20, 35 bytes +Memory Device Mapped Address + Starting Address: 0x00000000000 + Ending Address: 0x003FFFFFFFF + Range Size: 16 GB + Physical Device Handle: 0x0012 + Memory Array Mapped Address Handle: 0x0015 + Partition Row Position: Unknown + Interleave Position: 1 + Interleaved Data Depth: 2 + +Handle 0x0018, DMI type 20, 35 bytes +Memory Device Mapped Address + Starting Address: 0x00400000000 + Ending Address: 0x007FFFFFFFF + Range Size: 16 GB + Physical Device Handle: 0x0014 + Memory Array Mapped Address Handle: 0x0015 + Partition Row Position: Unknown + Interleave Position: 2 + Interleaved Data Depth: 2 + +Handle 0x0019, DMI type 221, 26 bytes +OEM-specific Type + Header and Data: + DD 1A 19 00 03 01 00 04 01 00 08 00 02 00 00 00 + 00 C6 00 03 00 00 05 00 00 00 + Strings: + Reference Code - CPU + uCode Version + TXT ACM version + +Handle 0x001A, DMI type 221, 68 bytes +OEM-specific Type + Header and Data: + DD 44 1A 00 09 01 00 04 01 00 08 00 02 03 FF FF + FF FF FF 04 00 FF FF FF 31 00 05 00 FF FF FF 31 + 00 06 00 FF FF FF FF FF 07 00 3E 00 00 00 00 08 + 00 34 00 00 00 00 09 00 3E 00 00 00 00 0A 00 34 + 00 00 00 00 + Strings: + Reference Code - SKL PCH + PCH-CRID Status + Disabled + PCH-CRID Original Value + PCH-CRID New Value + OPROM - RST - RAID + SKL PCH H Bx Hsio Version + SKL PCH H Dx Hsio Version + SKL PCH LP Bx Hsio Version + SKL PCH LP Cx Hsio Version + +Handle 0x001B, DMI type 221, 54 bytes +OEM-specific Type + Header and Data: + DD 36 1B 00 07 01 00 04 01 00 08 00 02 00 02 01 + 00 00 00 03 00 04 01 00 00 00 04 05 FF FF FF FF + FF 06 00 FF FF FF 07 00 07 00 FF FF FF 07 00 08 + 00 FF FF FF 00 00 + Strings: + Reference Code - SA - System Agent + Reference Code - MRC + SA - PCIe Version + SA-CRID Status + Disabled + SA-CRID Original Value + SA-CRID New Value + OPROM - VBIOS + +Handle 0x001C, DMI type 40, 27 bytes +Additional Information 1 + +Handle 0x001D, DMI type 40, 27 bytes +Additional Information 1 + +Handle 0x001E, DMI type 136, 6 bytes +OEM-specific Type + Header and Data: + 88 06 1E 00 00 00 + +Handle 0x001F, DMI type 127, 4 bytes +End Of Table + diff --git a/tests/server.py b/tests/server.py index 6e781ad..328e167 100644 --- a/tests/server.py +++ b/tests/server.py @@ -1,5 +1,8 @@ from netbox_agent.dmidecode import parse from netbox_agent.server import ServerBase +from netbox_agent.vendors.hp import HPHost +from netbox_agent.vendors.qct import QCTHost +from netbox_agent.vendors.supermicro import SupermicroHost from tests.conftest import parametrize_with_fixtures @@ -19,10 +22,31 @@ def test_init(fixture): ]) def test_hp_service_tag(fixture): dmi = parse(fixture) - server = ServerBase(dmi) + server = HPHost(dmi) assert server.get_service_tag() == '4242' +@parametrize_with_fixtures( + 'dmidecode/', only_filenames=[ + 'SYS-5039MS-H12TRF-OS012.txt' + ]) +def test_supermicro_blade(fixture): + dmi = parse(fixture) + server = SupermicroHost(dmi) + assert server.get_service_tag() == 'E235735X6B01665' + assert server.get_chassis_service_tag() == 'C9390AF40A20098' + assert server.is_blade() is True + +@parametrize_with_fixtures( + 'dmidecode/', only_filenames=[ + 'SM_SYS-6018R' + ]) +def test_supermicro_pizza(fixture): + dmi = parse(fixture) + server = SupermicroHost(dmi) + assert server.get_service_tag() == 'A177950X7709591' + assert server.is_blade() is False + @parametrize_with_fixtures( 'dmidecode/', only_filenames=[ 'unknown.txt' From 1685ecab167ab15b8520be0ada1f6e060d0ef535 Mon Sep 17 00:00:00 2001 From: Solvik Date: Sun, 19 Jul 2020 11:32:32 +0200 Subject: [PATCH 031/148] Update issue templates --- .github/ISSUE_TEMPLATE/bug_report.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..990fe99 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,25 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Configuration file** +Paste here your netbox agent configuration file + +**Environment:** + - OS: [e.g. Ubuntu 18.04] + - Netbox agent version [e.g. master, v0.6.0] + +**Additional context** +Add any other context about the problem here. From e6472c623d7f8c54a66df8a79e3a83ba8e15c237 Mon Sep 17 00:00:00 2001 From: Solvik Blum Date: Tue, 28 Jul 2020 18:42:10 +0200 Subject: [PATCH 032/148] update README.md to change the datacenter example to use a lowercase slug --- README.md | 4 ++-- netbox_agent.yaml.example | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 2ee8ced..8951731 100644 --- a/README.md +++ b/README.md @@ -127,8 +127,8 @@ network: # Enable datacenter location feature in Netbox datacenter_location: - driver: "cmd:cat /etc/qualification | tr [a-z] [A-Z]" - regex: "DATACENTER: (?P[A-Za-z0-9]+)" + driver: "cmd:cat /etc/qualification | tr [A-Z] [a-z]" + regex: "datacenter: (?P[A-Za-z0-9]+)" # driver: 'cmd:lldpctl' # regex: 'SysName: .*\.([A-Za-z0-9]+)' # diff --git a/netbox_agent.yaml.example b/netbox_agent.yaml.example index 9d6f3ac..a769fb4 100644 --- a/netbox_agent.yaml.example +++ b/netbox_agent.yaml.example @@ -27,8 +27,8 @@ network: # regex: "(.*)"  datacenter_location: - driver: "cmd:cat /etc/qualification | tr [a-z] [A-Z]" - regex: "DATACENTER: (?P[A-Za-z0-9]+)" + driver: "cmd:cat /etc/qualification | tr [A-Z] [a-z]" + regex: "datacenter: (?P[A-Za-z0-9]+)" # driver: 'cmd:lldpctl' # regex: 'SysName: .*\.([A-Za-z0-9]+)' # From d1a0ec232469d0d8f57aee9534a6be41b11fe574 Mon Sep 17 00:00:00 2001 From: Solvik Blum Date: Tue, 28 Jul 2020 18:51:15 +0200 Subject: [PATCH 033/148] fix unused import and add QCT test --- tests/server.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/server.py b/tests/server.py index 328e167..36437d7 100644 --- a/tests/server.py +++ b/tests/server.py @@ -47,6 +47,15 @@ def test_supermicro_pizza(fixture): assert server.get_service_tag() == 'A177950X7709591' assert server.is_blade() is False +@parametrize_with_fixtures( + 'dmidecode/', only_filenames=[ + 'QCT_X10E-9N' + ]) +def test_qct_x10(fixture): + dmi = parse(fixture) + server = QCTHost(dmi) + assert server.get_service_tag() == 'QTFCQ57140285' + @parametrize_with_fixtures( 'dmidecode/', only_filenames=[ 'unknown.txt' From 4f0db3478f83978ae73e1cb28bb2deecbe05e921 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Tue, 28 Jul 2020 16:54:34 +0000 Subject: [PATCH 034/148] Update dependency jsonargparse to v2.32.2 --- requirements.txt | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 92d8d1f..5b38492 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,4 @@ pynetbox==5.0.1 netaddr==0.7.20 netifaces==0.10.9 pyyaml==5.3.1 -jsonargparse==2.31.2 +jsonargparse==2.32.2 diff --git a/setup.py b/setup.py index 98662b7..4cce28f 100644 --- a/setup.py +++ b/setup.py @@ -18,7 +18,7 @@ setup( 'netaddr==0.7.20', 'netifaces==0.10.9', 'pyyaml==5.3.1', - 'jsonargparse==2.31.2', + 'jsonargparse==2.32.2', ], zip_safe=False, keywords=['netbox'], From df18f6ea00501e790ac113043ab3985309dcc6f2 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Tue, 28 Jul 2020 16:54:48 +0000 Subject: [PATCH 035/148] Update dependency netaddr to v0.8.0 --- requirements.txt | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 92d8d1f..4bab46a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ pynetbox==5.0.1 -netaddr==0.7.20 +netaddr==0.8.0 netifaces==0.10.9 pyyaml==5.3.1 jsonargparse==2.31.2 diff --git a/setup.py b/setup.py index 98662b7..3185494 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ setup( use_scm_version=True, install_requires=[ 'pynetbox==5.0.1', - 'netaddr==0.7.20', + 'netaddr==0.8.0', 'netifaces==0.10.9', 'pyyaml==5.3.1', 'jsonargparse==2.31.2', From da5e8ad2e7e95531e8c3ac5c90e58f2a5f7507b3 Mon Sep 17 00:00:00 2001 From: Solvik Date: Wed, 29 Jul 2020 00:21:08 +0200 Subject: [PATCH 036/148] Update server.py --- tests/server.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/server.py b/tests/server.py index 36437d7..b14d92d 100644 --- a/tests/server.py +++ b/tests/server.py @@ -37,6 +37,7 @@ def test_supermicro_blade(fixture): assert server.get_chassis_service_tag() == 'C9390AF40A20098' assert server.is_blade() is True + @parametrize_with_fixtures( 'dmidecode/', only_filenames=[ 'SM_SYS-6018R' @@ -47,6 +48,7 @@ def test_supermicro_pizza(fixture): assert server.get_service_tag() == 'A177950X7709591' assert server.is_blade() is False + @parametrize_with_fixtures( 'dmidecode/', only_filenames=[ 'QCT_X10E-9N' @@ -56,6 +58,7 @@ def test_qct_x10(fixture): server = QCTHost(dmi) assert server.get_service_tag() == 'QTFCQ57140285' + @parametrize_with_fixtures( 'dmidecode/', only_filenames=[ 'unknown.txt' From f8c0732c2b051ceb92e7157d5cc6711e5e7d60aa Mon Sep 17 00:00:00 2001 From: Solvik Blum Date: Wed, 29 Jul 2020 00:45:24 +0200 Subject: [PATCH 037/148] fix agent crash on server creation if not blade --- netbox_agent/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox_agent/server.py b/netbox_agent/server.py index df612fc..7fee83c 100644 --- a/netbox_agent/server.py +++ b/netbox_agent/server.py @@ -283,7 +283,7 @@ class ServerBase(): else: server = nb.dcim.devices.get(serial=self.get_service_tag()) if not server: - self._netbox_create_server(datacenter, tenant, rack) + server = self._netbox_create_server(datacenter, tenant, rack) logging.debug('Updating Server...') # check network cards From 3aea4fbc3b4d0c19e52431dba406691dec58a183 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Tue, 28 Jul 2020 22:50:26 +0000 Subject: [PATCH 038/148] Update dependency pynetbox to v5.0.5 --- requirements.txt | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index a337611..9540b2f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -pynetbox==5.0.1 +pynetbox==5.0.5 netaddr==0.8.0 netifaces==0.10.9 pyyaml==5.3.1 diff --git a/setup.py b/setup.py index 8b52e2d..d58f7bc 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ setup( packages=find_packages(exclude=["*.tests", "*.tests.*", "tests.*", "tests"]), use_scm_version=True, install_requires=[ - 'pynetbox==5.0.1', + 'pynetbox==5.0.5', 'netaddr==0.8.0', 'netifaces==0.10.9', 'pyyaml==5.3.1', From e95a66d93af0fa1953f809acf04bf226c44d7c94 Mon Sep 17 00:00:00 2001 From: Cyrinux Date: Thu, 20 Aug 2020 18:03:36 +0200 Subject: [PATCH 039/148] Add HP Moonshot 1500 support (#161) * Add HP Moonshot 1500 support --- README.md | 1 + netbox_agent/vendors/hp.py | 44 +- tests/fixtures/dmidecode/HP_ProLiant_m710x | 921 +++++++++++++++++++++ tests/server.py | 12 + 4 files changed, 964 insertions(+), 14 deletions(-) create mode 100644 tests/fixtures/dmidecode/HP_ProLiant_m710x diff --git a/README.md b/README.md index 8951731..5e6adf4 100644 --- a/README.md +++ b/README.md @@ -202,6 +202,7 @@ Tested on: * HP ProLiant BL460c Gen8 * HP ProLiant BL460c Gen9 * HP ProLiant BL460c Gen10 +* HP Moonshot 1500 Enclosure (your `DeviceType` should have slots batch create with `Bay c[1-45n1]`) with HP ProLiant m750, m710x, m510 Server Cartridge ### Pizzas diff --git a/netbox_agent/vendors/hp.py b/netbox_agent/vendors/hp.py index 30b36ce..23885f2 100644 --- a/netbox_agent/vendors/hp.py +++ b/netbox_agent/vendors/hp.py @@ -5,12 +5,18 @@ from netbox_agent.server import ServerBase class HPHost(ServerBase): def __init__(self, *args, **kwargs): super(HPHost, self).__init__(*args, **kwargs) + self.manufacturer = "HP" + self.product = self.get_product_name() if self.is_blade(): self.hp_rack_locator = self._find_rack_locator() - self.manufacturer = 'HP' def is_blade(self): - return self.get_product_name().startswith('ProLiant BL') + if self.product.startswith("ProLiant BL"): + return True + elif self.product.startswith("ProLiant m") and self.product.endswith("Server Cartridge"): + return True + else: + return False def _find_rack_locator(self): """ @@ -21,34 +27,44 @@ class HPHost(ServerBase): # FIXME: make a dmidecode function get_by_dminame() ? if self.is_blade(): locator = dmidecode.get_by_type(self.dmi, 204) - if self.get_product_name() == 'ProLiant BL460c Gen10': - locator = locator[0]['Strings'] + if self.product == "ProLiant BL460c Gen10": + locator = locator[0]["Strings"] return { - 'Enclosure Model': locator[2].strip(), - 'Enclosure Name': locator[0].strip(), - 'Server Bay': locator[3].strip(), - 'Enclosure Serial': locator[4].strip(), + "Enclosure Model": locator[2].strip(), + "Enclosure Name": locator[0].strip(), + "Server Bay": locator[3].strip(), + "Enclosure Serial": locator[4].strip(), } + + # HP ProLiant m750, m710x, m510 Server Cartridge + if self.product.startswith("ProLiant m") and self.product.endswith("Server Cartridge"): + locator = dmidecode.get_by_type(self.dmi, 2) + chassis = dmidecode.get_by_type(self.dmi, 3) + return { + "Enclosure Model": "Moonshot 1500 Chassis", + "Enclosure Name": "Unknown", + "Server Bay": locator[0]["Location In Chassis"].strip(), + "Enclosure Serial": chassis[0]["Serial Number"].strip(), + } + return locator[0] def get_blade_slot(self): if self.is_blade(): - return 'Bay {}'.format( - int(self.hp_rack_locator['Server Bay'].strip()) - ) + return "Bay {}".format(str(self.hp_rack_locator["Server Bay"].strip())) return None def get_chassis(self): if self.is_blade(): - return self.hp_rack_locator['Enclosure Model'].strip() + return self.hp_rack_locator["Enclosure Model"].strip() return self.get_product_name() def get_chassis_name(self): if not self.is_blade(): return None - return self.hp_rack_locator['Enclosure Name'].strip() + return self.hp_rack_locator["Enclosure Name"].strip() def get_chassis_service_tag(self): if self.is_blade(): - return self.hp_rack_locator['Enclosure Serial'].strip() + return self.hp_rack_locator["Enclosure Serial"].strip() return self.get_service_tag() diff --git a/tests/fixtures/dmidecode/HP_ProLiant_m710x b/tests/fixtures/dmidecode/HP_ProLiant_m710x new file mode 100644 index 0000000..40f9ffc --- /dev/null +++ b/tests/fixtures/dmidecode/HP_ProLiant_m710x @@ -0,0 +1,921 @@ +# dmidecode 3.1 +Getting SMBIOS data from sysfs. +SMBIOS 2.8 present. +93 structures occupying 4311 bytes. +Table at 0x766AD000. + +Handle 0x0000, DMI type 7, 19 bytes +Cache Information + Socket Designation: L1-Cache + Configuration: Enabled, Not Socketed, Level 1 + Operational Mode: Write Back + Location: Internal + Installed Size: 256 kB + Maximum Size: 256 kB + Supported SRAM Types: + Synchronous + Installed SRAM Type: Synchronous + Speed: Unknown + Error Correction Type: Single-bit ECC + System Type: Unified + Associativity: 8-way Set-associative + +Handle 0x0001, DMI type 7, 19 bytes +Cache Information + Socket Designation: L2-Cache + Configuration: Enabled, Not Socketed, Level 2 + Operational Mode: Varies With Memory Address + Location: Internal + Installed Size: 1024 kB + Maximum Size: 1024 kB + Supported SRAM Types: + Synchronous + Installed SRAM Type: Synchronous + Speed: Unknown + Error Correction Type: Single-bit ECC + System Type: Unified + Associativity: 4-way Set-associative + +Handle 0x0002, DMI type 7, 19 bytes +Cache Information + Socket Designation: L3-Cache + Configuration: Enabled, Not Socketed, Level 3 + Operational Mode: Varies With Memory Address + Location: Internal + Installed Size: 8192 kB + Maximum Size: 8192 kB + Supported SRAM Types: + Synchronous + Installed SRAM Type: Synchronous + Speed: Unknown + Error Correction Type: Single-bit ECC + System Type: Unified + Associativity: 16-way Set-associative + +Handle 0x0003, DMI type 4, 48 bytes +Processor Information + Socket Designation: Proc 1 + Type: Central Processor + Family: Xeon + Manufacturer: Intel(R) Corporation + ID: E3 06 05 00 FF FB EB BF + Signature: Type 0, Family 6, Model 94, Stepping 3 + Flags: + FPU (Floating-point unit on-chip) + VME (Virtual mode extension) + DE (Debugging extension) + PSE (Page size extension) + TSC (Time stamp counter) + MSR (Model specific registers) + PAE (Physical address extension) + MCE (Machine check exception) + CX8 (CMPXCHG8 instruction supported) + APIC (On-chip APIC hardware supported) + SEP (Fast system call) + MTRR (Memory type range registers) + PGE (Page global enable) + MCA (Machine check architecture) + CMOV (Conditional move instruction supported) + PAT (Page attribute table) + PSE-36 (36-bit page size extension) + CLFSH (CLFLUSH instruction supported) + DS (Debug store) + ACPI (ACPI supported) + MMX (MMX technology supported) + FXSR (FXSAVE and FXSTOR instructions supported) + SSE (Streaming SIMD extensions) + SSE2 (Streaming SIMD extensions 2) + SS (Self-snoop) + HTT (Multi-threading) + TM (Thermal monitor supported) + PBE (Pending break enabled) + Version: Intel(R) Xeon(R) CPU E3-1585L v5 @ 3.00GHz + Voltage: 1.0 V + External Clock: 100 MHz + Max Speed: 3700 MHz + Current Speed: 3000 MHz + Status: Populated, Enabled + Upgrade: Other + L1 Cache Handle: 0x0000 + L2 Cache Handle: 0x0001 + L3 Cache Handle: 0x0002 + Serial Number: To Be Filled By O.E.M. + Asset Tag: To Be Filled By O.E.M. + Part Number: To Be Filled By O.E.M. + Core Count: 4 + Core Enabled: 4 + Thread Count: 8 + Characteristics: + 64-bit capable + Multi-Core + Hardware Thread + Execute Protection + Enhanced Virtualization + Power/Performance Control + +Handle 0x0004, DMI type 0, 24 bytes +BIOS Information + Vendor: HP + Version: H07 + Release Date: 05/23/2016 + Address: 0xF0000 + Runtime Size: 64 kB + ROM Size: 16 MB + Characteristics: + PCI is supported + PNP is supported + BIOS is upgradeable + BIOS shadowing is allowed + ESCD support is available + Boot from CD is supported + Selectable boot is supported + EDD is supported + 5.25"/360 kB floppy services are supported (int 13h) + 5.25"/1.2 MB floppy services are supported (int 13h) + 3.5"/720 kB floppy services are supported (int 13h) + Print screen service is supported (int 5h) + 8042 keyboard services are supported (int 9h) + Serial services are supported (int 14h) + Printer services are supported (int 17h) + CGA/mono video services are supported (int 10h) + ACPI is supported + USB legacy is supported + BIOS boot specification is supported + Function key-initiated network boot is supported + Targeted content distribution is supported + UEFI is supported + BIOS Revision: 1.0 + Firmware Revision: 2.40 + +Handle 0x0005, DMI type 1, 27 bytes +System Information + Manufacturer: HP + Product Name: ProLiant m710x Server Cartridge + Version: Not Specified + Serial Number: CN66480BLA + UUID: CF6D6DE0-A5AB-512C-BCD4-5CF40EC490C1 + Wake-up Type: Power Switch + SKU Number: 833105-B21 + Family: ProLiant + +Handle 0x0006, DMI type 16, 23 bytes +Physical Memory Array + Location: System Board Or Motherboard + Use: System Memory + Error Correction Type: Multi-bit ECC + Maximum Capacity: 64 GB + Error Information Handle: Not Provided + Number Of Devices: 4 + +Handle 0x0007, DMI type 17, 40 bytes +Memory Device + Array Handle: 0x0006 + Error Information Handle: Not Provided + Total Width: 72 bits + Data Width: 64 bits + Size: 16384 MB + Form Factor: DIMM + Set: None + Locator: PROC 1 DIMM 1 + Bank Locator: Not Specified + Type: DDR4 + Type Detail: Synchronous + Speed: 2400 MT/s + Manufacturer: HP + Serial Number: Not Specified + Asset Tag: Not Specified + Part Number: 853289-091 + Rank: 2 + Configured Clock Speed: 2133 MT/s + Minimum Voltage: 1.2 V + Maximum Voltage: 1.2 V + Configured Voltage: 1.2 V + +Handle 0x0008, DMI type 17, 40 bytes +Memory Device + Array Handle: 0x0006 + Error Information Handle: Not Provided + Total Width: 72 bits + Data Width: 64 bits + Size: 16384 MB + Form Factor: DIMM + Set: 1 + Locator: PROC 1 DIMM 2 + Bank Locator: Not Specified + Type: DDR4 + Type Detail: Synchronous + Speed: 2400 MT/s + Manufacturer: HP + Serial Number: Not Specified + Asset Tag: Not Specified + Part Number: 853289-091 + Rank: 2 + Configured Clock Speed: 2133 MT/s + Minimum Voltage: 1.2 V + Maximum Voltage: 1.2 V + Configured Voltage: 1.2 V + +Handle 0x0009, DMI type 17, 40 bytes +Memory Device + Array Handle: 0x0006 + Error Information Handle: Not Provided + Total Width: 72 bits + Data Width: 64 bits + Size: 16384 MB + Form Factor: DIMM + Set: 2 + Locator: PROC 1 DIMM 3 + Bank Locator: Not Specified + Type: DDR4 + Type Detail: Synchronous + Speed: 2400 MT/s + Manufacturer: HP + Serial Number: Not Specified + Asset Tag: Not Specified + Part Number: 853289-091 + Rank: 2 + Configured Clock Speed: 2133 MT/s + Minimum Voltage: 1.2 V + Maximum Voltage: 1.2 V + Configured Voltage: 1.2 V + +Handle 0x000A, DMI type 17, 40 bytes +Memory Device + Array Handle: 0x0006 + Error Information Handle: Not Provided + Total Width: 72 bits + Data Width: 64 bits + Size: 16384 MB + Form Factor: DIMM + Set: 3 + Locator: PROC 1 DIMM 4 + Bank Locator: Not Specified + Type: DDR4 + Type Detail: Synchronous + Speed: 2400 MT/s + Manufacturer: HP + Serial Number: Not Specified + Asset Tag: Not Specified + Part Number: 853289-091 + Rank: 2 + Configured Clock Speed: 2133 MT/s + Minimum Voltage: 1.2 V + Maximum Voltage: 1.2 V + Configured Voltage: 1.2 V + +Handle 0x000B, DMI type 19, 31 bytes +Memory Array Mapped Address + Starting Address: 0x00000000000 + Ending Address: 0x0007E7FFFFF + Range Size: 2024 MB + Physical Array Handle: 0x0006 + Partition Width: 1 + +Handle 0x000C, DMI type 19, 31 bytes +Memory Array Mapped Address + Starting Address: 0x0000000100000000k + Ending Address: 0x00000010817FFFFFk + Range Size: 63512 MB + Physical Array Handle: 0x0006 + Partition Width: 1 + +Handle 0x000D, DMI type 211, 7 bytes +OEM-specific Type + Header and Data: + D3 07 0D 00 03 00 00 + +Handle 0x000E, DMI type 2, 17 bytes +Base Board Information + Manufacturer: HP + Product Name: ProLiant m710x Server Cartridge + Version: Not Specified + Serial Number: CN66480ANY + Asset Tag: + Features: + Board is a hosting board + Board is removable + Board is replaceable + Location In Chassis: c2n1 + Chassis Handle: 0x0000 + Type: Motherboard + Contained Object Handles: 0 + +Handle 0x000F, DMI type 3, 21 bytes +Chassis Information + Manufacturer: HP + Type: Multi-system + Lock: Not Present + Version: Not Specified + Serial Number: CZ3702MD5K + Asset Tag: + Boot-up State: Safe + Power Supply State: Safe + Thermal State: Safe + Security Status: Unknown + OEM Information: 0x00000000 + Height: 5 U + Number Of Power Cords: Unspecified + Contained Elements: 0 + +Handle 0x0010, DMI type 11, 5 bytes +OEM Strings + String 1: PSF: + String 2: Product ID: 833105-B21 + String 3: CPN: HP Moonshot 1500 Chassis + String 4: OEM String: + +Handle 0x0011, DMI type 38, 18 bytes +IPMI Device Information + Interface Type: KCS (Keyboard Control Style) + Specification Version: 2.0 + I2C Slave Address: 0x10 + NV Storage Device: Not Present + Base Address: 0x0000000000000CA2 (I/O) + Register Spacing: Successive Byte Boundaries + +Handle 0x0012, DMI type 193, 9 bytes +OEM-specific Type + Header and Data: + C1 09 12 00 00 01 00 02 03 + Strings: + 0/0000 + + + +Handle 0x0013, DMI type 194, 5 bytes +OEM-specific Type + Header and Data: + C2 05 13 00 10 + +Handle 0x0014, DMI type 195, 7 bytes +OEM-specific Type + Header and Data: + C3 07 14 00 01 12 01 + Strings: + $0E110855 + +Handle 0x0015, DMI type 197, 16 bytes +OEM-specific Type + Header and Data: + C5 10 15 00 03 00 00 01 FF 01 2D 00 00 00 00 00 + +Handle 0x0016, DMI type 198, 14 bytes +OEM-specific Type + Header and Data: + C6 0E 16 00 17 00 00 00 00 00 01 0A FF FF + +Handle 0x0017, DMI type 199, 28 bytes +OEM-specific Type + Header and Data: + C7 1C 17 00 2C 00 00 00 15 20 01 07 E2 06 05 00 + 88 00 00 00 16 20 16 03 E3 06 05 00 + +Handle 0x0018, DMI type 201, 16 bytes +OEM-specific Type + Header and Data: + C9 10 18 00 10 02 00 00 40 0D 01 00 0E 00 00 80 + +Handle 0x0019, DMI type 215, 6 bytes +OEM-specific Type + Header and Data: + D7 06 19 00 00 05 + +Handle 0x001A, DMI type 216, 23 bytes +OEM-specific Type + Header and Data: + D8 17 1A 00 01 00 01 02 07 01 00 05 17 E0 07 00 + 00 00 00 00 00 00 00 + Strings: + System ROM + v1.00 (05/23/2016) + +Handle 0x001B, DMI type 216, 23 bytes +OEM-specific Type + Header and Data: + D8 17 1B 00 04 00 01 02 04 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 + Strings: + Power Management Controller Firmware + 0.0 + +Handle 0x001C, DMI type 216, 23 bytes +OEM-specific Type + Header and Data: + D8 17 1C 00 05 00 01 02 02 60 00 00 00 00 00 00 + 00 00 00 00 00 00 00 + Strings: + Power Management Controller FW Bootloader + 6.0 + +Handle 0x001D, DMI type 216, 23 bytes +OEM-specific Type + Header and Data: + D8 17 1D 00 08 00 01 00 01 09 09 00 00 00 00 00 + 00 00 00 00 00 00 00 + Strings: + System Programmable Logic Device + +Handle 0x001E, DMI type 216, 23 bytes +OEM-specific Type + Header and Data: + D8 17 1E 00 09 00 01 00 03 04 00 00 00 03 00 96 + 00 0A 00 00 00 01 00 + Strings: + Server Platform Services (SPS) Firmware + +Handle 0x001F, DMI type 219, 32 bytes +HP ProLiant Information + Power Features: 0x000000df + Omega Features: 0x00000000 + Misc. Features: 0x00009001 + iCRU: Yes + UEFI: No + +Handle 0x0020, DMI type 223, 11 bytes +OEM-specific Type + Header and Data: + DF 0B 20 00 66 46 70 00 00 00 00 + +Handle 0x0021, DMI type 224, 10 bytes +OEM-specific Type + Header and Data: + E0 0A 21 00 00 00 02 03 FE FF + +Handle 0x0022, DMI type 226, 21 bytes +OEM-specific Type + Header and Data: + E2 15 22 00 38 33 33 31 30 35 43 4E 36 36 34 38 + 30 41 4E 59 01 + Strings: + CN66480ANY + +Handle 0x0023, DMI type 227, 22 bytes +OEM-specific Type + Header and Data: + E3 16 23 00 03 00 07 00 00 A0 01 00 FF FF FF FF + 00 00 00 00 00 00 + +Handle 0x0024, DMI type 227, 22 bytes +OEM-specific Type + Header and Data: + E3 16 24 00 03 00 08 00 00 A2 01 00 FF FF FF FF + 00 00 00 00 00 00 + +Handle 0x0025, DMI type 227, 22 bytes +OEM-specific Type + Header and Data: + E3 16 25 00 03 00 09 00 00 A4 01 00 FF FF FF FF + 01 00 00 00 00 00 + +Handle 0x0026, DMI type 227, 22 bytes +OEM-specific Type + Header and Data: + E3 16 26 00 03 00 0A 00 00 A6 01 00 FF FF FF FF + 01 00 00 00 00 00 + +Handle 0x0027, DMI type 228, 14 bytes +OEM-specific Type + Header and Data: + E4 0E 27 00 00 00 00 00 00 00 00 FF 00 00 + +Handle 0x0028, DMI type 228, 14 bytes +OEM-specific Type + Header and Data: + E4 0E 28 00 01 00 00 00 00 00 00 FF 00 00 + +Handle 0x0029, DMI type 228, 14 bytes +OEM-specific Type + Header and Data: + E4 0E 29 00 02 00 00 00 00 00 00 FF 00 00 + +Handle 0x002A, DMI type 228, 14 bytes +OEM-specific Type + Header and Data: + E4 0E 2A 00 03 00 00 00 00 00 00 FF 00 00 + +Handle 0x002B, DMI type 228, 14 bytes +OEM-specific Type + Header and Data: + E4 0E 2B 00 04 00 00 00 00 00 00 FF 00 00 + +Handle 0x002C, DMI type 228, 14 bytes +OEM-specific Type + Header and Data: + E4 0E 2C 00 05 00 00 00 00 00 00 FF 00 00 + +Handle 0x002D, DMI type 228, 14 bytes +OEM-specific Type + Header and Data: + E4 0E 2D 00 06 00 00 00 00 00 00 FF 00 00 + +Handle 0x002E, DMI type 228, 14 bytes +OEM-specific Type + Header and Data: + E4 0E 2E 00 07 00 00 00 00 00 00 FF 00 00 + +Handle 0x002F, DMI type 228, 14 bytes +OEM-specific Type + Header and Data: + E4 0E 2F 00 08 06 00 00 00 00 00 FF 01 00 + +Handle 0x0030, DMI type 229, 100 bytes +OEM-specific Type + Header and Data: + E5 64 30 00 24 4F 43 53 00 B0 F1 77 00 00 00 00 + 00 40 00 00 24 4F 43 42 00 50 F0 77 00 00 00 00 + 00 60 01 00 24 48 44 44 00 30 F0 77 00 00 00 00 + 00 20 00 00 24 57 48 45 00 20 EF 77 00 00 00 00 + 00 10 00 00 24 53 4D 56 98 2E F2 77 00 00 00 00 + 08 00 00 00 24 5A 58 54 00 10 EF 77 00 00 00 00 + 3B 00 00 00 + +Handle 0x0031, DMI type 232, 14 bytes +OEM-specific Type + Header and Data: + E8 0E 31 00 07 00 11 00 00 00 B0 04 B0 04 + +Handle 0x0032, DMI type 232, 14 bytes +OEM-specific Type + Header and Data: + E8 0E 32 00 08 00 11 00 00 00 B0 04 B0 04 + +Handle 0x0033, DMI type 232, 14 bytes +OEM-specific Type + Header and Data: + E8 0E 33 00 09 00 11 00 00 00 B0 04 B0 04 + +Handle 0x0034, DMI type 232, 14 bytes +OEM-specific Type + Header and Data: + E8 0E 34 00 0A 00 11 00 00 00 B0 04 B0 04 + +Handle 0x0035, DMI type 237, 9 bytes +OEM-specific Type + Header and Data: + ED 09 35 00 07 00 01 02 03 + Strings: + Hynix + HMA82GS7AFR8N-UH + 31F9E4B8 + +Handle 0x0036, DMI type 237, 9 bytes +OEM-specific Type + Header and Data: + ED 09 36 00 08 00 01 02 03 + Strings: + Hynix + HMA82GS7AFR8N-UH + 31F9E4B6 + +Handle 0x0037, DMI type 237, 9 bytes +OEM-specific Type + Header and Data: + ED 09 37 00 09 00 01 02 03 + Strings: + Hynix + HMA82GS7AFR8N-UH + 31F9E96B + +Handle 0x0038, DMI type 237, 9 bytes +OEM-specific Type + Header and Data: + ED 09 38 00 0A 00 01 02 03 + Strings: + Hynix + HMA82GS7AFR8N-UH + 31F9E4B9 + +Handle 0x0039, DMI type 9, 17 bytes +System Slot Information + Designation: PCI-E Slot 2 + Type: x4 PCI Express 2 + Current Usage: Available + Length: Other + ID: 2 + Characteristics: + 3.3 V is provided + PME signal is supported + Bus Address: 0000:02:00.0 + +Handle 0x003A, DMI type 9, 17 bytes +System Slot Information + Designation: PCI-E Slot 1 + Type: x1 PCI Express 2 + Current Usage: Available + Length: Other + ID: 1 + Characteristics: + 3.3 V is provided + PME signal is supported + Bus Address: 0000:05:00.0 + +Handle 0x003B, DMI type 9, 17 bytes +System Slot Information + Designation: PCI-E Slot 5 + Type: x2 PCI Express 2 + Current Usage: Available + Length: Other + ID: 5 + Characteristics: + 3.3 V is provided + PME signal is supported + Bus Address: 0000:08:00.0 + +Handle 0x003C, DMI type 9, 17 bytes +System Slot Information + Designation: PCI-E Slot 0 + Type: x1 PCI Express 3 x4 + Current Usage: Available + Length: Unknown + ID: 0 + Characteristics: + 3.3 V is provided + PME signal is supported + Bus Address: 0000:fe:00.0 + +Handle 0x003D, DMI type 9, 17 bytes +System Slot Information + Designation: PCI-E Slot 0 + Type: x1 PCI Express 3 x4 + Current Usage: Available + Length: Unknown + ID: 0 + Characteristics: + 3.3 V is provided + PME signal is supported + Bus Address: 0000:fe:00.0 + +Handle 0x003E, DMI type 9, 17 bytes +System Slot Information + Designation: PCI-E Slot 4 + Type: x4 PCI Express 2 + Current Usage: Available + Length: Unknown + ID: 4 + Characteristics: + 3.3 V is provided + PME signal is supported + Bus Address: 0000:0b:00.0 + +Handle 0x003F, DMI type 9, 17 bytes +System Slot Information + Designation: PCI-E Slot 3 + Type: x4 PCI Express 2 + Current Usage: In Use + Length: Other + ID: 3 + Characteristics: + 3.3 V is provided + PME signal is supported + Bus Address: 0000:0e:00.0 + +Handle 0x0040, DMI type 9, 17 bytes +System Slot Information + Designation: PCI-E Slot 1 + Type: x4 PCI Express 3 x4 + Current Usage: Available + Length: Unknown + ID: 1 + Characteristics: + 3.3 V is provided + PME signal is supported + Bus Address: 0000:fe:00.0 + +Handle 0x0041, DMI type 9, 17 bytes +System Slot Information + Designation: PCI-E Slot 1 + Type: x4 PCI Express 3 x4 + Current Usage: Available + Length: Unknown + ID: 1 + Characteristics: + 3.3 V is provided + PME signal is supported + Bus Address: 0000:fe:00.0 + +Handle 0x0042, DMI type 233, 41 bytes +HP BIOS PXE NIC PCI and MAC Information + NIC 1: PCI device 11:00.0, MAC address 00:FD:45:50:6C:01 + +Handle 0x0043, DMI type 233, 41 bytes +HP BIOS PXE NIC PCI and MAC Information + NIC 2: PCI device 11:00.0, MAC address 00:FD:45:50:6C:02 + +Handle 0x0044, DMI type 32, 11 bytes +System Boot Information + Status: No errors detected + +Handle 0x0045, DMI type 196, 15 bytes +OEM-specific Type + Header and Data: + C4 0F 45 00 00 00 00 00 00 00 02 01 00 01 02 + +Handle 0x0046, DMI type 41, 11 bytes +Onboard Device + Reference Designation: Embedded LOM 1 Port 1 + Type: Ethernet + Status: Enabled + Type Instance: 1 + Bus Address: 0000:11:00.0 + +Handle 0x0047, DMI type 41, 11 bytes +Onboard Device + Reference Designation: Embedded LOM 1 Port 2 + Type: Ethernet + Status: Enabled + Type Instance: 2 + Bus Address: 0000:11:00.0 + +Handle 0x0048, DMI type 41, 11 bytes +Onboard Device + Reference Designation: Embedded SATA Controller #1 + Type: SATA Controller + Status: Enabled + Type Instance: 1 + Bus Address: 0000:00:17.0 + +Handle 0x0049, DMI type 202, 13 bytes +OEM-specific Type + Header and Data: + CA 0D 49 00 07 00 FF 01 01 01 00 00 00 + +Handle 0x004A, DMI type 202, 13 bytes +OEM-specific Type + Header and Data: + CA 0D 4A 00 08 00 FF 02 01 02 00 00 00 + +Handle 0x004B, DMI type 202, 13 bytes +OEM-specific Type + Header and Data: + CA 0D 4B 00 09 00 FF 03 01 03 00 00 00 + +Handle 0x004C, DMI type 202, 13 bytes +OEM-specific Type + Header and Data: + CA 0D 4C 00 0A 00 FF 04 01 04 00 00 00 + +Handle 0x004D, DMI type 203, 34 bytes +OEM-specific Type + Header and Data: + CB 22 4D 00 39 00 FE FF FF FF FF FF FF FF FF FF + FF FF FE FF 00 00 09 0A 02 01 FF FF 01 02 03 04 + FE FF + Strings: + PciRoot(0x0)/Pci(0x1D,0x0)/Pci(0x0,0x0) + PCI.Slot.2.1 + Empty slot 2 + Slot 2 + +Handle 0x004E, DMI type 203, 34 bytes +OEM-specific Type + Header and Data: + CB 22 4E 00 3A 00 FE FF FF FF FF FF FF FF FF FF + FF FF FE FF 00 00 09 0A 01 01 FF FF 01 02 03 04 + FE FF + Strings: + PciRoot(0x0)/Pci(0x1D,0x5)/Pci(0x0,0x0) + PCI.Slot.1.1 + Empty slot 1 + Slot 1 + +Handle 0x004F, DMI type 203, 34 bytes +OEM-specific Type + Header and Data: + CB 22 4F 00 3B 00 FE FF FF FF FF FF FF FF FF FF + FF FF FE FF 00 00 09 0A 05 01 FF FF 01 02 03 04 + FE FF + Strings: + PciRoot(0x0)/Pci(0x1C,0x0)/Pci(0x0,0x0) + PCI.Slot.5.1 + Empty slot 5 + Slot 5 + +Handle 0x0050, DMI type 203, 34 bytes +OEM-specific Type + Header and Data: + CB 22 50 00 3C 00 FE FF FF FF FF FF FF FF FF FF + FF FF FE FF 00 00 09 0A 01 01 FF FF 01 02 03 04 + FE FF + Strings: + PciRoot(0x0)/Pci(0x1C,0x2)/Pci(0x0,0x0) + PCI.Slot.1.1 + Unknown + Slot 0 + +Handle 0x0051, DMI type 203, 34 bytes +OEM-specific Type + Header and Data: + CB 22 51 00 3D 00 FE FF FF FF FF FF FF FF FF FF + FF FF FE FF 00 00 09 0A 01 01 FF FF 01 02 03 04 + FE FF + Strings: + PciRoot(0x0)/Pci(0x1C,0x2)/Pci(0x0,0x0) + PCI.Slot.1.1 + Unknown + Slot 0 + +Handle 0x0052, DMI type 203, 34 bytes +OEM-specific Type + Header and Data: + CB 22 52 00 3E 00 FE FF FF FF FF FF FF FF FF FF + FF FF FE FF 00 00 09 0A 04 01 FF FF 01 02 03 04 + FE FF + Strings: + PciRoot(0x0)/Pci(0x1C,0x4)/Pci(0x0,0x0) + PCI.Slot.4.1 + Empty slot 4 + Slot 4 + +Handle 0x0053, DMI type 203, 34 bytes +OEM-specific Type + Header and Data: + CB 22 53 00 3F 00 FE FF 79 11 0F 01 79 11 01 00 + 01 08 FE FF 00 00 10 0A 03 01 FF FF 01 02 03 04 + FE FF + Strings: + PciRoot(0x0)/Pci(0x1B,0x0)/Pci(0x0,0x0) + NVMe.Slot.3.1 + NVM Express Controller + Slot 3 + +Handle 0x0054, DMI type 203, 34 bytes +OEM-specific Type + Header and Data: + CB 22 54 00 40 00 FE FF FF FF FF FF FF FF FF FF + FF FF FE FF 00 00 09 0A 01 01 FF FF 01 02 03 04 + FE FF + Strings: + PciRoot(0x0)/Pci(0x1C,0x2)/Pci(0x0,0x0) + PCI.Slot.1.1 + Empty slot 1 + Slot 0 + +Handle 0x0055, DMI type 203, 34 bytes +OEM-specific Type + Header and Data: + CB 22 55 00 41 00 FE FF FF FF FF FF FF FF FF FF + FF FF FE FF 00 00 09 0A 01 01 FF FF 01 02 03 04 + FE FF + Strings: + PciRoot(0x0)/Pci(0x1C,0x2)/Pci(0x0,0x0) + PCI.Slot.1.1 + Empty slot 1 + Slot 0 + +Handle 0x0056, DMI type 203, 34 bytes +OEM-specific Type + Header and Data: + CB 22 56 00 46 00 FE FF B3 15 07 10 90 15 04 22 + 02 00 FE FF 00 00 04 01 01 01 FF FF 01 02 03 04 + FE FF + Strings: + PciRoot(0x0)/Pci(0x1,0x0)/Pci(0x0,0x0)/Ctrl(0x1) + NIC.LOM.1.1 + Port 1 - Mellanox Network Adapter + Embedded LOM 1 + +Handle 0x0057, DMI type 203, 34 bytes +OEM-specific Type + Header and Data: + CB 22 57 00 47 00 FE FF B3 15 07 10 90 15 04 22 + 02 00 FE FF 00 00 04 01 01 02 FF FF 01 02 03 04 + FE FF + Strings: + PciRoot(0x0)/Pci(0x1,0x0)/Pci(0x0,0x0)/Ctrl(0x2) + NIC.LOM.1.2 + Port 2 - Mellanox Network Adapter + Embedded LOM 1 + +Handle 0x0058, DMI type 203, 34 bytes +OEM-specific Type + Header and Data: + CB 22 58 00 48 00 FE FF 86 80 02 A1 3C 10 65 81 + 01 06 FE FF 00 00 06 08 01 01 FF FF 01 02 03 04 + FE FF + Strings: + PciRoot(0x0)/Pci(0x17,0x0) + SATA.Emb.1.1 + Embedded SATA Controller #1 + Embedded SATA Controller #1 + +Handle 0x0059, DMI type 234, 16 bytes +OEM-specific Type + Header and Data: + EA 10 59 00 FE FF C0 00 01 A0 00 00 00 00 00 00 + +Handle 0x005A, DMI type 234, 12 bytes +OEM-specific Type + Header and Data: + EA 0C 5A 00 27 00 60 03 01 01 00 00 + +Handle 0x005B, DMI type 240, 39 bytes +OEM-specific Type + Header and Data: + F0 27 5B 00 56 00 18 1E 24 00 01 08 48 0D 00 00 + 00 00 00 0F 00 00 00 00 00 00 00 0B 00 00 00 00 + 00 00 00 02 00 00 00 + Strings: + 02.36.70.00 + +Handle 0xFEFF, DMI type 127, 4 bytes +End Of Table diff --git a/tests/server.py b/tests/server.py index b14d92d..64cc6e6 100644 --- a/tests/server.py +++ b/tests/server.py @@ -26,6 +26,18 @@ def test_hp_service_tag(fixture): assert server.get_service_tag() == '4242' +@parametrize_with_fixtures( + 'dmidecode/', only_filenames=[ + 'HP_ProLiant_m710x' + ]) +def test_moonshot_blade(fixture): + dmi = parse(fixture) + server = HPHost(dmi) + assert server.get_service_tag() == 'CN66480BLA' + assert server.get_chassis_service_tag() == 'CZ3702MD5K' + assert server.is_blade() is True + + @parametrize_with_fixtures( 'dmidecode/', only_filenames=[ 'SYS-5039MS-H12TRF-OS012.txt' From e20e6a7deeb78108584c15830c0925776c2d3efb Mon Sep 17 00:00:00 2001 From: Cyrinux Date: Mon, 24 Aug 2020 12:52:25 +0200 Subject: [PATCH 040/148] Fix travis (#162) --- netbox_agent/vendors/supermicro.py | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/netbox_agent/vendors/supermicro.py b/netbox_agent/vendors/supermicro.py index 3e9fd6e..736decf 100644 --- a/netbox_agent/vendors/supermicro.py +++ b/netbox_agent/vendors/supermicro.py @@ -1,23 +1,21 @@ - from netbox_agent.location import Slot from netbox_agent.server import ServerBase -""" - Supermicro DMI can be messed up. They depend on the vendor - to set the correct values. The endusers cannot - change them without buying a license from Supermicro. - - There are 3 serial numbers in the system - - 1) System - this is used for the chassis information. - 2) Baseboard - this is used for the blade. - 3) Chassis - this is ignored. - -""" - - class SupermicroHost(ServerBase): + """ + Supermicro DMI can be messed up. They depend on the vendor + to set the correct values. The endusers cannot + change them without buying a license from Supermicro. + + There are 3 serial numbers in the system + + 1) System - this is used for the chassis information. + 2) Baseboard - this is used for the blade. + 3) Chassis - this is ignored. + + """ + def __init__(self, *args, **kwargs): super(SupermicroHost, self).__init__(*args, **kwargs) self.manufacturer = 'Supermicro' From fc930b9ef42f1789fe24ac9f37d7fa5016edc406 Mon Sep 17 00:00:00 2001 From: Solvik Blum Date: Mon, 24 Aug 2020 13:45:18 +0200 Subject: [PATCH 041/148] bump version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index d58f7bc..d74fe86 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import find_packages, setup setup( name='netbox_agent', - version='0.6.1', + version='0.6.2', description='NetBox agent for server', long_description=open('README.md', encoding="utf-8").read(), long_description_content_type='text/markdown', From 28955612be4bbc2ca70c0b9e640c9f6c0c29338d Mon Sep 17 00:00:00 2001 From: "Anton A. Grishin" Date: Mon, 7 Sep 2020 15:20:32 +0300 Subject: [PATCH 042/148] fix allocated_draw for inactive PSU (#168) Co-authored-by: Anton Grishyn --- netbox_agent/power.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/netbox_agent/power.py b/netbox_agent/power.py index 9e763a8..fcc1890 100644 --- a/netbox_agent/power.py +++ b/netbox_agent/power.py @@ -117,6 +117,9 @@ class PowerSupply(): for i, nb_psu in enumerate(nb_psus): nb_psu.allocated_draw = float(psu_cons[i]) * voltage + if nb_psu.allocated_draw < 1: + logging.info('PSU is not connected or in standby mode') + continue nb_psu.save() logging.info('Updated power consumption for PSU {}: {}W'.format( nb_psu.name, From 0fe17c968764dd176370dbeb0aa73071a5375040 Mon Sep 17 00:00:00 2001 From: Cyrinux Date: Fri, 18 Sep 2020 12:29:05 +0200 Subject: [PATCH 043/148] Add GPU inventory support (#164) * Add GPU support * Some update in the doc --- README.md | 3 ++- netbox_agent/inventory.py | 31 +++++++++++++++++++++++++++++++ netbox_agent/lshw.py | 16 ++++++++++++++++ 3 files changed, 49 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 5e6adf4..06a2ba6 100644 --- a/README.md +++ b/README.md @@ -13,8 +13,9 @@ The goal is to generate an existing infrastructure on Netbox and have the abilit * Generic ability to guess datacenters and rack location through drivers (`cmd` and `file` and custom ones) * Update existing `Device` and `Interface` * Handle blade moving (new slot, new chassis) +* Handle blade GPU expansions * Automatic cabling (server's interface to switch's interface) using lldp -* Local inventory using `Inventory Item` for CPU, RAM, RAID cards, physical disks (behind raid cards) +* Local inventory using `Inventory Item` for CPU, GPU, RAM, RAID cards, physical disks (behind raid cards) * PSUs creation and power consumption reporting (based on vendor's tools) # Requirements diff --git a/netbox_agent/inventory.py b/netbox_agent/inventory.py index d571e6a..97e7ec4 100644 --- a/netbox_agent/inventory.py +++ b/netbox_agent/inventory.py @@ -13,6 +13,7 @@ from netbox_agent.raid.storcli import StorcliRaid INVENTORY_TAG = { 'cpu': {'name': 'hw:cpu', 'slug': 'hw-cpu'}, + 'gpu': {'name': 'hw:gpu', 'slug': 'hw-gpu'}, 'disk': {'name': 'hw:disk', 'slug': 'hw-disk'}, 'interface': {'name': 'hw:interface', 'slug': 'hw-interface'}, 'memory': {'name': 'hw:memory', 'slug': 'hw-memory'}, @@ -32,6 +33,7 @@ class Inventory(): * cpu * raid cards * disks + * gpus methods that: * get local item @@ -438,6 +440,34 @@ class Inventory(): if memory.get('serial') not in [x.serial for x in nb_memories]: self.create_netbox_memory(memory) + def create_netbox_gpus(self): + for gpu in self.lshw.get_hw_linux('gpu'): + manufacturer = self.find_or_create_manufacturer(gpu["vendor"]) + _ = nb.dcim.inventory_items.create( + device=self.device_id, + manufacturer=manufacturer.id, + discovered=True, + tags=[INVENTORY_TAG['gpu']['name']], + name=gpu['product'], + description='GPU {}'.format(gpu['product']), + ) + + logging.info('Creating GPU model {}'.format(gpu['product'])) + + def do_netbox_gpus(self): + gpus = self.lshw.get_hw_linux('gpu') + nb_gpus = self.get_netbox_inventory( + device_id=self.device_id, + tag=INVENTORY_TAG['gpu']['slug'], + ) + + if not len(nb_gpus) or \ + len(nb_gpus) and len(gpus) != len(nb_gpus): + for x in nb_gpus: + x.delete() + + self.create_netbox_gpus() + def create_or_update(self): if config.inventory is None or config.update_inventory is None: return False @@ -447,4 +477,5 @@ class Inventory(): self.do_netbox_disks() self.do_netbox_interfaces() self.do_netbox_motherboard() + self.do_netbox_gpus() return True diff --git a/netbox_agent/lshw.py b/netbox_agent/lshw.py index dadc818..112c489 100644 --- a/netbox_agent/lshw.py +++ b/netbox_agent/lshw.py @@ -22,6 +22,7 @@ class LSHW(): self.cpus = [] self.power = [] self.disks = [] + self.gpus = [] self.vendor = self.hw_info["vendor"] self.product = self.hw_info["product"] self.chassis_serial = self.hw_info["serial"] @@ -53,6 +54,8 @@ class LSHW(): def get_hw_linux(self, hwclass): if hwclass == "cpu": return self.cpus + if hwclass == "gpu": + return self.gpus if hwclass == "network": return self.interfaces if hwclass == 'storage': @@ -132,6 +135,15 @@ class LSHW(): self.memories.append(d) + def find_gpus(self, obj): + if "product" in obj: + c = {} + c["product"] = obj["product"] + c["vendor"] = obj["vendor"] + c["description"] = obj["description"] + + self.gpus.append(c) + def walk_bridge(self, obj): if "children" not in obj: return @@ -139,6 +151,8 @@ class LSHW(): for bus in obj["children"]: if bus["class"] == "storage": self.find_storage(bus) + if bus["class"] == "display": + self.find_gpus(bus) if "children" in bus: for b in bus["children"]: @@ -146,6 +160,8 @@ class LSHW(): self.find_storage(b) if b["class"] == "network": self.find_network(b) + if b["class"] == "display": + self.find_gpus(b) if __name__ == "__main__": From 137728be1f70bd7546de8d5e5b7285b1ff0aee20 Mon Sep 17 00:00:00 2001 From: Cyrinux Date: Fri, 18 Sep 2020 12:29:17 +0200 Subject: [PATCH 044/148] Add "ProLiant BL460c Gen10 Graphics Exp" support and GPU expansion bay (#165) * Add "ProLiant BL460c Gen10 Graphics Exp" * Add GPU expansion support for HP_ProLiant_BL460c_Gen10_Graphics_Exp * Add ProLiant BL460c Graphics Expansion Blade support in README * Dont crash if vendor other than HP * Typo --- README.md | 1 + netbox_agent/server.py | 77 + netbox_agent/vendors/dell.py | 26 + netbox_agent/vendors/generic.py | 26 + netbox_agent/vendors/hp.py | 44 +- netbox_agent/vendors/supermicro.py | 27 + .../HP_ProLiant_BL460c_Gen10_Graphics_Exp | 1798 +++++++++++++++++ tests/server.py | 16 + 8 files changed, 2008 insertions(+), 7 deletions(-) create mode 100644 tests/fixtures/dmidecode/HP_ProLiant_BL460c_Gen10_Graphics_Exp diff --git a/README.md b/README.md index 06a2ba6..9ecf0a2 100644 --- a/README.md +++ b/README.md @@ -203,6 +203,7 @@ Tested on: * HP ProLiant BL460c Gen8 * HP ProLiant BL460c Gen9 * HP ProLiant BL460c Gen10 +* HP ProLiant BL460c Gen10 Graphics Exp its expansion HP ProLiant BL460c Graphics Expansion Blade * HP Moonshot 1500 Enclosure (your `DeviceType` should have slots batch create with `Bay c[1-45n1]`) with HP ProLiant m750, m710x, m510 Server Cartridge ### Pizzas diff --git a/netbox_agent/server.py b/netbox_agent/server.py index 7fee83c..68db8da 100644 --- a/netbox_agent/server.py +++ b/netbox_agent/server.py @@ -121,6 +121,13 @@ class ServerBase(): """ return self.system[0]['Serial Number'].strip() + def get_expansion_service_tag(self): + """ + Return the virtual Service Tag from dmidecode info host + with 'expansion' + """ + return self.system[0]['Serial Number'].strip() + " expansion" + def get_hostname(self): if config.hostname_cmd is None: return '{}'.format(socket.gethostname()) @@ -153,6 +160,9 @@ class ServerBase(): def get_power_consumption(self): raise NotImplementedError + def get_expansion_product(self): + raise NotImplementedError + def _netbox_create_chassis(self, datacenter, tenant, rack): device_type = get_device_type(self.get_chassis()) device_role = get_device_role(config.device.chassis_role) @@ -193,6 +203,28 @@ class ServerBase(): ) return new_blade + def _netbox_create_blade_expansion(self, chassis, datacenter, tenant, rack): + device_role = get_device_role(config.device.blade_role) + device_type = get_device_type(self.get_expansion_product()) + serial = self.get_expansion_service_tag() + hostname = self.get_hostname() + " expansion" + logging.info( + 'Creating expansion (serial: {serial}) {hostname} on chassis {chassis_serial}'.format( + serial=serial, hostname=hostname, chassis_serial=chassis.serial + )) + new_blade = nb.dcim.devices.create( + name=hostname, + serial=serial, + device_role=device_role.id, + device_type=device_type.id, + parent_device=chassis.id, + site=datacenter.id if datacenter else None, + tenant=tenant.id if tenant else None, + rack=rack.id if rack else None, + tags=self.tags, + ) + return new_blade + def _netbox_create_server(self, datacenter, tenant, rack): device_role = get_device_role(config.device.server_role) device_type = get_device_type(self.get_product_name()) @@ -250,6 +282,41 @@ class ServerBase(): slot=slot )) + def _netbox_set_or_update_blade_expansion_slot(self, server, chassis, datacenter): + # before everything check if right chassis + actual_device_bay = server.parent_device.device_bay if server.parent_device else None + actual_chassis = actual_device_bay.device if actual_device_bay else None + slot = self.get_blade_expansion_slot() + if actual_chassis and \ + actual_chassis.serial == chassis.serial and \ + actual_device_bay.name == slot: + return + + server.name += " expansion" + + real_device_bays = nb.dcim.device_bays.filter( + device_id=chassis.id, + name=slot, + ) + if len(real_device_bays) > 0: + logging.info( + 'Setting device expansion ({serial}) new slot on {slot} ' + '(Chassis {chassis_serial})..'.format( + serial=server.serial, slot=slot, chassis_serial=chassis.serial + )) + # reset actual device bay if set + if actual_device_bay: + actual_device_bay.installed_device = None + actual_device_bay.save() + # setup new device bay + real_device_bay = real_device_bays[0] + real_device_bay.installed_device = server + real_device_bay.save() + else: + logging.error('Could not find slot {slot} expansion for chassis'.format( + slot=slot + )) + def netbox_create_or_update(self, config): """ Netbox method to create or update info about our server/blade @@ -300,6 +367,15 @@ class ServerBase(): self.power.create_or_update_power_supply() self.power.report_power_consumption() + if self.own_expansion_slot(): + logging.debug('Update Server expansion...') + expansion = nb.dcim.devices.get(serial=self.get_expansion_service_tag()) + if not expansion: + expansion = self._netbox_create_blade_expansion(chassis, datacenter, tenant, rack) + + # set slot for blade expansion + self._netbox_set_or_update_blade_expansion_slot(expansion, chassis, datacenter) + update = 0 # for every other specs # check hostname @@ -326,6 +402,7 @@ class ServerBase(): print('Rack:', self.get_rack()) print('Netbox Rack:', self.get_netbox_rack()) print('Is blade:', self.is_blade()) + print('Got expansion:', self.own_expansion_slot()) print('Product Name:', self.get_product_name()) print('Chassis:', self.get_chassis()) print('Chassis service tag:', self.get_chassis_service_tag()) diff --git a/netbox_agent/vendors/dell.py b/netbox_agent/vendors/dell.py index 82bad93..b9a3212 100644 --- a/netbox_agent/vendors/dell.py +++ b/netbox_agent/vendors/dell.py @@ -67,3 +67,29 @@ class DellHost(ServerBase): break return value + + def get_expansion_product(self): + """ + Get the extension slot that is on a pair slot number + next to the compute slot that is on an odd slot number + """ + raise NotImplementedError + + def is_expansion_slot(self, server): + """ + Return True if its an extension slot + """ + raise NotImplementedError + + def get_blade_expansion_slot(self): + """ + Expansion slot are always the compute bay number + 1 + """ + raise NotImplementedError + + def own_expansion_slot(self): + """ + Say if the device can host an extension card based + on the product name + """ + pass diff --git a/netbox_agent/vendors/generic.py b/netbox_agent/vendors/generic.py index 6080112..eddd8fa 100644 --- a/netbox_agent/vendors/generic.py +++ b/netbox_agent/vendors/generic.py @@ -21,3 +21,29 @@ class GenericHost(ServerBase): def get_chassis_service_tag(self): return self.get_service_tag() + + def get_expansion_product(self): + """ + Get the extension slot that is on a pair slot number + next to the compute slot that is on an odd slot number + """ + raise NotImplementedError + + def is_expansion_slot(self, server): + """ + Return True if its an extension slot + """ + raise NotImplementedError + + def get_blade_expansion_slot(self): + """ + Expansion slot are always the compute bay number + 1 + """ + raise NotImplementedError + + def own_expansion_slot(self): + """ + Say if the device can host an extension card based + on the product name + """ + pass diff --git a/netbox_agent/vendors/hp.py b/netbox_agent/vendors/hp.py index 23885f2..30d8f07 100644 --- a/netbox_agent/vendors/hp.py +++ b/netbox_agent/vendors/hp.py @@ -11,12 +11,9 @@ class HPHost(ServerBase): self.hp_rack_locator = self._find_rack_locator() def is_blade(self): - if self.product.startswith("ProLiant BL"): - return True - elif self.product.startswith("ProLiant m") and self.product.endswith("Server Cartridge"): - return True - else: - return False + blade = self.product.startswith("ProLiant BL") + blade |= self.product.startswith("ProLiant m") and self.product.endswith("Server Cartridge") + return blade def _find_rack_locator(self): """ @@ -27,7 +24,7 @@ class HPHost(ServerBase): # FIXME: make a dmidecode function get_by_dminame() ? if self.is_blade(): locator = dmidecode.get_by_type(self.dmi, 204) - if self.product == "ProLiant BL460c Gen10": + if self.product.startswith("ProLiant BL460c Gen10"): locator = locator[0]["Strings"] return { "Enclosure Model": locator[2].strip(), @@ -68,3 +65,36 @@ class HPHost(ServerBase): if self.is_blade(): return self.hp_rack_locator["Enclosure Serial"].strip() return self.get_service_tag() + + def get_expansion_product(self): + """ + Get the extension slot that is on a pair slot number + next to the compute slot that is on an odd slot number + I only know on model of slot GPU extension card that. + """ + if self.own_expansion_slot(): + return "ProLiant BL460c Graphics Expansion Blade" + return None + + def is_expansion_slot(self, server): + """ + Return True if its an extension slot, based on the name + """ + return server.name.endswith(" expansion") + + def get_blade_expansion_slot(self): + """ + Expansion slot are always the compute bay number + 1 + """ + if self.is_blade() and self.own_expansion_slot(): + return 'Bay {}'.format( + str(int(self.hp_rack_locator['Server Bay'].strip()) + 1) + ) + return None + + def own_expansion_slot(self): + """ + Say if the device can host an extension card based + on the product name + """ + return self.get_product_name().endswith('Graphics Exp') diff --git a/netbox_agent/vendors/supermicro.py b/netbox_agent/vendors/supermicro.py index 736decf..794a8f5 100644 --- a/netbox_agent/vendors/supermicro.py +++ b/netbox_agent/vendors/supermicro.py @@ -63,3 +63,30 @@ class SupermicroHost(ServerBase): if not self.is_blade(): return None return 'Chassis {}'.format(self.get_chassis_service_tag()) + + def get_expansion_product(self): + """ + Get the extension slot that is on a pair slot number + next to the compute slot that is on an odd slot number + I only know on model of slot GPU extension card that. + """ + raise NotImplementedError + + def is_expansion_slot(self, server): + """ + Return True if its an extension slot, based on the name + """ + raise NotImplementedError + + def get_blade_expansion_slot(self): + """ + Expansion slot are always the compute bay number + 1 + """ + raise NotImplementedError + + def own_expansion_slot(self): + """ + Say if the device can host an extension card based + on the product name + """ + pass diff --git a/tests/fixtures/dmidecode/HP_ProLiant_BL460c_Gen10_Graphics_Exp b/tests/fixtures/dmidecode/HP_ProLiant_BL460c_Gen10_Graphics_Exp new file mode 100644 index 0000000..53b2054 --- /dev/null +++ b/tests/fixtures/dmidecode/HP_ProLiant_BL460c_Gen10_Graphics_Exp @@ -0,0 +1,1798 @@ +# dmidecode 3.1 +Getting SMBIOS data from sysfs. +SMBIOS 3.2 present. +# SMBIOS implementations newer than version 3.1.1 are not +# fully supported by this version of dmidecode. +175 structures occupying 9075 bytes. +Table at 0xA064A000. + +Handle 0x0000, DMI type 194, 5 bytes +OEM-specific Type + Header and Data: + C2 05 00 00 11 + +Handle 0x0001, DMI type 199, 52 bytes +OEM-specific Type + Header and Data: + C7 34 01 00 5E 00 00 02 19 20 02 04 54 06 05 00 + 0F 00 00 03 18 20 08 10 55 06 05 00 21 00 00 04 + 19 20 27 02 56 06 05 00 21 00 00 05 19 20 27 02 + 57 06 05 00 + +Handle 0x0002, DMI type 201, 16 bytes +OEM-specific Type + Header and Data: + C9 10 02 00 10 02 00 00 40 0D 01 00 0E 00 00 80 + +Handle 0x0003, DMI type 0, 26 bytes +BIOS Information + Vendor: HPE + Version: I41 + Release Date: 04/18/2019 + Address: 0xF0000 + Runtime Size: 64 kB + ROM Size: 64 MB + Characteristics: + PCI is supported + PNP is supported + BIOS is upgradeable + BIOS shadowing is allowed + ESCD support is available + Boot from CD is supported + Selectable boot is supported + EDD is supported + 5.25"/360 kB floppy services are supported (int 13h) + 5.25"/1.2 MB floppy services are supported (int 13h) + 3.5"/720 kB floppy services are supported (int 13h) + Print screen service is supported (int 5h) + 8042 keyboard services are supported (int 9h) + Serial services are supported (int 14h) + Printer services are supported (int 17h) + CGA/mono video services are supported (int 10h) + ACPI is supported + USB legacy is supported + BIOS boot specification is supported + Function key-initiated network boot is supported + Targeted content distribution is supported + UEFI is supported + BIOS Revision: 2.4 + Firmware Revision: 1.40 + +Handle 0x0004, DMI type 8, 9 bytes +Port Connector Information + Internal Reference Designator: U44 + Internal Connector Type: Access Bus (USB) + External Reference Designator: USB PORT 3 + External Connector Type: Access Bus (USB) + Port Type: USB + +Handle 0x0005, DMI type 8, 9 bytes +Port Connector Information + Internal Reference Designator: J62 + Internal Connector Type: Access Bus (USB) + External Reference Designator: Front USB #1 + External Connector Type: Access Bus (USB) + Port Type: USB + +Handle 0x0006, DMI type 8, 9 bytes +Port Connector Information + Internal Reference Designator: J62 + Internal Connector Type: Access Bus (USB) + External Reference Designator: Front USB #2 + External Connector Type: Access Bus (USB) + Port Type: USB + +Handle 0x0007, DMI type 8, 9 bytes +Port Connector Information + Internal Reference Designator: J62 + Internal Connector Type: None + External Reference Designator: Com PORT + External Connector Type: DB-9 male + Port Type: Serial Port 16550A Compatible + +Handle 0x0008, DMI type 8, 9 bytes +Port Connector Information + Internal Reference Designator: J1 + Internal Connector Type: Access Bus (USB) + External Reference Designator: Internal USB key #1 + External Connector Type: Access Bus (USB) + Port Type: USB + +Handle 0x0009, DMI type 8, 9 bytes +Port Connector Information + Internal Reference Designator: J3 + Internal Connector Type: Access Bus (USB) + External Reference Designator: USB PORT 8 + External Connector Type: Access Bus (USB) + Port Type: USB + +Handle 0x000A, DMI type 8, 9 bytes +Port Connector Information + Internal Reference Designator: J62 + Internal Connector Type: None + External Reference Designator: Video PORT + External Connector Type: DB-15 female + Port Type: Video Port + +Handle 0x000B, DMI type 8, 9 bytes +Port Connector Information + Internal Reference Designator: J13 + Internal Connector Type: None + External Reference Designator: ILO NIC PORT + External Connector Type: RJ-45 + Port Type: Network Port + +Handle 0x000C, DMI type 16, 23 bytes +Physical Memory Array + Location: System Board Or Motherboard + Use: System Memory + Error Correction Type: Multi-bit ECC + Maximum Capacity: 2 TB + Error Information Handle: Not Provided + Number Of Devices: 8 + +Handle 0x000D, DMI type 16, 23 bytes +Physical Memory Array + Location: System Board Or Motherboard + Use: System Memory + Error Correction Type: Multi-bit ECC + Maximum Capacity: 2 TB + Error Information Handle: Not Provided + Number Of Devices: 8 + +Handle 0x000E, DMI type 38, 18 bytes +IPMI Device Information + Interface Type: KCS (Keyboard Control Style) + Specification Version: 2.0 + I2C Slave Address: 0x10 + NV Storage Device: Not Present + Base Address: 0x0000000000000CA2 (I/O) + Register Spacing: Successive Byte Boundaries + +Handle 0x000F, DMI type 193, 9 bytes +OEM-specific Type + Header and Data: + C1 09 0F 00 01 01 00 02 03 + Strings: + v2.04 (04/18/2019) + + + +Handle 0x0010, DMI type 195, 7 bytes +OEM-specific Type + Header and Data: + C3 07 10 00 01 06 02 + Strings: + $0E11084B + +Handle 0x0011, DMI type 198, 14 bytes +OEM-specific Type + Header and Data: + C6 0E 11 00 01 00 00 00 00 00 01 0A FF FF + +Handle 0x0012, DMI type 215, 6 bytes +OEM-specific Type + Header and Data: + D7 06 12 00 00 05 + +Handle 0x0013, DMI type 223, 11 bytes +OEM-specific Type + Header and Data: + DF 0B 13 00 66 46 70 00 00 00 00 + +Handle 0x0014, DMI type 222, 70 bytes +OEM-specific Type + Header and Data: + DE 46 14 00 01 08 90 00 91 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 + +Handle 0x0015, DMI type 19, 31 bytes +Memory Array Mapped Address + Starting Address: 0x00000000000 + Ending Address: 0x000BFFFFFFF + Range Size: 3 GB + Physical Array Handle: 0x000C + Partition Width: 1 + +Handle 0x0016, DMI type 19, 31 bytes +Memory Array Mapped Address + Starting Address: 0x0000000100000000k + Ending Address: 0x000000203FFFFFFFk + Range Size: 125 GB + Physical Array Handle: 0x000D + Partition Width: 1 + +Handle 0x0017, DMI type 17, 84 bytes +Memory Device + Array Handle: 0x000C + Error Information Handle: Not Provided + Total Width: 72 bits + Data Width: 64 bits + Size: No Module Installed + Form Factor: DIMM + Set: None + Locator: PROC 1 DIMM 1 + Bank Locator: Not Specified + Type: Other + Type Detail: Synchronous + Speed: Unknown + Manufacturer: UNKNOWN + Serial Number: Not Specified + Asset Tag: Not Specified + Part Number: NOT AVAILABLE + Rank: Unknown + Configured Clock Speed: Unknown + Minimum Voltage: Unknown + Maximum Voltage: Unknown + Configured Voltage: Unknown + +Handle 0x0018, DMI type 17, 84 bytes +Memory Device + Array Handle: 0x000C + Error Information Handle: Not Provided + Total Width: 72 bits + Data Width: 64 bits + Size: 32 GB + Form Factor: DIMM + Set: 1 + Locator: PROC 1 DIMM 2 + Bank Locator: Not Specified + Type: DDR4 + Type Detail: Synchronous Registered (Buffered) + Speed: 2666 MT/s + Manufacturer: HPE + Serial Number: Not Specified + Asset Tag: Not Specified + Part Number: 840758-091 + Rank: 2 + Configured Clock Speed: 2666 MT/s + Minimum Voltage: 1.2 V + Maximum Voltage: 1.2 V + Configured Voltage: 1.2 V + +Handle 0x0019, DMI type 17, 84 bytes +Memory Device + Array Handle: 0x000C + Error Information Handle: Not Provided + Total Width: 72 bits + Data Width: 64 bits + Size: 32 GB + Form Factor: DIMM + Set: 2 + Locator: PROC 1 DIMM 3 + Bank Locator: Not Specified + Type: DDR4 + Type Detail: Synchronous Registered (Buffered) + Speed: 2666 MT/s + Manufacturer: HPE + Serial Number: Not Specified + Asset Tag: Not Specified + Part Number: 840758-091 + Rank: 2 + Configured Clock Speed: 2666 MT/s + Minimum Voltage: 1.2 V + Maximum Voltage: 1.2 V + Configured Voltage: 1.2 V + +Handle 0x001A, DMI type 17, 84 bytes +Memory Device + Array Handle: 0x000C + Error Information Handle: Not Provided + Total Width: 72 bits + Data Width: 64 bits + Size: No Module Installed + Form Factor: DIMM + Set: 3 + Locator: PROC 1 DIMM 4 + Bank Locator: Not Specified + Type: Other + Type Detail: Synchronous + Speed: Unknown + Manufacturer: UNKNOWN + Serial Number: Not Specified + Asset Tag: Not Specified + Part Number: NOT AVAILABLE + Rank: Unknown + Configured Clock Speed: Unknown + Minimum Voltage: Unknown + Maximum Voltage: Unknown + Configured Voltage: Unknown + +Handle 0x001B, DMI type 17, 84 bytes +Memory Device + Array Handle: 0x000C + Error Information Handle: Not Provided + Total Width: 72 bits + Data Width: 64 bits + Size: No Module Installed + Form Factor: DIMM + Set: 4 + Locator: PROC 1 DIMM 5 + Bank Locator: Not Specified + Type: Other + Type Detail: Synchronous + Speed: Unknown + Manufacturer: UNKNOWN + Serial Number: Not Specified + Asset Tag: Not Specified + Part Number: NOT AVAILABLE + Rank: Unknown + Configured Clock Speed: Unknown + Minimum Voltage: Unknown + Maximum Voltage: Unknown + Configured Voltage: Unknown + +Handle 0x001C, DMI type 17, 84 bytes +Memory Device + Array Handle: 0x000C + Error Information Handle: Not Provided + Total Width: 72 bits + Data Width: 64 bits + Size: No Module Installed + Form Factor: DIMM + Set: 5 + Locator: PROC 1 DIMM 6 + Bank Locator: Not Specified + Type: Other + Type Detail: Synchronous + Speed: Unknown + Manufacturer: UNKNOWN + Serial Number: Not Specified + Asset Tag: Not Specified + Part Number: NOT AVAILABLE + Rank: Unknown + Configured Clock Speed: Unknown + Minimum Voltage: Unknown + Maximum Voltage: Unknown + Configured Voltage: Unknown + +Handle 0x001D, DMI type 17, 84 bytes +Memory Device + Array Handle: 0x000C + Error Information Handle: Not Provided + Total Width: 72 bits + Data Width: 64 bits + Size: No Module Installed + Form Factor: DIMM + Set: 6 + Locator: PROC 1 DIMM 7 + Bank Locator: Not Specified + Type: Other + Type Detail: Synchronous + Speed: Unknown + Manufacturer: UNKNOWN + Serial Number: Not Specified + Asset Tag: Not Specified + Part Number: NOT AVAILABLE + Rank: Unknown + Configured Clock Speed: Unknown + Minimum Voltage: Unknown + Maximum Voltage: Unknown + Configured Voltage: Unknown + +Handle 0x001E, DMI type 17, 84 bytes +Memory Device + Array Handle: 0x000C + Error Information Handle: Not Provided + Total Width: 72 bits + Data Width: 64 bits + Size: No Module Installed + Form Factor: DIMM + Set: 7 + Locator: PROC 1 DIMM 8 + Bank Locator: Not Specified + Type: Other + Type Detail: Synchronous + Speed: Unknown + Manufacturer: UNKNOWN + Serial Number: Not Specified + Asset Tag: Not Specified + Part Number: NOT AVAILABLE + Rank: Unknown + Configured Clock Speed: Unknown + Minimum Voltage: Unknown + Maximum Voltage: Unknown + Configured Voltage: Unknown + +Handle 0x001F, DMI type 17, 84 bytes +Memory Device + Array Handle: 0x000D + Error Information Handle: Not Provided + Total Width: 72 bits + Data Width: 64 bits + Size: No Module Installed + Form Factor: DIMM + Set: 8 + Locator: PROC 2 DIMM 1 + Bank Locator: Not Specified + Type: Other + Type Detail: Synchronous + Speed: Unknown + Manufacturer: UNKNOWN + Serial Number: Not Specified + Asset Tag: Not Specified + Part Number: NOT AVAILABLE + Rank: Unknown + Configured Clock Speed: Unknown + Minimum Voltage: Unknown + Maximum Voltage: Unknown + Configured Voltage: Unknown + +Handle 0x0020, DMI type 17, 84 bytes +Memory Device + Array Handle: 0x000D + Error Information Handle: Not Provided + Total Width: 72 bits + Data Width: 64 bits + Size: 32 GB + Form Factor: DIMM + Set: 9 + Locator: PROC 2 DIMM 2 + Bank Locator: Not Specified + Type: DDR4 + Type Detail: Synchronous Registered (Buffered) + Speed: 2666 MT/s + Manufacturer: HPE + Serial Number: Not Specified + Asset Tag: Not Specified + Part Number: 840758-091 + Rank: 2 + Configured Clock Speed: 2666 MT/s + Minimum Voltage: 1.2 V + Maximum Voltage: 1.2 V + Configured Voltage: 1.2 V + +Handle 0x0021, DMI type 17, 84 bytes +Memory Device + Array Handle: 0x000D + Error Information Handle: Not Provided + Total Width: 72 bits + Data Width: 64 bits + Size: 32 GB + Form Factor: DIMM + Set: 10 + Locator: PROC 2 DIMM 3 + Bank Locator: Not Specified + Type: DDR4 + Type Detail: Synchronous Registered (Buffered) + Speed: 2666 MT/s + Manufacturer: HPE + Serial Number: Not Specified + Asset Tag: Not Specified + Part Number: 840758-091 + Rank: 2 + Configured Clock Speed: 2666 MT/s + Minimum Voltage: 1.2 V + Maximum Voltage: 1.2 V + Configured Voltage: 1.2 V + +Handle 0x0022, DMI type 17, 84 bytes +Memory Device + Array Handle: 0x000D + Error Information Handle: Not Provided + Total Width: 72 bits + Data Width: 64 bits + Size: No Module Installed + Form Factor: DIMM + Set: 11 + Locator: PROC 2 DIMM 4 + Bank Locator: Not Specified + Type: Other + Type Detail: Synchronous + Speed: Unknown + Manufacturer: UNKNOWN + Serial Number: Not Specified + Asset Tag: Not Specified + Part Number: NOT AVAILABLE + Rank: Unknown + Configured Clock Speed: Unknown + Minimum Voltage: Unknown + Maximum Voltage: Unknown + Configured Voltage: Unknown + +Handle 0x0023, DMI type 17, 84 bytes +Memory Device + Array Handle: 0x000D + Error Information Handle: Not Provided + Total Width: 72 bits + Data Width: 64 bits + Size: No Module Installed + Form Factor: DIMM + Set: 12 + Locator: PROC 2 DIMM 5 + Bank Locator: Not Specified + Type: Other + Type Detail: Synchronous + Speed: Unknown + Manufacturer: UNKNOWN + Serial Number: Not Specified + Asset Tag: Not Specified + Part Number: NOT AVAILABLE + Rank: Unknown + Configured Clock Speed: Unknown + Minimum Voltage: Unknown + Maximum Voltage: Unknown + Configured Voltage: Unknown + +Handle 0x0024, DMI type 17, 84 bytes +Memory Device + Array Handle: 0x000D + Error Information Handle: Not Provided + Total Width: 72 bits + Data Width: 64 bits + Size: No Module Installed + Form Factor: DIMM + Set: 13 + Locator: PROC 2 DIMM 6 + Bank Locator: Not Specified + Type: Other + Type Detail: Synchronous + Speed: Unknown + Manufacturer: UNKNOWN + Serial Number: Not Specified + Asset Tag: Not Specified + Part Number: NOT AVAILABLE + Rank: Unknown + Configured Clock Speed: Unknown + Minimum Voltage: Unknown + Maximum Voltage: Unknown + Configured Voltage: Unknown + +Handle 0x0025, DMI type 17, 84 bytes +Memory Device + Array Handle: 0x000D + Error Information Handle: Not Provided + Total Width: 72 bits + Data Width: 64 bits + Size: No Module Installed + Form Factor: DIMM + Set: 14 + Locator: PROC 2 DIMM 7 + Bank Locator: Not Specified + Type: Other + Type Detail: Synchronous + Speed: Unknown + Manufacturer: UNKNOWN + Serial Number: Not Specified + Asset Tag: Not Specified + Part Number: NOT AVAILABLE + Rank: Unknown + Configured Clock Speed: Unknown + Minimum Voltage: Unknown + Maximum Voltage: Unknown + Configured Voltage: Unknown + +Handle 0x0026, DMI type 17, 84 bytes +Memory Device + Array Handle: 0x000D + Error Information Handle: Not Provided + Total Width: 72 bits + Data Width: 64 bits + Size: No Module Installed + Form Factor: DIMM + Set: 15 + Locator: PROC 2 DIMM 8 + Bank Locator: Not Specified + Type: Other + Type Detail: Synchronous + Speed: Unknown + Manufacturer: UNKNOWN + Serial Number: Not Specified + Asset Tag: Not Specified + Part Number: NOT AVAILABLE + Rank: Unknown + Configured Clock Speed: Unknown + Minimum Voltage: Unknown + Maximum Voltage: Unknown + Configured Voltage: Unknown + +Handle 0x0027, DMI type 237, 9 bytes +OEM-specific Type + Header and Data: + ED 09 27 00 17 00 01 02 03 + Strings: + Unknown + NOT AVAILABLE + NOT AVAILABLE + +Handle 0x0028, DMI type 237, 9 bytes +OEM-specific Type + Header and Data: + ED 09 28 00 18 00 01 02 03 + Strings: + Hynix + HMA84GR7CJR4N-VK + 1260FBE5 + +Handle 0x0029, DMI type 237, 9 bytes +OEM-specific Type + Header and Data: + ED 09 29 00 19 00 01 02 03 + Strings: + Hynix + HMA84GR7CJR4N-VK + 1260FBB7 + +Handle 0x002A, DMI type 237, 9 bytes +OEM-specific Type + Header and Data: + ED 09 2A 00 1A 00 01 02 03 + Strings: + Unknown + NOT AVAILABLE + NOT AVAILABLE + +Handle 0x002B, DMI type 237, 9 bytes +OEM-specific Type + Header and Data: + ED 09 2B 00 1B 00 01 02 03 + Strings: + Unknown + NOT AVAILABLE + NOT AVAILABLE + +Handle 0x002C, DMI type 237, 9 bytes +OEM-specific Type + Header and Data: + ED 09 2C 00 1C 00 01 02 03 + Strings: + Unknown + NOT AVAILABLE + NOT AVAILABLE + +Handle 0x002D, DMI type 237, 9 bytes +OEM-specific Type + Header and Data: + ED 09 2D 00 1D 00 01 02 03 + Strings: + Unknown + NOT AVAILABLE + NOT AVAILABLE + +Handle 0x002E, DMI type 237, 9 bytes +OEM-specific Type + Header and Data: + ED 09 2E 00 1E 00 01 02 03 + Strings: + Unknown + NOT AVAILABLE + NOT AVAILABLE + +Handle 0x002F, DMI type 237, 9 bytes +OEM-specific Type + Header and Data: + ED 09 2F 00 1F 00 01 02 03 + Strings: + Unknown + NOT AVAILABLE + NOT AVAILABLE + +Handle 0x0030, DMI type 237, 9 bytes +OEM-specific Type + Header and Data: + ED 09 30 00 20 00 01 02 03 + Strings: + Hynix + HMA84GR7CJR4N-VK + 1260FBC0 + +Handle 0x0031, DMI type 237, 9 bytes +OEM-specific Type + Header and Data: + ED 09 31 00 21 00 01 02 03 + Strings: + Hynix + HMA84GR7CJR4N-VK + 1260FB43 + +Handle 0x0032, DMI type 237, 9 bytes +OEM-specific Type + Header and Data: + ED 09 32 00 22 00 01 02 03 + Strings: + Unknown + NOT AVAILABLE + NOT AVAILABLE + +Handle 0x0033, DMI type 237, 9 bytes +OEM-specific Type + Header and Data: + ED 09 33 00 23 00 01 02 03 + Strings: + Unknown + NOT AVAILABLE + NOT AVAILABLE + +Handle 0x0034, DMI type 237, 9 bytes +OEM-specific Type + Header and Data: + ED 09 34 00 24 00 01 02 03 + Strings: + Unknown + NOT AVAILABLE + NOT AVAILABLE + +Handle 0x0035, DMI type 237, 9 bytes +OEM-specific Type + Header and Data: + ED 09 35 00 25 00 01 02 03 + Strings: + Unknown + NOT AVAILABLE + NOT AVAILABLE + +Handle 0x0036, DMI type 237, 9 bytes +OEM-specific Type + Header and Data: + ED 09 36 00 26 00 01 02 03 + Strings: + Unknown + NOT AVAILABLE + NOT AVAILABLE + +Handle 0x0037, DMI type 232, 35 bytes +OEM-specific Type + Header and Data: + E8 23 37 00 17 00 10 00 00 00 00 00 00 00 01 00 + 01 00 FF FF FF FF 00 00 00 00 00 00 00 00 FF 00 + 01 FF 00 + +Handle 0x0038, DMI type 232, 35 bytes +OEM-specific Type + Header and Data: + E8 23 38 00 18 00 11 00 00 00 B0 04 B0 04 01 00 + 01 00 FF FF FF FF 00 80 00 00 00 00 00 00 FF 00 + 01 FF 00 + +Handle 0x0039, DMI type 232, 35 bytes +OEM-specific Type + Header and Data: + E8 23 39 00 19 00 11 00 00 00 B0 04 B0 04 01 00 + 01 00 FF FF FF FF 00 80 00 00 00 00 00 00 FF 00 + 01 FF 00 + +Handle 0x003A, DMI type 232, 35 bytes +OEM-specific Type + Header and Data: + E8 23 3A 00 1A 00 10 00 00 00 00 00 00 00 01 00 + 01 00 FF FF FF FF 00 00 00 00 00 00 00 00 FF 00 + 01 FF 00 + +Handle 0x003B, DMI type 232, 35 bytes +OEM-specific Type + Header and Data: + E8 23 3B 00 1B 00 10 00 00 00 00 00 00 00 01 00 + 01 00 FF FF FF FF 00 00 00 00 00 00 00 00 FF 00 + 01 FF 00 + +Handle 0x003C, DMI type 232, 35 bytes +OEM-specific Type + Header and Data: + E8 23 3C 00 1C 00 10 00 00 00 00 00 00 00 01 00 + 01 00 FF FF FF FF 00 00 00 00 00 00 00 00 FF 00 + 01 FF 00 + +Handle 0x003D, DMI type 232, 35 bytes +OEM-specific Type + Header and Data: + E8 23 3D 00 1D 00 10 00 00 00 00 00 00 00 01 00 + 01 00 FF FF FF FF 00 00 00 00 00 00 00 00 FF 00 + 01 FF 00 + +Handle 0x003E, DMI type 232, 35 bytes +OEM-specific Type + Header and Data: + E8 23 3E 00 1E 00 10 00 00 00 00 00 00 00 01 00 + 01 00 FF FF FF FF 00 00 00 00 00 00 00 00 FF 00 + 01 FF 00 + +Handle 0x003F, DMI type 232, 35 bytes +OEM-specific Type + Header and Data: + E8 23 3F 00 1F 00 10 00 00 00 00 00 00 00 01 00 + 01 00 FF FF FF FF 00 00 00 00 00 00 00 00 FF 00 + 01 FF 00 + +Handle 0x0040, DMI type 232, 35 bytes +OEM-specific Type + Header and Data: + E8 23 40 00 20 00 11 00 00 00 B0 04 B0 04 01 00 + 01 00 FF FF FF FF 00 80 00 00 00 00 00 00 FF 00 + 01 FF 00 + +Handle 0x0041, DMI type 232, 35 bytes +OEM-specific Type + Header and Data: + E8 23 41 00 21 00 11 00 00 00 B0 04 B0 04 01 00 + 01 00 FF FF FF FF 00 80 00 00 00 00 00 00 FF 00 + 01 FF 00 + +Handle 0x0042, DMI type 232, 35 bytes +OEM-specific Type + Header and Data: + E8 23 42 00 22 00 10 00 00 00 00 00 00 00 01 00 + 01 00 FF FF FF FF 00 00 00 00 00 00 00 00 FF 00 + 01 FF 00 + +Handle 0x0043, DMI type 232, 35 bytes +OEM-specific Type + Header and Data: + E8 23 43 00 23 00 10 00 00 00 00 00 00 00 01 00 + 01 00 FF FF FF FF 00 00 00 00 00 00 00 00 FF 00 + 01 FF 00 + +Handle 0x0044, DMI type 232, 35 bytes +OEM-specific Type + Header and Data: + E8 23 44 00 24 00 10 00 00 00 00 00 00 00 01 00 + 01 00 FF FF FF FF 00 00 00 00 00 00 00 00 FF 00 + 01 FF 00 + +Handle 0x0045, DMI type 232, 35 bytes +OEM-specific Type + Header and Data: + E8 23 45 00 25 00 10 00 00 00 00 00 00 00 01 00 + 01 00 FF FF FF FF 00 00 00 00 00 00 00 00 FF 00 + 01 FF 00 + +Handle 0x0046, DMI type 232, 35 bytes +OEM-specific Type + Header and Data: + E8 23 46 00 26 00 10 00 00 00 00 00 00 00 01 00 + 01 00 FF FF FF FF 00 00 00 00 00 00 00 00 FF 00 + 01 FF 00 + +Handle 0x0047, DMI type 7, 27 bytes +Cache Information + Socket Designation: L1-Cache + Configuration: Enabled, Not Socketed, Level 1 + Operational Mode: Write Back + Location: Internal + Installed Size: 896 kB + Maximum Size: 896 kB + Supported SRAM Types: + Synchronous + Installed SRAM Type: Synchronous + Speed: Unknown + Error Correction Type: Single-bit ECC + System Type: Unified + Associativity: 8-way Set-associative + +Handle 0x0048, DMI type 7, 27 bytes +Cache Information + Socket Designation: L2-Cache + Configuration: Enabled, Not Socketed, Level 2 + Operational Mode: Varies With Memory Address + Location: Internal + Installed Size: 14336 kB + Maximum Size: 14336 kB + Supported SRAM Types: + Synchronous + Installed SRAM Type: Synchronous + Speed: Unknown + Error Correction Type: Single-bit ECC + System Type: Unified + Associativity: 16-way Set-associative + +Handle 0x0049, DMI type 7, 27 bytes +Cache Information + Socket Designation: L3-Cache + Configuration: Enabled, Not Socketed, Level 3 + Operational Mode: Varies With Memory Address + Location: Internal + Installed Size: 19712 kB + Maximum Size: 19712 kB + Supported SRAM Types: + Synchronous + Installed SRAM Type: Synchronous + Speed: Unknown + Error Correction Type: Single-bit ECC + System Type: Unified + Associativity: Fully Associative + +Handle 0x004A, DMI type 4, 48 bytes +Processor Information + Socket Designation: Proc 1 + Type: Central Processor + Family: Xeon + Manufacturer: Intel(R) Corporation + ID: 54 06 05 00 FF FB EB BF + Signature: Type 0, Family 6, Model 85, Stepping 4 + Flags: + FPU (Floating-point unit on-chip) + VME (Virtual mode extension) + DE (Debugging extension) + PSE (Page size extension) + TSC (Time stamp counter) + MSR (Model specific registers) + PAE (Physical address extension) + MCE (Machine check exception) + CX8 (CMPXCHG8 instruction supported) + APIC (On-chip APIC hardware supported) + SEP (Fast system call) + MTRR (Memory type range registers) + PGE (Page global enable) + MCA (Machine check architecture) + CMOV (Conditional move instruction supported) + PAT (Page attribute table) + PSE-36 (36-bit page size extension) + CLFSH (CLFLUSH instruction supported) + DS (Debug store) + ACPI (ACPI supported) + MMX (MMX technology supported) + FXSR (FXSAVE and FXSTOR instructions supported) + SSE (Streaming SIMD extensions) + SSE2 (Streaming SIMD extensions 2) + SS (Self-snoop) + HTT (Multi-threading) + TM (Thermal monitor supported) + PBE (Pending break enabled) + Version: Intel(R) Xeon(R) Gold 6132 CPU @ 2.60GHz + Voltage: 1.6 V + External Clock: 100 MHz + Max Speed: 4000 MHz + Current Speed: 2600 MHz + Status: Populated, Enabled + Upgrade: Socket LGA3647-1 + L1 Cache Handle: 0x0047 + L2 Cache Handle: 0x0048 + L3 Cache Handle: 0x0049 + Serial Number: Not Specified + Asset Tag: UNKNOWN + Part Number: Not Specified + Core Count: 14 + Core Enabled: 14 + Thread Count: 28 + Characteristics: + 64-bit capable + Multi-Core + Hardware Thread + Execute Protection + Enhanced Virtualization + Power/Performance Control + +Handle 0x004B, DMI type 7, 27 bytes +Cache Information + Socket Designation: L1-Cache + Configuration: Enabled, Not Socketed, Level 1 + Operational Mode: Write Back + Location: Internal + Installed Size: 896 kB + Maximum Size: 896 kB + Supported SRAM Types: + Synchronous + Installed SRAM Type: Synchronous + Speed: Unknown + Error Correction Type: Single-bit ECC + System Type: Unified + Associativity: 8-way Set-associative + +Handle 0x004C, DMI type 7, 27 bytes +Cache Information + Socket Designation: L2-Cache + Configuration: Enabled, Not Socketed, Level 2 + Operational Mode: Varies With Memory Address + Location: Internal + Installed Size: 14336 kB + Maximum Size: 14336 kB + Supported SRAM Types: + Synchronous + Installed SRAM Type: Synchronous + Speed: Unknown + Error Correction Type: Single-bit ECC + System Type: Unified + Associativity: 16-way Set-associative + +Handle 0x004D, DMI type 7, 27 bytes +Cache Information + Socket Designation: L3-Cache + Configuration: Enabled, Not Socketed, Level 3 + Operational Mode: Varies With Memory Address + Location: Internal + Installed Size: 19712 kB + Maximum Size: 19712 kB + Supported SRAM Types: + Synchronous + Installed SRAM Type: Synchronous + Speed: Unknown + Error Correction Type: Single-bit ECC + System Type: Unified + Associativity: Fully Associative + +Handle 0x004E, DMI type 4, 48 bytes +Processor Information + Socket Designation: Proc 2 + Type: Central Processor + Family: Xeon + Manufacturer: Intel(R) Corporation + ID: 54 06 05 00 FF FB EB BF + Signature: Type 0, Family 6, Model 85, Stepping 4 + Flags: + FPU (Floating-point unit on-chip) + VME (Virtual mode extension) + DE (Debugging extension) + PSE (Page size extension) + TSC (Time stamp counter) + MSR (Model specific registers) + PAE (Physical address extension) + MCE (Machine check exception) + CX8 (CMPXCHG8 instruction supported) + APIC (On-chip APIC hardware supported) + SEP (Fast system call) + MTRR (Memory type range registers) + PGE (Page global enable) + MCA (Machine check architecture) + CMOV (Conditional move instruction supported) + PAT (Page attribute table) + PSE-36 (36-bit page size extension) + CLFSH (CLFLUSH instruction supported) + DS (Debug store) + ACPI (ACPI supported) + MMX (MMX technology supported) + FXSR (FXSAVE and FXSTOR instructions supported) + SSE (Streaming SIMD extensions) + SSE2 (Streaming SIMD extensions 2) + SS (Self-snoop) + HTT (Multi-threading) + TM (Thermal monitor supported) + PBE (Pending break enabled) + Version: Intel(R) Xeon(R) Gold 6132 CPU @ 2.60GHz + Voltage: 1.6 V + External Clock: 100 MHz + Max Speed: 4000 MHz + Current Speed: 2600 MHz + Status: Populated, Enabled + Upgrade: Socket LGA3647-1 + L1 Cache Handle: 0x004B + L2 Cache Handle: 0x004C + L3 Cache Handle: 0x004D + Serial Number: Not Specified + Asset Tag: UNKNOWN + Part Number: Not Specified + Core Count: 14 + Core Enabled: 14 + Thread Count: 28 + Characteristics: + 64-bit capable + Multi-Core + Hardware Thread + Execute Protection + Enhanced Virtualization + Power/Performance Control + +Handle 0x004F, DMI type 211, 7 bytes +OEM-specific Type + Header and Data: + D3 07 4F 00 4A 00 0A + +Handle 0x0050, DMI type 211, 7 bytes +OEM-specific Type + Header and Data: + D3 07 50 00 4E 00 0A + +Handle 0x0051, DMI type 227, 22 bytes +OEM-specific Type + Header and Data: + E3 16 51 00 4A 00 17 00 0A A8 01 00 FF FF FF FF + 02 00 00 00 00 00 + +Handle 0x0052, DMI type 227, 22 bytes +OEM-specific Type + Header and Data: + E3 16 52 00 4A 00 18 00 0A A4 01 00 FF FF FF FF + 01 00 00 00 00 00 + +Handle 0x0053, DMI type 227, 22 bytes +OEM-specific Type + Header and Data: + E3 16 53 00 4A 00 19 00 0A A0 01 00 FF FF FF FF + 00 00 00 00 00 00 + +Handle 0x0054, DMI type 227, 22 bytes +OEM-specific Type + Header and Data: + E3 16 54 00 4A 00 1A 00 0A A2 01 00 FF FF FF FF + 00 00 00 00 00 00 + +Handle 0x0055, DMI type 227, 22 bytes +OEM-specific Type + Header and Data: + E3 16 55 00 4A 00 1B 00 09 A2 01 01 FF FF FF FF + 03 00 00 00 00 00 + +Handle 0x0056, DMI type 227, 22 bytes +OEM-specific Type + Header and Data: + E3 16 56 00 4A 00 1C 00 09 A0 01 01 FF FF FF FF + 03 00 00 00 00 00 + +Handle 0x0057, DMI type 227, 22 bytes +OEM-specific Type + Header and Data: + E3 16 57 00 4A 00 1D 00 09 A4 01 01 FF FF FF FF + 04 00 00 00 00 00 + +Handle 0x0058, DMI type 227, 22 bytes +OEM-specific Type + Header and Data: + E3 16 58 00 4A 00 1E 00 09 A8 01 01 FF FF FF FF + 05 00 00 00 00 00 + +Handle 0x0059, DMI type 227, 22 bytes +OEM-specific Type + Header and Data: + E3 16 59 00 4E 00 1F 00 0C A8 01 02 FF FF FF FF + 02 00 00 00 00 00 + +Handle 0x005A, DMI type 227, 22 bytes +OEM-specific Type + Header and Data: + E3 16 5A 00 4E 00 20 00 0C A4 01 02 FF FF FF FF + 01 00 00 00 00 00 + +Handle 0x005B, DMI type 227, 22 bytes +OEM-specific Type + Header and Data: + E3 16 5B 00 4E 00 21 00 0C A0 01 02 FF FF FF FF + 00 00 00 00 00 00 + +Handle 0x005C, DMI type 227, 22 bytes +OEM-specific Type + Header and Data: + E3 16 5C 00 4E 00 22 00 0C A2 01 02 FF FF FF FF + 00 00 00 00 00 00 + +Handle 0x005D, DMI type 227, 22 bytes +OEM-specific Type + Header and Data: + E3 16 5D 00 4E 00 23 00 0B A2 01 03 FF FF FF FF + 03 00 00 00 00 00 + +Handle 0x005E, DMI type 227, 22 bytes +OEM-specific Type + Header and Data: + E3 16 5E 00 4E 00 24 00 0B A0 01 03 FF FF FF FF + 03 00 00 00 00 00 + +Handle 0x005F, DMI type 227, 22 bytes +OEM-specific Type + Header and Data: + E3 16 5F 00 4E 00 25 00 0B A4 01 03 FF FF FF FF + 04 00 00 00 00 00 + +Handle 0x0060, DMI type 227, 22 bytes +OEM-specific Type + Header and Data: + E3 16 60 00 4E 00 26 00 0B A8 01 03 FF FF FF FF + 05 00 00 00 00 00 + +Handle 0x0061, DMI type 197, 26 bytes +OEM-specific Type + Header and Data: + C5 1A 61 00 4A 00 00 07 FF 01 8C 00 00 00 00 00 + 48 55 E3 1F 86 2F 55 94 A0 28 + +Handle 0x0062, DMI type 197, 26 bytes +OEM-specific Type + Header and Data: + C5 1A 62 00 4E 00 20 06 FF 02 8C 00 20 00 00 00 + CF 96 95 A3 86 0C 5E 93 A0 28 + +Handle 0x0063, DMI type 1, 27 bytes +System Information + Manufacturer: HPE + Product Name: ProLiant BL460c Gen10 Graphics Exp + Version: Not Specified + Serial Number: 4242 + UUID: 34333638-3234-5A43-3239-323630424446 + Wake-up Type: Power Switch + SKU Number: 863442-B21 + Family: ProLiant + +Handle 0x0064, DMI type 226, 21 bytes +OEM-specific Type + Header and Data: + E2 15 64 00 38 36 33 34 34 32 43 5A 32 39 32 36 + 30 42 44 46 01 + Strings: + 4242 + +Handle 0x0065, DMI type 244, 20 bytes +OEM-specific Type + Header and Data: + F4 14 65 00 18 00 00 01 00 00 80 00 00 00 00 00 + 00 00 00 00 + +Handle 0x0066, DMI type 244, 20 bytes +OEM-specific Type + Header and Data: + F4 14 66 00 19 00 01 01 00 00 80 00 00 00 00 00 + 00 00 00 00 + +Handle 0x0067, DMI type 244, 20 bytes +OEM-specific Type + Header and Data: + F4 14 67 00 20 00 02 01 00 00 80 00 00 00 00 00 + 00 00 00 00 + +Handle 0x0068, DMI type 244, 20 bytes +OEM-specific Type + Header and Data: + F4 14 68 00 21 00 03 01 00 00 80 00 00 00 00 00 + 00 00 00 00 + +Handle 0x0069, DMI type 210, 12 bytes +OEM-specific Type + Header and Data: + D2 0C 69 00 94 01 00 00 00 00 00 00 + +Handle 0x006A, DMI type 204, 20 bytes +OEM-specific Type + Header and Data: + CC 14 6A 00 01 02 03 04 10 01 05 06 00 00 00 00 + 00 00 00 00 + Strings: + Z04b + blade-a03t + BladeSystem c7000 Enclosure G3 + 13 + 4343 + 10.172.60.194 + +Handle 0x006B, DMI type 229, 52 bytes +OEM-specific Type + Header and Data: + E5 34 6B 00 24 57 48 45 00 00 66 A0 00 00 00 00 + 00 10 01 00 24 53 4D 56 50 F9 BE B7 00 00 00 00 + 08 00 00 00 24 5A 58 54 00 F0 65 A0 00 00 00 00 + A9 00 00 00 + +Handle 0x006C, DMI type 219, 32 bytes +OEM-specific Type + Header and Data: + DB 20 6C 00 CF FB 00 00 0F 00 00 00 00 00 00 00 + 07 98 00 00 00 00 00 00 01 00 00 00 00 00 00 00 + +Handle 0x006D, DMI type 3, 17 bytes +Chassis Information + Manufacturer: HPE + Type: Blade + Lock: Not Present + Version: Not Specified + Serial Number: 4343 + Asset Tag: + Boot-up State: Unknown + Power Supply State: Unknown + Thermal State: Unknown + Security Status: Unknown + OEM Information: 0x00000000 + +Handle 0x006E, DMI type 11, 5 bytes +OEM Strings + String 1: PSF: + String 2: Product ID: 863442-B21 + String 3: OEM String: + +Handle 0x006F, DMI type 216, 23 bytes +OEM-specific Type + Header and Data: + D8 17 6F 00 01 00 01 02 07 02 04 04 12 E3 07 00 + 00 00 00 00 00 00 00 + Strings: + System ROM + v2.04 (04/18/2019) + +Handle 0x0070, DMI type 216, 23 bytes +OEM-specific Type + Header and Data: + D8 17 70 00 02 00 01 02 07 02 04 04 12 E3 07 00 + 00 00 00 00 00 00 00 + Strings: + Redundant System ROM + v2.04 (04/18/2019) + +Handle 0x0071, DMI type 216, 23 bytes +OEM-specific Type + Header and Data: + D8 17 71 00 04 00 01 02 04 10 04 00 00 00 00 00 + 00 00 00 00 00 00 00 + Strings: + Power Management Controller Firmware + 1.0.4 + +Handle 0x0072, DMI type 216, 23 bytes +OEM-specific Type + Header and Data: + D8 17 72 00 05 00 01 02 02 11 00 00 00 00 00 00 + 00 00 00 00 00 00 00 + Strings: + Power Management Controller FW Bootloader + 1.1 + +Handle 0x0073, DMI type 216, 23 bytes +OEM-specific Type + Header and Data: + D8 17 73 00 08 00 01 00 01 1E 1E 00 00 00 00 00 + 00 00 00 00 00 00 00 + Strings: + System Programmable Logic Device + +Handle 0x0074, DMI type 216, 23 bytes +OEM-specific Type + Header and Data: + D8 17 74 00 09 00 01 00 0C 04 00 01 00 04 00 FB + 00 00 00 00 00 00 00 + Strings: + Server Platform Services (SPS) Firmware + +Handle 0x0075, DMI type 216, 23 bytes +OEM-specific Type + Header and Data: + D8 17 75 00 0C 00 01 02 0A 08 05 00 12 00 00 00 + 00 00 00 00 00 00 00 + Strings: + Intelligent Platform Abstraction Data + 8.5.0 Build 18 + +Handle 0x0076, DMI type 216, 23 bytes +OEM-specific Type + Header and Data: + D8 17 76 00 0D 00 01 02 06 3C 00 00 00 00 00 00 + 00 00 00 00 00 00 00 + Strings: + HPE Smart Storage Battery 1 Firmware + 0.60 + +Handle 0x0077, DMI type 216, 23 bytes +OEM-specific Type + Header and Data: + D8 17 77 00 10 00 01 02 09 03 1E D5 00 00 00 00 + 00 00 00 00 00 00 00 + Strings: + Intelligent Provisioning + 3.30.213 + +Handle 0x0078, DMI type 216, 23 bytes +OEM-specific Type + Header and Data: + D8 17 78 00 11 00 01 00 0B 02 00 01 00 00 00 00 + 00 00 00 00 00 00 00 + Strings: + ME SPI Descriptor + +Handle 0x0079, DMI type 216, 23 bytes +OEM-specific Type + Header and Data: + D8 17 79 00 12 00 01 00 0C 00 00 02 00 00 00 0B + 00 00 00 00 00 00 00 + Strings: + Innovation Engine (IE) Firmware + +Handle 0x007A, DMI type 216, 23 bytes +OEM-specific Type + Header and Data: + D8 17 7A 00 30 00 01 02 02 25 00 00 00 00 00 00 + 00 00 00 00 00 00 00 + Strings: + Embedded Video Controller + 2.5 + +Handle 0x007B, DMI type 2, 15 bytes +Base Board Information + Manufacturer: HPE + Product Name: ProLiant BL460c Gen10 Graphics Exp + Version: Not Specified + Serial Number: PWWFB0DLMCC0OG + Asset Tag: + Features: + Board is a hosting board + Board is removable + Board is replaceable + Location In Chassis: Chassis Bay Number: 13 + Chassis Handle: 0x006D + Type: Motherboard + Contained Object Handles: 0 + +Handle 0x007C, DMI type 243, 38 bytes +OEM-specific Type + Header and Data: + F3 26 7C 00 74 00 77 56 4E B3 DC 21 D3 45 87 2B + 42 F7 6F EE 90 53 47 A4 B1 A6 2A 38 4F 5A 3C 10 + 86 80 0A 00 01 01 + +Handle 0x007D, DMI type 224, 12 bytes +OEM-specific Type + Header and Data: + E0 0C 7D 00 00 00 00 01 FE FF 00 00 + +Handle 0x007E, DMI type 41, 11 bytes +Onboard Device + Reference Designation: Embedded FlexibleLOM 1 Port 1 + Type: Ethernet + Status: Enabled + Type Instance: 1 + Bus Address: 0000:37:00.0 + +Handle 0x007F, DMI type 41, 11 bytes +Onboard Device + Reference Designation: Embedded FlexibleLOM 1 Port 2 + Type: Ethernet + Status: Enabled + Type Instance: 2 + Bus Address: 0000:37:00.1 + +Handle 0x0080, DMI type 41, 11 bytes +Onboard Device + Reference Designation: Embedded RAID 1 + Type: SAS Controller + Status: Enabled + Type Instance: 1 + Bus Address: 0000:38:00.0 + +Handle 0x0081, DMI type 41, 11 bytes +Onboard Device + Reference Designation: Embedded Device + Type: Video + Status: Enabled + Type Instance: 1 + Bus Address: 0000:01:00.1 + +Handle 0x0082, DMI type 9, 17 bytes +System Slot Information + Designation: NVMe Slot + Type: x2 M.2 Socket 1-DP + Current Usage: Available + Length: Other + Characteristics: + 3.3 V is provided + Hot-plug devices are supported + SMBus signal is supported + Bus Address: 0000:03:00.0 + +Handle 0x0083, DMI type 9, 17 bytes +System Slot Information + Designation: NVMe Slot + Type: x2 M.2 Socket 1-DP + Current Usage: Available + Length: Other + Characteristics: + 3.3 V is provided + Hot-plug devices are supported + SMBus signal is supported + Bus Address: 0000:04:00.0 + +Handle 0x0084, DMI type 9, 17 bytes +System Slot Information + Designation: Mezzanine Slot 1 + Type: x16 PCI Express 3 + Current Usage: In Use + Length: Other + ID: 1 + Characteristics: + 3.3 V is provided + PME signal is supported + Bus Address: 0000:5c:00.0 + +Handle 0x0085, DMI type 9, 17 bytes +System Slot Information + Designation: Mezzanine Slot 2 + Type: x16 PCI Express 3 + Current Usage: Available + Length: Other + ID: 2 + Characteristics: + 3.3 V is provided + PME signal is supported + Bus Address: 0000:d8:00.0 + +Handle 0x0086, DMI type 236, 21 bytes +OEM-specific Type + Header and Data: + EC 15 86 00 A0 01 00 EA 00 00 00 00 00 00 00 00 + 00 04 04 00 01 + Strings: + Gen10 1x2 SFF MB2 + +Handle 0x0087, DMI type 32, 11 bytes +System Boot Information + Status: No errors detected + +Handle 0x0088, DMI type 203, 34 bytes +OEM-specific Type + Header and Data: + CB 22 88 00 7E 00 FE FF 86 80 F8 10 3C 10 D0 18 + 02 00 FE FF 00 00 03 01 01 01 FF FF 01 02 03 04 + FE FF + Strings: + PciRoot(0x2)/Pci(0x0,0x0)/Pci(0x0,0x0) + NIC.FlexLOM.1.1 + HPE Ethernet 10Gb 2-port 560FLB Adapter - NIC + Embedded FlexibleLOM 1 + +Handle 0x0089, DMI type 203, 34 bytes +OEM-specific Type + Header and Data: + CB 22 89 00 7F 00 FE FF 86 80 F8 10 3C 10 D0 18 + 02 00 FE FF 00 00 03 01 01 02 FF FF 01 02 03 04 + FE FF + Strings: + PciRoot(0x2)/Pci(0x0,0x0)/Pci(0x0,0x1) + NIC.FlexLOM.1.2 + HPE Ethernet 10Gb 2-port 560FLB Adapter - NIC + Embedded FlexibleLOM 1 + +Handle 0x008A, DMI type 203, 34 bytes +OEM-specific Type + Header and Data: + CB 22 8A 00 80 00 FE FF 05 90 8F 02 3C 10 01 07 + 01 07 FE FF 00 00 07 09 01 01 FF FF 01 02 03 04 + FE FF + Strings: + PciRoot(0x2)/Pci(0x2,0x0)/Pci(0x0,0x0) + RAID.Emb.1.1 + HPE Smart Array P204i-b SR Gen10 + Embedded RAID 1 + +Handle 0x008B, DMI type 203, 34 bytes +OEM-specific Type + Header and Data: + CB 22 8B 00 81 00 FE FF 2B 10 38 05 90 15 E4 00 + 03 00 FE FF 00 00 09 01 01 01 FF FF 01 02 03 04 + FE FF + Strings: + PciRoot(0x0)/Pci(0x1C,0x4)/Pci(0x0,0x1) + PCI.Emb.1.1 + Embedded Video Controller + Embedded Video Controller + +Handle 0x008C, DMI type 203, 34 bytes +OEM-specific Type + Header and Data: + CB 22 8C 00 82 00 FE FF FF FF FF FF FF FF FF FF + FF FF FE FF 00 00 10 01 02 01 FF FF 01 02 03 04 + FE FF + Strings: + PciRoot(0x0)/Pci(0x1B,0x0)/Pci(0x0,0x0) + NVMe.Emb.2.1 + Empty Drive Bay 1 + Embedded NVMe M.2 Drive 1 + +Handle 0x008D, DMI type 203, 34 bytes +OEM-specific Type + Header and Data: + CB 22 8D 00 83 00 FE FF FF FF FF FF FF FF FF FF + FF FF FE FF 00 00 10 01 02 02 FF FF 01 02 03 04 + FE FF + Strings: + PciRoot(0x0)/Pci(0x1B,0x2)/Pci(0x0,0x0) + NVMe.Emb.2.2 + Empty Drive Bay 2 + Embedded NVMe M.2 Drive 2 + +Handle 0x008E, DMI type 203, 34 bytes +OEM-specific Type + Header and Data: + CB 22 8E 00 84 00 FE FF DE 10 38 1B DE 10 D9 11 + 03 02 FE FF 00 00 09 0A 01 01 FF FF 01 02 03 04 + FE FF + Strings: + PciRoot(0x3)/Pci(0x0,0x0)/Pci(0x0,0x0) + PCI.Slot.1.1 + Video Controller + Slot 1 + +Handle 0x008F, DMI type 203, 34 bytes +OEM-specific Type + Header and Data: + CB 22 8F 00 85 00 FE FF FF FF FF FF FF FF FF FF + FF FF FE FF 00 00 09 0A 02 01 FF FF 01 02 03 04 + FE FF + Strings: + PciRoot(0x9)/Pci(0x0,0x0)/Pci(0x0,0x0) + PCI.Slot.2.1 + Empty slot 2 + Slot 2 + +Handle 0x0090, DMI type 234, 16 bytes +OEM-specific Type + Header and Data: + EA 10 90 00 FE FF C4 00 01 A4 0C 00 00 00 03 00 + +Handle 0x0091, DMI type 238, 15 bytes +OEM-specific Type + Header and Data: + EE 0F 91 00 04 00 00 A0 05 00 00 01 00 02 01 + Strings: + PciRoot(0x0)/Pci(0x14,0x0)/USB(0x2,0x0) + +Handle 0x0092, DMI type 238, 15 bytes +OEM-specific Type + Header and Data: + EE 0F 92 00 05 00 00 A0 01 00 00 01 00 02 01 + Strings: + PciRoot(0x0)/Pci(0x14,0x0)/USB(0x4,0x0) + +Handle 0x0093, DMI type 238, 15 bytes +OEM-specific Type + Header and Data: + EE 0F 93 00 06 00 00 A0 01 00 00 02 00 02 01 + Strings: + PciRoot(0x0)/Pci(0x14,0x0)/USB(0x5,0x0) + +Handle 0x0094, DMI type 238, 15 bytes +OEM-specific Type + Header and Data: + EE 0F 94 00 08 00 00 A0 00 00 00 01 00 02 01 + Strings: + PciRoot(0x0)/Pci(0x14,0x0)/USB(0x3,0x0) + +Handle 0x0095, DMI type 238, 15 bytes +OEM-specific Type + Header and Data: + EE 0F 95 00 08 00 00 A0 00 00 00 01 00 03 01 + Strings: + PciRoot(0x0)/Pci(0x14,0x0)/USB(0x10,0x0) + +Handle 0x0096, DMI type 238, 15 bytes +OEM-specific Type + Header and Data: + EE 0F 96 00 09 00 00 A0 03 00 00 01 00 02 01 + Strings: + PciRoot(0x0)/Pci(0x14,0x0)/USB(0x7,0x0) + +Handle 0x0097, DMI type 209, 20 bytes +OEM-specific Type + Header and Data: + D1 14 97 00 00 37 48 DF 37 7F 3A 98 01 37 48 DF + 37 7F 3A 99 + +Handle 0x0098, DMI type 239, 23 bytes +OEM-specific Type + Header and Data: + EF 17 98 00 91 00 24 04 00 00 09 00 01 60 26 00 + 00 00 00 01 02 03 04 + Strings: + PciRoot(0x0)/Pci(0x14,0x0)/USB(0x2,0x0) + Unknown.Unknown.1.1 + Unknown Device + Smsc USB 0 + +Handle 0x0099, DMI type 196, 15 bytes +OEM-specific Type + Header and Data: + C4 0F 99 00 00 00 00 00 00 00 01 02 00 01 02 + +Handle 0x009A, DMI type 233, 41 bytes +OEM-specific Type + Header and Data: + E9 29 9A 00 00 00 37 00 48 DF 37 7F 3A 98 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 01 + +Handle 0x009B, DMI type 233, 41 bytes +OEM-specific Type + Header and Data: + E9 29 9B 00 00 00 37 01 48 DF 37 7F 3A 99 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 02 + +Handle 0x009C, DMI type 202, 26 bytes +OEM-specific Type + Header and Data: + CA 1A 9C 00 17 00 FF 01 01 01 00 00 00 01 03 04 + 44 00 00 00 F0 00 00 00 00 00 + +Handle 0x009D, DMI type 202, 26 bytes +OEM-specific Type + Header and Data: + CA 1A 9D 00 18 00 FF 02 01 02 00 00 00 01 02 02 + 42 00 AD 00 F0 00 00 00 00 00 + +Handle 0x009E, DMI type 202, 26 bytes +OEM-specific Type + Header and Data: + CA 1A 9E 00 19 00 FF 03 01 03 00 00 00 01 01 00 + 40 00 AD 00 F0 00 00 00 00 00 + +Handle 0x009F, DMI type 202, 26 bytes +OEM-specific Type + Header and Data: + CA 1A 9F 00 1A 00 FF 04 01 04 00 00 00 01 01 01 + 41 00 00 00 F0 00 00 00 00 00 + +Handle 0x00A0, DMI type 202, 26 bytes +OEM-specific Type + Header and Data: + CA 1A A0 00 1B 00 FF 05 01 05 00 00 00 02 04 07 + 47 00 00 00 F0 00 00 00 00 00 + +Handle 0x00A1, DMI type 202, 26 bytes +OEM-specific Type + Header and Data: + CA 1A A1 00 1C 00 FF 06 01 06 00 00 00 02 04 06 + 46 00 00 00 F0 00 00 00 00 00 + +Handle 0x00A2, DMI type 202, 26 bytes +OEM-specific Type + Header and Data: + CA 1A A2 00 1D 00 FF 07 01 07 00 00 00 02 05 08 + 48 00 00 00 F0 00 00 00 00 00 + +Handle 0x00A3, DMI type 202, 26 bytes +OEM-specific Type + Header and Data: + CA 1A A3 00 1E 00 FF 08 01 08 00 00 00 02 06 0A + 4A 00 00 00 F0 00 00 00 00 00 + +Handle 0x00A4, DMI type 202, 26 bytes +OEM-specific Type + Header and Data: + CA 1A A4 00 1F 00 FF 01 02 09 00 00 00 03 03 10 + 50 00 00 00 F0 00 00 00 00 00 + +Handle 0x00A5, DMI type 202, 26 bytes +OEM-specific Type + Header and Data: + CA 1A A5 00 20 00 FF 02 02 0A 00 00 00 03 02 0E + 4E 00 AD 00 F0 00 00 00 00 00 + +Handle 0x00A6, DMI type 202, 26 bytes +OEM-specific Type + Header and Data: + CA 1A A6 00 21 00 FF 03 02 0B 00 00 00 03 01 0C + 4C 00 AD 00 F0 00 00 00 00 00 + +Handle 0x00A7, DMI type 202, 26 bytes +OEM-specific Type + Header and Data: + CA 1A A7 00 22 00 FF 04 02 0C 00 00 00 03 01 0D + 4D 00 00 00 F0 00 00 00 00 00 + +Handle 0x00A8, DMI type 202, 26 bytes +OEM-specific Type + Header and Data: + CA 1A A8 00 23 00 FF 05 02 0D 00 00 00 04 04 13 + 53 00 00 00 F0 00 00 00 00 00 + +Handle 0x00A9, DMI type 202, 26 bytes +OEM-specific Type + Header and Data: + CA 1A A9 00 24 00 FF 06 02 0E 00 00 00 04 04 12 + 52 00 00 00 F0 00 00 00 00 00 + +Handle 0x00AA, DMI type 202, 26 bytes +OEM-specific Type + Header and Data: + CA 1A AA 00 25 00 FF 07 02 0F 00 00 00 04 05 14 + 54 00 00 00 F0 00 00 00 00 00 + +Handle 0x00AB, DMI type 202, 26 bytes +OEM-specific Type + Header and Data: + CA 1A AB 00 26 00 FF 08 02 10 00 00 00 04 06 16 + 56 00 00 00 F0 00 00 00 00 00 + +Handle 0x00AC, DMI type 240, 39 bytes +OEM-specific Type + Header and Data: + F0 27 AC 00 88 00 00 EC 07 01 01 E7 6A 04 00 00 + 00 00 00 03 00 00 00 00 00 00 00 03 00 00 00 00 + 00 00 00 03 00 00 00 + Strings: + 1.2028.0 + +Handle 0x00AD, DMI type 240, 39 bytes +OEM-specific Type + Header and Data: + F0 27 AD 00 8A 00 31 2E 39 38 01 00 00 80 00 00 + 00 00 00 03 00 00 00 00 00 00 00 03 00 00 00 00 + 00 00 00 00 00 00 00 + Strings: + 1.98 + +Handle 0xFEFF, DMI type 127, 4 bytes +End Of Table + diff --git a/tests/server.py b/tests/server.py index 64cc6e6..3d024a2 100644 --- a/tests/server.py +++ b/tests/server.py @@ -19,6 +19,7 @@ def test_init(fixture): 'HP_BL460c_Gen9', 'HP_DL380p_Gen8', 'HP_SL4540_Gen8' + 'HP_ProLiant_BL460c_Gen10_Graphics_Exp' ]) def test_hp_service_tag(fixture): dmi = parse(fixture) @@ -36,6 +37,7 @@ def test_moonshot_blade(fixture): assert server.get_service_tag() == 'CN66480BLA' assert server.get_chassis_service_tag() == 'CZ3702MD5K' assert server.is_blade() is True + assert server.own_expansion_slot() is False @parametrize_with_fixtures( @@ -89,3 +91,17 @@ def test_generic_host_product_name(fixture): dmi = parse(fixture) server = ServerBase(dmi) assert server.get_product_name() == 'SR' + + +@parametrize_with_fixtures( + 'dmidecode/', only_filenames=[ + 'HP_ProLiant_BL460c_Gen10_Graphics_Exp' + ]) +def test_hp_blade_with_gpu_expansion(fixture): + dmi = parse(fixture) + server = HPHost(dmi) + assert server.get_service_tag() == '4242' + assert server.get_chassis_service_tag() == '4343' + assert server.is_blade() is True + assert server.own_expansion_slot() is True + assert server.get_expansion_service_tag() == '4242 expansion' From 7999244096b0e803e95bcac430b783449073a4fd Mon Sep 17 00:00:00 2001 From: Cyrinux Date: Wed, 14 Oct 2020 12:39:40 +0200 Subject: [PATCH 045/148] Check if lldpctl present, and log debug if no lldpctl output. (#171) --- netbox_agent/lldp.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/netbox_agent/lldp.py b/netbox_agent/lldp.py index 8cb290c..a4c3fa9 100644 --- a/netbox_agent/lldp.py +++ b/netbox_agent/lldp.py @@ -1,8 +1,12 @@ import subprocess +import logging +from netbox_agent.misc import is_tool class LLDP(): def __init__(self, output=None): + if not is_tool('lldpctl'): + logging.debug('lldpd package seems to be missing or daemon not running.') if output: self.output = output else: @@ -40,6 +44,8 @@ class LLDP(): current_dict[final] = value for interface, vlan in vlans.items(): output_dict['lldp'][interface]['vlan'] = vlan + if not output_dict: + logging.debug('No LLDP output, please check your network config.') return output_dict def get_switch_ip(self, interface): From 00653628c615523f434a332ae670a9024dc571d5 Mon Sep 17 00:00:00 2001 From: Cyrinux Date: Wed, 14 Oct 2020 12:39:57 +0200 Subject: [PATCH 046/148] Truncate GPU product name to 50 characteres. (#169) Example: Hi1710 [iBMC Intelligent Management system chip w/VGA support] This product is too long, the api want max_length = 50 ... --- netbox_agent/inventory.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/netbox_agent/inventory.py b/netbox_agent/inventory.py index 97e7ec4..e10c34f 100644 --- a/netbox_agent/inventory.py +++ b/netbox_agent/inventory.py @@ -442,6 +442,8 @@ class Inventory(): def create_netbox_gpus(self): for gpu in self.lshw.get_hw_linux('gpu'): + if 'product' in gpu and len(gpu['product']) > 50: + gpu['product'] = (gpu['product'][:48] + '..') manufacturer = self.find_or_create_manufacturer(gpu["vendor"]) _ = nb.dcim.inventory_items.create( device=self.device_id, From 0f2cb531ae74226ff49bcaaaef1fc3c8193833ce Mon Sep 17 00:00:00 2001 From: Cyrinux Date: Wed, 14 Oct 2020 12:40:19 +0200 Subject: [PATCH 047/148] Fix inventory crash on nvme binary too old or absent (#170) * When nvme binary absent or too old and no json output, this crash the inventory, i prefer just pass nvme inventory and continue * log if nvme-cli is not installed --- netbox_agent/lshw.py | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/netbox_agent/lshw.py b/netbox_agent/lshw.py index 112c489..0a5a870 100644 --- a/netbox_agent/lshw.py +++ b/netbox_agent/lshw.py @@ -88,22 +88,28 @@ class LSHW(): self.disks.append(d) elif "nvme" in obj["configuration"]["driver"]: - nvme = json.loads( - subprocess.check_output( - ["nvme", '-list', '-o', 'json'], - encoding='utf8') - ) + if not is_tool('nvme'): + logging.error('nvme-cli >= 1.0 does not seem to be installed') + else: + try: + nvme = json.loads( + subprocess.check_output( + ["nvme", '-list', '-o', 'json'], + encoding='utf8') + ) - for device in nvme["Devices"]: - d = {} - d['logicalname'] = device["DevicePath"] - d['product'] = device["ModelNumber"] - d['serial'] = device["SerialNumber"] - d["version"] = device["Firmware"] - d['size'] = device["UsedSize"] - d['description'] = "NVME Disk" + for device in nvme["Devices"]: + d = {} + d['logicalname'] = device["DevicePath"] + d['product'] = device["ModelNumber"] + d['serial'] = device["SerialNumber"] + d["version"] = device["Firmware"] + d['size'] = device["UsedSize"] + d['description'] = "NVME Disk" - self.disks.append(d) + self.disks.append(d) + except Exception: + pass def find_cpus(self, obj): if "product" in obj: From 77a84b365fd92a2d071fcaa5845c4eb5d5512f99 Mon Sep 17 00:00:00 2001 From: Solvik Date: Tue, 15 Dec 2020 11:32:11 +0100 Subject: [PATCH 048/148] fix ugly supermicro blade support (#178) --- netbox_agent/vendors/supermicro.py | 10 +++++++--- tests/server.py | 2 ++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/netbox_agent/vendors/supermicro.py b/netbox_agent/vendors/supermicro.py index 794a8f5..0042377 100644 --- a/netbox_agent/vendors/supermicro.py +++ b/netbox_agent/vendors/supermicro.py @@ -21,7 +21,7 @@ class SupermicroHost(ServerBase): self.manufacturer = 'Supermicro' def is_blade(self): - product_name = self.get_product_name() + product_name = self.system[0]['Product Name'].strip() # Blades blade = product_name.startswith('SBI') blade |= product_name.startswith('SBA') @@ -44,19 +44,23 @@ class SupermicroHost(ServerBase): return None def get_service_tag(self): + if self.is_blade(): + return self.baseboard[0]['Serial Number'].strip() return self.system[0]['Serial Number'].strip() def get_product_name(self): + if self.is_blade(): + return self.baseboard[0]['Product Name'].strip() return self.system[0]['Product Name'].strip() def get_chassis(self): if self.is_blade(): - return self.chassis[0]['Product Name'].strip() + return self.system[0]['Product Name'].strip() return self.get_product_name() def get_chassis_service_tag(self): if self.is_blade(): - return self.chassis[0]['Serial Number'].strip() + return self.system[0]['Serial Number'].strip() return self.get_service_tag() def get_chassis_name(self): diff --git a/tests/server.py b/tests/server.py index 3d024a2..541e2f7 100644 --- a/tests/server.py +++ b/tests/server.py @@ -49,6 +49,7 @@ def test_supermicro_blade(fixture): server = SupermicroHost(dmi) assert server.get_service_tag() == 'E235735X6B01665' assert server.get_chassis_service_tag() == 'C9390AF40A20098' + assert server.get_chassis() == 'SYS-5039MS-H12TRF-OS012' assert server.is_blade() is True @@ -60,6 +61,7 @@ def test_supermicro_pizza(fixture): dmi = parse(fixture) server = SupermicroHost(dmi) assert server.get_service_tag() == 'A177950X7709591' + assert server.get_chassis() == 'SYS-6018R-TDTPR' assert server.is_blade() is False From 2a1196b52d6cfdd743d7471ad0bc0d45145666b8 Mon Sep 17 00:00:00 2001 From: Laurent Marchaud <16262531+Aluxima@users.noreply.github.com> Date: Fri, 18 Dec 2020 17:23:37 +0100 Subject: [PATCH 049/148] Add support for Supermicro TwinPro devices (#180) --- netbox_agent/vendors/supermicro.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/netbox_agent/vendors/supermicro.py b/netbox_agent/vendors/supermicro.py index 0042377..32e8fdf 100644 --- a/netbox_agent/vendors/supermicro.py +++ b/netbox_agent/vendors/supermicro.py @@ -27,6 +27,8 @@ class SupermicroHost(ServerBase): blade |= product_name.startswith('SBA') # Twin blade |= 'TR-' in product_name + # TwinPro + blade |= 'TP-' in product_name # BigTwin blade |= 'BT-' in product_name # Microcloud From 9e934af83582dd744c207bf65f0fa744a464a498 Mon Sep 17 00:00:00 2001 From: Mark David <44349634+markd69@users.noreply.github.com> Date: Mon, 15 Feb 2021 04:54:47 -0500 Subject: [PATCH 050/148] Remove an invalid character (#182) Invalid character that causes some errors to throw if you're copying and pasting the config --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9ecf0a2..12eb900 100644 --- a/README.md +++ b/README.md @@ -118,7 +118,7 @@ network: #tenant: # driver: "file:/tmp/tenant" # regex: "(.*)" - + ## Enable virtual machine support # virtual: # # not mandatory, can be guessed From 7147670255fedff0dc9d01fc40c43bac544361e3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 26 Mar 2021 16:00:40 +0100 Subject: [PATCH 051/148] Update dependency pyyaml to v5.4.1 (#181) Co-authored-by: Renovate Bot --- requirements.txt | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 9540b2f..4eb7c7c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ pynetbox==5.0.5 netaddr==0.8.0 netifaces==0.10.9 -pyyaml==5.3.1 +pyyaml==5.4.1 jsonargparse==2.32.2 diff --git a/setup.py b/setup.py index d74fe86..aded5f0 100644 --- a/setup.py +++ b/setup.py @@ -17,7 +17,7 @@ setup( 'pynetbox==5.0.5', 'netaddr==0.8.0', 'netifaces==0.10.9', - 'pyyaml==5.3.1', + 'pyyaml==5.4.1', 'jsonargparse==2.32.2', ], zip_safe=False, From c4eb8f34ac4cde6616a8a0e32d7b0d3114af192c Mon Sep 17 00:00:00 2001 From: Solvik Date: Wed, 12 May 2021 14:50:16 +0100 Subject: [PATCH 052/148] add 25G compatibility (#185) --- netbox_agent/network.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/netbox_agent/network.py b/netbox_agent/network.py index 334ae01..986bc60 100644 --- a/netbox_agent/network.py +++ b/netbox_agent/network.py @@ -177,14 +177,19 @@ class Network(object): return self.dcim_choices['interface:type']['Other'] if nic['ethtool']['speed'] == '10000Mb/s': - if nic['ethtool']['port'] == 'FIBRE': + if nic['ethtool']['port'] in ('FIBRE', 'Direct Attach Copper'): return self.dcim_choices['interface:type']['SFP+ (10GE)'] return self.dcim_choices['interface:type']['10GBASE-T (10GE)'] + elif nic['ethtool']['speed'] == '25000Mb/s': + if nic['ethtool']['port'] in ('FIBRE', 'Direct Attach Copper'): + return self.dcim_choices['interface:type']['SFP28 (25GE)'] + elif nic['ethtool']['speed'] == '1000Mb/s': - if nic['ethtool']['port'] == 'FIBRE': + if nic['ethtool']['port'] in ('FIBRE', 'Direct Attach Copper'): return self.dcim_choices['interface:type']['SFP (1GE)'] return self.dcim_choices['interface:type']['1000BASE-T (1GE)'] + return self.dcim_choices['interface:type']['Other'] def get_or_create_vlan(self, vlan_id): From 51efa8edbaf33ecf953ccaa02f3adea69233c3fe Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 12 May 2021 15:50:28 +0200 Subject: [PATCH 053/148] Update dependency jsonargparse to v3 (#179) Co-authored-by: Renovate Bot --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 4eb7c7c..7172368 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,4 @@ pynetbox==5.0.5 netaddr==0.8.0 netifaces==0.10.9 pyyaml==5.4.1 -jsonargparse==2.32.2 +jsonargparse==3.11.2 From dc582b5de69d6e4c375b2e9f4aa5c07dd67999e9 Mon Sep 17 00:00:00 2001 From: Solvik Blum Date: Sat, 28 Nov 2020 12:45:32 +0100 Subject: [PATCH 054/148] change .interface foreignkey with .assigned_object --- netbox_agent/network.py | 95 ++++++++++++++++++++++++++++++----------- 1 file changed, 69 insertions(+), 26 deletions(-) diff --git a/netbox_agent/network.py b/netbox_agent/network.py index 986bc60..0b66594 100644 --- a/netbox_agent/network.py +++ b/netbox_agent/network.py @@ -15,6 +15,7 @@ from netbox_agent.lldp import LLDP class Network(object): def __init__(self, server, *args, **kwargs): + self.netbox_version = float(nb.version) self.nics = [] self.server = server @@ -318,10 +319,15 @@ class Network(object): if not len(netbox_ips): logging.info('Create new IP {ip} on {interface}'.format( ip=ip, interface=interface)) + query_params = { + 'address': ip, + 'status': "active", + } + if self.netbox_version > 2.8: + query_params.update({self.intf_type: "assigned_object_id"}) + netbox_ip = nb.ipam.ip_addresses.create( - address=ip, - interface=interface.id, - status=1, + **query_params ) else: netbox_ip = netbox_ips[0] @@ -340,30 +346,49 @@ class Network(object): # or if everything is assigned to other servers elif not len(assigned_anycast_ip): logging.info('Creating Anycast IP {} and assigning it to interface'.format(ip)) - netbox_ip = nb.ipam.ip_addresses.create( - address=ip, - interface=interface.id, - status=1, - role=self.ipam_choices['ip-address:role']['Anycast'], - tenant=self.tenant.id if self.tenant else None, - ) + query_params = { + "address": ip, + "status": "active", + "role": self.ipam_choices['ip-address:role']['Anycast'], + "tenant": self.tenant.id if self.tenant else None, + } + if self.netbox_version > 2.8: + query_params.update({ + 'assigned_object_type': self.assigned_object_type, + 'assigned_object_id': interface.id + }) + else: + query_params.update({'interface': interface.id}) + netbox_ip = nb.ipam.ip_addresses.create(**query_params) return netbox_ip else: - if netbox_ip.interface is None: + if hasattr(netbox_ip, 'interface') and netbox_ip.interface is None or \ + hasattr(netbox_ip, 'assigned_object') and netbox_ip.assigned_object is None: logging.info('Assigning existing IP {ip} to {interface}'.format( ip=ip, interface=interface)) - elif netbox_ip.interface.id != interface.id: + elif hasattr(netbox_ip, 'interface') and netbox_ip.interface.id != interface.id or \ + hasattr(netbox_ip, 'assigned_object') and netbox_ip.assigned_object_id != interface.id: + if self.netbox_version > 2.8: + old_interface = netbox_ip.assigned_object + else: + old_interface = netbox_ip.interface + logging.info( 'Detected interface change for ip {ip}: old interface is ' '{old_interface} (id: {old_id}), new interface is {new_interface} ' ' (id: {new_id})' .format( - old_interface=netbox_ip.interface, new_interface=interface, + old_interface=old_interface, new_interface=interface, old_id=netbox_ip.id, new_id=interface.id, ip=netbox_ip.address )) else: return netbox_ip - netbox_ip.interface = interface + + if self.netbox_version > 2.8: + netbox_ip.assigned_object_type = self.assigned_object_type + netbox_ip.assigned_object_id = interface.id + else: + netbox_ip.interface = interface netbox_ip.save() return netbox_ip @@ -385,17 +410,29 @@ class Network(object): # delete IP on netbox that are not known on this server if len(nb_nics): - netbox_ips = nb.ipam.ip_addresses.filter( - interface_id=[x.id for x in nb_nics], - ) + if self.netbox_version > 2.8: + netbox_ips = nb.ipam.ip_addresses.filter( + assigned_object=[x.id for x in nb_nics], + ) + else: + netbox_ips = nb.ipam.ip_addresses.filter( + interface_id=[x.id for x in nb_nics], + ) + all_local_ips = list(chain.from_iterable([ x['ip'] for x in self.nics if x['ip'] is not None ])) for netbox_ip in netbox_ips: if netbox_ip.address not in all_local_ips: - logging.info('Unassigning IP {ip} from {interface}'.format( - ip=netbox_ip.address, interface=netbox_ip.interface)) - netbox_ip.interface = None + if self.netbox_version < 2.9: + logging.info('Unassigning IP {ip} from {interface}'.format( + ip=netbox_ip.address, interface=netbox_ip.interface)) + netbox_ip.interface = None + else: + logging.info('Unassigning IP {ip} from {interface}'.format( + ip=netbox_ip.address, interface=netbox_ip.assigned_object)) + netbox_ip.assigned_object_type = None + netbox_ip.assigned_object_id = None netbox_ip.save() # update each nic @@ -417,12 +454,13 @@ class Network(object): ret, interface = self.reset_vlan_on_interface(nic, interface) nic_update += ret - _type = self.get_netbox_type_for_nic(nic) - if not interface.type or \ - _type != interface.type.value: - logging.info('Interface type is wrong, resetting') - interface.type = _type - nic_update += 1 + if hasattr(interface, 'type'): + _type = self.get_netbox_type_for_nic(nic) + if not interface.type or \ + _type != interface.type.value: + logging.info('Interface type is wrong, resetting') + interface.type = _type + nic_update += 1 if hasattr(interface, 'lag') and interface.lag is not None: local_lag_int = next( @@ -465,6 +503,9 @@ class ServerNetwork(Network): self.nb_net = nb.dcim self.custom_arg = {'device': getattr(self.device, "id", None)} self.custom_arg_id = {'device_id': getattr(self.device, "id", None)} + self.intf_type = "interface_id" + self.assigned_object_type = "dcim.interface" + def get_network_type(self): return 'server' @@ -585,6 +626,8 @@ class VirtualNetwork(Network): self.nb_net = nb.virtualization self.custom_arg = {'virtual_machine': getattr(self.device, "id", None)} self.custom_arg_id = {'virtual_machine_id': getattr(self.device, "id", None)} + self.intf_type = "vminterface_id" + self.assigned_object_type = "virtualization.vminterface" dcim_c = nb.virtualization.interfaces.choices() for _choice_type in dcim_c: From 1d98d3c8e997f7bc92f8849b53fee4171384126d Mon Sep 17 00:00:00 2001 From: Solvik Blum Date: Sat, 28 Nov 2020 13:49:44 +0100 Subject: [PATCH 055/148] fix ip association --- netbox_agent/network.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/netbox_agent/network.py b/netbox_agent/network.py index 0b66594..15c72d0 100644 --- a/netbox_agent/network.py +++ b/netbox_agent/network.py @@ -324,7 +324,12 @@ class Network(object): 'status': "active", } if self.netbox_version > 2.8: - query_params.update({self.intf_type: "assigned_object_id"}) + query_params.update({ + 'assigned_object_type': self.assigned_object_type, + 'assigned_object_id': interface.id + }) + else: + query_params.update({'interface_id': interface.id}) netbox_ip = nb.ipam.ip_addresses.create( **query_params @@ -412,7 +417,7 @@ class Network(object): if len(nb_nics): if self.netbox_version > 2.8: netbox_ips = nb.ipam.ip_addresses.filter( - assigned_object=[x.id for x in nb_nics], + **{self.intf_type: [x.id for x in nb_nics]} ) else: netbox_ips = nb.ipam.ip_addresses.filter( From 3639662961ff2e603f8830a07e00b4549cba7292 Mon Sep 17 00:00:00 2001 From: Solvik Blum Date: Tue, 15 Dec 2020 12:47:47 +0100 Subject: [PATCH 056/148] ignore tag update for Netbox version >= 2.8 --- netbox_agent/misc.py | 3 ++- netbox_agent/server.py | 14 ++++++++------ setup.py | 2 +- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/netbox_agent/misc.py b/netbox_agent/misc.py index 7656f04..f7d15e6 100644 --- a/netbox_agent/misc.py +++ b/netbox_agent/misc.py @@ -2,6 +2,7 @@ import socket import subprocess from shutil import which +from slugify import slugify from netbox_agent.config import netbox_instance as nb @@ -67,5 +68,5 @@ def create_netbox_tags(tags): if not nb_tag: nb_tag = nb.extras.tags.create( name=tag, - slug=tag, + slug=slugify(tag), ) diff --git a/netbox_agent/server.py b/netbox_agent/server.py index 68db8da..6051855 100644 --- a/netbox_agent/server.py +++ b/netbox_agent/server.py @@ -28,7 +28,7 @@ class ServerBase(): self.network = None - self.tags = list(set(config.device.tags.split(','))) if config.device.tags else [] + self.tags = list(set([x.strip() for x in config.device.tags.split(',') if x.strip()])) if config.device.tags else [] if self.tags and len(self.tags): create_netbox_tags(self.tags) @@ -177,7 +177,7 @@ class ServerBase(): site=datacenter.id if datacenter else None, tenant=tenant.id if tenant else None, rack=rack.id if rack else None, - tags=self.tags, + tags=[{'name': x} for x in self.tags], ) return new_chassis @@ -199,7 +199,7 @@ class ServerBase(): site=datacenter.id if datacenter else None, tenant=tenant.id if tenant else None, rack=rack.id if rack else None, - tags=self.tags, + tags=[{'name': x} for x in self.tags], ) return new_blade @@ -221,7 +221,7 @@ class ServerBase(): site=datacenter.id if datacenter else None, tenant=tenant.id if tenant else None, rack=rack.id if rack else None, - tags=self.tags, + tags=[{'name': x} for x in self.tags], ) return new_blade @@ -242,7 +242,7 @@ class ServerBase(): site=datacenter.id if datacenter else None, tenant=tenant.id if tenant else None, rack=rack.id if rack else None, - tags=self.tags, + tags=[{'name': x} for x in self.tags], ) return new_server @@ -383,9 +383,11 @@ class ServerBase(): update += 1 server.name = self.get_hostname() - if sorted(set(server.tags)) != sorted(set(self.tags)): + if float(nb.version) < 2.8 and sorted(set(server.tags)) != sorted(set(self.tags)): server.tags = self.tags update += 1 + else: + logging.warning("netbox-agent doesn't support tag updates for Netbox version >=2.8") if config.update_all or config.update_location: ret, server = self.update_netbox_location(server) diff --git a/setup.py b/setup.py index aded5f0..8b22f05 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ setup( packages=find_packages(exclude=["*.tests", "*.tests.*", "tests.*", "tests"]), use_scm_version=True, install_requires=[ - 'pynetbox==5.0.5', + 'pynetbox==5.1.0', 'netaddr==0.8.0', 'netifaces==0.10.9', 'pyyaml==5.4.1', From 58c18fc2da90aa64b603c5e02d88eaee68e9aefe Mon Sep 17 00:00:00 2001 From: Solvik Blum Date: Tue, 11 May 2021 20:42:13 +0200 Subject: [PATCH 057/148] add more compatibility, now to netbox 2.10 --- netbox_agent/misc.py | 3 +++ netbox_agent/network.py | 26 +++++++++++++++----------- netbox_agent/power.py | 2 +- netbox_agent/server.py | 15 +++++++++------ 4 files changed, 28 insertions(+), 18 deletions(-) diff --git a/netbox_agent/misc.py b/netbox_agent/misc.py index f7d15e6..208a930 100644 --- a/netbox_agent/misc.py +++ b/netbox_agent/misc.py @@ -61,6 +61,7 @@ def get_hostname(config): def create_netbox_tags(tags): + ret = [] for tag in tags: nb_tag = nb.extras.tags.get( name=tag @@ -70,3 +71,5 @@ def create_netbox_tags(tags): name=tag, slug=slugify(tag), ) + ret.append(nb_tag) + return ret diff --git a/netbox_agent/network.py b/netbox_agent/network.py index 15c72d0..558a80e 100644 --- a/netbox_agent/network.py +++ b/netbox_agent/network.py @@ -12,10 +12,11 @@ from netbox_agent.ethtool import Ethtool from netbox_agent.ipmi import IPMI from netbox_agent.lldp import LLDP +from packaging import version class Network(object): def __init__(self, server, *args, **kwargs): - self.netbox_version = float(nb.version) + self.netbox_version = nb.version self.nics = [] self.server = server @@ -323,7 +324,7 @@ class Network(object): 'address': ip, 'status': "active", } - if self.netbox_version > 2.8: + if version.parse(self.netbox_version) > version.parse('2.8'): query_params.update({ 'assigned_object_type': self.assigned_object_type, 'assigned_object_id': interface.id @@ -335,7 +336,7 @@ class Network(object): **query_params ) else: - netbox_ip = netbox_ips[0] + netbox_ip = list(netbox_ips)[0] # If IP exists in anycast if netbox_ip.role and netbox_ip.role.label == 'Anycast': logging.debug('IP {} is Anycast..'.format(ip)) @@ -357,7 +358,7 @@ class Network(object): "role": self.ipam_choices['ip-address:role']['Anycast'], "tenant": self.tenant.id if self.tenant else None, } - if self.netbox_version > 2.8: + if version.parse(self.netbox_version) > version.parse('2.8'): query_params.update({ 'assigned_object_type': self.assigned_object_type, 'assigned_object_id': interface.id @@ -373,7 +374,7 @@ class Network(object): ip=ip, interface=interface)) elif hasattr(netbox_ip, 'interface') and netbox_ip.interface.id != interface.id or \ hasattr(netbox_ip, 'assigned_object') and netbox_ip.assigned_object_id != interface.id: - if self.netbox_version > 2.8: + if version.parse(self.netbox_version) > version.parse('2.8'): old_interface = netbox_ip.assigned_object else: old_interface = netbox_ip.interface @@ -389,7 +390,7 @@ class Network(object): else: return netbox_ip - if self.netbox_version > 2.8: + if version.parse(self.netbox_version) > version.parse('2.8'): netbox_ip.assigned_object_type = self.assigned_object_type netbox_ip.assigned_object_id = interface.id else: @@ -403,9 +404,9 @@ class Network(object): logging.debug('Creating/Updating NIC...') # delete unknown interface - nb_nics = self.get_netbox_network_cards() + nb_nics = list(self.get_netbox_network_cards()) local_nics = [x['name'] for x in self.nics] - for nic in nb_nics[:]: + for nic in nb_nics: if nic.name not in local_nics: logging.info('Deleting netbox interface {name} because not present locally'.format( name=nic.name @@ -415,7 +416,7 @@ class Network(object): # delete IP on netbox that are not known on this server if len(nb_nics): - if self.netbox_version > 2.8: + if version.parse(self.netbox_version) > version.parse('2.8'): netbox_ips = nb.ipam.ip_addresses.filter( **{self.intf_type: [x.id for x in nb_nics]} ) @@ -429,7 +430,7 @@ class Network(object): ])) for netbox_ip in netbox_ips: if netbox_ip.address not in all_local_ips: - if self.netbox_version < 2.9: + if version.parse(self.netbox_version) < version.parse('2.9'): logging.info('Unassigning IP {ip} from {interface}'.format( ip=netbox_ip.address, interface=netbox_ip.interface)) netbox_ip.interface = None @@ -531,7 +532,10 @@ class ServerNetwork(Network): return nb_server_interface try: - nb_switch = nb_mgmt_ip.interface.device + if version.parse(self.netbox_version) > version.parse('2.8'): + nb_switch = nb_mgmt_ip.assigned_object.device + else: + nb_switch = nb_mgmt_ip.interface.device logging.info('Found a switch in Netbox based on LLDP infos: {} (id: {})'.format( switch_ip, nb_switch.id diff --git a/netbox_agent/power.py b/netbox_agent/power.py index fcc1890..ea957e9 100644 --- a/netbox_agent/power.py +++ b/netbox_agent/power.py @@ -51,7 +51,7 @@ class PowerSupply(): ) def create_or_update_power_supply(self): - nb_psus = self.get_netbox_power_supply() + nb_psus = list(self.get_netbox_power_supply()) psus = self.get_power_supply() # Delete unknown PSU diff --git a/netbox_agent/server.py b/netbox_agent/server.py index 6051855..ea0138e 100644 --- a/netbox_agent/server.py +++ b/netbox_agent/server.py @@ -13,6 +13,8 @@ from netbox_agent.misc import create_netbox_tags, get_device_role, get_device_ty from netbox_agent.network import ServerNetwork from netbox_agent.power import PowerSupply +from packaging import version + class ServerBase(): def __init__(self, dmi=None): @@ -29,8 +31,8 @@ class ServerBase(): self.network = None self.tags = list(set([x.strip() for x in config.device.tags.split(',') if x.strip()])) if config.device.tags else [] - if self.tags and len(self.tags): - create_netbox_tags(self.tags) + self.nb_tags = list(create_netbox_tags(self.tags)) + self.netbox_version = nb.version def get_tenant(self): tenant = Tenant() @@ -383,11 +385,12 @@ class ServerBase(): update += 1 server.name = self.get_hostname() - if float(nb.version) < 2.8 and sorted(set(server.tags)) != sorted(set(self.tags)): - server.tags = self.tags + if sorted(set([x.name for x in server.tags])) != sorted(set(self.tags)): update += 1 - else: - logging.warning("netbox-agent doesn't support tag updates for Netbox version >=2.8") + if version.parse(self.netbox_version) > version.parse('2.8'): + server.tags = [x.id for x in self.nb_tags] + else: + server.tags = self.tags if config.update_all or config.update_location: ret, server = self.update_netbox_location(server) From a1af028df655e427a9919dd1812e8abac69700d9 Mon Sep 17 00:00:00 2001 From: Solvik Blum Date: Tue, 11 May 2021 20:47:16 +0200 Subject: [PATCH 058/148] last commit is based on pynetbox >= 6.1 --- requirements.txt | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 7172368..55b4d83 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -pynetbox==5.0.5 +pynetbox==6.1.2 netaddr==0.8.0 netifaces==0.10.9 pyyaml==5.4.1 diff --git a/setup.py b/setup.py index 8b22f05..1cad55b 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ setup( packages=find_packages(exclude=["*.tests", "*.tests.*", "tests.*", "tests"]), use_scm_version=True, install_requires=[ - 'pynetbox==5.1.0', + 'pynetbox==6.1.2', 'netaddr==0.8.0', 'netifaces==0.10.9', 'pyyaml==5.4.1', From 337e272eea9ff1e9c3cbeaa818cac2a9e3ff5f02 Mon Sep 17 00:00:00 2001 From: Solvik Blum Date: Tue, 11 May 2021 21:11:27 +0200 Subject: [PATCH 059/148] fix ipv6 deletion with regex --- netbox_agent/network.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/netbox_agent/network.py b/netbox_agent/network.py index 558a80e..f9552d7 100644 --- a/netbox_agent/network.py +++ b/netbox_agent/network.py @@ -59,6 +59,14 @@ class Network(object): ip_addr = netifaces.ifaddresses(interface).get(netifaces.AF_INET, []) ip6_addr = netifaces.ifaddresses(interface).get(netifaces.AF_INET6, []) + if config.network.ignore_ips and ip_addr: + for i, ip in enumerate(ip_addr): + if re.match(config.network.ignore_ips, ip['addr']): + ip_addr.pop(i) + for i, ip in enumerate(ip6_addr): + if re.match(config.network.ignore_ips, ip['addr']): + ip6_addr.pop(i) + # netifaces returns a ipv6 netmask that netaddr does not understand. # this strips the netmask down to the correct format for netaddr, # and remove the interface. @@ -79,10 +87,6 @@ class Network(object): addr["netmask"] = addr["netmask"].split('/')[0] ip_addr.append(addr) - if config.network.ignore_ips and ip_addr: - for i, ip in enumerate(ip_addr): - if re.match(config.network.ignore_ips, ip['addr']): - ip_addr.pop(i) mac = open('/sys/class/net/{}/address'.format(interface), 'r').read().strip() vlan = None @@ -425,6 +429,7 @@ class Network(object): interface_id=[x.id for x in nb_nics], ) + netbox_ips = list(netbox_ips) all_local_ips = list(chain.from_iterable([ x['ip'] for x in self.nics if x['ip'] is not None ])) From 576eb07dd7d85ec892844daaa1f676647fa9d693 Mon Sep 17 00:00:00 2001 From: Solvik Blum Date: Tue, 11 May 2021 21:45:00 +0200 Subject: [PATCH 060/148] make inventory compatible --- netbox_agent/inventory.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/netbox_agent/inventory.py b/netbox_agent/inventory.py index e10c34f..dfef77c 100644 --- a/netbox_agent/inventory.py +++ b/netbox_agent/inventory.py @@ -58,6 +58,7 @@ class Inventory(): self.lshw = LSHW() def create_netbox_tags(self): + ret = [] for key, tag in INVENTORY_TAG.items(): nb_tag = nb.extras.tags.get( name=tag['name'] @@ -68,6 +69,8 @@ class Inventory(): slug=tag['slug'], comments=tag['name'], ) + ret.append(nb_tag) + return ret def find_or_create_manufacturer(self, name): if name is None: @@ -97,7 +100,7 @@ class Inventory(): logging.info('Tag {tag} is missing, returning empty array.'.format(tag=tag)) items = [] - return items + return list(items) def create_netbox_inventory_item(self, device_id, tags, vendor, name, serial, description): manufacturer = self.find_or_create_manufacturer(vendor) @@ -152,7 +155,7 @@ class Inventory(): if motherboard.get('serial') not in [x.serial for x in nb_motherboards]: self.create_netbox_inventory_item( device_id=self.device_id, - tags=[INVENTORY_TAG['motherboard']['name']], + tags=[{'name': INVENTORY_TAG['motherboard']['name']}], vendor='{}'.format(motherboard.get('vendor', 'N/A')), serial='{}'.format(motherboard.get('serial', 'No SN')), name='{}'.format(motherboard.get('name')), @@ -165,7 +168,7 @@ class Inventory(): device=self.device_id, manufacturer=manufacturer.id, discovered=True, - tags=[INVENTORY_TAG['interface']['name']], + tags=[{'name': INVENTORY_TAG['interface']['name']}], name="{}".format(iface['product']), serial='{}'.format(iface['serial']), description='{} {}'.format(iface['description'], iface['name']) @@ -198,7 +201,7 @@ class Inventory(): device=self.device_id, manufacturer=manufacturer.id, discovered=True, - tags=[INVENTORY_TAG['cpu']['name']], + tags=[{'name': INVENTORY_TAG['cpu']['name']}], name=cpu['product'], description='CPU {}'.format(cpu['location']), # asset_tag=cpu['location'] @@ -250,7 +253,7 @@ class Inventory(): device=self.device_id, discovered=True, manufacturer=manufacturer.id if manufacturer else None, - tags=[INVENTORY_TAG['raid_card']['name']], + tags=[{'name': INVENTORY_TAG['raid_card']['name']}], name='{}'.format(name), serial='{}'.format(serial), description='RAID Card', @@ -367,7 +370,7 @@ class Inventory(): _ = nb.dcim.inventory_items.create( device=self.device_id, discovered=True, - tags=[INVENTORY_TAG['disk']['name']], + tags=[{'name': INVENTORY_TAG['disk']['name']}], name=name, serial=disk['SN'], part_id=disk['Model'], @@ -407,7 +410,7 @@ class Inventory(): device=self.device_id, discovered=True, manufacturer=manufacturer.id, - tags=[INVENTORY_TAG['memory']['name']], + tags=[{'name': INVENTORY_TAG['memory']['name']}], name=name, part_id=memory['product'], serial=memory['serial'], @@ -449,7 +452,7 @@ class Inventory(): device=self.device_id, manufacturer=manufacturer.id, discovered=True, - tags=[INVENTORY_TAG['gpu']['name']], + tags=[{'name': INVENTORY_TAG['gpu']['name']}], name=gpu['product'], description='GPU {}'.format(gpu['product']), ) From 95d7f98389836ba240f6545658c9b0cc1d49b9e2 Mon Sep 17 00:00:00 2001 From: Solvik Blum Date: Tue, 11 May 2021 21:54:37 +0200 Subject: [PATCH 061/148] add new dependencies --- requirements.txt | 2 ++ setup.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/requirements.txt b/requirements.txt index 55b4d83..0987b01 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,5 @@ netaddr==0.8.0 netifaces==0.10.9 pyyaml==5.4.1 jsonargparse==3.11.2 +python-slugify==5.0.2 +packaging==20.9 diff --git a/setup.py b/setup.py index 1cad55b..1ecdeb2 100644 --- a/setup.py +++ b/setup.py @@ -19,6 +19,8 @@ setup( 'netifaces==0.10.9', 'pyyaml==5.4.1', 'jsonargparse==2.32.2', + 'python-slugify==5.0.2', + 'packaging==20.9', ], zip_safe=False, keywords=['netbox'], From 9eafcbf21567c6cb5b8dc4cb58416e5c328644b3 Mon Sep 17 00:00:00 2001 From: Solvik Blum Date: Wed, 12 May 2021 19:40:36 +0200 Subject: [PATCH 062/148] this MR will drop compatibility with versions < 2.9 --- netbox_agent/cli.py | 8 ++++- netbox_agent/network.py | 68 +++++++++++------------------------------ netbox_agent/server.py | 9 +----- 3 files changed, 26 insertions(+), 59 deletions(-) diff --git a/netbox_agent/cli.py b/netbox_agent/cli.py index 5777f1a..eba437e 100644 --- a/netbox_agent/cli.py +++ b/netbox_agent/cli.py @@ -1,5 +1,5 @@ import netbox_agent.dmidecode as dmidecode -from netbox_agent.config import config +from netbox_agent.config import config, netbox_instance as nb from netbox_agent.logging import logging # NOQA from netbox_agent.vendors.dell import DellHost from netbox_agent.vendors.generic import GenericHost @@ -8,6 +8,8 @@ from netbox_agent.vendors.qct import QCTHost from netbox_agent.vendors.supermicro import SupermicroHost from netbox_agent.virtualmachine import VirtualMachine, is_vm +from packaging import version + MANUFACTURERS = { 'Dell Inc.': DellHost, 'HP': HPHost, @@ -32,6 +34,10 @@ def run(config): except KeyError: server = GenericHost(dmi=dmi) + if version.parse(nb.version) < version.parse('2.9'): + print('netbox-agent is not compatible with Netbox prior to verison 2.9') + return False + if config.debug: server.print_debug() if config.register or config.update_all or config.update_network or config.update_location or \ diff --git a/netbox_agent/network.py b/netbox_agent/network.py index f9552d7..7cbae29 100644 --- a/netbox_agent/network.py +++ b/netbox_agent/network.py @@ -12,11 +12,8 @@ from netbox_agent.ethtool import Ethtool from netbox_agent.ipmi import IPMI from netbox_agent.lldp import LLDP -from packaging import version - class Network(object): def __init__(self, server, *args, **kwargs): - self.netbox_version = nb.version self.nics = [] self.server = server @@ -91,7 +88,8 @@ class Network(object): mac = open('/sys/class/net/{}/address'.format(interface), 'r').read().strip() vlan = None if len(interface.split('.')) > 1: - vlan = int(interface.split('.')[1]) + vlan = interface.split('.')[1] + bonding = False bonding_slaves = [] if os.path.isdir('/sys/class/net/{}/bonding'.format(interface)): @@ -231,7 +229,7 @@ class Network(object): type(interface.mode) is not int and ( interface.mode.value == self.dcim_choices['interface:mode']['Access'] or len(interface.tagged_vlans) != 1 or - interface.tagged_vlans[0].vid != vlan_id)): + int(interface.tagged_vlans[0].vid) != int(vlan_id))): logging.info('Resetting tagged VLAN(s) on interface {interface}'.format( interface=interface)) update = True @@ -327,14 +325,9 @@ class Network(object): query_params = { 'address': ip, 'status': "active", + 'assigned_object_type': self.assigned_object_type, + 'assigned_object_id': interface.id } - if version.parse(self.netbox_version) > version.parse('2.8'): - query_params.update({ - 'assigned_object_type': self.assigned_object_type, - 'assigned_object_id': interface.id - }) - else: - query_params.update({'interface_id': interface.id}) netbox_ip = nb.ipam.ip_addresses.create( **query_params @@ -361,14 +354,9 @@ class Network(object): "status": "active", "role": self.ipam_choices['ip-address:role']['Anycast'], "tenant": self.tenant.id if self.tenant else None, + "assigned_object_type": self.assigned_object_type, + "assigned_object_id": interface.id } - if version.parse(self.netbox_version) > version.parse('2.8'): - query_params.update({ - 'assigned_object_type': self.assigned_object_type, - 'assigned_object_id': interface.id - }) - else: - query_params.update({'interface': interface.id}) netbox_ip = nb.ipam.ip_addresses.create(**query_params) return netbox_ip else: @@ -378,11 +366,7 @@ class Network(object): ip=ip, interface=interface)) elif hasattr(netbox_ip, 'interface') and netbox_ip.interface.id != interface.id or \ hasattr(netbox_ip, 'assigned_object') and netbox_ip.assigned_object_id != interface.id: - if version.parse(self.netbox_version) > version.parse('2.8'): - old_interface = netbox_ip.assigned_object - else: - old_interface = netbox_ip.interface - + old_interface = netbox_ip.assigned_object logging.info( 'Detected interface change for ip {ip}: old interface is ' '{old_interface} (id: {old_id}), new interface is {new_interface} ' @@ -394,11 +378,8 @@ class Network(object): else: return netbox_ip - if version.parse(self.netbox_version) > version.parse('2.8'): - netbox_ip.assigned_object_type = self.assigned_object_type - netbox_ip.assigned_object_id = interface.id - else: - netbox_ip.interface = interface + netbox_ip.assigned_object_type = self.assigned_object_type + netbox_ip.assigned_object_id = interface.id netbox_ip.save() return netbox_ip @@ -420,14 +401,9 @@ class Network(object): # delete IP on netbox that are not known on this server if len(nb_nics): - if version.parse(self.netbox_version) > version.parse('2.8'): - netbox_ips = nb.ipam.ip_addresses.filter( - **{self.intf_type: [x.id for x in nb_nics]} - ) - else: - netbox_ips = nb.ipam.ip_addresses.filter( - interface_id=[x.id for x in nb_nics], - ) + netbox_ips = nb.ipam.ip_addresses.filter( + **{self.intf_type: [x.id for x in nb_nics]} + ) netbox_ips = list(netbox_ips) all_local_ips = list(chain.from_iterable([ @@ -435,15 +411,10 @@ class Network(object): ])) for netbox_ip in netbox_ips: if netbox_ip.address not in all_local_ips: - if version.parse(self.netbox_version) < version.parse('2.9'): - logging.info('Unassigning IP {ip} from {interface}'.format( - ip=netbox_ip.address, interface=netbox_ip.interface)) - netbox_ip.interface = None - else: - logging.info('Unassigning IP {ip} from {interface}'.format( - ip=netbox_ip.address, interface=netbox_ip.assigned_object)) - netbox_ip.assigned_object_type = None - netbox_ip.assigned_object_id = None + logging.info('Unassigning IP {ip} from {interface}'.format( + ip=netbox_ip.address, interface=netbox_ip.assigned_object)) + netbox_ip.assigned_object_type = None + netbox_ip.assigned_object_id = None netbox_ip.save() # update each nic @@ -537,10 +508,7 @@ class ServerNetwork(Network): return nb_server_interface try: - if version.parse(self.netbox_version) > version.parse('2.8'): - nb_switch = nb_mgmt_ip.assigned_object.device - else: - nb_switch = nb_mgmt_ip.interface.device + nb_switch = nb_mgmt_ip.assigned_object.device logging.info('Found a switch in Netbox based on LLDP infos: {} (id: {})'.format( switch_ip, nb_switch.id diff --git a/netbox_agent/server.py b/netbox_agent/server.py index ea0138e..557b696 100644 --- a/netbox_agent/server.py +++ b/netbox_agent/server.py @@ -13,9 +13,6 @@ from netbox_agent.misc import create_netbox_tags, get_device_role, get_device_ty from netbox_agent.network import ServerNetwork from netbox_agent.power import PowerSupply -from packaging import version - - class ServerBase(): def __init__(self, dmi=None): if dmi: @@ -32,7 +29,6 @@ class ServerBase(): self.tags = list(set([x.strip() for x in config.device.tags.split(',') if x.strip()])) if config.device.tags else [] self.nb_tags = list(create_netbox_tags(self.tags)) - self.netbox_version = nb.version def get_tenant(self): tenant = Tenant() @@ -387,10 +383,7 @@ class ServerBase(): if sorted(set([x.name for x in server.tags])) != sorted(set(self.tags)): update += 1 - if version.parse(self.netbox_version) > version.parse('2.8'): - server.tags = [x.id for x in self.nb_tags] - else: - server.tags = self.tags + server.tags = [x.id for x in self.nb_tags] if config.update_all or config.update_location: ret, server = self.update_netbox_location(server) From aaea0a24770c967dfbe4eb570feebf990a969144 Mon Sep 17 00:00:00 2001 From: Solvik Blum Date: Wed, 12 May 2021 20:11:20 +0200 Subject: [PATCH 063/148] fix ipv6 cleanup on interface with only one ipv6 and not ipv4 --- netbox_agent/network.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/netbox_agent/network.py b/netbox_agent/network.py index 7cbae29..78aa90a 100644 --- a/netbox_agent/network.py +++ b/netbox_agent/network.py @@ -55,8 +55,7 @@ class Network(object): ip_addr = netifaces.ifaddresses(interface).get(netifaces.AF_INET, []) ip6_addr = netifaces.ifaddresses(interface).get(netifaces.AF_INET6, []) - - if config.network.ignore_ips and ip_addr: + if config.network.ignore_ips: for i, ip in enumerate(ip_addr): if re.match(config.network.ignore_ips, ip['addr']): ip_addr.pop(i) @@ -88,7 +87,7 @@ class Network(object): mac = open('/sys/class/net/{}/address'.format(interface), 'r').read().strip() vlan = None if len(interface.split('.')) > 1: - vlan = interface.split('.')[1] + vlan = int(interface.split('.')[1]) bonding = False bonding_slaves = [] From b738f4bfeffb9187c8c64e53e1f6b9e8c25d459a Mon Sep 17 00:00:00 2001 From: Solvik Blum Date: Fri, 14 May 2021 10:00:50 +0200 Subject: [PATCH 064/148] fix last few bugs due to pynetbox 6 upgrade --- netbox_agent/network.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/netbox_agent/network.py b/netbox_agent/network.py index 78aa90a..b086f4b 100644 --- a/netbox_agent/network.py +++ b/netbox_agent/network.py @@ -212,8 +212,12 @@ class Network(object): update = False vlan_id = nic['vlan'] lldp_vlan = self.lldp.get_switch_vlan(nic['name']) if config.network.lldp else None + # For strange reason, we need to get the object from scratch + # The object returned by pynetbox's save isn't always working (since pynetbox 6) + interface = nb.dcim.interfaces.get(id=interface.id) - # if local interface isn't a interface vlan or lldp doesn't report a vlan-id + # Handle the case were the local interface isn't an interface vlan as reported by Netbox + # and that LLDP doesn't report a vlan-id if vlan_id is None and lldp_vlan is None and \ (interface.mode is not None or len(interface.tagged_vlans) > 0): logging.info('Interface {interface} is not tagged, reseting mode'.format( @@ -222,11 +226,13 @@ class Network(object): interface.mode = None interface.tagged_vlans = [] interface.untagged_vlan = None - # if it's a vlan interface + # if the local interface is configured with a vlan, it's supposed to be taggued + # if mode is either not set or not correctly configured or vlan are not correctly configured + # we reset the vlan elif vlan_id and ( interface.mode is None or type(interface.mode) is not int and ( - interface.mode.value == self.dcim_choices['interface:mode']['Access'] or + hasattr(interface.mode, 'value') and interface.mode.value == self.dcim_choices['interface:mode']['Access'] or len(interface.tagged_vlans) != 1 or int(interface.tagged_vlans[0].vid) != int(vlan_id))): logging.info('Resetting tagged VLAN(s) on interface {interface}'.format( @@ -236,7 +242,7 @@ class Network(object): interface.mode = self.dcim_choices['interface:mode']['Tagged'] interface.tagged_vlans = [nb_vlan] if nb_vlan else [] interface.untagged_vlan = None - # if lldp reports a vlan-id with pvid + # Finally if LLDP reports a vlan-id with the pvid attribute elif lldp_vlan: pvid_vlan = [key for (key, value) in lldp_vlan.items() if value['pvid']] if len(pvid_vlan) > 0 and ( @@ -274,7 +280,7 @@ class Network(object): if nic['vlan']: nb_vlan = self.get_or_create_vlan(nic['vlan']) - interface.mode = 200 + interface.mode = self.dcim_choices['interface:mode']['Tagged'] interface.tagged_vlans = [nb_vlan.id] interface.save() elif config.network.lldp and self.lldp.get_switch_vlan(nic['name']) is not None: From 86527af1c071c54ffc8a8d861b931a808de48caf Mon Sep 17 00:00:00 2001 From: Solvik Blum Date: Tue, 18 May 2021 13:59:04 +0200 Subject: [PATCH 065/148] lint --- netbox_agent/cli.py | 7 ++++--- netbox_agent/lldp.py | 3 ++- netbox_agent/misc.py | 1 + netbox_agent/network.py | 17 ++++++++++------- netbox_agent/server.py | 5 ++++- netbox_agent/vendors/hp.py | 3 ++- 6 files changed, 23 insertions(+), 13 deletions(-) diff --git a/netbox_agent/cli.py b/netbox_agent/cli.py index eba437e..a379ae2 100644 --- a/netbox_agent/cli.py +++ b/netbox_agent/cli.py @@ -1,5 +1,8 @@ +from packaging import version + import netbox_agent.dmidecode as dmidecode -from netbox_agent.config import config, netbox_instance as nb +from netbox_agent.config import config +from netbox_agent.config import netbox_instance as nb from netbox_agent.logging import logging # NOQA from netbox_agent.vendors.dell import DellHost from netbox_agent.vendors.generic import GenericHost @@ -8,8 +11,6 @@ from netbox_agent.vendors.qct import QCTHost from netbox_agent.vendors.supermicro import SupermicroHost from netbox_agent.virtualmachine import VirtualMachine, is_vm -from packaging import version - MANUFACTURERS = { 'Dell Inc.': DellHost, 'HP': HPHost, diff --git a/netbox_agent/lldp.py b/netbox_agent/lldp.py index a4c3fa9..51083f5 100644 --- a/netbox_agent/lldp.py +++ b/netbox_agent/lldp.py @@ -1,5 +1,6 @@ -import subprocess import logging +import subprocess + from netbox_agent.misc import is_tool diff --git a/netbox_agent/misc.py b/netbox_agent/misc.py index 208a930..0f72d69 100644 --- a/netbox_agent/misc.py +++ b/netbox_agent/misc.py @@ -3,6 +3,7 @@ import subprocess from shutil import which from slugify import slugify + from netbox_agent.config import netbox_instance as nb diff --git a/netbox_agent/network.py b/netbox_agent/network.py index b086f4b..3a390eb 100644 --- a/netbox_agent/network.py +++ b/netbox_agent/network.py @@ -12,6 +12,7 @@ from netbox_agent.ethtool import Ethtool from netbox_agent.ipmi import IPMI from netbox_agent.lldp import LLDP + class Network(object): def __init__(self, server, *args, **kwargs): self.nics = [] @@ -83,7 +84,6 @@ class Network(object): addr["netmask"] = addr["netmask"].split('/')[0] ip_addr.append(addr) - mac = open('/sys/class/net/{}/address'.format(interface), 'r').read().strip() vlan = None if len(interface.split('.')) > 1: @@ -227,12 +227,13 @@ class Network(object): interface.tagged_vlans = [] interface.untagged_vlan = None # if the local interface is configured with a vlan, it's supposed to be taggued - # if mode is either not set or not correctly configured or vlan are not correctly configured - # we reset the vlan + # if mode is either not set or not correctly configured or vlan are not + # correctly configured, we reset the vlan elif vlan_id and ( interface.mode is None or type(interface.mode) is not int and ( - hasattr(interface.mode, 'value') and interface.mode.value == self.dcim_choices['interface:mode']['Access'] or + hasattr(interface.mode, 'value') and + interface.mode.value == self.dcim_choices['interface:mode']['Access'] or len(interface.tagged_vlans) != 1 or int(interface.tagged_vlans[0].vid) != int(vlan_id))): logging.info('Resetting tagged VLAN(s) on interface {interface}'.format( @@ -369,8 +370,11 @@ class Network(object): hasattr(netbox_ip, 'assigned_object') and netbox_ip.assigned_object is None: logging.info('Assigning existing IP {ip} to {interface}'.format( ip=ip, interface=interface)) - elif hasattr(netbox_ip, 'interface') and netbox_ip.interface.id != interface.id or \ - hasattr(netbox_ip, 'assigned_object') and netbox_ip.assigned_object_id != interface.id: + elif hasattr(netbox_ip, 'interface') and \ + netbox_ip.interface.id != interface.id or \ + hasattr(netbox_ip, 'assigned_object') and \ + netbox_ip.assigned_object_id != interface.id: + old_interface = netbox_ip.assigned_object logging.info( 'Detected interface change for ip {ip}: old interface is ' @@ -493,7 +497,6 @@ class ServerNetwork(Network): self.intf_type = "interface_id" self.assigned_object_type = "dcim.interface" - def get_network_type(self): return 'server' diff --git a/netbox_agent/server.py b/netbox_agent/server.py index 557b696..06a2742 100644 --- a/netbox_agent/server.py +++ b/netbox_agent/server.py @@ -13,6 +13,7 @@ from netbox_agent.misc import create_netbox_tags, get_device_role, get_device_ty from netbox_agent.network import ServerNetwork from netbox_agent.power import PowerSupply + class ServerBase(): def __init__(self, dmi=None): if dmi: @@ -27,7 +28,9 @@ class ServerBase(): self.network = None - self.tags = list(set([x.strip() for x in config.device.tags.split(',') if x.strip()])) if config.device.tags else [] + self.tags = list(set([ + x.strip() for x in config.device.tags.split(',') if x.strip() + ])) if config.device.tags else [] self.nb_tags = list(create_netbox_tags(self.tags)) def get_tenant(self): diff --git a/netbox_agent/vendors/hp.py b/netbox_agent/vendors/hp.py index 30d8f07..ef479cf 100644 --- a/netbox_agent/vendors/hp.py +++ b/netbox_agent/vendors/hp.py @@ -12,7 +12,8 @@ class HPHost(ServerBase): def is_blade(self): blade = self.product.startswith("ProLiant BL") - blade |= self.product.startswith("ProLiant m") and self.product.endswith("Server Cartridge") + blade |= self.product.startswith("ProLiant m") and \ + self.product.endswith("Server Cartridge") return blade def _find_rack_locator(self): From 794f9787f01f6f6f95423952b1663d62cc8783d8 Mon Sep 17 00:00:00 2001 From: Cyril Levis Date: Wed, 16 Jun 2021 14:04:57 +0200 Subject: [PATCH 066/148] NVME inventory fix. The inventory crash with default nvme-cli version 1.8.1 on Centos7. --- netbox_agent/lshw.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/netbox_agent/lshw.py b/netbox_agent/lshw.py index 0a5a870..8070633 100644 --- a/netbox_agent/lshw.py +++ b/netbox_agent/lshw.py @@ -104,7 +104,10 @@ class LSHW(): d['product'] = device["ModelNumber"] d['serial'] = device["SerialNumber"] d["version"] = device["Firmware"] - d['size'] = device["UsedSize"] + if "UsedSize" in device: + d['size'] = device["UsedSize"] + if "UsedBytes" in device: + d['size'] = device["UsedBytes"] d['description'] = "NVME Disk" self.disks.append(d) From bdc450ef6af183460e924308d41e0345e7573dff Mon Sep 17 00:00:00 2001 From: ramnes Date: Fri, 9 Jul 2021 11:10:43 +0200 Subject: [PATCH 067/148] Make flake8 and isort happy --- netbox_agent/lldp.py | 3 ++- netbox_agent/vendors/hp.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/netbox_agent/lldp.py b/netbox_agent/lldp.py index a4c3fa9..51083f5 100644 --- a/netbox_agent/lldp.py +++ b/netbox_agent/lldp.py @@ -1,5 +1,6 @@ -import subprocess import logging +import subprocess + from netbox_agent.misc import is_tool diff --git a/netbox_agent/vendors/hp.py b/netbox_agent/vendors/hp.py index 30d8f07..e9cc991 100644 --- a/netbox_agent/vendors/hp.py +++ b/netbox_agent/vendors/hp.py @@ -12,7 +12,8 @@ class HPHost(ServerBase): def is_blade(self): blade = self.product.startswith("ProLiant BL") - blade |= self.product.startswith("ProLiant m") and self.product.endswith("Server Cartridge") + blade |= (self.product.startswith("ProLiant m") + and self.product.endswith("Server Cartridge")) return blade def _find_rack_locator(self): From 8a46af19b8ebcaecb4075762866fd8d2926edbab Mon Sep 17 00:00:00 2001 From: Cyrinux Date: Tue, 20 Jul 2021 21:55:47 +0200 Subject: [PATCH 068/148] Fix a crash when missing rack id (#172) --- netbox_agent/server.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/netbox_agent/server.py b/netbox_agent/server.py index 68db8da..9cedf62 100644 --- a/netbox_agent/server.py +++ b/netbox_agent/server.py @@ -66,7 +66,6 @@ class ServerBase(): def update_netbox_location(self, server): dc = self.get_datacenter() - rack = self.get_rack() nb_rack = self.get_netbox_rack() nb_dc = self.get_netbox_datacenter() @@ -79,7 +78,11 @@ class ServerBase(): update = True server.site = nb_dc.id - if rack and server.rack and server.rack.id != nb_rack.id: + if ( + server.rack + and nb_rack + and server.rack.id != nb_rack.id + ): logging.info('Rack location has changed from {} to {}, updating'.format( server.rack, nb_rack, From 58775c0950a3944173afeb1163f132977eaf0fd6 Mon Sep 17 00:00:00 2001 From: Christophe Simon Date: Tue, 8 Feb 2022 18:45:44 +0100 Subject: [PATCH 069/148] Added option to purge remaining devices This patch adds an option to clear remaining devices in the context of a new device replacing an old one with the same name but with a different hardware. The serial leading to the bug is described below: - A first server is registered under name `A` with serial `X` - A second server is registered under name `B` with serial `Y` - The server with serial `X` is decomissionned, but not removed - The server with serial `Y` is reinstalled with name `A` In this case, a device with serial `X` is well found, and netbox agent tries to update its name. But this raises an error because of the unique constraint on name, as another device already holds this name. The proposed solution to handle this situation is to delete any device with same `name` but different `serial` before handling a device. As this is not necessarily the expected behavior, it can be enabled by using the `--purge-old-devices` to avoid breaking existing inventory. --- netbox_agent/config.py | 2 ++ netbox_agent/server.py | 10 ++++++++++ 2 files changed, 12 insertions(+) diff --git a/netbox_agent/config.py b/netbox_agent/config.py index cf88f69..44d1019 100644 --- a/netbox_agent/config.py +++ b/netbox_agent/config.py @@ -28,6 +28,8 @@ def get_config(): p.add_argument('--update-inventory', action='store_true', help='Update inventory') p.add_argument('--update-location', action='store_true', help='Update location') p.add_argument('--update-psu', action='store_true', help='Update PSU') + p.add_argument('--purge-old-devices', action='store_true', + help='Purge existing (old ?) devices having same name but different serial') p.add_argument('--log_level', default='debug') p.add_argument('--netbox.url', help='Netbox URL') diff --git a/netbox_agent/server.py b/netbox_agent/server.py index 9cedf62..e052a44 100644 --- a/netbox_agent/server.py +++ b/netbox_agent/server.py @@ -228,6 +228,13 @@ class ServerBase(): ) return new_blade + def _netbox_deduplicate_server(self): + serial = self.get_service_tag() + hostname = self.get_hostname() + server = nb.dcim.devices.get(name=hostname) + if server and server.serial != serial: + server.delete() + def _netbox_create_server(self, datacenter, tenant, rack): device_role = get_device_role(config.device.server_role) device_type = get_device_type(self.get_product_name()) @@ -336,6 +343,9 @@ class ServerBase(): rack = self.get_netbox_rack() tenant = self.get_netbox_tenant() + if config.purge_old_devices: + self._netbox_deduplicate_server() + if self.is_blade(): chassis = nb.dcim.devices.get( serial=self.get_chassis_service_tag() From 2f09cf8d42095c8aa273bd168b557b0fa52a9e4c Mon Sep 17 00:00:00 2001 From: Christophe Simon Date: Fri, 11 Feb 2022 18:22:13 +0100 Subject: [PATCH 070/148] Manage blade expansions as independent devices This patch adds the ability to detect and manage GPU and Disk expansion bays, and either add their internal components into the device corresponding to the blade server, or into a dedicated device. It takes advantage of the work made by @cyrinux on GPU bays management, and applies the same principle to the external disk bays, but harmonize the inventory management: - If no argument is specified on the command line, the GPU cards, RAID controllers and their attached disks are added in the blade device, and the device corresponding to an expansion device is deleted. - If the `--expansion-as-device` option is specified on the command line, a dedicated device corresponding to the expansion bay is created, and the GPUs, RAID card and attached disks are removed from the blade device and added to the expansion device. --- netbox_agent/config.py | 2 + netbox_agent/inventory.py | 42 ++++---- netbox_agent/raid/base.py | 3 + netbox_agent/raid/hp.py | 190 ++++++++++++++----------------------- netbox_agent/server.py | 85 ++++++++++++----- netbox_agent/vendors/hp.py | 48 ++++++---- 6 files changed, 195 insertions(+), 175 deletions(-) diff --git a/netbox_agent/config.py b/netbox_agent/config.py index cf88f69..cc58a12 100644 --- a/netbox_agent/config.py +++ b/netbox_agent/config.py @@ -28,6 +28,8 @@ def get_config(): p.add_argument('--update-inventory', action='store_true', help='Update inventory') p.add_argument('--update-location', action='store_true', help='Update location') p.add_argument('--update-psu', action='store_true', help='Update PSU') + p.add_argument('--expansion-as-device', action='store_true', + help='Manage blade expansions as external devices') p.add_argument('--log_level', default='debug') p.add_argument('--netbox.url', help='Netbox URL') diff --git a/netbox_agent/inventory.py b/netbox_agent/inventory.py index e10c34f..0f84fd5 100644 --- a/netbox_agent/inventory.py +++ b/netbox_agent/inventory.py @@ -46,10 +46,11 @@ class Inventory(): - no scan of NVMe devices """ - def __init__(self, server): + def __init__(self, server, update_expansion=False): self.create_netbox_tags() self.server = server - netbox_server = self.server.get_netbox_server() + self.update_expansion = update_expansion + netbox_server = self.server.get_netbox_server(update_expansion) self.device_id = netbox_server.id if netbox_server else None self.raid = None @@ -220,7 +221,7 @@ class Inventory(): self.create_netbox_cpus() - def get_raid_cards(self): + def get_raid_cards(self, filter_cards=False): raid_class = None if self.server.manufacturer == 'Dell': if is_tool('omreport'): @@ -235,9 +236,15 @@ class Inventory(): return [] self.raid = raid_class() - controllers = self.raid.get_controllers() - if len(self.raid.get_controllers()): - return controllers + + if filter_cards and config.expansion_as_device \ + and self.server.own_expansion_slot(): + return [ + c for c in self.raid.get_controllers() + if c.is_external() is self.update_expansion + ] + else: + return self.raid.get_controllers() def create_netbox_raid_card(self, raid_card): manufacturer = self.find_or_create_manufacturer( @@ -276,7 +283,7 @@ class Inventory(): device_id=self.device_id, tag=[INVENTORY_TAG['raid_card']['slug']] ) - raid_cards = self.get_raid_cards() + raid_cards = self.get_raid_cards(filter_cards=True) # delete cards that are in netbox but not locally # use the serial_number has the comparison element @@ -336,7 +343,7 @@ class Inventory(): d['Vendor'] = get_vendor(disk['product']) disks.append(d) - for raid_card in self.get_raid_cards(): + for raid_card in self.get_raid_cards(filter_cards=True): disks += raid_card.get_physical_disks() # remove duplicate serials @@ -463,21 +470,24 @@ class Inventory(): tag=INVENTORY_TAG['gpu']['slug'], ) - if not len(nb_gpus) or \ + if config.expansion_as_device and len(nb_gpus): + for x in nb_gpus: + x.delete() + elif not len(nb_gpus) or \ len(nb_gpus) and len(gpus) != len(nb_gpus): for x in nb_gpus: x.delete() - self.create_netbox_gpus() def create_or_update(self): if config.inventory is None or config.update_inventory is None: return False - self.do_netbox_cpus() - self.do_netbox_memories() - self.do_netbox_raid_cards() - self.do_netbox_disks() - self.do_netbox_interfaces() - self.do_netbox_motherboard() + if self.update_expansion is False: + self.do_netbox_cpus() + self.do_netbox_memories() + self.do_netbox_interfaces() + self.do_netbox_motherboard() self.do_netbox_gpus() + self.do_netbox_disks() + self.do_netbox_raid_cards() return True diff --git a/netbox_agent/raid/base.py b/netbox_agent/raid/base.py index a3d3081..97b8274 100644 --- a/netbox_agent/raid/base.py +++ b/netbox_agent/raid/base.py @@ -15,6 +15,9 @@ class RaidController(): def get_physical_disks(self): raise NotImplementedError + def is_external(self): + return False + class Raid(): def get_controllers(self): diff --git a/netbox_agent/raid/hp.py b/netbox_agent/raid/hp.py index 144e73b..5d5d140 100644 --- a/netbox_agent/raid/hp.py +++ b/netbox_agent/raid/hp.py @@ -1,106 +1,65 @@ import re import subprocess +from netbox_agent.config import config from netbox_agent.misc import get_vendor from netbox_agent.raid.base import Raid, RaidController REGEXP_CONTROLLER_HP = re.compile(r'Smart Array ([a-zA-Z0-9- ]+) in Slot ([0-9]+)') -def _get_indentation(string): - """Return the number of spaces before the current line.""" - return len(string) - len(string.lstrip(' ')) +def _parse_ctrl_output(lines): + controllers = {} + current_ctrl = None - -def _get_key_value(string): - """Return the (key, value) as a tuple from a string.""" - # Normally all properties look like this: - # Unique Identifier: 600508B1001CE4ACF473EE9C826230FF - # Disk Name: /dev/sda - # Mount Points: None - key = '' - value = '' - try: - key, value = string.split(':') - except ValueError: - # This handles the case when the property of a logical drive - # returned is as follows. Here we cannot split by ':' because - # the disk id has colon in it. So if this is about disk, - # then strip it accordingly. - # Mirror Group 0: physicaldrive 6I:1:5 - string = string.lstrip(' ') - if string.startswith('physicaldrive'): - fields = string.split(' ') - key = fields[0] - value = fields[1] - else: - # TODO(rameshg87): Check if this ever occurs. - return None, None - - return key.lstrip(' ').rstrip(' '), value.lstrip(' ').rstrip(' ') - - -def _get_dict(lines, start_index, indentation): - """Recursive function for parsing hpssacli/ssacli output.""" - - info = {} - current_item = None - - i = start_index - while i < len(lines): - current_line = lines[i] - if current_line.startswith('Note:'): - i = i + 1 + for line in lines: + if not line or line.startswith('Note:'): continue - - current_line_indentation = _get_indentation(current_line) - # This check ignore some useless information that make - # crash the parsing - product_name = REGEXP_CONTROLLER_HP.search(current_line) - if current_line_indentation == 0 and not product_name: - i = i + 1 + ctrl = REGEXP_CONTROLLER_HP.search(line) + if ctrl is not None: + current_ctrl = ctrl.group(1) + controllers[current_ctrl] = {"Slot": ctrl.group(2)} + if "Embedded" not in line: + controllers[current_ctrl]["External"] = True continue + attr, val = line.split(": ", 1) + attr = attr.strip() + val = val.strip() + controllers[current_ctrl][attr] = val + return controllers - if current_line_indentation == indentation: - current_item = current_line.lstrip(' ') - info[current_item] = {} - i = i + 1 +def _parse_pd_output(lines): + drives = {} + current_array = None + current_drv = None + + for line in lines: + line = line.strip() + if not line or line.startswith('Note:'): continue - - if i >= len(lines) - 1: - key, value = _get_key_value(current_line) - # If this is some unparsable information, then - # just skip it. - if key: - info[current_item][key] = value - return info, i - - next_line = lines[i + 1] - next_line_indentation = _get_indentation(next_line) - - if current_line_indentation == next_line_indentation: - key, value = _get_key_value(current_line) - if key: - info[current_item][key] = value - i = i + 1 - elif next_line_indentation > current_line_indentation: - ret_dict, j = _get_dict(lines, i, current_line_indentation) - info[current_item].update(ret_dict) - i = j + 1 - elif next_line_indentation < current_line_indentation: - key, value = _get_key_value(current_line) - if key: - info[current_item][key] = value - return info, i - - return info, i + # Parses the Array the drives are in + if line.startswith("Array"): + current_array = line.split(None, 1)[1] + # Detects new physical drive + if line.startswith("physicaldrive"): + current_drv = line.split(None, 1)[1] + drives[current_drv] = {} + if current_array is not None: + drives[current_drv]["Array"] = current_array + continue + if ": " not in line: + continue + attr, val = line.split(": ", 1) + drives.setdefault(current_drv, {})[attr] = val + return drives class HPRaidController(RaidController): def __init__(self, controller_name, data): self.controller_name = controller_name self.data = data + self.drives = self._get_physical_disks() def get_product_name(self): return self.controller_name @@ -114,40 +73,42 @@ class HPRaidController(RaidController): def get_firmware_version(self): return self.data['Firmware Version'] - def get_physical_disks(self): - ret = [] + def is_external(self): + return self.data.get('External', False) + + def _get_physical_disks(self): output = subprocess.getoutput( 'ssacli ctrl slot={slot} pd all show detail'.format(slot=self.data['Slot']) ) lines = output.split('\n') lines = list(filter(None, lines)) - j = -1 - while j < len(lines): - info_dict, j = _get_dict(lines, j + 1, 0) + drives = _parse_pd_output(lines) + ret = [] - key = next(iter(info_dict)) - for array, physical_disk in info_dict[key].items(): - for _, pd_attr in physical_disk.items(): - model = pd_attr.get('Model', '').strip() - vendor = None - if model.startswith('HP'): - vendor = 'HP' - elif len(model.split()) > 1: - vendor = get_vendor(model.split()[1]) - else: - vendor = get_vendor(model) + for name, attrs in drives.items(): + model = attrs.get('Model', '').strip() + vendor = None + if model.startswith('HP'): + vendor = 'HP' + elif len(model.split()) > 1: + vendor = get_vendor(model.split()[1]) + else: + vendor = get_vendor(model) - ret.append({ - 'Model': model, - 'Vendor': vendor, - 'SN': pd_attr.get('Serial Number', '').strip(), - 'Size': pd_attr.get('Size', '').strip(), - 'Type': 'SSD' if pd_attr.get('Interface Type') == 'Solid State SATA' - else 'HDD', - '_src': self.__class__.__name__, - }) + ret.append({ + 'Model': model, + 'Vendor': vendor, + 'SN': attrs.get('Serial Number', '').strip(), + 'Size': attrs.get('Size', '').strip(), + 'Type': 'SSD' if attrs.get('Interface Type') == 'Solid State SATA' + else 'HDD', + '_src': self.__class__.__name__, + }) return ret + def get_physical_disks(self): + return self.drives + class HPRaid(Raid): def __init__(self): @@ -158,16 +119,11 @@ class HPRaid(Raid): def convert_to_dict(self): lines = self.output.split('\n') lines = list(filter(None, lines)) - j = -1 - while j < len(lines): - info_dict, j = _get_dict(lines, j + 1, 0) - if len(info_dict.keys()): - _product_name = list(info_dict.keys())[0] - product_name = REGEXP_CONTROLLER_HP.search(_product_name) - if product_name: - self.controllers.append( - HPRaidController(product_name.group(1), info_dict[_product_name]) - ) + controllers = _parse_ctrl_output(lines) + for controller, attrs in controllers.items(): + self.controllers.append( + HPRaidController(controller, attrs) + ) def get_controllers(self): return self.controllers diff --git a/netbox_agent/server.py b/netbox_agent/server.py index 9cedf62..0613b8f 100644 --- a/netbox_agent/server.py +++ b/netbox_agent/server.py @@ -25,6 +25,7 @@ class ServerBase(): self.bios = dmidecode.get_by_type(self.dmi, 'BIOS') self.chassis = dmidecode.get_by_type(self.dmi, 'Chassis') self.system = dmidecode.get_by_type(self.dmi, 'System') + self.inventory = Inventory(server=self) self.network = None @@ -94,6 +95,19 @@ class ServerBase(): server.position = None return update, server + def update_netbox_expansion_location(self, server, expansion): + update = False + if expansion.tenant != server.tenant: + expansion.tenant = server.tenant + update = True + if expansion.site != server.site: + expansion.site = server.site + update = True + if expansion.rack != server.rack: + expansion.rack = server.rack + update = True + return update + def get_rack(self): rack = Rack() return rack.get() @@ -249,8 +263,11 @@ class ServerBase(): ) return new_server - def get_netbox_server(self): - return nb.dcim.devices.get(serial=self.get_service_tag()) + def get_netbox_server(self, expansion=False): + if expansion is False: + return nb.dcim.devices.get(serial=self.get_service_tag()) + else: + return nb.dcim.devices.get(serial=self.get_expansion_service_tag()) def _netbox_set_or_update_blade_slot(self, server, chassis, datacenter): # before everything check if right chassis @@ -285,9 +302,9 @@ class ServerBase(): slot=slot )) - def _netbox_set_or_update_blade_expansion_slot(self, server, chassis, datacenter): + def _netbox_set_or_update_blade_expansion_slot(self, expansion, chassis, datacenter): # before everything check if right chassis - actual_device_bay = server.parent_device.device_bay if server.parent_device else None + actual_device_bay = expansion.parent_device.device_bay if expansion.parent_device else None actual_chassis = actual_device_bay.device if actual_device_bay else None slot = self.get_blade_expansion_slot() if actual_chassis and \ @@ -295,30 +312,28 @@ class ServerBase(): actual_device_bay.name == slot: return - server.name += " expansion" - real_device_bays = nb.dcim.device_bays.filter( device_id=chassis.id, name=slot, ) - if len(real_device_bays) > 0: - logging.info( - 'Setting device expansion ({serial}) new slot on {slot} ' - '(Chassis {chassis_serial})..'.format( - serial=server.serial, slot=slot, chassis_serial=chassis.serial - )) - # reset actual device bay if set - if actual_device_bay: - actual_device_bay.installed_device = None - actual_device_bay.save() - # setup new device bay - real_device_bay = real_device_bays[0] - real_device_bay.installed_device = server - real_device_bay.save() - else: + if len(real_device_bays) == 0: logging.error('Could not find slot {slot} expansion for chassis'.format( slot=slot )) + return + logging.info( + 'Setting device expansion ({serial}) new slot on {slot} ' + '(Chassis {chassis_serial})..'.format( + serial=expansion.serial, slot=slot, chassis_serial=chassis.serial + )) + # reset actual device bay if set + if actual_device_bay: + actual_device_bay.installed_device = None + actual_device_bay.save() + # setup new device bay + real_device_bay = real_device_bays[0] + real_device_bay.installed_device = expansion + real_device_bay.save() def netbox_create_or_update(self, config): """ @@ -360,9 +375,10 @@ class ServerBase(): if config.register or config.update_all or config.update_network: self.network = ServerNetwork(server=self) self.network.create_or_update_netbox_network_cards() + update_inventory = config.inventory and (config.register or + config.update_all or config.update_inventory) # update inventory if feature is enabled - if config.inventory and (config.register or config.update_all or config.update_inventory): - self.inventory = Inventory(server=self) + if update_inventory: self.inventory.create_or_update() # update psu if config.register or config.update_all or config.update_psu: @@ -370,14 +386,21 @@ class ServerBase(): self.power.create_or_update_power_supply() self.power.report_power_consumption() - if self.own_expansion_slot(): + expansion = nb.dcim.devices.get(serial=self.get_expansion_service_tag()) + if self.own_expansion_slot() and config.expansion_as_device: logging.debug('Update Server expansion...') - expansion = nb.dcim.devices.get(serial=self.get_expansion_service_tag()) if not expansion: expansion = self._netbox_create_blade_expansion(chassis, datacenter, tenant, rack) # set slot for blade expansion self._netbox_set_or_update_blade_expansion_slot(expansion, chassis, datacenter) + if update_inventory: + # Updates expansion inventory + inventory = Inventory(server=self, update_expansion=True) + inventory.create_or_update() + elif self.own_expansion_slot() and expansion: + expansion.delete() + expansion = None update = 0 # for every other specs @@ -386,6 +409,7 @@ class ServerBase(): update += 1 server.name = self.get_hostname() + if sorted(set(server.tags)) != sorted(set(self.tags)): server.tags = self.tags update += 1 @@ -396,6 +420,17 @@ class ServerBase(): if update: server.save() + + if expansion: + update = 0 + expansion_name = server.name + ' expansion' + if expansion.name != expansion_name: + expansion.name = expansion_name + update += 1 + if self.update_netbox_expansion_location(server, expansion): + update += 1 + if update: + expansion.save() logging.debug('Finished updating Server!') def print_debug(self): diff --git a/netbox_agent/vendors/hp.py b/netbox_agent/vendors/hp.py index e9cc991..b04ea04 100644 --- a/netbox_agent/vendors/hp.py +++ b/netbox_agent/vendors/hp.py @@ -67,35 +67,49 @@ class HPHost(ServerBase): return self.hp_rack_locator["Enclosure Serial"].strip() return self.get_service_tag() + def get_blade_expansion_slot(self): + """ + Expansion slot are always the compute bay number + 1 + """ + if self.is_blade() and self.own_gpu_expansion_slot() or \ + self.own_disk_expansion_slot() or True: + return 'Bay {}'.format( + str(int(self.hp_rack_locator['Server Bay'].strip()) + 1) + ) + return None + def get_expansion_product(self): """ Get the extension slot that is on a pair slot number next to the compute slot that is on an odd slot number I only know on model of slot GPU extension card that. """ - if self.own_expansion_slot(): + if self.own_gpu_expansion_slot(): return "ProLiant BL460c Graphics Expansion Blade" - return None - - def is_expansion_slot(self, server): - """ - Return True if its an extension slot, based on the name - """ - return server.name.endswith(" expansion") - - def get_blade_expansion_slot(self): - """ - Expansion slot are always the compute bay number + 1 - """ - if self.is_blade() and self.own_expansion_slot(): - return 'Bay {}'.format( - str(int(self.hp_rack_locator['Server Bay'].strip()) + 1) - ) + elif self.own_disk_expansion_slot(): + return "ProLiant BL460c Disk Expansion Blade" return None def own_expansion_slot(self): + """ + Say if the device can host an extension card based + on the product name + """ + return self.own_gpu_expansion_slot() or self.own_disk_expansion_slot() + + def own_gpu_expansion_slot(self): """ Say if the device can host an extension card based on the product name """ return self.get_product_name().endswith('Graphics Exp') + + def own_disk_expansion_slot(self): + """ + Say if the device can host an extension card based + on the product name + """ + for raid_card in self.inventory.get_raid_cards(): + if self.is_blade() and raid_card.is_external(): + return True + return False From 305d4d41ec13db32da113973074207c568456e70 Mon Sep 17 00:00:00 2001 From: Christophe Simon Date: Mon, 21 Feb 2022 18:12:16 +0100 Subject: [PATCH 071/148] Various changes - Added an option to specify an SSL CA certificates file to talk to the Netbox API - Enhanced GPU expansion bays inventory: internal GPU (VGA) goes into the blade, external GPU (3D) goes into the expansion bay - Unified the way expansion bays are managed (GPU and drive exansion bays) - Started to refactor `network` module to make it more readable - Dependencies in `setup.py` now reads its requirements from `requirements.txt` to avoid double maintenance --- netbox_agent/cli.py | 7 +- netbox_agent/config.py | 13 +++- netbox_agent/inventory.py | 41 ++++++++--- netbox_agent/network.py | 113 ++++++++++++++--------------- netbox_agent/server.py | 38 +++++++--- netbox_agent/vendors/dell.py | 7 -- netbox_agent/vendors/generic.py | 26 ------- netbox_agent/vendors/hp.py | 17 +++-- netbox_agent/vendors/supermicro.py | 19 ----- setup.py | 26 ++++--- 10 files changed, 155 insertions(+), 152 deletions(-) diff --git a/netbox_agent/cli.py b/netbox_agent/cli.py index a379ae2..e112469 100644 --- a/netbox_agent/cli.py +++ b/netbox_agent/cli.py @@ -1,5 +1,4 @@ from packaging import version - import netbox_agent.dmidecode as dmidecode from netbox_agent.config import config from netbox_agent.config import netbox_instance as nb @@ -39,11 +38,11 @@ def run(config): print('netbox-agent is not compatible with Netbox prior to verison 2.9') return False + if config.register or config.update_all or config.update_network or \ + config.update_location or config.update_inventory or config.update_psu: + server.netbox_create_or_update(config) if config.debug: server.print_debug() - if config.register or config.update_all or config.update_network or config.update_location or \ - config.update_inventory or config.update_psu: - server.netbox_create_or_update(config) return True diff --git a/netbox_agent/config.py b/netbox_agent/config.py index 68d7d82..6913f65 100644 --- a/netbox_agent/config.py +++ b/netbox_agent/config.py @@ -34,6 +34,7 @@ def get_config(): help='Manage blade expansions as external devices') p.add_argument('--log_level', default='debug') + p.add_argument('--netbox.ssl_ca_certs_file', help='SSL CA certificates file') p.add_argument('--netbox.url', help='Netbox URL') p.add_argument('--netbox.token', help='Netbox API Token') p.add_argument('--netbox.ssl_verify', default=True, action='store_true', @@ -80,8 +81,10 @@ def get_config(): return options +config = get_config() + + def get_netbox_instance(): - config = get_config() if config.netbox.url is None or config.netbox.token is None: logging.error('Netbox URL and token are mandatory') sys.exit(1) @@ -90,7 +93,12 @@ def get_netbox_instance(): url=get_config().netbox.url, token=get_config().netbox.token, ) - if get_config().netbox.ssl_verify is False: + ca_certs_file = config.netbox.ssl_ca_certs_file + if ca_certs_file is not None: + session = requests.Session() + session.verify = ca_certs_file + nb.http_session = session + elif config.netbox.ssl_verify is False: urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) session = requests.Session() session.verify = False @@ -99,5 +107,4 @@ def get_netbox_instance(): return nb -config = get_config() netbox_instance = get_netbox_instance() diff --git a/netbox_agent/inventory.py b/netbox_agent/inventory.py index 6525bea..6f7fab6 100644 --- a/netbox_agent/inventory.py +++ b/netbox_agent/inventory.py @@ -450,10 +450,11 @@ class Inventory(): if memory.get('serial') not in [x.serial for x in nb_memories]: self.create_netbox_memory(memory) - def create_netbox_gpus(self): - for gpu in self.lshw.get_hw_linux('gpu'): + def create_netbox_gpus(self, gpus): + for gpu in gpus: if 'product' in gpu and len(gpu['product']) > 50: gpu['product'] = (gpu['product'][:48] + '..') + manufacturer = self.find_or_create_manufacturer(gpu["vendor"]) _ = nb.dcim.inventory_items.create( device=self.device_id, @@ -461,26 +462,44 @@ class Inventory(): discovered=True, tags=[{'name': INVENTORY_TAG['gpu']['name']}], name=gpu['product'], - description='GPU {}'.format(gpu['product']), + description=gpu['description'], ) logging.info('Creating GPU model {}'.format(gpu['product'])) + def is_external_gpu(self, gpu): + is_3d_gpu = gpu['description'].startswith('3D') + return self.server.is_blade() and \ + self.server.own_gpu_expansion_slot() and is_3d_gpu + def do_netbox_gpus(self): - gpus = self.lshw.get_hw_linux('gpu') + gpus = [] + gpu_models = {} + for gpu in self.lshw.get_hw_linux('gpu'): + # Filters GPU if an expansion bay is detected: + # The internal (VGA) GPU only goes into the blade inventory, + # the external (3D) GPU goes into the expansion blade. + if config.expansion_as_device and \ + self.update_expansion ^ self.is_external_gpu(gpu): + continue + gpus.append(gpu) + gpu_models.setdefault(gpu["product"], 0) + gpu_models[gpu["product"]] += 1 + nb_gpus = self.get_netbox_inventory( device_id=self.device_id, tag=INVENTORY_TAG['gpu']['slug'], ) - - if config.expansion_as_device and len(nb_gpus): + nb_gpu_models = {} + for gpu in nb_gpus: + nb_gpu_models.setdefault(str(gpu), 0) + nb_gpu_models[str(gpu)] += 1 + up_to_date = set(gpu_models) == set(nb_gpu_models) + if not gpus or not up_to_date: for x in nb_gpus: x.delete() - elif not len(nb_gpus) or \ - len(nb_gpus) and len(gpus) != len(nb_gpus): - for x in nb_gpus: - x.delete() - self.create_netbox_gpus() + if gpus and not up_to_date: + self.create_netbox_gpus(gpus) def create_or_update(self): if config.inventory is None or config.update_inventory is None: diff --git a/netbox_agent/network.py b/netbox_agent/network.py index 3a390eb..cb5437e 100644 --- a/netbox_agent/network.py +++ b/netbox_agent/network.py @@ -148,19 +148,19 @@ class Network(object): if nic['mac'] is None: interface = self.nb_net.interfaces.get( name=nic['name'], - **self.custom_arg_id, + **self.custom_arg_id ) else: interface = self.nb_net.interfaces.get( mac_address=nic['mac'], name=nic['name'], - **self.custom_arg_id, + **self.custom_arg_id ) return interface def get_netbox_network_cards(self): return self.nb_net.interfaces.filter( - **self.custom_arg_id, + **self.custom_arg_id ) def get_netbox_type_for_nic(self, nic): @@ -267,12 +267,12 @@ class Network(object): nb_vlan = None - params = { + params = dict(self.custom_arg) + params.update({ 'name': nic['name'], 'type': type, 'mgmt_only': mgmt, - **self.custom_arg, - } + }) if not nic.get('virtual', False): params['mac_address'] = nic['mac'] @@ -338,59 +338,58 @@ class Network(object): netbox_ip = nb.ipam.ip_addresses.create( **query_params ) - else: - netbox_ip = list(netbox_ips)[0] - # If IP exists in anycast - if netbox_ip.role and netbox_ip.role.label == 'Anycast': - logging.debug('IP {} is Anycast..'.format(ip)) - unassigned_anycast_ip = [x for x in netbox_ips if x.interface is None] - assigned_anycast_ip = [x for x in netbox_ips if - x.interface and x.interface.id == interface.id] - # use the first available anycast ip - if len(unassigned_anycast_ip): - logging.info('Assigning existing Anycast IP {} to interface'.format(ip)) - netbox_ip = unassigned_anycast_ip[0] - netbox_ip.interface = interface - netbox_ip.save() - # or if everything is assigned to other servers - elif not len(assigned_anycast_ip): - logging.info('Creating Anycast IP {} and assigning it to interface'.format(ip)) - query_params = { - "address": ip, - "status": "active", - "role": self.ipam_choices['ip-address:role']['Anycast'], - "tenant": self.tenant.id if self.tenant else None, - "assigned_object_type": self.assigned_object_type, - "assigned_object_id": interface.id - } - netbox_ip = nb.ipam.ip_addresses.create(**query_params) - return netbox_ip - else: - if hasattr(netbox_ip, 'interface') and netbox_ip.interface is None or \ - hasattr(netbox_ip, 'assigned_object') and netbox_ip.assigned_object is None: - logging.info('Assigning existing IP {ip} to {interface}'.format( - ip=ip, interface=interface)) - elif hasattr(netbox_ip, 'interface') and \ - netbox_ip.interface.id != interface.id or \ - hasattr(netbox_ip, 'assigned_object') and \ - netbox_ip.assigned_object_id != interface.id: + return netbox_ip - old_interface = netbox_ip.assigned_object - logging.info( - 'Detected interface change for ip {ip}: old interface is ' - '{old_interface} (id: {old_id}), new interface is {new_interface} ' - ' (id: {new_id})' - .format( - old_interface=old_interface, new_interface=interface, - old_id=netbox_ip.id, new_id=interface.id, ip=netbox_ip.address - )) - else: - return netbox_ip - - netbox_ip.assigned_object_type = self.assigned_object_type - netbox_ip.assigned_object_id = interface.id + netbox_ip = next(netbox_ips) + # If IP exists in anycast + if netbox_ip.role and netbox_ip.role.label == 'Anycast': + logging.debug('IP {} is Anycast..'.format(ip)) + unassigned_anycast_ip = [x for x in netbox_ips if x.interface is None] + assigned_anycast_ip = [x for x in netbox_ips if + x.interface and x.interface.id == interface.id] + # use the first available anycast ip + if len(unassigned_anycast_ip): + logging.info('Assigning existing Anycast IP {} to interface'.format(ip)) + netbox_ip = unassigned_anycast_ip[0] + netbox_ip.interface = interface netbox_ip.save() - return netbox_ip + # or if everything is assigned to other servers + elif not len(assigned_anycast_ip): + logging.info('Creating Anycast IP {} and assigning it to interface'.format(ip)) + query_params = { + "address": ip, + "status": "active", + "role": self.ipam_choices['ip-address:role']['Anycast'], + "tenant": self.tenant.id if self.tenant else None, + "assigned_object_type": self.assigned_object_type, + "assigned_object_id": interface.id + } + netbox_ip = nb.ipam.ip_addresses.create(**query_params) + return netbox_ip + else: + ip_interface = getattr(netbox_ip, 'interface', None) + assigned_object = getattr(netbox_ip, 'assigned_object', None) + if not ip_interface or not assigned_object: + logging.info('Assigning existing IP {ip} to {interface}'.format( + ip=ip, interface=interface)) + elif (ip_interface and ip_interface.id != interface.id) or \ + (assigned_object and assigned_object_id != interface.id): + + old_interface = getattr(netbox_ip, "assigned_object", "n/a") + logging.info( + 'Detected interface change for ip {ip}: old interface is ' + '{old_interface} (id: {old_id}), new interface is {new_interface} ' + ' (id: {new_id})' + .format( + old_interface=old_interface, new_interface=interface, + old_id=netbox_ip.id, new_id=interface.id, ip=netbox_ip.address + )) + else: + return netbox_ip + + netbox_ip.assigned_object_type = self.assigned_object_type + netbox_ip.assigned_object_id = interface.id + netbox_ip.save() def create_or_update_netbox_network_cards(self): if config.update_all is None or config.update_network is None: diff --git a/netbox_agent/server.py b/netbox_agent/server.py index a20023b..24758ae 100644 --- a/netbox_agent/server.py +++ b/netbox_agent/server.py @@ -25,7 +25,6 @@ class ServerBase(): self.bios = dmidecode.get_by_type(self.dmi, 'BIOS') self.chassis = dmidecode.get_by_type(self.dmi, 'Chassis') self.system = dmidecode.get_by_type(self.dmi, 'System') - self.inventory = Inventory(server=self) self.network = None @@ -279,8 +278,10 @@ class ServerBase(): def _netbox_set_or_update_blade_slot(self, server, chassis, datacenter): # before everything check if right chassis - actual_device_bay = server.parent_device.device_bay if server.parent_device else None - actual_chassis = actual_device_bay.device if actual_device_bay else None + actual_device_bay = server.parent_device.device_bay \ + if server.parent_device else None + actual_chassis = actual_device_bay.device \ + if actual_device_bay else None slot = self.get_blade_slot() if actual_chassis and \ actual_chassis.serial == chassis.serial and \ @@ -291,7 +292,11 @@ class ServerBase(): device_id=chassis.id, name=slot, ) - if len(real_device_bays) > 0: + real_device_bays = nb.dcim.device_bays.filter( + device_id=chassis.id, + name=slot, + ) + if real_device_bays: logging.info( 'Setting device ({serial}) new slot on {slot} ' '(Chassis {chassis_serial})..'.format( @@ -299,10 +304,14 @@ class ServerBase(): )) # reset actual device bay if set if actual_device_bay: + # Forces the evaluation of the installed_device attribute to + # workaround a bug probably due to lazy loading optimization + # that prevents the value change detection + actual_device_bay.installed_device actual_device_bay.installed_device = None actual_device_bay.save() # setup new device bay - real_device_bay = real_device_bays[0] + real_device_bay = next(real_device_bays) real_device_bay.installed_device = server real_device_bay.save() else: @@ -324,7 +333,7 @@ class ServerBase(): device_id=chassis.id, name=slot, ) - if len(real_device_bays) == 0: + if not real_device_bays: logging.error('Could not find slot {slot} expansion for chassis'.format( slot=slot )) @@ -336,10 +345,14 @@ class ServerBase(): )) # reset actual device bay if set if actual_device_bay: + # Forces the evaluation of the installed_device attribute to + # workaround a bug probably due to lazy loading optimization + # that prevents the value change detection + actual_device_bay.installed_device actual_device_bay.installed_device = None actual_device_bay.save() # setup new device bay - real_device_bay = real_device_bays[0] + real_device_bay = next(real_device_bays) real_device_bay.installed_device = expansion real_device_bay.save() @@ -389,6 +402,7 @@ class ServerBase(): update_inventory = config.inventory and (config.register or config.update_all or config.update_inventory) # update inventory if feature is enabled + self.inventory = Inventory(server=self) if update_inventory: self.inventory.create_or_update() # update psu @@ -417,12 +431,12 @@ class ServerBase(): # for every other specs # check hostname if server.name != self.get_hostname(): - update += 1 server.name = self.get_hostname() + update += 1 if sorted(set([x.name for x in server.tags])) != sorted(set(self.tags)): - update += 1 server.tags = [x.id for x in self.nb_tags] + update += 1 if config.update_all or config.update_location: ret, server = self.update_netbox_location(server) @@ -458,3 +472,9 @@ class ServerBase(): print('NIC:',) pprint(self.network.get_network_cards()) pass + + def own_expansion_slot(self): + """ + Indicates if the device hosts an expansion card + """ + return False diff --git a/netbox_agent/vendors/dell.py b/netbox_agent/vendors/dell.py index b9a3212..dddb67a 100644 --- a/netbox_agent/vendors/dell.py +++ b/netbox_agent/vendors/dell.py @@ -86,10 +86,3 @@ class DellHost(ServerBase): Expansion slot are always the compute bay number + 1 """ raise NotImplementedError - - def own_expansion_slot(self): - """ - Say if the device can host an extension card based - on the product name - """ - pass diff --git a/netbox_agent/vendors/generic.py b/netbox_agent/vendors/generic.py index eddd8fa..6080112 100644 --- a/netbox_agent/vendors/generic.py +++ b/netbox_agent/vendors/generic.py @@ -21,29 +21,3 @@ class GenericHost(ServerBase): def get_chassis_service_tag(self): return self.get_service_tag() - - def get_expansion_product(self): - """ - Get the extension slot that is on a pair slot number - next to the compute slot that is on an odd slot number - """ - raise NotImplementedError - - def is_expansion_slot(self, server): - """ - Return True if its an extension slot - """ - raise NotImplementedError - - def get_blade_expansion_slot(self): - """ - Expansion slot are always the compute bay number + 1 - """ - raise NotImplementedError - - def own_expansion_slot(self): - """ - Say if the device can host an extension card based - on the product name - """ - pass diff --git a/netbox_agent/vendors/hp.py b/netbox_agent/vendors/hp.py index 5545972..78b3092 100644 --- a/netbox_agent/vendors/hp.py +++ b/netbox_agent/vendors/hp.py @@ -1,5 +1,6 @@ import netbox_agent.dmidecode as dmidecode from netbox_agent.server import ServerBase +from netbox_agent.inventory import Inventory class HPHost(ServerBase): @@ -92,24 +93,28 @@ class HPHost(ServerBase): def own_expansion_slot(self): """ - Say if the device can host an extension card based - on the product name + Indicates if the device hosts an expension card """ return self.own_gpu_expansion_slot() or self.own_disk_expansion_slot() def own_gpu_expansion_slot(self): """ - Say if the device can host an extension card based + Indicates if the device hosts a GPU expansion card based on the product name """ return self.get_product_name().endswith('Graphics Exp') def own_disk_expansion_slot(self): """ - Say if the device can host an extension card based - on the product name + Indicates if the device hosts a drive expansion card based + on raid card attributes. """ - for raid_card in self.inventory.get_raid_cards(): + # Uses already parsed inventory if available + # parses it otherwise + inventory = getattr(self, "inventory", None) + if inventory is None: + inventory = Inventory(self) + for raid_card in inventory.get_raid_cards(): if self.is_blade() and raid_card.is_external(): return True return False diff --git a/netbox_agent/vendors/supermicro.py b/netbox_agent/vendors/supermicro.py index 32e8fdf..d27b343 100644 --- a/netbox_agent/vendors/supermicro.py +++ b/netbox_agent/vendors/supermicro.py @@ -77,22 +77,3 @@ class SupermicroHost(ServerBase): I only know on model of slot GPU extension card that. """ raise NotImplementedError - - def is_expansion_slot(self, server): - """ - Return True if its an extension slot, based on the name - """ - raise NotImplementedError - - def get_blade_expansion_slot(self): - """ - Expansion slot are always the compute bay number + 1 - """ - raise NotImplementedError - - def own_expansion_slot(self): - """ - Say if the device can host an extension card based - on the product name - """ - pass diff --git a/setup.py b/setup.py index 1ecdeb2..ad2ff7f 100644 --- a/setup.py +++ b/setup.py @@ -1,8 +1,22 @@ from setuptools import find_packages, setup +import os + +def get_requirements(): + reqs_path = os.path.join( + os.path.dirname(__file__), + 'requirements.txt' + ) + with open(reqs_path, 'r') as f: + reqs = [ + r.strip() for r in f + if r.strip() + ] + return reqs + setup( name='netbox_agent', - version='0.6.2', + version='0.6.3', description='NetBox agent for server', long_description=open('README.md', encoding="utf-8").read(), long_description_content_type='text/markdown', @@ -13,15 +27,7 @@ setup( include_package_data=True, packages=find_packages(exclude=["*.tests", "*.tests.*", "tests.*", "tests"]), use_scm_version=True, - install_requires=[ - 'pynetbox==6.1.2', - 'netaddr==0.8.0', - 'netifaces==0.10.9', - 'pyyaml==5.4.1', - 'jsonargparse==2.32.2', - 'python-slugify==5.0.2', - 'packaging==20.9', - ], + install_requires=get_requirements(), zip_safe=False, keywords=['netbox'], classifiers=[ From d1ee380ffb2ac552329b9b22afb7fd20f9a11120 Mon Sep 17 00:00:00 2001 From: Cyril LEVIS Date: Wed, 16 Feb 2022 11:22:51 +0100 Subject: [PATCH 072/148] feat: add custom_fields suppport ```bash $ netbox_agent -du --device.custom_fields="last_run=$(date),last_run_ts=$(date +'%s')" ``` obviously, custom_fields need to be create manually in netbox admin --- README.md | 2 +- netbox_agent.yaml.example | 5 +++-- netbox_agent/config.py | 2 ++ netbox_agent/server.py | 15 +++++++++++++++ 4 files changed, 21 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 12eb900..af6d2b2 100644 --- a/README.md +++ b/README.md @@ -111,7 +111,7 @@ network: # blade_role: "Blade" # server_role: "Server" # tags: server, blade, ,just a comma,delimited,list -# +# custom_fields: field1=value1,field2=value2# # # Can use this to set the tenant # diff --git a/netbox_agent.yaml.example b/netbox_agent.yaml.example index a769fb4..a1a8e02 100644 --- a/netbox_agent.yaml.example +++ b/netbox_agent.yaml.example @@ -18,14 +18,15 @@ network: # blade_role: "Blade" # server_role: "Server" # tags: server, blade, ,just a comma,delimited,list - +# custom_fields: field1=value1,field2=value2 +# # # Use this to set the tenant # #tenant: # driver: "file:/tmp/tenant" # regex: "(.*)" - + datacenter_location: driver: "cmd:cat /etc/qualification | tr [A-Z] [a-z]" regex: "datacenter: (?P[A-Za-z0-9]+)" diff --git a/netbox_agent/config.py b/netbox_agent/config.py index 6913f65..d820836 100644 --- a/netbox_agent/config.py +++ b/netbox_agent/config.py @@ -45,6 +45,8 @@ def get_config(): help="Command to output hostname, used as Device's name in netbox") p.add_argument('--device.tags', default=r'', help='tags to use for a host') + p.add_argument('--device.custom_fields', default=r'', + help='custom_fields to use for a host, eg: field1=v1,field2=v2') p.add_argument('--device.blade_role', default=r'Blade', help='role to use for a blade server') p.add_argument('--device.chassis_role', default=r'Server Chassis', diff --git a/netbox_agent/server.py b/netbox_agent/server.py index 24758ae..df9a5a5 100644 --- a/netbox_agent/server.py +++ b/netbox_agent/server.py @@ -32,6 +32,15 @@ class ServerBase(): x.strip() for x in config.device.tags.split(',') if x.strip() ])) if config.device.tags else [] self.nb_tags = list(create_netbox_tags(self.tags)) + config_cf = set([ + f.strip() for f in config.device.custom_fields.split(",") + if f.strip() + ]) + self.custom_fields = {} + self.custom_fields.update(dict([ + (k.strip(), v.strip()) for k, v in + [f.split("=", 1) for f in config_cf] + ])) def get_tenant(self): tenant = Tenant() @@ -195,6 +204,7 @@ class ServerBase(): tenant=tenant.id if tenant else None, rack=rack.id if rack else None, tags=[{'name': x} for x in self.tags], + custom_fields=self.custom_fields, ) return new_chassis @@ -217,6 +227,7 @@ class ServerBase(): tenant=tenant.id if tenant else None, rack=rack.id if rack else None, tags=[{'name': x} for x in self.tags], + custom_fields=self.custom_fields, ) return new_blade @@ -438,6 +449,10 @@ class ServerBase(): server.tags = [x.id for x in self.nb_tags] update += 1 + if server.custom_fields != self.custom_fields: + server.custom_fields = self.custom_fields + update += 1 + if config.update_all or config.update_location: ret, server = self.update_netbox_location(server) update += ret From 1fc0aee929fb37e0079e9ed62d3577bfc2615d01 Mon Sep 17 00:00:00 2001 From: Christophe Simon Date: Tue, 22 Feb 2022 17:58:01 +0100 Subject: [PATCH 073/148] Bumped version number Those changes deserve a major release :) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index ad2ff7f..f46a5f9 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ def get_requirements(): setup( name='netbox_agent', - version='0.6.3', + version='0.7.0', description='NetBox agent for server', long_description=open('README.md', encoding="utf-8").read(), long_description_content_type='text/markdown', From 021c5db7d3daef12f1c24081ef4b39b07b20e266 Mon Sep 17 00:00:00 2001 From: Cyril LEVIS Date: Wed, 23 Feb 2022 11:24:44 +0100 Subject: [PATCH 074/148] fix: add MANIFEST.in for packaging --- MANIFEST.in | 1 + 1 file changed, 1 insertion(+) create mode 100644 MANIFEST.in diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..f9bd145 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +include requirements.txt From b092079820bb34e4f9138ab18a75480240f4a7c1 Mon Sep 17 00:00:00 2001 From: Cyril LEVIS Date: Wed, 23 Feb 2022 14:44:30 +0100 Subject: [PATCH 075/148] chore: add rpmenv config for rpm packaging as a virtualenv https://github.com/kevinconway/rpmvenv --- rpmenv.json | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 rpmenv.json diff --git a/rpmenv.json b/rpmenv.json new file mode 100644 index 0000000..0c70c7d --- /dev/null +++ b/rpmenv.json @@ -0,0 +1,27 @@ +{ + "extensions": { + "enabled": ["python_venv", "blocks"] + }, + "core": { + "group": "Application/System", + "license": "Apache2", + "name": "netbox-agent", + "summary": "NetBox agent for server", + "url": "https://github.com/Solvik/netbox-agent", + "version": "0.7.0", + "requires": ["lshw"] + }, + "python_venv": { + "python": "python3.6", + "requirements": ["requirements.txt"], + "name": "netbox-agent", + "path": "/opt/" + }, + "blocks": { + "post": ["ln -sf /opt/netbox-agent/bin/netbox_agent /usr/bin/netbox_agent"], + "desc": [ + "This project aims to create hardware automatically into Netbox based on standard tools (dmidecode, lldpd, parsing /sys/, etc).", + "The goal is to generate an existing infrastructure on Netbox and have the ability to update it regularly by executing the agent." + ] + } +} From af9df9ab4bdeac44c061e16ce289f87c700745b1 Mon Sep 17 00:00:00 2001 From: Christophe Simon Date: Wed, 23 Feb 2022 15:48:41 +0100 Subject: [PATCH 076/148] Fixed PSU power consumption calculation When PSUs were found, the voltage value was never set, raising an exception when trying to multiply current * voltage (float * None). This patch reads per PSU voltage (with 230V as default) and define power consumption from it. --- netbox_agent/power.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/netbox_agent/power.py b/netbox_agent/power.py index ea957e9..2d14e5e 100644 --- a/netbox_agent/power.py +++ b/netbox_agent/power.py @@ -105,18 +105,20 @@ class PowerSupply(): return False # find power feeds for rack or dc - voltage = None pwr_feeds = None if self.netbox_server.rack: pwr_feeds = nb.dcim.power_feeds.filter( rack=self.netbox_server.rack.id ) - if pwr_feeds is None or not len(pwr_feeds): + + if pwr_feeds: + voltage = [p['voltage'] for p in pwr_feeds] + else: logging.info('Could not find power feeds for Rack, defaulting value to 230') - voltage = 230 + voltage = [230 for _ in nb_psu] for i, nb_psu in enumerate(nb_psus): - nb_psu.allocated_draw = float(psu_cons[i]) * voltage + nb_psu.allocated_draw = int(float(psu_cons[i]) * voltage[i]) if nb_psu.allocated_draw < 1: logging.info('PSU is not connected or in standby mode') continue From e789619b34d18ec893b5d84560133a880160bd48 Mon Sep 17 00:00:00 2001 From: Christophe Simon Date: Fri, 25 Feb 2022 18:43:09 +0100 Subject: [PATCH 077/148] Added disks extended attributes This patch brings some of the physical and virtual drive attributes as `custom_fields` to the disks inventory. The goal is to have this information present to ease disks maintenance when a drive becomes unavailable and its attributes can't be read anymore from the RAID controller. It also helps to standardize the extended disk attributes across the different manufacturers. As the disk physical identifers were not available under the correct format (hexadecimal format using the `xml` output as opposed as `X:Y:Z` format using the default `list` format), the command line parser has been refactored to read the `list` format, rather than `xml` one in the `omreport` raid controller parser. As the custom fields have to be created prior being able to register the disks extended attributes, this feature is only activated using the `--process-virtual-drives` command line parameter, or by setting `process_virtual_drives` to `true` in the configuration file. The custom fields to create as `DCIM > inventory item` `Text` are described below. NAME LABEL DESCRIPTION mount_point Mount point Device mount point(s) pd_identifier Physical disk identifier Physical disk identifier in the RAID controller vd_array Virtual drive array Virtual drive array the disk is member of vd_consistency Virtual drive consistency Virtual disk array consistency vd_device Virtual drive device Virtual drive system device vd_raid_type Virtual drive RAID Virtual drive array RAID type vd_size Virtual drive size Virtual drive array size In the current implementation, the disks attributes ore not updated: if a disk with the correct serial number is found, it's sufficient to consider it as up to date. To force the reprocessing of the disks extended attributes, the `--force-disk-refresh` command line option can be used: it removes all existing disks to before populating them with the correct parsing. Unless this option is specified, the extended attributes won't be modified unless a disk is replaced. It is possible to dump the physical/virtual disks map on the filesystem under the JSON notation to ease or automate disks management. The file path has to be provided using the `--dump-disks-map` command line parameter. --- README.md | 40 ++++++++- netbox_agent/config.py | 7 ++ netbox_agent/inventory.py | 142 ++++++++++++++++++------------ netbox_agent/lshw.py | 82 +++++++++--------- netbox_agent/misc.py | 27 ++++-- netbox_agent/network.py | 4 +- netbox_agent/power.py | 2 +- netbox_agent/raid/hp.py | 113 ++++++++++++++++++------ netbox_agent/raid/omreport.py | 138 +++++++++++++++++++---------- netbox_agent/raid/storcli.py | 148 ++++++++++++++++++++++++-------- netbox_agent/server.py | 23 +++-- netbox_agent/vendors/generic.py | 2 +- 12 files changed, 505 insertions(+), 223 deletions(-) diff --git a/README.md b/README.md index af6d2b2..0833a64 100644 --- a/README.md +++ b/README.md @@ -92,10 +92,12 @@ netbox: token: supersecrettoken # uncomment to disable ssl verification # ssl_verify: false + # uncomment to use the system's CA certificates + # ssl_ca_certs_file: /etc/ssl/certs/ca-certificates.crt # Network configuration network: - # Regex to ignore interfaces + # Regex to ignore interfaces ignore_interfaces: "(dummy.*|docker.*)" # Regex to ignore IP addresses ignore_ips: (127\.0\.0\..*) @@ -119,7 +121,7 @@ network: # driver: "file:/tmp/tenant" # regex: "(.*)" -## Enable virtual machine support +## Enable virtual machine support # virtual: # # not mandatory, can be guessed # enabled: True @@ -145,7 +147,7 @@ rack_location: # driver: "file:/tmp/datacenter" # regex: "(.*)" -# Enable local inventory reporting +# Enable local inventory reporting inventory: true ``` @@ -160,6 +162,36 @@ The `get_blade_slot` method return the name of the `Device Bay`. Certain vendors don't report the blade slot in `dmidecode`, so we can use the `slot_location` regex feature of the configuration file. +Some blade servers can be equipped with additional hardware using expansion blades, next to the processing blade, such as GPU expansion, or drives bay expansion. By default, the hardware from the expnasion is associated with the blade server itself, but it's possible to register the expansion as its own device using the `--expansion-as-device` command line parameter, or by setting `expansion_as_device` to `true` in the configuration file. + +## Drives attributes processing + +It is possible to process drives extended attributes such as the drive's physical or logical identifier, logical drive RAID type, size, consistency and so on. + +Those attributes as set as `custom_fields` in Netbox, and need to be registered properly before being able to specify them during the inventory phase. + +As the custom fields have to be created prior being able to register the disks extended attributes, this feature is only activated using the `--process-virtual-drives` command line parameter, or by setting `process_virtual_drives` to `true` in the configuration file. + +The custom fields to create as `DCIM > inventory item` `Text` are described below. + +``` +NAME LABEL DESCRIPTION +mount_point Mount point Device mount point(s) +pd_identifier Physical disk identifier Physical disk identifier in the RAID controller +vd_array Virtual drive array Virtual drive array the disk is member of +vd_consistency Virtual drive consistency Virtual disk array consistency +vd_device Virtual drive device Virtual drive system device +vd_raid_type Virtual drive RAID Virtual drive array RAID type +vd_size Virtual drive size Virtual drive array size +``` + +In the current implementation, the disks attributes ore not updated: if a disk with the correct serial number is found, it's sufficient to consider it as up to date. + +To force the reprocessing of the disks extended attributes, the `--force-disk-refresh` command line option can be used: it removes all existing disks to before populating them with the correct parsing. Unless this option is specified, the extended attributes won't be modified unless a disk is replaced. + +It is possible to dump the physical/virtual disks map on the filesystem under the JSON notation to ease or automate disks management. The file path has to be provided using the `--dump-disks-map` command line parameter. + + ## Anycast IP The default behavior of the agent is to assign an interface to an IP. @@ -256,5 +288,5 @@ On a personal note, I use the docker image from [netbox-community/netbox-docker] # git clone https://github.com/netbox-community/netbox-docker # cd netbox-docker # docker-compose pull -# docker-compose up +# docker-compose up ``` diff --git a/netbox_agent/config.py b/netbox_agent/config.py index d820836..0ca1b64 100644 --- a/netbox_agent/config.py +++ b/netbox_agent/config.py @@ -78,6 +78,13 @@ def get_config(): p.add_argument('--network.lldp', help='Enable auto-cabling feature through LLDP infos') p.add_argument('--inventory', action='store_true', help='Enable HW inventory (CPU, Memory, RAID Cards, Disks) feature') + p.add_argument('--process-virtual-drives', action='store_true', + help='Process virtual drives information from RAID ' + 'controllers to fill disk custom_fields') + p.add_argument('--force-disk-refresh', action='store_true', + help='Forces disks detection reprocessing') + p.add_argument('--dump-disks-map', + help='File path to dump physical/virtual disks map') options = p.parse_args() return options diff --git a/netbox_agent/inventory.py b/netbox_agent/inventory.py index 6f7fab6..0c8fcfe 100644 --- a/netbox_agent/inventory.py +++ b/netbox_agent/inventory.py @@ -1,8 +1,3 @@ -import logging -import re - -import pynetbox - from netbox_agent.config import config from netbox_agent.config import netbox_instance as nb from netbox_agent.lshw import LSHW @@ -10,6 +5,12 @@ from netbox_agent.misc import get_vendor, is_tool from netbox_agent.raid.hp import HPRaid from netbox_agent.raid.omreport import OmreportRaid from netbox_agent.raid.storcli import StorcliRaid +import traceback +import pynetbox +import logging +import json +import re + INVENTORY_TAG = { 'cpu': {'name': 'hw:cpu', 'slug': 'hw-cpu'}, @@ -226,7 +227,7 @@ class Inventory(): def get_raid_cards(self, filter_cards=False): raid_class = None - if self.server.manufacturer == 'Dell': + if self.server.manufacturer in ('Dell', 'Huawei'): if is_tool('omreport'): raid_class = OmreportRaid if is_tool('storcli'): @@ -302,53 +303,60 @@ class Inventory(): if raid_card.get_serial_number() not in [x.serial for x in nb_raid_cards]: self.create_netbox_raid_card(raid_card) - def is_virtual_disk(self, disk): + def is_virtual_disk(self, disk, raid_devices): + disk_type = disk.get('type') logicalname = disk.get('logicalname') description = disk.get('description') size = disk.get('size') product = disk.get('product') - + if logicalname in raid_devices or disk_type is None: + return True non_raid_disks = [ 'MR9361-8i', ] - if size is None and logicalname is None or \ - 'virtual' in product.lower() or 'logical' in product.lower() or \ + if logicalname in raid_devices or \ + disk_type is None or \ product in non_raid_disks or \ + 'virtual' in product.lower() or \ + 'logical' in product.lower() or \ + 'volume' in description.lower() or \ description == 'SCSI Enclosure' or \ - 'volume' in description.lower(): + (size is None and logicalname is None): return True return False def get_hw_disks(self): disks = [] + for raid_card in self.get_raid_cards(filter_cards=True): + disks.extend(raid_card.get_physical_disks()) + + raid_devices = [ + d.get('custom_fields', {}).get('vd_device') + for d in disks + if d.get('custom_fields', {}).get('vd_device') + ] + for disk in self.lshw.get_hw_linux("storage"): - if self.is_virtual_disk(disk): + if self.is_virtual_disk(disk, raid_devices): continue - - logicalname = disk.get('logicalname') - description = disk.get('description') - size = disk.get('size', 0) - product = disk.get('product') - serial = disk.get('serial') - - d = {} - d["name"] = "" - d['Size'] = '{} GB'.format(int(size / 1024 / 1024 / 1024)) - d['logicalname'] = logicalname - d['description'] = description - d['SN'] = serial - d['Model'] = product + size =int(disk.get('size', 0)) / 1073741824 + d = { + "name": "", + 'Size': '{} GB'.format(size), + 'logicalname': disk.get('logicalname'), + 'description': disk.get('description'), + 'SN': disk.get('serial'), + 'Model': disk.get('product'), + 'Type': disk.get('type'), + } if disk.get('vendor'): d['Vendor'] = disk['vendor'] else: d['Vendor'] = get_vendor(disk['product']) disks.append(d) - for raid_card in self.get_raid_cards(filter_cards=True): - disks += raid_card.get_physical_disks() - # remove duplicate serials seen = set() uniq = [x for x in disks if x['SN'] not in seen and not seen.add(x['SN'])] @@ -361,53 +369,79 @@ class Inventory(): logicalname = disk.get('logicalname') desc = disk.get('description') - # nonraid disk - if logicalname and desc: - if type(logicalname) is list: - logicalname = logicalname[0] - name = '{} - {} ({})'.format( - desc, - logicalname, - disk.get('Size', 0)) - description = 'Device {}'.format(disk.get('logicalname', 'Unknown')) - else: - name = '{} ({})'.format(disk['Model'], disk['Size']) - description = '{}'.format(disk['Type']) + name = '{} ({})'.format(disk['Model'], disk['Size']) + description = disk['Type'] - _ = nb.dcim.inventory_items.create( - device=self.device_id, - discovered=True, - tags=[{'name': INVENTORY_TAG['disk']['name']}], - name=name, - serial=disk['SN'], - part_id=disk['Model'], - description=description, - manufacturer=manufacturer.id if manufacturer else None - ) + parms = { + 'device': self.device_id, + 'discovered': True, + 'tags': [{'name': INVENTORY_TAG['disk']['name']}], + 'name': name, + 'serial': disk['SN'], + 'part_id': disk['Model'], + 'description': description, + 'manufacturer': getattr(manufacturer, "id", None), + } + if config.process_virtual_drives: + parms['custom_fields'] = disk.get("custom_fields", {}) + + _ = nb.dcim.inventory_items.create(**parms) logging.info('Creating Disk {model} {serial}'.format( model=disk['Model'], serial=disk['SN'], )) + def dump_disks_map(self, disks): + disk_map = [d['custom_fields'] for d in disks if 'custom_fields' in d] + if config.dump_disks_map == "-": + f = sys.stdout + else: + f = open(config.dump_disks_map, "w") + f.write( + json.dumps( + disk_map, + separators=(',', ':'), + indent=4, + sort_keys=True + ) + ) + if config.dump_disks_map != "-": + f.close() + def do_netbox_disks(self): nb_disks = self.get_netbox_inventory( device_id=self.device_id, - tag=INVENTORY_TAG['disk']['slug']) + tag=INVENTORY_TAG['disk']['slug'] + ) disks = self.get_hw_disks() + if config.dump_disks_map: + try: + self.dump_disks_map(disks) + except Exception as e: + logging.error("Failed to dump disks map: {}".format(e)) + logging.debug(traceback.format_exc()) + disk_serials = [d['SN'] for d in disks if 'SN' in d] # delete disks that are in netbox but not locally # use the serial_number has the comparison element for nb_disk in nb_disks: - if nb_disk.serial not in [x['SN'] for x in disks if x.get('SN')]: + if nb_disk.serial not in disk_serials or \ + config.force_disk_refresh: logging.info('Deleting unknown locally Disk {serial}'.format( serial=nb_disk.serial, )) nb_disk.delete() + if config.force_disk_refresh: + nb_disks = self.get_netbox_inventory( + device_id=self.device_id, + tag=INVENTORY_TAG['disk']['slug'] + ) + # create disks that are not in netbox for disk in disks: - if disk.get('SN') not in [x.serial for x in nb_disks]: + if disk.get('SN') not in [d.serial for d in nb_disks]: self.create_netbox_disk(disk) def create_netbox_memory(self, memory): diff --git a/netbox_agent/lshw.py b/netbox_agent/lshw.py index 8070633..e19effc 100644 --- a/netbox_agent/lshw.py +++ b/netbox_agent/lshw.py @@ -1,9 +1,8 @@ -import json -import logging -import subprocess -import sys - from netbox_agent.misc import is_tool +import subprocess +import logging +import json +import sys class LSHW(): @@ -15,7 +14,13 @@ class LSHW(): data = subprocess.getoutput( 'lshw -quiet -json' ) - self.hw_info = json.loads(data) + json_data = json.loads(data) + # Starting from version 02.18, `lshw -json` wraps its result in a list + # rather than returning directly a dictionary + if isinstance(json_data, list): + self.hw_info = json_data[0] + else: + self.hw_info = json_data self.info = {} self.memories = [] self.interfaces = [] @@ -77,42 +82,41 @@ class LSHW(): def find_storage(self, obj): if "children" in obj: for device in obj["children"]: - d = {} - d["logicalname"] = device.get("logicalname") - d["product"] = device.get("product") - d["serial"] = device.get("serial") - d["version"] = device.get("version") - d["size"] = device.get("size") - d["description"] = device.get("description") - - self.disks.append(d) - + self.disks.append({ + "logicalname": device.get("logicalname"), + "product": device.get("product"), + "serial": device.get("serial"), + "version": device.get("version"), + "size": device.get("size"), + "description": device.get("description"), + "type": device.get("description"), + }) elif "nvme" in obj["configuration"]["driver"]: if not is_tool('nvme'): logging.error('nvme-cli >= 1.0 does not seem to be installed') - else: - try: - nvme = json.loads( - subprocess.check_output( - ["nvme", '-list', '-o', 'json'], - encoding='utf8') - ) - - for device in nvme["Devices"]: - d = {} - d['logicalname'] = device["DevicePath"] - d['product'] = device["ModelNumber"] - d['serial'] = device["SerialNumber"] - d["version"] = device["Firmware"] - if "UsedSize" in device: - d['size'] = device["UsedSize"] - if "UsedBytes" in device: - d['size'] = device["UsedBytes"] - d['description'] = "NVME Disk" - - self.disks.append(d) - except Exception: - pass + return + try: + nvme = json.loads( + subprocess.check_output( + ["nvme", '-list', '-o', 'json'], + encoding='utf8') + ) + for device in nvme["Devices"]: + d = { + 'logicalname': device["DevicePath"], + 'product': device["ModelNumber"], + 'serial': device["SerialNumber"], + "version": device["Firmware"], + 'description': "NVME", + 'type': "NVME", + } + if "UsedSize" in device: + d['size'] = device["UsedSize"] + if "UsedBytes" in device: + d['size'] = device["UsedBytes"] + self.disks.append(d) + except Exception: + pass def find_cpus(self, obj): if "product" in obj: diff --git a/netbox_agent/misc.py b/netbox_agent/misc.py index 0f72d69..df4638c 100644 --- a/netbox_agent/misc.py +++ b/netbox_agent/misc.py @@ -1,10 +1,9 @@ -import socket -import subprocess -from shutil import which - -from slugify import slugify - from netbox_agent.config import netbox_instance as nb +from slugify import slugify +from shutil import which +import subprocess +import socket +import re def is_tool(name): @@ -74,3 +73,19 @@ def create_netbox_tags(tags): ) ret.append(nb_tag) return ret + + +def get_mount_points(): + mount_points = {} + output = subprocess.getoutput('mount') + for r in output.split("\n"): + if not r.startswith("/dev/"): + continue + mount_info = r.split() + device = mount_info[0] + device = re.sub(r'\d+$', '', device) + mp = mount_info[2] + mount_points.setdefault(device, []).append(mp) + return mount_points + + diff --git a/netbox_agent/network.py b/netbox_agent/network.py index cb5437e..3d641c7 100644 --- a/netbox_agent/network.py +++ b/netbox_agent/network.py @@ -325,7 +325,7 @@ class Network(object): netbox_ips = nb.ipam.ip_addresses.filter( address=ip, ) - if not len(netbox_ips): + if not netbox_ips: logging.info('Create new IP {ip} on {interface}'.format( ip=ip, interface=interface)) query_params = { @@ -340,7 +340,7 @@ class Network(object): ) return netbox_ip - netbox_ip = next(netbox_ips) + netbox_ip = list(netbox_ips)[0] # If IP exists in anycast if netbox_ip.role and netbox_ip.role.label == 'Anycast': logging.debug('IP {} is Anycast..'.format(ip)) diff --git a/netbox_agent/power.py b/netbox_agent/power.py index 2d14e5e..477c00e 100644 --- a/netbox_agent/power.py +++ b/netbox_agent/power.py @@ -115,7 +115,7 @@ class PowerSupply(): voltage = [p['voltage'] for p in pwr_feeds] else: logging.info('Could not find power feeds for Rack, defaulting value to 230') - voltage = [230 for _ in nb_psu] + voltage = [230 for _ in nb_psus] for i, nb_psu in enumerate(nb_psus): nb_psu.allocated_draw = int(float(psu_cons[i]) * voltage[i]) diff --git a/netbox_agent/raid/hp.py b/netbox_agent/raid/hp.py index 5d5d140..7089918 100644 --- a/netbox_agent/raid/hp.py +++ b/netbox_agent/raid/hp.py @@ -1,13 +1,20 @@ -import re -import subprocess - -from netbox_agent.config import config -from netbox_agent.misc import get_vendor from netbox_agent.raid.base import Raid, RaidController +from netbox_agent.misc import get_vendor +from netbox_agent.config import config +import subprocess +import logging +import re REGEXP_CONTROLLER_HP = re.compile(r'Smart Array ([a-zA-Z0-9- ]+) in Slot ([0-9]+)') +def ssacli(command): + output = subprocess.getoutput('ssacli {}'.format(command) ) + lines = output.split('\n') + lines = list(filter(None, lines)) + return lines + + def _parse_ctrl_output(lines): controllers = {} current_ctrl = None @@ -18,11 +25,11 @@ def _parse_ctrl_output(lines): ctrl = REGEXP_CONTROLLER_HP.search(line) if ctrl is not None: current_ctrl = ctrl.group(1) - controllers[current_ctrl] = {"Slot": ctrl.group(2)} - if "Embedded" not in line: - controllers[current_ctrl]["External"] = True + controllers[current_ctrl] = {'Slot': ctrl.group(2)} + if 'Embedded' not in line: + controllers[current_ctrl]['External'] = True continue - attr, val = line.split(": ", 1) + attr, val = line.split(': ', 1) attr = attr.strip() val = val.strip() controllers[current_ctrl][attr] = val @@ -39,27 +46,54 @@ def _parse_pd_output(lines): if not line or line.startswith('Note:'): continue # Parses the Array the drives are in - if line.startswith("Array"): + if line.startswith('Array'): current_array = line.split(None, 1)[1] # Detects new physical drive - if line.startswith("physicaldrive"): + if line.startswith('physicaldrive'): current_drv = line.split(None, 1)[1] drives[current_drv] = {} if current_array is not None: - drives[current_drv]["Array"] = current_array + drives[current_drv]['Array'] = current_array continue - if ": " not in line: + if ': ' not in line: continue - attr, val = line.split(": ", 1) + attr, val = line.split(': ', 1) drives.setdefault(current_drv, {})[attr] = val return drives +def _parse_ld_output(lines): + drives = {} + current_array = None + current_drv = None + + for line in lines: + line = line.strip() + if not line or line.startswith('Note:'): + continue + # Parses the Array the drives are in + if line.startswith('Array'): + current_array = line.split(None, 1)[1] + drives[current_array] = {} + # Detects new physical drive + if line.startswith('Logical Drive'): + current_drv = line.split(': ', 1)[1] + drives.setdefault(current_array, {})['LogicalDrive'] = current_drv + continue + if ': ' not in line: + continue + attr, val = line.split(': ', 1) + drives.setdefault(current_array, {})[attr] = val + return drives + + class HPRaidController(RaidController): def __init__(self, controller_name, data): self.controller_name = controller_name self.data = data - self.drives = self._get_physical_disks() + self.pdrives = self._get_physical_disks() + self.ldrives = self._get_logical_drives() + self._get_virtual_drives_map() def get_product_name(self): return self.controller_name @@ -77,15 +111,12 @@ class HPRaidController(RaidController): return self.data.get('External', False) def _get_physical_disks(self): - output = subprocess.getoutput( - 'ssacli ctrl slot={slot} pd all show detail'.format(slot=self.data['Slot']) - ) - lines = output.split('\n') - lines = list(filter(None, lines)) - drives = _parse_pd_output(lines) - ret = [] + lines = ssacli('ctrl slot={} pd all show detail'.format(self.data['Slot'])) + pdrives = _parse_pd_output(lines) + ret = {} - for name, attrs in drives.items(): + for name, attrs in pdrives.items(): + array = attrs.get('Array', '') model = attrs.get('Model', '').strip() vendor = None if model.startswith('HP'): @@ -95,7 +126,8 @@ class HPRaidController(RaidController): else: vendor = get_vendor(model) - ret.append({ + ret[name] = { + 'Array': array, 'Model': model, 'Vendor': vendor, 'SN': attrs.get('Serial Number', '').strip(), @@ -103,11 +135,40 @@ class HPRaidController(RaidController): 'Type': 'SSD' if attrs.get('Interface Type') == 'Solid State SATA' else 'HDD', '_src': self.__class__.__name__, - }) + } return ret + def _get_logical_drives(self): + lines = ssacli('ctrl slot={} ld all show detail'.format(self.data['Slot'])) + ldrives = _parse_ld_output(lines) + ret = {} + + for array, attrs in ldrives.items(): + ret[array] = { + 'vd_array': array, + 'vd_size': attrs['Size'], + 'vd_consistency': attrs['Status'], + 'vd_raid_type': 'RAID {}'.format(attrs['Fault Tolerance']), + 'vd_device': attrs['LogicalDrive'], + 'mount_point': attrs['Mount Points'] + } + return ret + + def _get_virtual_drives_map(self): + for name, attrs in self.pdrives.items(): + array = attrs["Array"] + ld = self.ldrives.get(array) + if ld is None: + logging.error( + "Failed to find array information for physical drive {}." + " Ignoring.".format(name) + ) + continue + attrs['custom_fields'] = ld + attrs['custom_fields']['pd_identifier'] = name + def get_physical_disks(self): - return self.drives + return list(self.pdrives.values()) class HPRaid(Raid): diff --git a/netbox_agent/raid/omreport.py b/netbox_agent/raid/omreport.py index 1a065db..6b4f780 100644 --- a/netbox_agent/raid/omreport.py +++ b/netbox_agent/raid/omreport.py @@ -1,25 +1,32 @@ -import re -import subprocess -import xml.etree.ElementTree as ET # NOQA - -from netbox_agent.misc import get_vendor from netbox_agent.raid.base import Raid, RaidController - -# Inspiration from https://github.com/asciiphil/perc-status/blob/master/perc-status +from netbox_agent.misc import get_vendor, get_mount_points +from netbox_agent.config import config +import subprocess +import logging +import re -def get_field(obj, fieldname): - f = obj.find(fieldname) - if f is None: - return None - if f.attrib['type'] in ['u32', 'u64']: - if re.search('Mask$', fieldname): - return int(f.text, 2) - else: - return int(f.text) - if f.attrib['type'] == 'astring': - return f.text - return f.text +def omreport(sub_command): + command = 'omreport {}'.format(sub_command) + output = subprocess.getoutput(command) + res = {} + section_re = re.compile('^[A-Z]') + current_section = None + current_obj = None + + for line in output.split('\n'): + if ': ' in line: + attr, value = line.split(': ', 1) + attr = attr.strip() + value = value.strip() + if attr == 'ID': + obj = {} + res.setdefault(current_section, []).append(obj) + current_obj = obj + current_obj[attr] = value + elif section_re.search(line) is not None: + current_section = line.strip() + return res class OmreportController(RaidController): @@ -28,49 +35,88 @@ class OmreportController(RaidController): self.controller_index = controller_index def get_product_name(self): - return get_field(self.data, 'Name') + return self.data['Name'] def get_manufacturer(self): return None def get_serial_number(self): - return get_field(self.data, 'DeviceSerialNumber') + return self.data.get('DeviceSerialNumber') def get_firmware_version(self): - return get_field(self.data, 'Firmware Version') + return self.data.get('Firmware Version') + + def _get_physical_disks(self): + pds = {} + res = omreport('storage pdisk controller={}'.format( + self.controller_index + )) + for pdisk in [d for d in list(res.values())[0]]: + disk_id = pdisk['ID'] + size = re.sub('B .*$', 'B', pdisk['Capacity']) + pds[disk_id] = { + 'Vendor': get_vendor(pdisk['Vendor ID']), + 'Model': pdisk['Product ID'], + 'SN': pdisk['Serial No.'], + 'Size': size, + 'Type': pdisk['Media'], + '_src': self.__class__.__name__, + } + return pds + + def _get_virtual_drives_map(self): + pds = {} + res = omreport('storage vdisk controller={}'.format( + self.controller_index + )) + for vdisk in [d for d in list(res.values())[0]]: + vdisk_id = vdisk['ID'] + device = vdisk['Device Name'] + mount_points = get_mount_points() + mp = mount_points.get(device, 'n/a') + size = re.sub('B .*$', 'B', vdisk['Size']) + vd = { + 'vd_array': vdisk_id, + 'vd_size': size, + 'vd_consistency': vdisk['State'], + 'vd_raid_type': vdisk['Layout'], + 'vd_device': vdisk['Device Name'], + 'mount_point': ', '.join(sorted(mp)), + } + drives_res = omreport( + 'storage pdisk controller={} vdisk={}'.format( + self.controller_index, vdisk_id + )) + for pdisk in [d for d in list(drives_res.values())[0]]: + pds[pdisk['ID']] = vd + return pds def get_physical_disks(self): - ret = [] - output = subprocess.getoutput( - 'omreport storage controller controller={} -fmt xml'.format(self.controller_index) - ) - root = ET.fromstring(output) - et_array_disks = root.find('ArrayDisks') - if et_array_disks is not None: - for obj in et_array_disks.findall('DCStorageObject'): - ret.append({ - 'Vendor': get_vendor(get_field(obj, 'Vendor')), - 'Model': get_field(obj, 'ProductID'), - 'SN': get_field(obj, 'DeviceSerialNumber'), - 'Size': '{:.0f}GB'.format( - int(get_field(obj, 'Length')) / 1024 / 1024 / 1024 - ), - 'Type': 'HDD' if int(get_field(obj, 'MediaType')) == 1 else 'SSD', - '_src': self.__class__.__name__, - }) - return ret + pds = self._get_physical_disks() + vds = self._get_virtual_drives_map() + for pd_identifier, vd in vds.items(): + if pd_identifier not in pds: + logging.error( + 'Physical drive {} listed in virtual drive {} not ' + 'found in drives list'.format( + pd_identifier, vd['vd_array'] + ) + ) + continue + pds[pd_identifier].setdefault('custom_fields', {}).update(vd) + pds[pd_identifier]['custom_fields']['pd_identifier'] = pd_identifier + return list(pds.values()) class OmreportRaid(Raid): def __init__(self): - output = subprocess.getoutput('omreport storage controller -fmt xml') - controller_xml = ET.fromstring(output) self.controllers = [] + res = omreport('storage controller') - for obj in controller_xml.find('Controllers').findall('DCStorageObject'): - ctrl_index = get_field(obj, 'ControllerNum') + for controller in res['Controller']: + ctrl_index = controller['ID'] self.controllers.append( - OmreportController(ctrl_index, obj) + OmreportController(ctrl_index, controller) ) def get_controllers(self): diff --git a/netbox_agent/raid/storcli.py b/netbox_agent/raid/storcli.py index 2435643..ee15fcf 100644 --- a/netbox_agent/raid/storcli.py +++ b/netbox_agent/raid/storcli.py @@ -1,8 +1,31 @@ -import json -import subprocess - -from netbox_agent.misc import get_vendor from netbox_agent.raid.base import Raid, RaidController +from netbox_agent.misc import get_vendor, get_mount_points +from netbox_agent.config import config +import subprocess +import logging +import json +import re +import os + + +def storecli(sub_command): + command = 'storcli {} J'.format(sub_command) + output = subprocess.getoutput(command) + data = json.loads(output) + controllers = dict([ + ( + c['Command Status']['Controller'], + c['Response Data'] + ) for c in data['Controllers'] + if c['Command Status']['Status'] == 'Success' + ]) + if not controllers: + logging.error( + "Failed to execute command '{}'. " + "Ignoring data.".format(command) + ) + return {} + return controllers class StorcliController(RaidController): @@ -22,52 +45,101 @@ class StorcliController(RaidController): def get_firmware_version(self): return self.data['FW Package Build'] - def get_physical_disks(self): - ret = [] - output = subprocess.getoutput( - 'storcli /c{}/eall/sall show all J'.format(self.controller_index) - ) - drive_infos = json.loads(output)['Controllers'][self.controller_index]['Response Data'] + def _get_physical_disks(self): + pds = {} + cmd = '/c{}/eall/sall show all'.format(self.controller_index) + controllers = storecli(cmd) + pd_info = controllers[self.controller_index] + pd_re = re.compile(r'^Drive (/c\d+/e\d+/s\d+)$') - for physical_drive in self.data['PD LIST']: - enclosure = physical_drive.get('EID:Slt').split(':')[0] - slot = physical_drive.get('EID:Slt').split(':')[1] - size = physical_drive.get('Size').strip() - media_type = physical_drive.get('Med').strip() - drive_identifier = 'Drive /c{}/e{}/s{}'.format( - str(self.controller_index), str(enclosure), str(slot) - ) - drive_attr = drive_infos['{} - Detailed Information'.format(drive_identifier)][ - '{} Device attributes'.format(drive_identifier)] - model = drive_attr.get('Model Number', '').strip() - ret.append({ + for section, attrs in pd_info.items(): + reg = pd_re.search(section) + if reg is None: + continue + pd_name = reg.group(1) + pd_attr = attrs[0] + pd_identifier = pd_attr['EID:Slt'] + size = pd_attr.get('Size', '').strip() + media_type = pd_attr.get('Med', '').strip() + pd_details = pd_info['{} - Detailed Information'.format(section)] + pd_dev_attr = pd_details['{} Device attributes'.format(section)] + model = pd_dev_attr.get('Model Number', '').strip() + pd = { 'Model': model, 'Vendor': get_vendor(model), - 'SN': drive_attr.get('SN', '').strip(), + 'SN': pd_dev_attr.get('SN', '').strip(), 'Size': size, 'Type': media_type, '_src': self.__class__.__name__, - }) - return ret + } + if config.process_virtual_drives: + pd.setdefault('custom_fields', {})['pd_identifier'] = pd_name + pds[pd_identifier] = pd + return pds + + def _get_virtual_drives_map(self): + vds = {} + cmd = '/c{}/vall show all'.format(self.controller_index) + controllers = storecli(cmd) + vd_info = controllers[self.controller_index] + mount_points = get_mount_points() + + for vd_identifier, vd_attrs in vd_info.items(): + if not vd_identifier.startswith("/c{}/v".format(self.controller_index)): + continue + volume = vd_identifier.split("/")[-1].lstrip("v") + vd_attr = vd_attrs[0] + vd_pd_identifier = 'PDs for VD {}'.format(volume) + vd_pds = vd_info[vd_pd_identifier] + vd_prop_identifier = 'VD{} Properties'.format(volume) + vd_properties = vd_info[vd_prop_identifier] + for pd in vd_pds: + pd_identifier = pd["EID:Slt"] + wwn = vd_properties["SCSI NAA Id"] + wwn_path = "/dev/disk/by-id/wwn-0x{}".format(wwn) + device = os.path.realpath(wwn_path) + mp = mount_points.get(device, "n/a") + vds[pd_identifier] = { + "vd_array": vd_identifier, + "vd_size": vd_attr["Size"], + "vd_consistency": vd_attr["Consist"], + "vd_raid_type": vd_attr["TYPE"], + "vd_device": device, + "mount_point": ", ".join(sorted(mp)) + } + return vds + + def get_physical_disks(self): + # Parses physical disks information + pds = self._get_physical_disks() + + # Parses virtual drives information and maps them to physical disks + vds = self._get_virtual_drives_map() + for pd_identifier, vd in vds.items(): + if pd_identifier not in pds: + logging.error( + "Physical drive {} listed in virtual drive {} not " + "found in drives list".format( + pd_identifier, vd["vd_array"] + ) + ) + continue + pds[pd_identifier].setdefault("custom_fields", {}).update(vd) + + return list(pds.values()) class StorcliRaid(Raid): def __init__(self): - self.output = subprocess.getoutput('storcli /call show J') - self.data = json.loads(self.output) self.controllers = [] - - if len([ - x for x in self.data['Controllers'] - if x['Command Status']['Status'] == 'Success' - ]) > 0: - for controller in self.data['Controllers']: - self.controllers.append( - StorcliController( - controller['Command Status']['Controller'], - controller['Response Data'] - ) + controllers = storecli('/call show') + for controller_id, controller_data in controllers.items(): + self.controllers.append( + StorcliController( + controller_id, + controller_data ) + ) def get_controllers(self): return self.controllers diff --git a/netbox_agent/server.py b/netbox_agent/server.py index df9a5a5..9653eb2 100644 --- a/netbox_agent/server.py +++ b/netbox_agent/server.py @@ -1,9 +1,3 @@ -import logging -import socket -import subprocess -import sys -from pprint import pprint - import netbox_agent.dmidecode as dmidecode from netbox_agent.config import config from netbox_agent.config import netbox_instance as nb @@ -12,6 +6,11 @@ from netbox_agent.location import Datacenter, Rack, Tenant from netbox_agent.misc import create_netbox_tags, get_device_role, get_device_type from netbox_agent.network import ServerNetwork from netbox_agent.power import PowerSupply +from pprint import pprint +import subprocess +import logging +import socket +import sys class ServerBase(): @@ -493,3 +492,15 @@ class ServerBase(): Indicates if the device hosts an expansion card """ return False + + def own_gpu_expansion_slot(self): + """ + Indicates if the device hosts a GPU expansion card + """ + return False + + def own_drive_expansion_slot(self): + """ + Indicates if the device hosts a drive expansion bay + """ + return False diff --git a/netbox_agent/vendors/generic.py b/netbox_agent/vendors/generic.py index 6080112..c57d2d3 100644 --- a/netbox_agent/vendors/generic.py +++ b/netbox_agent/vendors/generic.py @@ -8,7 +8,7 @@ class GenericHost(ServerBase): self.manufacturer = dmidecode.get_by_type(self.dmi, 'Baseboard')[0].get('Manufacturer') def is_blade(self): - return None + return False def get_blade_slot(self): return None From 84a9aca14163b111d2184c201d3f4ce6e77687e2 Mon Sep 17 00:00:00 2001 From: Christophe Simon Date: Mon, 7 Mar 2022 17:14:00 +0100 Subject: [PATCH 078/148] Unnamed network interfaces. Some interfaces do not have device (logical) name (eth0, for instance), such as not connected network mezzanine cards in blade servers. In such situations, the card will be named `unknown[0-9]`. --- netbox_agent/lshw.py | 67 +++++++++++++++++++++++--------------------- setup.py | 2 +- 2 files changed, 36 insertions(+), 33 deletions(-) diff --git a/netbox_agent/lshw.py b/netbox_agent/lshw.py index e19effc..15ac086 100644 --- a/netbox_agent/lshw.py +++ b/netbox_agent/lshw.py @@ -69,15 +69,21 @@ class LSHW(): return self.memories def find_network(self, obj): - d = {} - d["name"] = obj["logicalname"] - d["macaddress"] = obj["serial"] - d["serial"] = obj["serial"] - d["product"] = obj["product"] - d["vendor"] = obj["vendor"] - d["description"] = obj["description"] - - self.interfaces.append(d) + # Some interfaces do not have device (logical) name (eth0, for + # instance), such as not connected network mezzanine cards in blade + # servers. In such situations, the card will be named `unknown[0-9]`. + unkn_intfs = [ + i for i in self.interfaces if i["name"].startswith("unknown") + ] + unkn_name = "unknown{}".format(len(unkn_intfs)) + self.interfaces.append({ + "name": obj.get("logicalname", unkn_name), + "macaddress": obj.get("serial", ""), + "serial": obj.get("serial", ""), + "product": obj["product"], + "vendor": obj["vendor"], + "description": obj["description"], + }) def find_storage(self, obj): if "children" in obj: @@ -120,13 +126,12 @@ class LSHW(): def find_cpus(self, obj): if "product" in obj: - c = {} - c["product"] = obj["product"] - c["vendor"] = obj["vendor"] - c["description"] = obj["description"] - c["location"] = obj["slot"] - - self.cpus.append(c) + self.cpus.append({ + "product": obj["product"], + "vendor": obj["vendor"], + "description": obj["description"], + "location": obj["slot"], + }) def find_memories(self, obj): if "children" not in obj: @@ -137,25 +142,23 @@ class LSHW(): if "empty" in dimm["description"]: continue - d = {} - d["slot"] = dimm.get("slot") - d["description"] = dimm.get("description") - d["id"] = dimm.get("id") - d["serial"] = dimm.get("serial", 'N/A') - d["vendor"] = dimm.get("vendor", 'N/A') - d["product"] = dimm.get("product", 'N/A') - d["size"] = dimm.get("size", 0) / 2 ** 20 / 1024 - - self.memories.append(d) + self.memories.append({ + "slot": dimm.get("slot"), + "description": dimm.get("description"), + "id": dimm.get("id"), + "serial": dimm.get("serial", 'N/A'), + "vendor": dimm.get("vendor", 'N/A'), + "product": dimm.get("product", 'N/A'), + "size": dimm.get("size", 0) / 2 ** 20 / 1024, + }) def find_gpus(self, obj): if "product" in obj: - c = {} - c["product"] = obj["product"] - c["vendor"] = obj["vendor"] - c["description"] = obj["description"] - - self.gpus.append(c) + self.gpus.append({ + "product": obj["product"], + "vendor": obj["vendor"], + "description": obj["description"], + }) def walk_bridge(self, obj): if "children" not in obj: diff --git a/setup.py b/setup.py index f46a5f9..3233cb1 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ def get_requirements(): setup( name='netbox_agent', - version='0.7.0', + version='0.7.1', description='NetBox agent for server', long_description=open('README.md', encoding="utf-8").read(), long_description_content_type='text/markdown', From 3b8917aaf3aca48d990ee6a499c37bb1cb64fd6b Mon Sep 17 00:00:00 2001 From: Christophe Simon Date: Tue, 8 Mar 2022 17:19:20 +0100 Subject: [PATCH 079/148] Fixed virtual network cards creation This patch fixes a mismatch between the way network interfaces are looked for from network cards, and the way they are created: - If a network card has a NAC address, it used in the search criteria to find the network interface - When creating virtual interfaces, the MAC address is not specified in the creation parameters This raises an exception when a virtual card has a MAC address: - The virtual interface has been created without MAC address - But the network card has one, so the interface is looked for using it - The network interface is not found, so it's created - An error is risen because an interface with the same name already exists This patch sets the MAC address on the interface if it exists, no matter if it is virtual or not. --- netbox_agent/network.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/netbox_agent/network.py b/netbox_agent/network.py index 3d641c7..fb97409 100644 --- a/netbox_agent/network.py +++ b/netbox_agent/network.py @@ -261,7 +261,7 @@ class Network(object): def create_netbox_nic(self, nic, mgmt=False): # TODO: add Optic Vendor, PN and Serial - type = self.get_netbox_type_for_nic(nic) + nic_type = self.get_netbox_type_for_nic(nic) logging.info('Creating NIC {name} ({mac}) on {device}'.format( name=nic['name'], mac=nic['mac'], device=self.device.name)) @@ -270,11 +270,10 @@ class Network(object): params = dict(self.custom_arg) params.update({ 'name': nic['name'], - 'type': type, + 'type': nic_type, 'mgmt_only': mgmt, }) - - if not nic.get('virtual', False): + if nic['mac']: params['mac_address'] = nic['mac'] interface = self.nb_net.interfaces.create(**params) From ad951b9288e2a1703883a3d7b7f63c5b172a1423 Mon Sep 17 00:00:00 2001 From: Christophe Simon Date: Fri, 11 Mar 2022 15:55:07 +0100 Subject: [PATCH 080/148] Fixed HP raid parser when disks are in JBOD The function parsing the RAID logical volumes in the HP module did not manage the case where disks were set in JBOD mode, thus having no RAID array set. This patch fixes this by checking if arrays are defined on pdisks before parsing the logical disks. Also added returncode read when executing the RAID related commands to raise a more precise error. --- netbox_agent/raid/hp.py | 32 +++++++++++++++++++++++++------- netbox_agent/raid/omreport.py | 23 ++++++++++++++++++++--- netbox_agent/raid/storcli.py | 25 +++++++++++++++++++++---- 3 files changed, 66 insertions(+), 14 deletions(-) diff --git a/netbox_agent/raid/hp.py b/netbox_agent/raid/hp.py index 7089918..82d3c06 100644 --- a/netbox_agent/raid/hp.py +++ b/netbox_agent/raid/hp.py @@ -7,10 +7,26 @@ import re REGEXP_CONTROLLER_HP = re.compile(r'Smart Array ([a-zA-Z0-9- ]+) in Slot ([0-9]+)') +class HPRaidControllerError(Exception): + pass -def ssacli(command): - output = subprocess.getoutput('ssacli {}'.format(command) ) - lines = output.split('\n') + +def ssacli(sub_command): + command = ["ssacli"] + command.extend(sub_command.split()) + p = subprocess.Popen( + command, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT + ) + p.wait() + stdout = p.stdout.read().decode("utf-8") + if p.returncode != 0: + mesg = "Failed to execute command '{}':\n{}".format( + " ".join(command), stdout + ) + raise HPRaidControllerError(mesg) + lines = stdout.split('\n') lines = list(filter(None, lines)) return lines @@ -92,8 +108,10 @@ class HPRaidController(RaidController): self.controller_name = controller_name self.data = data self.pdrives = self._get_physical_disks() - self.ldrives = self._get_logical_drives() - self._get_virtual_drives_map() + arrays = [d['Array'] for d in self.pdrives.values() if d.get('Array')] + if arrays: + self.ldrives = self._get_logical_drives() + self._get_virtual_drives_map() def get_product_name(self): return self.controller_name @@ -135,6 +153,7 @@ class HPRaidController(RaidController): 'Type': 'SSD' if attrs.get('Interface Type') == 'Solid State SATA' else 'HDD', '_src': self.__class__.__name__, + 'custom_fields': {'pd_identifier': name} } return ret @@ -164,8 +183,7 @@ class HPRaidController(RaidController): " Ignoring.".format(name) ) continue - attrs['custom_fields'] = ld - attrs['custom_fields']['pd_identifier'] = name + attrs['custom_fields'].update(ld) def get_physical_disks(self): return list(self.pdrives.values()) diff --git a/netbox_agent/raid/omreport.py b/netbox_agent/raid/omreport.py index 6b4f780..811761b 100644 --- a/netbox_agent/raid/omreport.py +++ b/netbox_agent/raid/omreport.py @@ -6,15 +6,32 @@ import logging import re +class OmreportControllerError(Exception): + pass + + def omreport(sub_command): - command = 'omreport {}'.format(sub_command) - output = subprocess.getoutput(command) + command = ["omreport"] + command.extend(sub_command.split()) + p = subprocess.Popen( + command, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT + ) + p.wait() + stdout = p.stdout.read().decode("utf-8") + if p.returncode != 0: + mesg = "Failed to execute command '{}':\n{}".format( + " ".join(command), stdout + ) + raise OmreportControllerError(mesg) + res = {} section_re = re.compile('^[A-Z]') current_section = None current_obj = None - for line in output.split('\n'): + for line in stdout.split('\n'): if ': ' in line: attr, value = line.split(': ', 1) attr = attr.strip() diff --git a/netbox_agent/raid/storcli.py b/netbox_agent/raid/storcli.py index ee15fcf..c80380c 100644 --- a/netbox_agent/raid/storcli.py +++ b/netbox_agent/raid/storcli.py @@ -8,10 +8,27 @@ import re import os +class StorcliControllerError(Exception): + pass + + def storecli(sub_command): - command = 'storcli {} J'.format(sub_command) - output = subprocess.getoutput(command) - data = json.loads(output) + command = ["storcli"] + command.extend(sub_command.split()) + command.append("J") + p = subprocess.Popen( + command, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT + ) + p.wait() + stdout = p.stdout.read().decode("utf-8") + if p.returncode != 0: + mesg = "Failed to execute command '{}':\n{}".format( + " ".join(command), stdout + ) + raise StorcliControllerError(mesg) + data = json.loads(stdout) controllers = dict([ ( c['Command Status']['Controller'], @@ -22,7 +39,7 @@ def storecli(sub_command): if not controllers: logging.error( "Failed to execute command '{}'. " - "Ignoring data.".format(command) + "Ignoring data.".format(" ".join(command)) ) return {} return controllers From 75f14fa895f81390df6e8e4b9afc3f86eeaa6b90 Mon Sep 17 00:00:00 2001 From: Cyril Levis Date: Sat, 12 Mar 2022 10:40:34 +0100 Subject: [PATCH 081/148] feat: add QEMU support --- netbox_agent/virtualmachine.py | 1 + 1 file changed, 1 insertion(+) diff --git a/netbox_agent/virtualmachine.py b/netbox_agent/virtualmachine.py index a58eb3d..6669b3b 100644 --- a/netbox_agent/virtualmachine.py +++ b/netbox_agent/virtualmachine.py @@ -17,6 +17,7 @@ def is_vm(dmi): 'Xen' in bios[0]['Version'] or \ 'Google Compute Engine' in system[0]['Product Name'] or \ 'RHEV Hypervisor' in system[0]['Product Name'] or \ + 'QEMU' in system[0]['Manufacturer'] or \ 'VirtualBox' in bios[0]['Version'] or \ 'VMware' in system[0]['Manufacturer']: return True From dd4d935fb28980b6e2208825e0b7e1417c6e27ee Mon Sep 17 00:00:00 2001 From: Cyril Levis Date: Mon, 28 Mar 2022 14:37:21 +0200 Subject: [PATCH 082/148] fix(#212): prevent server tags to be override. Add a param `--replace-tags` to override all tags, default behavior is to append new tags. --- netbox_agent/config.py | 1 + netbox_agent/server.py | 11 +++++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/netbox_agent/config.py b/netbox_agent/config.py index 0ca1b64..7bb67d7 100644 --- a/netbox_agent/config.py +++ b/netbox_agent/config.py @@ -45,6 +45,7 @@ def get_config(): help="Command to output hostname, used as Device's name in netbox") p.add_argument('--device.tags', default=r'', help='tags to use for a host') + p.add_argument('--preserve-tags', action='store_true', help='Append new unique tags, preserve those already present') p.add_argument('--device.custom_fields', default=r'', help='custom_fields to use for a host, eg: field1=v1,field2=v2') p.add_argument('--device.blade_role', default=r'Blade', diff --git a/netbox_agent/server.py b/netbox_agent/server.py index 9653eb2..5904a60 100644 --- a/netbox_agent/server.py +++ b/netbox_agent/server.py @@ -444,8 +444,15 @@ class ServerBase(): server.name = self.get_hostname() update += 1 - if sorted(set([x.name for x in server.tags])) != sorted(set(self.tags)): - server.tags = [x.id for x in self.nb_tags] + server_tags = sorted(set([x.name for x in server.tags])) + tags = sorted(set(self.tags)) + if server_tags != tags: + new_tags_ids = [x.id for x in self.nb_tags] + if not config.preserve_tags: + server.tags = new_tags_ids + else: + server_tags_ids = [x.id for x in server.tags] + server.tags = sorted(set(new_tags_ids + server_tags_ids)) update += 1 if server.custom_fields != self.custom_fields: From ddf2e4d1cc4e83c3d2a34282f2dc0ecfa4ab5fa7 Mon Sep 17 00:00:00 2001 From: "Yukiya Hayashi A.k.a morihaya" Date: Wed, 30 Mar 2022 00:17:27 +0900 Subject: [PATCH 083/148] Fix netbox_agent.yml to netbox_agent.yaml Unify the notation of netbox_agent.yaml in the README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0833a64..30b8240 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ The agent can be run from a shell and get its configuration from either the conf Configuration values are overridden based on the following precedence: command line arguments (might include config file) > environment variables > default config file > defaults. ``` -# netbox_agent -c /etc/netbox_agent.yml --register +# netbox_agent -c /etc/netbox_agent.yaml --register INFO:root:Creating chassis blade (serial: QTFCQ574502EF) INFO:root:Creating blade (serial: QTFCQ574502D2) myserver on chassis QTFCQ574502EF INFO:root:Setting device (QTFCQ574502D2) new slot on Slot 9 (Chassis QTFCQ574502EF).. From a7b965a8b5f74e58533b01bd710ae5360dcc521f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Bergstr=C3=B6m?= Date: Wed, 30 Mar 2022 11:57:31 +0200 Subject: [PATCH 084/148] Added platform to the config --- netbox_agent/config.py | 2 ++ netbox_agent/misc.py | 12 ++++++++++++ netbox_agent/server.py | 9 ++++++++- netbox_agent/virtualmachine.py | 10 +++++++++- 4 files changed, 31 insertions(+), 2 deletions(-) diff --git a/netbox_agent/config.py b/netbox_agent/config.py index 7bb67d7..5a03117 100644 --- a/netbox_agent/config.py +++ b/netbox_agent/config.py @@ -43,6 +43,8 @@ def get_config(): p.add_argument('--virtual.cluster_name', help='Cluster name of VM') p.add_argument('--hostname_cmd', default=None, help="Command to output hostname, used as Device's name in netbox") + p.add_argument('--device.platform', default=None, + help='Device platform. Here we use OS distribution.') p.add_argument('--device.tags', default=r'', help='tags to use for a host') p.add_argument('--preserve-tags', action='store_true', help='Append new unique tags, preserve those already present') diff --git a/netbox_agent/misc.py b/netbox_agent/misc.py index df4638c..8084a84 100644 --- a/netbox_agent/misc.py +++ b/netbox_agent/misc.py @@ -29,6 +29,18 @@ def get_device_type(type): return device_type +def get_device_platform(config): + device_platform = nb.dcim.platforms.get( + name=config.device.platform + ) + if device_platform is None: + device_platform = nb.dcim.platforms.create( + name=config.device.platform, + slug=slugify(config.device.platform) + ) + return device_platform + + def get_vendor(name): vendors = { 'PERC': 'Dell', diff --git a/netbox_agent/server.py b/netbox_agent/server.py index 5904a60..2a9619e 100644 --- a/netbox_agent/server.py +++ b/netbox_agent/server.py @@ -3,7 +3,7 @@ from netbox_agent.config import config from netbox_agent.config import netbox_instance as nb from netbox_agent.inventory import Inventory from netbox_agent.location import Datacenter, Rack, Tenant -from netbox_agent.misc import create_netbox_tags, get_device_role, get_device_type +from netbox_agent.misc import create_netbox_tags, get_device_role, get_device_type, get_device_platform from netbox_agent.network import ServerNetwork from netbox_agent.power import PowerSupply from pprint import pprint @@ -262,6 +262,7 @@ class ServerBase(): def _netbox_create_server(self, datacenter, tenant, rack): device_role = get_device_role(config.device.server_role) device_type = get_device_type(self.get_product_name()) + device_platform = get_device_platform(config) if not device_type: raise Exception('Chassis "{}" doesn\'t exist'.format(self.get_chassis())) serial = self.get_service_tag() @@ -273,6 +274,7 @@ class ServerBase(): serial=serial, device_role=device_role.id, device_type=device_type.id, + platform=device_platform.id, site=datacenter.id if datacenter else None, tenant=tenant.id if tenant else None, rack=rack.id if rack else None, @@ -463,6 +465,11 @@ class ServerBase(): ret, server = self.update_netbox_location(server) update += ret + if get_device_platform(config) is not None: + if server.platform != get_device_platform(config).name: + update += 1 + server.platform = get_device_platform(config).id + if update: server.save() diff --git a/netbox_agent/virtualmachine.py b/netbox_agent/virtualmachine.py index 6669b3b..f9b779a 100644 --- a/netbox_agent/virtualmachine.py +++ b/netbox_agent/virtualmachine.py @@ -5,7 +5,7 @@ from netbox_agent.config import config from netbox_agent.config import netbox_instance as nb from netbox_agent.location import Tenant from netbox_agent.logging import logging # NOQA -from netbox_agent.misc import create_netbox_tags, get_hostname +from netbox_agent.misc import create_netbox_tags, get_hostname, get_device_platform from netbox_agent.network import VirtualNetwork @@ -90,10 +90,13 @@ class VirtualMachine(object): if not vm: logging.debug('Creating Virtual machine..') cluster = self.get_netbox_cluster(config.virtual.cluster_name) + device_platform = get_device_platform(config) vm = nb.virtualization.virtual_machines.create( name=hostname, cluster=cluster.id, + platform=device_platform.id, + device_platform = get_device_platform(config), vcpus=vcpus, memory=memory, tenant=tenant.id if tenant else None, @@ -114,6 +117,11 @@ class VirtualMachine(object): if sorted(set(vm.tags)) != sorted(set(self.tags)): vm.tags = self.tags updated += 1 + if get_device_platform(config) is not None: + if vm.platform != get_device_platform(config).name: + updated += 1 + vm.platform = get_device_platform(config).id + logging.debug('Finished updating Platform!') if updated: vm.save() From 2a4f24f00a24a995e00ae1c703c7fc8b85ddb002 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Bergstr=C3=B6m?= Date: Thu, 31 Mar 2022 11:14:26 +0200 Subject: [PATCH 085/148] not failing if platform is not set --- netbox_agent/misc.py | 3 +++ netbox_agent/server.py | 7 ++++--- netbox_agent/virtualmachine.py | 4 ++-- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/netbox_agent/misc.py b/netbox_agent/misc.py index 8084a84..6f45d86 100644 --- a/netbox_agent/misc.py +++ b/netbox_agent/misc.py @@ -30,6 +30,9 @@ def get_device_type(type): def get_device_platform(config): + if config.device.platform is None: + return None + device_platform = nb.dcim.platforms.get( name=config.device.platform ) diff --git a/netbox_agent/server.py b/netbox_agent/server.py index 2a9619e..c902e2f 100644 --- a/netbox_agent/server.py +++ b/netbox_agent/server.py @@ -465,10 +465,11 @@ class ServerBase(): ret, server = self.update_netbox_location(server) update += ret - if get_device_platform(config) is not None: - if server.platform != get_device_platform(config).name: + if config.device.platform is not None: + platform = get_device_platform(config) + if server.platform != platform.name: update += 1 - server.platform = get_device_platform(config).id + server.platform = platform.id if update: server.save() diff --git a/netbox_agent/virtualmachine.py b/netbox_agent/virtualmachine.py index f9b779a..ae1afe4 100644 --- a/netbox_agent/virtualmachine.py +++ b/netbox_agent/virtualmachine.py @@ -95,8 +95,8 @@ class VirtualMachine(object): vm = nb.virtualization.virtual_machines.create( name=hostname, cluster=cluster.id, - platform=device_platform.id, - device_platform = get_device_platform(config), + platform=device_platform.id if device_platform is not None else None, + device_platform=device_platform, vcpus=vcpus, memory=memory, tenant=tenant.id if tenant else None, From cf2cc54da40daabc9f7838cb24443ce3da45b68b Mon Sep 17 00:00:00 2001 From: Cyril Levis Date: Thu, 31 Mar 2022 17:08:16 +0200 Subject: [PATCH 086/148] feat: auto-detect device platform this platform module still works for python3.6 we use. Signed-off-by: Cyril Levis --- netbox_agent/misc.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/netbox_agent/misc.py b/netbox_agent/misc.py index 6f45d86..0c73522 100644 --- a/netbox_agent/misc.py +++ b/netbox_agent/misc.py @@ -31,19 +31,24 @@ def get_device_type(type): def get_device_platform(config): if config.device.platform is None: - return None + try: + import platform - device_platform = nb.dcim.platforms.get( - name=config.device.platform - ) + linux_distribution = " ".join(platform.linux_distribution()) + if not linux_distribution: + return None + except ModuleNotFoundError: + return None + else: + linux_distribution = config.device.platform + + device_platform = nb.dcim.platforms.get(name=linux_distribution) if device_platform is None: device_platform = nb.dcim.platforms.create( - name=config.device.platform, - slug=slugify(config.device.platform) + name=linux_distribution, slug=slugify(linux_distribution) ) return device_platform - def get_vendor(name): vendors = { 'PERC': 'Dell', From 9e797c376ed19c5f9b2c648c54f549233d7e7137 Mon Sep 17 00:00:00 2001 From: Cyril Levis Date: Thu, 31 Mar 2022 19:54:34 +0200 Subject: [PATCH 087/148] fix: always get_device_platform Signed-off-by: Cyril Levis --- netbox_agent/server.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/netbox_agent/server.py b/netbox_agent/server.py index c902e2f..f0a7e5e 100644 --- a/netbox_agent/server.py +++ b/netbox_agent/server.py @@ -465,11 +465,10 @@ class ServerBase(): ret, server = self.update_netbox_location(server) update += ret - if config.device.platform is not None: - platform = get_device_platform(config) - if server.platform != platform.name: - update += 1 - server.platform = platform.id + platform = get_device_platform(config) + if server.platform != platform.name: + server.platform = platform.id + update += 1 if update: server.save() From dfb6b234ba3c63db69b4e4100aec28c6efe551c4 Mon Sep 17 00:00:00 2001 From: Cyril Levis Date: Thu, 31 Mar 2022 20:58:23 +0200 Subject: [PATCH 088/148] chore: cleanup, add device_platform in summary Signed-off-by: Cyril Levis --- netbox_agent/misc.py | 8 ++++---- netbox_agent/server.py | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/netbox_agent/misc.py b/netbox_agent/misc.py index 0c73522..6e7e92d 100644 --- a/netbox_agent/misc.py +++ b/netbox_agent/misc.py @@ -29,18 +29,18 @@ def get_device_type(type): return device_type -def get_device_platform(config): - if config.device.platform is None: +def get_device_platform(device_platform): + if device_platform is None: try: import platform linux_distribution = " ".join(platform.linux_distribution()) if not linux_distribution: return None - except ModuleNotFoundError: + except (ModuleNotFoundError, NameError): return None else: - linux_distribution = config.device.platform + linux_distribution = device_platform device_platform = nb.dcim.platforms.get(name=linux_distribution) if device_platform is None: diff --git a/netbox_agent/server.py b/netbox_agent/server.py index f0a7e5e..f7008bf 100644 --- a/netbox_agent/server.py +++ b/netbox_agent/server.py @@ -24,6 +24,7 @@ class ServerBase(): self.bios = dmidecode.get_by_type(self.dmi, 'BIOS') self.chassis = dmidecode.get_by_type(self.dmi, 'Chassis') self.system = dmidecode.get_by_type(self.dmi, 'System') + self.device_platform = get_device_platform(config.device.platform) self.network = None @@ -262,7 +263,6 @@ class ServerBase(): def _netbox_create_server(self, datacenter, tenant, rack): device_role = get_device_role(config.device.server_role) device_type = get_device_type(self.get_product_name()) - device_platform = get_device_platform(config) if not device_type: raise Exception('Chassis "{}" doesn\'t exist'.format(self.get_chassis())) serial = self.get_service_tag() @@ -274,7 +274,7 @@ class ServerBase(): serial=serial, device_role=device_role.id, device_type=device_type.id, - platform=device_platform.id, + platform=self.device_platform, site=datacenter.id if datacenter else None, tenant=tenant.id if tenant else None, rack=rack.id if rack else None, @@ -465,9 +465,8 @@ class ServerBase(): ret, server = self.update_netbox_location(server) update += ret - platform = get_device_platform(config) - if server.platform != platform.name: - server.platform = platform.id + if server.platform != self.device_platform: + server.platform = self.device_platform update += 1 if update: @@ -494,6 +493,7 @@ class ServerBase(): print('Is blade:', self.is_blade()) print('Got expansion:', self.own_expansion_slot()) print('Product Name:', self.get_product_name()) + print('Platform:', self.device_platform) print('Chassis:', self.get_chassis()) print('Chassis service tag:', self.get_chassis_service_tag()) print('Service tag:', self.get_service_tag()) From 3dbeb5b9de5bcd74788d99eafaf686760659cc54 Mon Sep 17 00:00:00 2001 From: Cyril Levis Date: Thu, 31 Mar 2022 21:04:32 +0200 Subject: [PATCH 089/148] chore: cleanup virtual_machines get_device_platform Signed-off-by: Cyril Levis --- netbox_agent/virtualmachine.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/netbox_agent/virtualmachine.py b/netbox_agent/virtualmachine.py index ae1afe4..f86c082 100644 --- a/netbox_agent/virtualmachine.py +++ b/netbox_agent/virtualmachine.py @@ -31,6 +31,7 @@ class VirtualMachine(object): else: self.dmi = dmidecode.parse() self.network = None + self.device_platform = get_device_platform(config.device.platform) self.tags = list(set(config.device.tags.split(','))) if config.device.tags else [] if self.tags and len(self.tags): @@ -90,13 +91,12 @@ class VirtualMachine(object): if not vm: logging.debug('Creating Virtual machine..') cluster = self.get_netbox_cluster(config.virtual.cluster_name) - device_platform = get_device_platform(config) vm = nb.virtualization.virtual_machines.create( name=hostname, cluster=cluster.id, - platform=device_platform.id if device_platform is not None else None, - device_platform=device_platform, + platform=self.device_platform, + device_platform=self.device_platform, vcpus=vcpus, memory=memory, tenant=tenant.id if tenant else None, @@ -117,11 +117,9 @@ class VirtualMachine(object): if sorted(set(vm.tags)) != sorted(set(self.tags)): vm.tags = self.tags updated += 1 - if get_device_platform(config) is not None: - if vm.platform != get_device_platform(config).name: - updated += 1 - vm.platform = get_device_platform(config).id - logging.debug('Finished updating Platform!') + if vm.platform != self.device_platform: + vm.platform = self.device_platform + updated += 1 if updated: vm.save() From 46354865d31a2555fe148e6a765a7b7b72d27eb8 Mon Sep 17 00:00:00 2001 From: Cyril Levis Date: Thu, 31 Mar 2022 21:21:19 +0200 Subject: [PATCH 090/148] chore: update Help Signed-off-by: Cyril Levis --- netbox_agent/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox_agent/config.py b/netbox_agent/config.py index 5a03117..702c9b3 100644 --- a/netbox_agent/config.py +++ b/netbox_agent/config.py @@ -44,7 +44,7 @@ def get_config(): p.add_argument('--hostname_cmd', default=None, help="Command to output hostname, used as Device's name in netbox") p.add_argument('--device.platform', default=None, - help='Device platform. Here we use OS distribution.') + help='Override device platform. Here we use OS distribution.') p.add_argument('--device.tags', default=r'', help='tags to use for a host') p.add_argument('--preserve-tags', action='store_true', help='Append new unique tags, preserve those already present') From 6717b43cc985f363fe309085a81b112a7df9a3e2 Mon Sep 17 00:00:00 2001 From: ChrNic Date: Mon, 4 Apr 2022 09:05:42 +0200 Subject: [PATCH 091/148] Solves if raidcard have no discs --- netbox_agent/raid/hp.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/netbox_agent/raid/hp.py b/netbox_agent/raid/hp.py index 82d3c06..a5cc723 100644 --- a/netbox_agent/raid/hp.py +++ b/netbox_agent/raid/hp.py @@ -10,7 +10,6 @@ REGEXP_CONTROLLER_HP = re.compile(r'Smart Array ([a-zA-Z0-9- ]+) in Slot ([0-9]+ class HPRaidControllerError(Exception): pass - def ssacli(sub_command): command = ["ssacli"] command.extend(sub_command.split()) @@ -21,16 +20,19 @@ def ssacli(sub_command): ) p.wait() stdout = p.stdout.read().decode("utf-8") - if p.returncode != 0: + if p.returncode == 1 and stdout.find('does not have any physical') == -1: mesg = "Failed to execute command '{}':\n{}".format( " ".join(command), stdout ) raise HPRaidControllerError(mesg) - lines = stdout.split('\n') - lines = list(filter(None, lines)) + else: + if stdout.find('does not have any physical') != -1: + return list() + else: + lines = stdout.split('\n') + lines = list(filter(None, lines)) return lines - def _parse_ctrl_output(lines): controllers = {} current_ctrl = None From 2f23844dfda599500b9d7fe5bea3b36e05215a5f Mon Sep 17 00:00:00 2001 From: Cyril Levis Date: Mon, 4 Apr 2022 10:51:18 +0200 Subject: [PATCH 092/148] fix: hp raid, prevent us to miss some errors when matching only returncode 1 Signed-off-by: Cyril Levis --- netbox_agent/raid/hp.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/netbox_agent/raid/hp.py b/netbox_agent/raid/hp.py index a5cc723..c8e0df8 100644 --- a/netbox_agent/raid/hp.py +++ b/netbox_agent/raid/hp.py @@ -20,13 +20,13 @@ def ssacli(sub_command): ) p.wait() stdout = p.stdout.read().decode("utf-8") - if p.returncode == 1 and stdout.find('does not have any physical') == -1: + if p.returncode != 0 and 'does not have any physical' not in stdout: mesg = "Failed to execute command '{}':\n{}".format( " ".join(command), stdout ) raise HPRaidControllerError(mesg) else: - if stdout.find('does not have any physical') != -1: + if 'does not have any physical' in stdout: return list() else: lines = stdout.split('\n') From be770a947a85d29c832a063d08a23bc484f15dbc Mon Sep 17 00:00:00 2001 From: illes Date: Mon, 20 Jun 2022 13:13:31 +0200 Subject: [PATCH 093/148] Fix for missing mgmt-ip in LLDP output (#225) --- netbox_agent/lldp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox_agent/lldp.py b/netbox_agent/lldp.py index 51083f5..7b991d5 100644 --- a/netbox_agent/lldp.py +++ b/netbox_agent/lldp.py @@ -53,7 +53,7 @@ class LLDP(): # lldp.eth0.chassis.mgmt-ip=100.66.7.222 if self.data['lldp'].get(interface) is None: return None - return self.data['lldp'][interface]['chassis']['mgmt-ip'] + return self.data['lldp'][interface]['chassis'].get('mgmt-ip') def get_switch_port(self, interface): # lldp.eth0.port.descr=GigabitEthernet1/0/1 From 0cff7d3477dd1aa9e97bdeba5da919ca3b74b1fe Mon Sep 17 00:00:00 2001 From: illes Date: Mon, 18 Jul 2022 22:06:33 +0200 Subject: [PATCH 094/148] Fix module platform has no attribute linux_distribution (#224) --- netbox_agent/misc.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/netbox_agent/misc.py b/netbox_agent/misc.py index 6e7e92d..8672cad 100644 --- a/netbox_agent/misc.py +++ b/netbox_agent/misc.py @@ -32,12 +32,17 @@ def get_device_type(type): def get_device_platform(device_platform): if device_platform is None: try: - import platform + # Python 3.8+ moved linux_distribution() to distro + try: + import distro + linux_distribution = " ".join(distro.linux_distribution()) + except ImportError: + import platform + linux_distribution = " ".join(platform.linux_distribution()) - linux_distribution = " ".join(platform.linux_distribution()) if not linux_distribution: return None - except (ModuleNotFoundError, NameError): + except (ModuleNotFoundError, NameError, AttributeError): return None else: linux_distribution = device_platform From ea66becd3d304872bef34827914f162238b9fce2 Mon Sep 17 00:00:00 2001 From: DeathRabbit679 <37566060+DeathRabbit679@users.noreply.github.com> Date: Mon, 18 Jul 2022 15:08:03 -0500 Subject: [PATCH 095/148] Use platform id instead of record (#231) --- netbox_agent/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox_agent/server.py b/netbox_agent/server.py index f7008bf..bdc1407 100644 --- a/netbox_agent/server.py +++ b/netbox_agent/server.py @@ -274,7 +274,7 @@ class ServerBase(): serial=serial, device_role=device_role.id, device_type=device_type.id, - platform=self.device_platform, + platform=self.device_platform.id, site=datacenter.id if datacenter else None, tenant=tenant.id if tenant else None, rack=rack.id if rack else None, From 838ffd8e410b05cd3dba9a94fefc6a63a794ebbf Mon Sep 17 00:00:00 2001 From: ChrNic Date: Thu, 21 Jul 2022 08:32:24 +0200 Subject: [PATCH 096/148] using dynamic ref to NIC --- netbox_agent/network.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox_agent/network.py b/netbox_agent/network.py index fb97409..500dc95 100644 --- a/netbox_agent/network.py +++ b/netbox_agent/network.py @@ -214,7 +214,7 @@ class Network(object): lldp_vlan = self.lldp.get_switch_vlan(nic['name']) if config.network.lldp else None # For strange reason, we need to get the object from scratch # The object returned by pynetbox's save isn't always working (since pynetbox 6) - interface = nb.dcim.interfaces.get(id=interface.id) + interface = self.nb_net.interfaces.get(id=interface.id) # Handle the case were the local interface isn't an interface vlan as reported by Netbox # and that LLDP doesn't report a vlan-id From 29d2f23986bf3d0610c94cdaf0f94d5fc529e34e Mon Sep 17 00:00:00 2001 From: bds-congnguyen <67733377+bds-congnguyen@users.noreply.github.com> Date: Mon, 8 Aug 2022 20:26:58 +0700 Subject: [PATCH 097/148] Update requirements.txt (#233) * Update requirements.txt Fix `ModuleNotFoundError: No module named 'distro'` cause linux_distribution return None --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 0987b01..b66afc1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,4 @@ pyyaml==5.4.1 jsonargparse==3.11.2 python-slugify==5.0.2 packaging==20.9 +distro==1.7.0 From 79a08359aec938214e5e0d5c4151d70e0ecf5cdd Mon Sep 17 00:00:00 2001 From: Sylvain Rabot Date: Mon, 8 Aug 2022 15:37:13 +0200 Subject: [PATCH 098/148] Add Amazon EC2 to virtual machines (#236) --- netbox_agent/virtualmachine.py | 1 + 1 file changed, 1 insertion(+) diff --git a/netbox_agent/virtualmachine.py b/netbox_agent/virtualmachine.py index f86c082..db2cdb0 100644 --- a/netbox_agent/virtualmachine.py +++ b/netbox_agent/virtualmachine.py @@ -16,6 +16,7 @@ def is_vm(dmi): if 'Hyper-V' in bios[0]['Version'] or \ 'Xen' in bios[0]['Version'] or \ 'Google Compute Engine' in system[0]['Product Name'] or \ + ('Amazon EC2' in system[0]['Manufacturer'] and not system[0]['Product Name'].endswith('.metal')) or \ 'RHEV Hypervisor' in system[0]['Product Name'] or \ 'QEMU' in system[0]['Manufacturer'] or \ 'VirtualBox' in bios[0]['Version'] or \ From 0b9087fa416ddf5c60d80d5e9753bad40f351632 Mon Sep 17 00:00:00 2001 From: Cyril Levis Date: Tue, 23 Aug 2022 08:41:59 +0200 Subject: [PATCH 099/148] fix: inventory disk issue if size None ``` Traceback (most recent call last): File "/usr/bin/netbox_agent", line 33, in sys.exit(load_entry_point('netbox-agent==0.7.1', 'console_scripts', 'netbox_agent')()) File "/opt/netbox-agent/lib/python3.6/site-packages/netbox_agent/cli.py", line 50, in main return run(config) File "/opt/netbox-agent/lib/python3.6/site-packages/netbox_agent/cli.py", line 43, in run server.netbox_create_or_update(config) File "/opt/netbox-agent/lib/python3.6/site-packages/netbox_agent/server.py", line 417, in netbox_create_or_update self.inventory.create_or_update() File "/opt/netbox-agent/lib/python3.6/site-packages/netbox_agent/inventory.py", line 550, in create_or_update self.do_netbox_disks() File "/opt/netbox-agent/lib/python3.6/site-packages/netbox_agent/inventory.py", line 420, in do_netbox_disks disks = self.get_hw_disks() File "/opt/netbox-agent/lib/python3.6/site-packages/netbox_agent/inventory.py", line 344, in get_hw_disks size = int(disk.get('size', 0)) TypeError: int() argument must be a string, a bytes-like object or a number, not 'NoneType' ``` --- netbox_agent/inventory.py | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/netbox_agent/inventory.py b/netbox_agent/inventory.py index 0c8fcfe..a1adce9 100644 --- a/netbox_agent/inventory.py +++ b/netbox_agent/inventory.py @@ -341,21 +341,23 @@ class Inventory(): for disk in self.lshw.get_hw_linux("storage"): if self.is_virtual_disk(disk, raid_devices): continue - size =int(disk.get('size', 0)) / 1073741824 - d = { - "name": "", - 'Size': '{} GB'.format(size), - 'logicalname': disk.get('logicalname'), - 'description': disk.get('description'), - 'SN': disk.get('serial'), - 'Model': disk.get('product'), - 'Type': disk.get('type'), - } - if disk.get('vendor'): - d['Vendor'] = disk['vendor'] - else: - d['Vendor'] = get_vendor(disk['product']) - disks.append(d) + size = int(getattr(disk, "size", 0)) + if size > 0: + size /= 1073741824 + d = { + "name": "", + 'Size': '{} GB'.format(size), + 'logicalname': disk.get('logicalname'), + 'description': disk.get('description'), + 'SN': disk.get('serial'), + 'Model': disk.get('product'), + 'Type': disk.get('type'), + } + if disk.get('vendor'): + d['Vendor'] = disk['vendor'] + else: + d['Vendor'] = get_vendor(disk['product']) + disks.append(d) # remove duplicate serials seen = set() From f4d7796094843834e3f4175f6e6a41c234ad8980 Mon Sep 17 00:00:00 2001 From: Cyril Levis Date: Tue, 23 Aug 2022 08:55:02 +0200 Subject: [PATCH 100/148] fix: dvd-ram should be consider as virtual to be ignore --- netbox_agent/inventory.py | 33 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/netbox_agent/inventory.py b/netbox_agent/inventory.py index a1adce9..e19608a 100644 --- a/netbox_agent/inventory.py +++ b/netbox_agent/inventory.py @@ -321,6 +321,7 @@ class Inventory(): 'virtual' in product.lower() or \ 'logical' in product.lower() or \ 'volume' in description.lower() or \ + 'dvd-ram' in description.lower() or \ description == 'SCSI Enclosure' or \ (size is None and logicalname is None): return True @@ -341,23 +342,21 @@ class Inventory(): for disk in self.lshw.get_hw_linux("storage"): if self.is_virtual_disk(disk, raid_devices): continue - size = int(getattr(disk, "size", 0)) - if size > 0: - size /= 1073741824 - d = { - "name": "", - 'Size': '{} GB'.format(size), - 'logicalname': disk.get('logicalname'), - 'description': disk.get('description'), - 'SN': disk.get('serial'), - 'Model': disk.get('product'), - 'Type': disk.get('type'), - } - if disk.get('vendor'): - d['Vendor'] = disk['vendor'] - else: - d['Vendor'] = get_vendor(disk['product']) - disks.append(d) + size = int(getattr(disk, "size", 0)) / 1073741824 + d = { + "name": "", + 'Size': '{} GB'.format(size), + 'logicalname': disk.get('logicalname'), + 'description': disk.get('description'), + 'SN': disk.get('serial'), + 'Model': disk.get('product'), + 'Type': disk.get('type'), + } + if disk.get('vendor'): + d['Vendor'] = disk['vendor'] + else: + d['Vendor'] = get_vendor(disk['product']) + disks.append(d) # remove duplicate serials seen = set() From dfe937d54e58bb694ab072fd4e30b4124f357683 Mon Sep 17 00:00:00 2001 From: Robin Schneider Date: Thu, 3 Nov 2022 14:56:38 +0100 Subject: [PATCH 101/148] fix: TypeError: Object of type Record is not JSON serializable --- netbox_agent/virtualmachine.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/netbox_agent/virtualmachine.py b/netbox_agent/virtualmachine.py index db2cdb0..3d8dd47 100644 --- a/netbox_agent/virtualmachine.py +++ b/netbox_agent/virtualmachine.py @@ -96,8 +96,8 @@ class VirtualMachine(object): vm = nb.virtualization.virtual_machines.create( name=hostname, cluster=cluster.id, - platform=self.device_platform, - device_platform=self.device_platform, + platform=self.device_platform.id, + device_platform=self.device_platform.id, vcpus=vcpus, memory=memory, tenant=tenant.id if tenant else None, From d286fde999f7f8a00191ad462271cee8557621cb Mon Sep 17 00:00:00 2001 From: Robin Schneider Date: Thu, 3 Nov 2022 14:57:30 +0100 Subject: [PATCH 102/148] chore: Remove unused API parameter: device_platform --- netbox_agent/virtualmachine.py | 1 - 1 file changed, 1 deletion(-) diff --git a/netbox_agent/virtualmachine.py b/netbox_agent/virtualmachine.py index 3d8dd47..03a5008 100644 --- a/netbox_agent/virtualmachine.py +++ b/netbox_agent/virtualmachine.py @@ -97,7 +97,6 @@ class VirtualMachine(object): name=hostname, cluster=cluster.id, platform=self.device_platform.id, - device_platform=self.device_platform.id, vcpus=vcpus, memory=memory, tenant=tenant.id if tenant else None, From 4bd4b6bb9473550526f172075624c3a7a110ab45 Mon Sep 17 00:00:00 2001 From: Robin Schneider Date: Thu, 3 Nov 2022 16:48:04 +0100 Subject: [PATCH 103/148] fix: disk is also virtual if `product` is None Example on QEMU/KVM: ```json {'logicalname': '/dev/vda', 'product': None, 'serial': None, 'version': None, 'size': 10737418240, 'description': 'Virtual I/O device', 'type': 'Virtual I/O device'} ``` --- netbox_agent/inventory.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox_agent/inventory.py b/netbox_agent/inventory.py index e19608a..164ecf0 100644 --- a/netbox_agent/inventory.py +++ b/netbox_agent/inventory.py @@ -309,7 +309,7 @@ class Inventory(): description = disk.get('description') size = disk.get('size') product = disk.get('product') - if logicalname in raid_devices or disk_type is None: + if logicalname in raid_devices or disk_type is None or product is None: return True non_raid_disks = [ 'MR9361-8i', From 5d0f0bf2fae2b0a11f5d2b478b822392acf04401 Mon Sep 17 00:00:00 2001 From: Robin Schneider Date: Thu, 3 Nov 2022 16:50:25 +0100 Subject: [PATCH 104/148] chore: Fix typo --- netbox_agent/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox_agent/server.py b/netbox_agent/server.py index bdc1407..c755a71 100644 --- a/netbox_agent/server.py +++ b/netbox_agent/server.py @@ -62,7 +62,7 @@ class ServerBase(): def get_netbox_datacenter(self): dc = self.get_datacenter() if dc is None: - logging.error("Specificing a datacenter (Site) is mandatory in Netbox") + logging.error("Specifying a datacenter (Site) is mandatory in Netbox") sys.exit(1) nb_dc = nb.dcim.sites.get( From 8c12fa8e86746880e77f50211c0242a8098c4f61 Mon Sep 17 00:00:00 2001 From: Cyril Levis Date: Thu, 10 Nov 2022 14:27:17 +0100 Subject: [PATCH 105/148] fix: crash if product or description None --- netbox_agent/inventory.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/netbox_agent/inventory.py b/netbox_agent/inventory.py index e19608a..8e55a4c 100644 --- a/netbox_agent/inventory.py +++ b/netbox_agent/inventory.py @@ -309,14 +309,13 @@ class Inventory(): description = disk.get('description') size = disk.get('size') product = disk.get('product') - if logicalname in raid_devices or disk_type is None: + if logicalname in raid_devices or disk_type is None or product is None or description is None: return True non_raid_disks = [ 'MR9361-8i', ] if logicalname in raid_devices or \ - disk_type is None or \ product in non_raid_disks or \ 'virtual' in product.lower() or \ 'logical' in product.lower() or \ From 776f951e3b0c611dd73c9c0f1f9a7dc3ea901175 Mon Sep 17 00:00:00 2001 From: Cyril Levis Date: Thu, 10 Nov 2022 15:33:04 +0100 Subject: [PATCH 106/148] fix: hp inventory crash is controller return some warning about the cache to be re-enable for example ``` DEBUG:urllib3.connectionpool:https://netbox.local:443 "GET /api/dcim/inventory-items/?device_id=9&tag=hw-disk&limit=0 HTTP/1.1" 200 52 ('A cache backup failure has occurred. Please execute the "reenablecache" ' 'command') Traceback (most recent call last): File "/usr/bin/netbox_agent", line 33, in sys.exit(load_entry_point('netbox-agent==0.7.1', 'console_scripts', 'netbox_agent')()) File "/opt/netbox-agent/lib/python3.6/site-packages/netbox_agent/cli.py", line 50, in main return run(config) File "/opt/netbox-agent/lib/python3.6/site-packages/netbox_agent/cli.py", line 43, in run server.netbox_create_or_update(config) File "/opt/netbox-agent/lib/python3.6/site-packages/netbox_agent/server.py", line 417, in netbox_create_or_update self.inventory.create_or_update() File "/opt/netbox-agent/lib/python3.6/site-packages/netbox_agent/inventory.py", line 547, in create_or_update self.do_netbox_disks() File "/opt/netbox-agent/lib/python3.6/site-packages/netbox_agent/inventory.py", line 417, in do_netbox_disks disks = self.get_hw_disks() File "/opt/netbox-agent/lib/python3.6/site-packages/netbox_agent/inventory.py", line 332, in get_hw_disks for raid_card in self.get_raid_cards(filter_cards=True): File "/opt/netbox-agent/lib/python3.6/site-packages/netbox_agent/inventory.py", line 242, in get_raid_cards self.raid = raid_class() File "/opt/netbox-agent/lib/python3.6/site-packages/netbox_agent/raid/hp.py", line 180, in __init__ self.convert_to_dict() File "/opt/netbox-agent/lib/python3.6/site-packages/netbox_agent/raid/hp.py", line 185, in convert_to_dict controllers = _parse_ctrl_output(lines) File "/opt/netbox-agent/lib/python3.6/site-packages/netbox_agent/raid/hp.py", line 34, in _parse_ctrl_output attr, val = line.split(': ', 1) ``` --- netbox_agent/raid/hp.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/netbox_agent/raid/hp.py b/netbox_agent/raid/hp.py index c8e0df8..81f2ab9 100644 --- a/netbox_agent/raid/hp.py +++ b/netbox_agent/raid/hp.py @@ -63,6 +63,8 @@ def _parse_pd_output(lines): line = line.strip() if not line or line.startswith('Note:'): continue + if 'cache' in line or 'reboot' in line: + continue # Parses the Array the drives are in if line.startswith('Array'): current_array = line.split(None, 1)[1] From 9b06584fed964aca462e309ad86486d4a174fd43 Mon Sep 17 00:00:00 2001 From: Cyril Levis Date: Wed, 16 Nov 2022 10:45:25 +0100 Subject: [PATCH 107/148] fix: ignore some more hp controllers errors while parsing --- netbox_agent/raid/hp.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/netbox_agent/raid/hp.py b/netbox_agent/raid/hp.py index 81f2ab9..610c958 100644 --- a/netbox_agent/raid/hp.py +++ b/netbox_agent/raid/hp.py @@ -36,9 +36,14 @@ def ssacli(sub_command): def _parse_ctrl_output(lines): controllers = {} current_ctrl = None - + ignore_patterns = ['Note:', 'Error:', 'is not loaded', 'README'] + ignore_match = False for line in lines: - if not line or line.startswith('Note:'): + for pattern in ignore_patterns: + if not line or pattern in line: + ignore_match = True + break + if ignore_match: continue ctrl = REGEXP_CONTROLLER_HP.search(line) if ctrl is not None: From 633b6d3851e4627a69d8030b9fcbf5c208f0a729 Mon Sep 17 00:00:00 2001 From: Cyril Levis Date: Wed, 16 Nov 2022 14:43:35 +0100 Subject: [PATCH 108/148] fix: ignore more cache errors --- netbox_agent/raid/hp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox_agent/raid/hp.py b/netbox_agent/raid/hp.py index 610c958..a2d9971 100644 --- a/netbox_agent/raid/hp.py +++ b/netbox_agent/raid/hp.py @@ -36,7 +36,7 @@ def ssacli(sub_command): def _parse_ctrl_output(lines): controllers = {} current_ctrl = None - ignore_patterns = ['Note:', 'Error:', 'is not loaded', 'README'] + ignore_patterns = ['Note:', 'Error:', 'is not loaded', 'README', 'failure'] ignore_match = False for line in lines: for pattern in ignore_patterns: From e96a50379b384bd6b5cffbb15c1484d1ca50f304 Mon Sep 17 00:00:00 2001 From: Cyril Levis Date: Wed, 16 Nov 2022 15:46:57 +0100 Subject: [PATCH 109/148] chore: refactor hp raid ignore errors handling --- netbox_agent/raid/hp.py | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/netbox_agent/raid/hp.py b/netbox_agent/raid/hp.py index a2d9971..5990009 100644 --- a/netbox_agent/raid/hp.py +++ b/netbox_agent/raid/hp.py @@ -33,17 +33,21 @@ def ssacli(sub_command): lines = list(filter(None, lines)) return lines +def _test_if_valid_line(line): + ignore_patterns = ['Note:', 'Error:', 'is not loaded', 'README', 'failure', 'cache'] + for pattern in ignore_patterns: + if not line or pattern in line: + return None + return line + def _parse_ctrl_output(lines): controllers = {} current_ctrl = None - ignore_patterns = ['Note:', 'Error:', 'is not loaded', 'README', 'failure'] - ignore_match = False + for line in lines: - for pattern in ignore_patterns: - if not line or pattern in line: - ignore_match = True - break - if ignore_match: + line = line.strip() + line = _test_if_valid_line(line) + if line is None: continue ctrl = REGEXP_CONTROLLER_HP.search(line) if ctrl is not None: @@ -51,7 +55,10 @@ def _parse_ctrl_output(lines): controllers[current_ctrl] = {'Slot': ctrl.group(2)} if 'Embedded' not in line: controllers[current_ctrl]['External'] = True + continue + if ': ' not in line: continue + attr, val = line.split(': ', 1) attr = attr.strip() val = val.strip() @@ -66,9 +73,8 @@ def _parse_pd_output(lines): for line in lines: line = line.strip() - if not line or line.startswith('Note:'): - continue - if 'cache' in line or 'reboot' in line: + line = _test_if_valid_line(line) + if line is None: continue # Parses the Array the drives are in if line.startswith('Array'): @@ -83,10 +89,13 @@ def _parse_pd_output(lines): if ': ' not in line: continue attr, val = line.split(': ', 1) + attr = attr.strip() + val = val.strip() drives.setdefault(current_drv, {})[attr] = val return drives + def _parse_ld_output(lines): drives = {} current_array = None @@ -94,7 +103,8 @@ def _parse_ld_output(lines): for line in lines: line = line.strip() - if not line or line.startswith('Note:'): + line = _test_if_valid_line(line) + if line is None: continue # Parses the Array the drives are in if line.startswith('Array'): From 2d8ec831e6aa7b885ae9a4e28b2fa42ae7943a6b Mon Sep 17 00:00:00 2001 From: Cyril Levis Date: Thu, 17 Nov 2022 11:00:48 +0100 Subject: [PATCH 110/148] fix: dvd rom inventory crash dvdrom dont have serial number, we fallback on a 'unknown' value now to prevent crash --- netbox_agent/inventory.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/netbox_agent/inventory.py b/netbox_agent/inventory.py index 8e55a4c..69d5b97 100644 --- a/netbox_agent/inventory.py +++ b/netbox_agent/inventory.py @@ -371,13 +371,14 @@ class Inventory(): desc = disk.get('description') name = '{} ({})'.format(disk['Model'], disk['Size']) description = disk['Type'] + sn = getattr(disk, 'SN', 'unknown') parms = { 'device': self.device_id, 'discovered': True, 'tags': [{'name': INVENTORY_TAG['disk']['name']}], 'name': name, - 'serial': disk['SN'], + 'serial': sn, 'part_id': disk['Model'], 'description': description, 'manufacturer': getattr(manufacturer, "id", None), @@ -389,7 +390,7 @@ class Inventory(): logging.info('Creating Disk {model} {serial}'.format( model=disk['Model'], - serial=disk['SN'], + serial=sn, )) def dump_disks_map(self, disks): From bf65da0c58adcd14710492d083666a42981a9146 Mon Sep 17 00:00:00 2001 From: Cyril Levis Date: Wed, 30 Nov 2022 09:49:51 +0100 Subject: [PATCH 111/148] fix: deadlock storcli inventory When there is a lot of data return by storcli due to a lot of disk, the inventory was in deadlock Its a known problem, see: https://docs.python.org/3/library/subprocess.html#subprocess.Popen.wait --- netbox_agent/raid/storcli.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/netbox_agent/raid/storcli.py b/netbox_agent/raid/storcli.py index c80380c..8eacae6 100644 --- a/netbox_agent/raid/storcli.py +++ b/netbox_agent/raid/storcli.py @@ -21,14 +21,17 @@ def storecli(sub_command): stdout=subprocess.PIPE, stderr=subprocess.STDOUT ) - p.wait() - stdout = p.stdout.read().decode("utf-8") - if p.returncode != 0: + + stdout, stderr = p.communicate() + if stderr: mesg = "Failed to execute command '{}':\n{}".format( " ".join(command), stdout ) raise StorcliControllerError(mesg) + + stdout = stdout.decode("utf-8") data = json.loads(stdout) + controllers = dict([ ( c['Command Status']['Controller'], From e0685e7167f72740f1ba647e982881a9dd780825 Mon Sep 17 00:00:00 2001 From: Cyril Levis Date: Fri, 2 Dec 2022 12:28:47 +0100 Subject: [PATCH 112/148] fix: add space in ignore_patterns to prevent false positive like `/data/cache/temp` for eg --- netbox_agent/raid/hp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox_agent/raid/hp.py b/netbox_agent/raid/hp.py index 5990009..2d6bcd6 100644 --- a/netbox_agent/raid/hp.py +++ b/netbox_agent/raid/hp.py @@ -34,7 +34,7 @@ def ssacli(sub_command): return lines def _test_if_valid_line(line): - ignore_patterns = ['Note:', 'Error:', 'is not loaded', 'README', 'failure', 'cache'] + ignore_patterns = ['Note:', 'Error:', 'is not loaded', 'README', ' failure', ' cache'] for pattern in ignore_patterns: if not line or pattern in line: return None From ca2a69b66f6851a25b9eb1cd222a7c5b64cb5519 Mon Sep 17 00:00:00 2001 From: Cyril Levis Date: Fri, 9 Dec 2022 14:19:26 +0100 Subject: [PATCH 113/148] fix: enable disk inventory for HPE --- netbox_agent/inventory.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox_agent/inventory.py b/netbox_agent/inventory.py index 69d5b97..3b9e305 100644 --- a/netbox_agent/inventory.py +++ b/netbox_agent/inventory.py @@ -232,7 +232,7 @@ class Inventory(): raid_class = OmreportRaid if is_tool('storcli'): raid_class = StorcliRaid - elif self.server.manufacturer == 'HP': + elif self.server.manufacturer in ('HP', 'HPE'): if is_tool('ssacli'): raid_class = HPRaid From a9af96bba2540116907717127190730f32521397 Mon Sep 17 00:00:00 2001 From: Cyril Levis Date: Tue, 13 Dec 2022 17:26:14 +0100 Subject: [PATCH 114/148] fix: get disk serial number --- netbox_agent/inventory.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox_agent/inventory.py b/netbox_agent/inventory.py index 3b9e305..40e73d4 100644 --- a/netbox_agent/inventory.py +++ b/netbox_agent/inventory.py @@ -371,7 +371,7 @@ class Inventory(): desc = disk.get('description') name = '{} ({})'.format(disk['Model'], disk['Size']) description = disk['Type'] - sn = getattr(disk, 'SN', 'unknown') + sn = disk.get('SN', 'unknown') parms = { 'device': self.device_id, From cedb6818a381e397ed22489f61f51564dae13dfa Mon Sep 17 00:00:00 2001 From: Cyril Levis Date: Tue, 13 Dec 2022 17:26:45 +0100 Subject: [PATCH 115/148] fix: handle big list of hp disk we cant use p.wait() if too much data. --- netbox_agent/raid/hp.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/netbox_agent/raid/hp.py b/netbox_agent/raid/hp.py index 2d6bcd6..7308625 100644 --- a/netbox_agent/raid/hp.py +++ b/netbox_agent/raid/hp.py @@ -18,20 +18,20 @@ def ssacli(sub_command): stdout=subprocess.PIPE, stderr=subprocess.STDOUT ) - p.wait() - stdout = p.stdout.read().decode("utf-8") - if p.returncode != 0 and 'does not have any physical' not in stdout: + stdout, stderr = p.communicate() + stdout = stdout.decode("utf-8") + if p.returncode != 0: mesg = "Failed to execute command '{}':\n{}".format( " ".join(command), stdout ) raise HPRaidControllerError(mesg) + + if 'does not have any physical' in stdout: + return list() else: - if 'does not have any physical' in stdout: - return list() - else: - lines = stdout.split('\n') - lines = list(filter(None, lines)) - return lines + lines = stdout.split('\n') + lines = list(filter(None, lines)) + return lines def _test_if_valid_line(line): ignore_patterns = ['Note:', 'Error:', 'is not loaded', 'README', ' failure', ' cache'] From 49b269efa6f54dda41b58283759456d331f8e5b6 Mon Sep 17 00:00:00 2001 From: Cyril Levis Date: Tue, 13 Dec 2022 17:27:35 +0100 Subject: [PATCH 116/148] chore: fetch more data as custom_fields for hp inventory --- netbox_agent/raid/hp.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/netbox_agent/raid/hp.py b/netbox_agent/raid/hp.py index 7308625..ce30deb 100644 --- a/netbox_agent/raid/hp.py +++ b/netbox_agent/raid/hp.py @@ -172,7 +172,12 @@ class HPRaidController(RaidController): 'Type': 'SSD' if attrs.get('Interface Type') == 'Solid State SATA' else 'HDD', '_src': self.__class__.__name__, - 'custom_fields': {'pd_identifier': name} + 'custom_fields': { + 'pd_identifier': name, + 'mount_point': attrs['Mount Points'], + 'vd_device': attrs['Disk Name'], + 'vd_size': attrs['Size'], + } } return ret From 4cf054278f38929b2215137195b5f85a7e55a08d Mon Sep 17 00:00:00 2001 From: Cyril Levis Date: Wed, 14 Dec 2022 09:00:37 +0100 Subject: [PATCH 117/148] fix: prevent crash if disk fields attrs missing --- netbox_agent/raid/hp.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/netbox_agent/raid/hp.py b/netbox_agent/raid/hp.py index ce30deb..c1235c5 100644 --- a/netbox_agent/raid/hp.py +++ b/netbox_agent/raid/hp.py @@ -174,9 +174,9 @@ class HPRaidController(RaidController): '_src': self.__class__.__name__, 'custom_fields': { 'pd_identifier': name, - 'mount_point': attrs['Mount Points'], - 'vd_device': attrs['Disk Name'], - 'vd_size': attrs['Size'], + 'mount_point': attrs.get('Mount Points', '').strip(), + 'vd_device': attrs.get('Disk Name', '').strip(), + 'vd_size': attrs.get('Size', '').strip(), } } return ret @@ -189,11 +189,11 @@ class HPRaidController(RaidController): for array, attrs in ldrives.items(): ret[array] = { 'vd_array': array, - 'vd_size': attrs['Size'], - 'vd_consistency': attrs['Status'], - 'vd_raid_type': 'RAID {}'.format(attrs['Fault Tolerance']), - 'vd_device': attrs['LogicalDrive'], - 'mount_point': attrs['Mount Points'] + 'vd_size': attrs.get('Size', '').strip(), + 'vd_consistency': attrs.get('Status', '').strip(), + 'vd_raid_type': 'RAID {}'.format(attrs.get('Fault Tolerance', 'N/A').strip()), + 'vd_device': attrs.get('LogicalDrive', '').strip(), + 'mount_point': attrs.get('Mount Points', '').strip() } return ret From 528ecc09b05de23188f6e6bf4397c9c29a12446f Mon Sep 17 00:00:00 2001 From: Cyril Levis Date: Fri, 13 Jan 2023 17:12:25 +0100 Subject: [PATCH 118/148] fix: allow the inventory of several hp controller of the same model --- netbox_agent/raid/hp.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/netbox_agent/raid/hp.py b/netbox_agent/raid/hp.py index c1235c5..aff1f2f 100644 --- a/netbox_agent/raid/hp.py +++ b/netbox_agent/raid/hp.py @@ -51,8 +51,9 @@ def _parse_ctrl_output(lines): continue ctrl = REGEXP_CONTROLLER_HP.search(line) if ctrl is not None: - current_ctrl = ctrl.group(1) - controllers[current_ctrl] = {'Slot': ctrl.group(2)} + slot = ctrl.group(2) + current_ctrl = "{} - Slot {}".format(ctrl.group(1), slot) + controllers[current_ctrl] = {'Slot': slot} if 'Embedded' not in line: controllers[current_ctrl]['External'] = True continue From 221ac16e87c6ff4c733c3e95bab78e84183c4b09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Blondel?= Date: Tue, 14 Feb 2023 23:32:26 +0100 Subject: [PATCH 119/148] feat: Retrieve and manage MTU for the interfaces --- netbox_agent/ipmi.py | 1 + netbox_agent/network.py | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/netbox_agent/ipmi.py b/netbox_agent/ipmi.py index 10ccb8c..ff2b42b 100644 --- a/netbox_agent/ipmi.py +++ b/netbox_agent/ipmi.py @@ -55,6 +55,7 @@ class IPMI(): ret = {} ret['name'] = 'IPMI' + ret["mtu"] = 1500 ret['bonding'] = False ret['mac'] = _ipmi['MAC Address'] ret['vlan'] = int(_ipmi['802.1q VLAN ID']) \ diff --git a/netbox_agent/network.py b/netbox_agent/network.py index 500dc95..922e2aa 100644 --- a/netbox_agent/network.py +++ b/netbox_agent/network.py @@ -85,6 +85,7 @@ class Network(object): ip_addr.append(addr) mac = open('/sys/class/net/{}/address'.format(interface), 'r').read().strip() + mtu = int(open('/sys/class/net/{}/mtu'.format(interface), 'r').read().strip()) vlan = None if len(interface.split('.')) > 1: vlan = int(interface.split('.')[1]) @@ -114,6 +115,7 @@ class Network(object): 'ethtool': Ethtool(interface).parse(), 'virtual': virtual, 'vlan': vlan, + 'mtu': mtu, 'bonding': bonding, 'bonding_slaves': bonding_slaves, } @@ -276,6 +278,9 @@ class Network(object): if nic['mac']: params['mac_address'] = nic['mac'] + if nic['mtu']: + params['mtu'] = nic['mtu'] + interface = self.nb_net.interfaces.create(**params) if nic['vlan']: @@ -443,6 +448,13 @@ class Network(object): ret, interface = self.reset_vlan_on_interface(nic, interface) nic_update += ret + if hasattr(interface, 'mtu'): + if nic['mtu'] != interface.mtu: + logging.info('Interface mtu is wrong, updating to: {mtu}'.format( + mtu=nic['mtu'])) + interface.mtu = nic['mtu'] + nic_update += 1 + if hasattr(interface, 'type'): _type = self.get_netbox_type_for_nic(nic) if not interface.type or \ From 8cba98ec43209b180e8cdfe8d88bd83a417b1cfe Mon Sep 17 00:00:00 2001 From: Clay Sweetser Date: Fri, 24 Feb 2023 19:15:23 -0500 Subject: [PATCH 120/148] Simplify `is_vm` function --- netbox_agent/virtualmachine.py | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/netbox_agent/virtualmachine.py b/netbox_agent/virtualmachine.py index 03a5008..b32d3bd 100644 --- a/netbox_agent/virtualmachine.py +++ b/netbox_agent/virtualmachine.py @@ -10,19 +10,26 @@ from netbox_agent.network import VirtualNetwork def is_vm(dmi): - bios = dmidecode.get_by_type(dmi, 'BIOS') - system = dmidecode.get_by_type(dmi, 'System') + bios = dmidecode.get_by_type(dmi, 'BIOS')[0] + system = dmidecode.get_by_type(dmi, 'System')[0] - if 'Hyper-V' in bios[0]['Version'] or \ - 'Xen' in bios[0]['Version'] or \ - 'Google Compute Engine' in system[0]['Product Name'] or \ - ('Amazon EC2' in system[0]['Manufacturer'] and not system[0]['Product Name'].endswith('.metal')) or \ - 'RHEV Hypervisor' in system[0]['Product Name'] or \ - 'QEMU' in system[0]['Manufacturer'] or \ - 'VirtualBox' in bios[0]['Version'] or \ - 'VMware' in system[0]['Manufacturer']: - return True - return False + return ( + ( + 'Hyper-V' in bios['Version'] or + 'Xen' in bios['Version'] or + 'Google Compute Engine' in system['Product Name'] + ) or + ( + ( + 'Amazon EC2' in system['Manufacturer'] and + not system['Product Name'].endswith('.metal') + ) or + 'RHEV Hypervisor' in system['Product Name'] or + 'QEMU' in system['Manufacturer'] or + 'VirtualBox' in bios['Version'] or + 'VMware' in system['Manufacturer'] + ) + ) class VirtualMachine(object): From dc224e209b8917418bd3fd2946800d8fedc6ff59 Mon Sep 17 00:00:00 2001 From: Clay Sweetser Date: Mon, 27 Feb 2023 19:06:14 -0500 Subject: [PATCH 121/148] Make IPMI information gathering optional. --- netbox_agent/config.py | 1 + netbox_agent/network.py | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/netbox_agent/config.py b/netbox_agent/config.py index 702c9b3..4cd4d58 100644 --- a/netbox_agent/config.py +++ b/netbox_agent/config.py @@ -78,6 +78,7 @@ def get_config(): help='Regex to ignore interfaces') p.add_argument('--network.ignore_ips', default=r'^(127\.0\.0\..*|fe80.*|::1.*)', help='Regex to ignore IPs') + p.add_argument('--network.ipmi', default=True, help='Enable gathering IPMI information') p.add_argument('--network.lldp', help='Enable auto-cabling feature through LLDP infos') p.add_argument('--inventory', action='store_true', help='Enable HW inventory (CPU, Memory, RAID Cards, Disks) feature') diff --git a/netbox_agent/network.py b/netbox_agent/network.py index 922e2aa..673dfc1 100644 --- a/netbox_agent/network.py +++ b/netbox_agent/network.py @@ -496,9 +496,12 @@ class Network(object): class ServerNetwork(Network): def __init__(self, server, *args, **kwargs): super(ServerNetwork, self).__init__(server, args, kwargs) - self.ipmi = self.get_ipmi() + + if config.network.ipmi: + self.ipmi = self.get_ipmi() if self.ipmi: self.nics.append(self.ipmi) + self.server = server self.device = self.server.get_netbox_server() self.nb_net = nb.dcim From 08360bafbb933bb05583fe409752e53e1078f918 Mon Sep 17 00:00:00 2001 From: Alexander Daichendt Date: Sat, 8 Apr 2023 11:17:23 +0200 Subject: [PATCH 122/148] fix: lshw crashes due to duplicate logical name --- netbox_agent/lshw.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/netbox_agent/lshw.py b/netbox_agent/lshw.py index 15ac086..e88993c 100644 --- a/netbox_agent/lshw.py +++ b/netbox_agent/lshw.py @@ -72,9 +72,17 @@ class LSHW(): # Some interfaces do not have device (logical) name (eth0, for # instance), such as not connected network mezzanine cards in blade # servers. In such situations, the card will be named `unknown[0-9]`. - unkn_intfs = [ - i for i in self.interfaces if i["name"].startswith("unknown") - ] + unkn_intfs = [] + for i in self.interfaces: + # newer versions of lshw can return a list of names, see issue #227 + if not isinstance(i["name"], list): + if i["name"].startswith("unknown"): + unkn_intfs.push(i) + else: + for j in i["name"]: + if j.startswith("unknown"): + unkn_intfs.push(j) + unkn_name = "unknown{}".format(len(unkn_intfs)) self.interfaces.append({ "name": obj.get("logicalname", unkn_name), From 0fdb56e01d878bfae0dc66ba0e02e56f3f64d762 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 18 Aug 2023 15:44:16 +0200 Subject: [PATCH 123/148] chore(deps): update dependency pyyaml to v6 (#196) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index b66afc1..c4a84a2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ pynetbox==6.1.2 netaddr==0.8.0 netifaces==0.10.9 -pyyaml==5.4.1 +pyyaml==6.0.1 jsonargparse==3.11.2 python-slugify==5.0.2 packaging==20.9 From fa51ca31cadf04f7b0471a60e7fab94785163211 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 18 Aug 2023 17:13:35 +0200 Subject: [PATCH 124/148] chore(deps): update dependency python-slugify to v8 (#263) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index c4a84a2..6cbe4b2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,6 +3,6 @@ netaddr==0.8.0 netifaces==0.10.9 pyyaml==6.0.1 jsonargparse==3.11.2 -python-slugify==5.0.2 +python-slugify==8.0.1 packaging==20.9 distro==1.7.0 From bb05e12f6eeba982af06e047e3c675d5aaf1efc0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 18 Aug 2023 17:22:25 +0200 Subject: [PATCH 125/148] chore(deps): update dependency packaging to v23 (#261) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 6cbe4b2..b413055 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,5 +4,5 @@ netifaces==0.10.9 pyyaml==6.0.1 jsonargparse==3.11.2 python-slugify==8.0.1 -packaging==20.9 +packaging==23.1 distro==1.7.0 From b6a3acd6b4d765b0b1ba17316ce7bbf515f2c45f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 18 Aug 2023 17:22:58 +0200 Subject: [PATCH 126/148] chore(deps): update dependency netifaces to v0.11.0 (#188) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index b413055..d203a09 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ pynetbox==6.1.2 netaddr==0.8.0 -netifaces==0.10.9 +netifaces==0.11.0 pyyaml==6.0.1 jsonargparse==3.11.2 python-slugify==8.0.1 From 7ab7bbb9e1d3e8616b818e2c76b4d31596b45e95 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 18 Aug 2023 17:24:00 +0200 Subject: [PATCH 127/148] chore(deps): update dependency distro to v1.8.0 (#244) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index d203a09..10909f7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,4 +5,4 @@ pyyaml==6.0.1 jsonargparse==3.11.2 python-slugify==8.0.1 packaging==23.1 -distro==1.7.0 +distro==1.8.0 From ba4cdb217bc678f301eb7875cf1f0eda5f30ca9c Mon Sep 17 00:00:00 2001 From: Oleg Zagrebelsky Date: Mon, 28 Aug 2023 19:54:53 +0300 Subject: [PATCH 128/148] fix lldp.py (#280) Co-authored-by: kszd487 --- netbox_agent/lldp.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/netbox_agent/lldp.py b/netbox_agent/lldp.py index 7b991d5..21b645f 100644 --- a/netbox_agent/lldp.py +++ b/netbox_agent/lldp.py @@ -31,8 +31,9 @@ class LLDP(): vlans[interface] = {} for path_component in path_components: - current_dict[path_component] = current_dict.get(path_component, {}) - current_dict = current_dict[path_component] + if not isinstance(current_dict.get(path_component), dict): + current_dict[path_component] = {} + current_dict = current_dict.get(path_component) if 'vlan-id' in path: vid = value vlans[interface][value] = vlans[interface].get(vid, {}) From 116334be2f4bdae996a0d8dc623ae6e24d20e8aa Mon Sep 17 00:00:00 2001 From: sinavir Date: Sat, 23 Mar 2024 01:45:20 +0100 Subject: [PATCH 129/148] fix: make lshw props finding more resilient --- netbox_agent/lshw.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/netbox_agent/lshw.py b/netbox_agent/lshw.py index e88993c..c79f95e 100644 --- a/netbox_agent/lshw.py +++ b/netbox_agent/lshw.py @@ -88,9 +88,9 @@ class LSHW(): "name": obj.get("logicalname", unkn_name), "macaddress": obj.get("serial", ""), "serial": obj.get("serial", ""), - "product": obj["product"], - "vendor": obj["vendor"], - "description": obj["description"], + "product": obj.get("product", "Unknown NIC"), + "vendor": obj.get("vendor", "Unknown"), + "description": obj.get("description", ""), }) def find_storage(self, obj): @@ -135,10 +135,10 @@ class LSHW(): def find_cpus(self, obj): if "product" in obj: self.cpus.append({ - "product": obj["product"], - "vendor": obj["vendor"], - "description": obj["description"], - "location": obj["slot"], + "product": obj.get("product", "Unknown CPU"), + "vendor": obj.get("vendor", "Unknown vendor"), + "description": obj.get("description", ""), + "location": obj.get("slot", ""), }) def find_memories(self, obj): @@ -162,11 +162,12 @@ class LSHW(): def find_gpus(self, obj): if "product" in obj: - self.gpus.append({ - "product": obj["product"], - "vendor": obj["vendor"], - "description": obj["description"], - }) + infos = { + "product": obj.get("product", "Unknown GPU"), + "vendor": obj.get("vendor", "Unknown"), + "description": obj.get("description", ""), + } + self.gpus.append(infos) def walk_bridge(self, obj): if "children" not in obj: From f512e7a0a971013eb49f233c72963fd54a78c8ca Mon Sep 17 00:00:00 2001 From: sinavir Date: Wed, 27 Mar 2024 18:49:05 +0100 Subject: [PATCH 130/148] fix: replace list.push by list.append --- netbox_agent/lshw.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/netbox_agent/lshw.py b/netbox_agent/lshw.py index c79f95e..876df97 100644 --- a/netbox_agent/lshw.py +++ b/netbox_agent/lshw.py @@ -77,11 +77,11 @@ class LSHW(): # newer versions of lshw can return a list of names, see issue #227 if not isinstance(i["name"], list): if i["name"].startswith("unknown"): - unkn_intfs.push(i) + unkn_intfs.append(i) else: for j in i["name"]: if j.startswith("unknown"): - unkn_intfs.push(j) + unkn_intfs.append(j) unkn_name = "unknown{}".format(len(unkn_intfs)) self.interfaces.append({ From c9a57de843e301483a0d3030aca475697026afea Mon Sep 17 00:00:00 2001 From: sinavir Date: Thu, 28 Mar 2024 12:07:17 +0100 Subject: [PATCH 131/148] fix: vm tags --- netbox_agent/virtualmachine.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/netbox_agent/virtualmachine.py b/netbox_agent/virtualmachine.py index b32d3bd..3b3f89c 100644 --- a/netbox_agent/virtualmachine.py +++ b/netbox_agent/virtualmachine.py @@ -42,8 +42,7 @@ class VirtualMachine(object): self.device_platform = get_device_platform(config.device.platform) self.tags = list(set(config.device.tags.split(','))) if config.device.tags else [] - if self.tags and len(self.tags): - create_netbox_tags(self.tags) + self.nb_tags = create_netbox_tags(self.tags) def get_memory(self): mem_bytes = os.sysconf('SC_PAGE_SIZE') * os.sysconf('SC_PHYS_PAGES') # e.g. 4015976448 @@ -107,7 +106,7 @@ class VirtualMachine(object): vcpus=vcpus, memory=memory, tenant=tenant.id if tenant else None, - tags=self.tags, + tags=[{'name': x} for x in self.tags], ) created = True @@ -121,9 +120,18 @@ class VirtualMachine(object): if vm.memory != memory: vm.memory = memory updated += 1 - if sorted(set(vm.tags)) != sorted(set(self.tags)): - vm.tags = self.tags + + vm_tags = sorted(set([x.name for x in vm.tags])) + tags = sorted(set(self.tags)) + if vm_tags != tags: + new_tags_ids = [x.id for x in self.nb_tags] + if not config.preserve_tags: + vm.tags = new_tags_ids + else: + vm_tags_ids = [x.id for x in vm.tags] + vm.tags = sorted(set(new_tags_ids + vm_tags_ids)) updated += 1 + if vm.platform != self.device_platform: vm.platform = self.device_platform updated += 1 From e04d0c6d59f165a4ed7c9b2a4e13b7c9b9c70034 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Tue, 7 May 2024 13:54:27 +0200 Subject: [PATCH 132/148] feat(network): Batch requests when filtering on interfaces This avoids an issue where the requested URI is too long when many interfaces are present --- netbox_agent/network.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/netbox_agent/network.py b/netbox_agent/network.py index 673dfc1..ae32e31 100644 --- a/netbox_agent/network.py +++ b/netbox_agent/network.py @@ -1,7 +1,7 @@ import logging import os import re -from itertools import chain +from itertools import chain, islice import netifaces from netaddr import IPAddress @@ -413,11 +413,16 @@ class Network(object): # delete IP on netbox that are not known on this server if len(nb_nics): - netbox_ips = nb.ipam.ip_addresses.filter( - **{self.intf_type: [x.id for x in nb_nics]} - ) + def batched(it, n): + while batch := tuple(islice(it, n)): + yield batch + + netbox_ips = [] + for ids in batched((x.id for x in nb_nics), 25): + netbox_ips += list( + nb.ipam.ip_addresses.filter(**{self.intf_type: ids}) + ) - netbox_ips = list(netbox_ips) all_local_ips = list(chain.from_iterable([ x['ip'] for x in self.nics if x['ip'] is not None ])) From ee41fb4fc26e9ccef1114d84cb96b64d29229937 Mon Sep 17 00:00:00 2001 From: clbu Date: Wed, 7 Aug 2024 12:24:49 +0200 Subject: [PATCH 133/148] replace device_role with role --- .gitignore | 1 + netbox_agent/cli.py | 4 ++-- netbox_agent/server.py | 8 ++++---- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index a95d78e..bc7b989 100644 --- a/.gitignore +++ b/.gitignore @@ -181,3 +181,4 @@ dmypy.json # End of https://www.gitignore.io/api/emacs,python netbox-docker +/.vscode diff --git a/netbox_agent/cli.py b/netbox_agent/cli.py index e112469..296317e 100644 --- a/netbox_agent/cli.py +++ b/netbox_agent/cli.py @@ -34,8 +34,8 @@ def run(config): except KeyError: server = GenericHost(dmi=dmi) - if version.parse(nb.version) < version.parse('2.9'): - print('netbox-agent is not compatible with Netbox prior to verison 2.9') + if version.parse(nb.version) < version.parse('3.7'): + print('netbox-agent is not compatible with Netbox prior to verison 3.7') return False if config.register or config.update_all or config.update_network or \ diff --git a/netbox_agent/server.py b/netbox_agent/server.py index c755a71..5077b7f 100644 --- a/netbox_agent/server.py +++ b/netbox_agent/server.py @@ -199,7 +199,7 @@ class ServerBase(): name=self.get_chassis_name(), device_type=device_type.id, serial=serial, - device_role=device_role.id, + role=device_role.id, site=datacenter.id if datacenter else None, tenant=tenant.id if tenant else None, rack=rack.id if rack else None, @@ -220,7 +220,7 @@ class ServerBase(): new_blade = nb.dcim.devices.create( name=hostname, serial=serial, - device_role=device_role.id, + role=device_role.id, device_type=device_type.id, parent_device=chassis.id, site=datacenter.id if datacenter else None, @@ -243,7 +243,7 @@ class ServerBase(): new_blade = nb.dcim.devices.create( name=hostname, serial=serial, - device_role=device_role.id, + role=device_role.id, device_type=device_type.id, parent_device=chassis.id, site=datacenter.id if datacenter else None, @@ -272,7 +272,7 @@ class ServerBase(): new_server = nb.dcim.devices.create( name=hostname, serial=serial, - device_role=device_role.id, + role=device_role.id, device_type=device_type.id, platform=self.device_platform.id, site=datacenter.id if datacenter else None, From a7104b6b944e69bfc72979e1783d26e6f9caf787 Mon Sep 17 00:00:00 2001 From: clbu Date: Wed, 7 Aug 2024 12:29:19 +0200 Subject: [PATCH 134/148] updating dependencies --- README.md | 2 +- requirements.txt | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 30b8240..f0e8473 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ The goal is to generate an existing infrastructure on Netbox and have the abilit # Requirements - Netbox >= 2.6 -- Python >= 3.4 +- Python >= 3.8 - [pynetbox](https://github.com/digitalocean/pynetbox/) - [python3-netaddr](https://github.com/drkjam/netaddr) - [python3-netifaces](https://github.com/al45tair/netifaces) diff --git a/requirements.txt b/requirements.txt index 10909f7..95e0ea2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,8 @@ -pynetbox==6.1.2 -netaddr==0.8.0 -netifaces==0.11.0 -pyyaml==6.0.1 -jsonargparse==3.11.2 -python-slugify==8.0.1 -packaging==23.1 -distro==1.8.0 +pynetbox~=7.3.4 +netaddr~=1.3.0 +netifaces~=0.11.0 +pyyaml~=6.0.1 +jsonargparse~=4.32.0 +python-slugify~=8.0.4 +packaging~=23.2 +distro~=1.9.0 From 1d69f4e2f07364a28a273294ee0e0723a47430f9 Mon Sep 17 00:00:00 2001 From: clbu Date: Wed, 9 Oct 2024 15:12:39 +0200 Subject: [PATCH 135/148] python version compatible with dependencies --- .gitignore | 1 + README.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index bc7b989..b33172f 100644 --- a/.gitignore +++ b/.gitignore @@ -182,3 +182,4 @@ dmypy.json netbox-docker /.vscode +class_diagram/ diff --git a/README.md b/README.md index f0e8473..fce44ce 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ The goal is to generate an existing infrastructure on Netbox and have the abilit # Requirements - Netbox >= 2.6 -- Python >= 3.8 +- Python >= 3.7 - [pynetbox](https://github.com/digitalocean/pynetbox/) - [python3-netaddr](https://github.com/drkjam/netaddr) - [python3-netifaces](https://github.com/al45tair/netifaces) From 40af19e8017ba0d3ccbdbface3b2741afd86c17a Mon Sep 17 00:00:00 2001 From: clbu Date: Wed, 9 Oct 2024 15:32:25 +0200 Subject: [PATCH 136/148] fix(readme): required Netbox version --- .gitignore | 1 - README.md | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index b33172f..bc7b989 100644 --- a/.gitignore +++ b/.gitignore @@ -182,4 +182,3 @@ dmypy.json netbox-docker /.vscode -class_diagram/ diff --git a/README.md b/README.md index fce44ce..d9414bf 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ The goal is to generate an existing infrastructure on Netbox and have the abilit # Requirements -- Netbox >= 2.6 +- Netbox >= 3.7 - Python >= 3.7 - [pynetbox](https://github.com/digitalocean/pynetbox/) - [python3-netaddr](https://github.com/drkjam/netaddr) From 818c835711b14601a4909ecb08669b10dea05719 Mon Sep 17 00:00:00 2001 From: clbu Date: Wed, 9 Oct 2024 15:45:07 +0200 Subject: [PATCH 137/148] fix(requirements): update dependencies version --- requirements.txt | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/requirements.txt b/requirements.txt index 95e0ea2..1d5cb4d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,8 @@ -pynetbox~=7.3.4 -netaddr~=1.3.0 -netifaces~=0.11.0 -pyyaml~=6.0.1 -jsonargparse~=4.32.0 -python-slugify~=8.0.4 -packaging~=23.2 -distro~=1.9.0 +pynetbox==7.3.4 +netaddr==1.3.0 +netifaces==0.11.0 +pyyaml==6.0.1 +jsonargparse==4.32.0 +python-slugify==8.0.4 +packaging==23.2 +distro==1.9.0 From 1429fedb9d892254c705e54cedcda7f41368ab65 Mon Sep 17 00:00:00 2001 From: clbu Date: Thu, 10 Oct 2024 23:15:55 +0200 Subject: [PATCH 138/148] fix(network): cable https://github.com/netbox-community/netbox/issues/9102 --- netbox_agent/network.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/netbox_agent/network.py b/netbox_agent/network.py index 673dfc1..ff1a8bf 100644 --- a/netbox_agent/network.py +++ b/netbox_agent/network.py @@ -556,10 +556,12 @@ class ServerNetwork(Network): switch_ip, )) cable = nb.dcim.cables.create( - termination_a_id=nb_server_interface.id, - termination_a_type="dcim.interface", - termination_b_id=nb_switch_interface.id, - termination_b_type="dcim.interface", + a_terminations=[ + {"object_type": "dcim.interface", "object_id": nb_server_interface.id}, + ], + b_terminations=[ + {"object_type": "dcim.interface", "object_id": nb_switch_interface.id}, + ], ) nb_server_interface.cable = cable logging.info( @@ -579,7 +581,7 @@ class ServerNetwork(Network): switch_ip, switch_interface, nb_server_interface ) else: - nb_sw_int = nb_server_interface.cable.termination_b + nb_sw_int = nb_server_interface.cable.b_terminations[0] nb_sw = nb_sw_int.device nb_mgmt_int = nb.dcim.interfaces.get( device_id=nb_sw.id, From e44fd2fe78d3b00a7e7722a9ebd6f89abea90d27 Mon Sep 17 00:00:00 2001 From: clbu Date: Wed, 16 Oct 2024 10:44:08 +0200 Subject: [PATCH 139/148] fix(network): retrieve switch interface using id instead of name --- netbox_agent/network.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox_agent/network.py b/netbox_agent/network.py index ff1a8bf..41db084 100644 --- a/netbox_agent/network.py +++ b/netbox_agent/network.py @@ -544,7 +544,7 @@ class ServerNetwork(Network): switch_interface = self.lldp.get_switch_port(nb_server_interface.name) nb_switch_interface = nb.dcim.interfaces.get( - device=nb_switch, + device_id=nb_switch.id, name=switch_interface, ) if nb_switch_interface is None: From 4b54a0a3db9073a31c6b56f9db1322051d169536 Mon Sep 17 00:00:00 2001 From: CllaudiaB <112865371+CllaudiaB@users.noreply.github.com> Date: Thu, 17 Oct 2024 10:26:41 +0200 Subject: [PATCH 140/148] fix: typo Co-authored-by: n1nj4- <39305378+n1nj444@users.noreply.github.com> --- netbox_agent/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox_agent/cli.py b/netbox_agent/cli.py index 296317e..2d1de19 100644 --- a/netbox_agent/cli.py +++ b/netbox_agent/cli.py @@ -35,7 +35,7 @@ def run(config): server = GenericHost(dmi=dmi) if version.parse(nb.version) < version.parse('3.7'): - print('netbox-agent is not compatible with Netbox prior to verison 3.7') + print('netbox-agent is not compatible with Netbox prior to version 3.7') return False if config.register or config.update_all or config.update_network or \ From de88ca85b974dffe883346dbf0d67cd4ef960269 Mon Sep 17 00:00:00 2001 From: clbu Date: Wed, 16 Oct 2024 10:54:40 +0200 Subject: [PATCH 141/148] fix(network): use netifaces2 --- netbox_agent/network.py | 4 ++-- requirements.txt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/netbox_agent/network.py b/netbox_agent/network.py index 41db084..a303a83 100644 --- a/netbox_agent/network.py +++ b/netbox_agent/network.py @@ -81,7 +81,7 @@ class Network(object): # for addr in ip6_addr: addr["addr"] = addr["addr"].replace('%{}'.format(interface), '') - addr["netmask"] = addr["netmask"].split('/')[0] + addr["mask"] = addr["mask"].split('/')[0] ip_addr.append(addr) mac = open('/sys/class/net/{}/address'.format(interface), 'r').read().strip() @@ -109,7 +109,7 @@ class Network(object): 'ip': [ '{}/{}'.format( x['addr'], - IPAddress(x['netmask']).netmask_bits() + IPAddress(x['mask']).netmask_bits() ) for x in ip_addr ] if ip_addr else None, # FIXME: handle IPv6 addresses 'ethtool': Ethtool(interface).parse(), diff --git a/requirements.txt b/requirements.txt index 1d5cb4d..f0c1dc2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ pynetbox==7.3.4 netaddr==1.3.0 -netifaces==0.11.0 +netifaces2==0.0.22 pyyaml==6.0.1 jsonargparse==4.32.0 python-slugify==8.0.4 From 56627c1aa997d6bcdcf15794d122111afbe0e405 Mon Sep 17 00:00:00 2001 From: Mathis Ribet Date: Sat, 12 Oct 2024 15:48:40 +0200 Subject: [PATCH 142/148] Fix disk size --- netbox_agent/inventory.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox_agent/inventory.py b/netbox_agent/inventory.py index 40e73d4..a88b35e 100644 --- a/netbox_agent/inventory.py +++ b/netbox_agent/inventory.py @@ -341,7 +341,7 @@ class Inventory(): for disk in self.lshw.get_hw_linux("storage"): if self.is_virtual_disk(disk, raid_devices): continue - size = int(getattr(disk, "size", 0)) / 1073741824 + size = round(int(disk.get("size", 0)) / 1073741824, 1) d = { "name": "", 'Size': '{} GB'.format(size), From c3d3e6857a7ce1c4eb0db9a7878f44d7bd209cd7 Mon Sep 17 00:00:00 2001 From: Mathis Ribet Date: Fri, 11 Oct 2024 10:15:43 +0200 Subject: [PATCH 143/148] Replaced deprecated module pkg_resources Removed in py3.12 --- netbox_agent/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/netbox_agent/__init__.py b/netbox_agent/__init__.py index e8fa341..cb47dd4 100644 --- a/netbox_agent/__init__.py +++ b/netbox_agent/__init__.py @@ -1,6 +1,6 @@ -from pkg_resources import DistributionNotFound, get_distribution +from importlib.metadata import version as _get_version, PackageNotFoundError try: - __version__ = get_distribution(__name__).version -except DistributionNotFound: + __version__ = _get_version(__name__) +except PackageNotFoundError: pass From 6ef23eae4d60715ed530331cb29382c5747f7c60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Compagnon?= Date: Mon, 21 Aug 2023 20:38:41 +0200 Subject: [PATCH 144/148] Add missing prtint debug --- netbox_agent/virtualmachine.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/netbox_agent/virtualmachine.py b/netbox_agent/virtualmachine.py index 3b3f89c..314c2c3 100644 --- a/netbox_agent/virtualmachine.py +++ b/netbox_agent/virtualmachine.py @@ -7,6 +7,7 @@ from netbox_agent.location import Tenant from netbox_agent.logging import logging # NOQA from netbox_agent.misc import create_netbox_tags, get_hostname, get_device_platform from netbox_agent.network import VirtualNetwork +from pprint import pprint def is_vm(dmi): @@ -138,3 +139,14 @@ class VirtualMachine(object): if updated: vm.save() + + def print_debug(self): + self.network = VirtualNetwork(server=self) + print('Cluster:', self.get_netbox_cluster(config.virtual.cluster_name)) + print('Platform:', self.device_platform) + print('VM:', self.get_netbox_vm()) + print('vCPU:', self.get_vcpus()) + print('Memory:', f"{self.get_memory()} MB") + print('NIC:',) + pprint(self.network.get_network_cards()) + pass From 9d496c685408da548accf9d6686ced5274e4dfe8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Compagnon?= Date: Mon, 21 Aug 2023 20:39:26 +0200 Subject: [PATCH 145/148] Check if it's a VM before running lldp related actions --- netbox_agent/network.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/netbox_agent/network.py b/netbox_agent/network.py index e24a229..8630cc7 100644 --- a/netbox_agent/network.py +++ b/netbox_agent/network.py @@ -213,7 +213,7 @@ class Network(object): def reset_vlan_on_interface(self, nic, interface): update = False vlan_id = nic['vlan'] - lldp_vlan = self.lldp.get_switch_vlan(nic['name']) if config.network.lldp else None + lldp_vlan = self.lldp.get_switch_vlan(nic['name']) if config.network.lldp and isinstance(self, ServerNetwork) else None # For strange reason, we need to get the object from scratch # The object returned by pynetbox's save isn't always working (since pynetbox 6) interface = self.nb_net.interfaces.get(id=interface.id) @@ -301,7 +301,7 @@ class Network(object): interface.save() # cable the interface - if config.network.lldp: + if config.network.lldp and isinstance(self, ServerNetwork): switch_ip = self.lldp.get_switch_ip(interface.name) switch_interface = self.lldp.get_switch_port(interface.name) @@ -478,7 +478,7 @@ class Network(object): interface.lag = None # cable the interface - if config.network.lldp: + if config.network.lldp and isinstance(self, ServerNetwork): switch_ip = self.lldp.get_switch_ip(interface.name) switch_interface = self.lldp.get_switch_port(interface.name) if switch_ip and switch_interface: From 7d268ea0e8ee54e0c5332a642de9b9329c36862a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Compagnon?= Date: Mon, 21 Aug 2023 20:40:10 +0200 Subject: [PATCH 146/148] Return 0 if everything ok as excepted in a shell --- netbox_agent/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox_agent/cli.py b/netbox_agent/cli.py index 2d1de19..414e231 100644 --- a/netbox_agent/cli.py +++ b/netbox_agent/cli.py @@ -47,7 +47,7 @@ def run(config): def main(): - return run(config) + return 0 if run(config) else 1 if __name__ == '__main__': From 837860e31aebfbe5dae5ab821ba062bba084c539 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Mon, 21 Oct 2024 12:55:40 +0200 Subject: [PATCH 147/148] feat: Add nix tooling --- .envrc | 1 + .gitignore | 2 ++ default.nix | 55 +++++++++++++++++++++++++++++++ nix/netifaces2.nix | 46 ++++++++++++++++++++++++++ npins/default.nix | 80 ++++++++++++++++++++++++++++++++++++++++++++++ npins/sources.json | 23 +++++++++++++ shell.nix | 1 + 7 files changed, 208 insertions(+) create mode 100644 .envrc create mode 100644 default.nix create mode 100644 nix/netifaces2.nix create mode 100644 npins/default.nix create mode 100644 npins/sources.json create mode 100644 shell.nix diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..1d953f4 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use nix diff --git a/.gitignore b/.gitignore index bc7b989..16abf57 100644 --- a/.gitignore +++ b/.gitignore @@ -182,3 +182,5 @@ dmypy.json netbox-docker /.vscode +.direnv +.pre-commit-config.yaml diff --git a/default.nix b/default.nix new file mode 100644 index 0000000..84f7943 --- /dev/null +++ b/default.nix @@ -0,0 +1,55 @@ +{ + sources ? import ./npins, + pkgs ? import sources.nixpkgs { }, +}: + +let + checks = (import sources.git-hooks).run { + src = ./.; + + hooks = + { + commitizen.enable = true; + } + // (pkgs.lib.genAttrs + [ + "black" + "isort" + "ruff" + ] + (hook: { + enable = true; + stages = [ "pre-push" ]; + }) + ); + }; + + python3 = pkgs.python3.override { + packageOverrides = self: _: { + netifaces2 = self.callPackage ./nix/netifaces2.nix { }; + }; + }; +in + +{ + devShell = pkgs.mkShell { + name = "netbox-agent.dev"; + + packages = [ + (python3.withPackages (ps: [ + ps.pynetbox + ps.netaddr + ps.netifaces2 + ps.pyyaml + ps.jsonargparse + ps.python-slugify + ps.packaging + ps.distro + ])) + ] ++ checks.enabledPackages; + + shellHook = '' + ${checks.shellHook} + ''; + }; +} diff --git a/nix/netifaces2.nix b/nix/netifaces2.nix new file mode 100644 index 0000000..1c2cdf9 --- /dev/null +++ b/nix/netifaces2.nix @@ -0,0 +1,46 @@ +{ + lib, + buildPythonPackage, + fetchFromGitHub, + cargo, + rustPlatform, + rustc, + typing-extensions, +}: + +buildPythonPackage rec { + pname = "netifaces-2"; + version = "0.0.22"; + pyproject = true; + + src = fetchFromGitHub { + owner = "SamuelYvon"; + repo = "netifaces-2"; + rev = "V${version}"; + hash = "sha256-XO3HWq8FOVzvpbK8mIBOup6hFMnhDpqOK/5bPziPZQ8="; + }; + + cargoDeps = rustPlatform.fetchCargoTarball { + inherit src; + name = "${pname}-${version}"; + hash = "sha256-uoUa6DSBuIV3RrE7svT1TVLxPHdx8BFu/C6mbpRmor0="; + }; + + build-system = [ + cargo + rustPlatform.cargoSetupHook + rustPlatform.maturinBuildHook + rustc + ]; + + dependencies = [ typing-extensions ]; + + pythonImportsCheck = [ "netifaces" ]; + + meta = { + description = "Netifaces reborn"; + homepage = "https://github.com/SamuelYvon/netifaces-2.git"; + license = lib.licenses.mit; + maintainers = with lib.maintainers; [ ]; + }; +} diff --git a/npins/default.nix b/npins/default.nix new file mode 100644 index 0000000..5e7d086 --- /dev/null +++ b/npins/default.nix @@ -0,0 +1,80 @@ +# Generated by npins. Do not modify; will be overwritten regularly +let + data = builtins.fromJSON (builtins.readFile ./sources.json); + version = data.version; + + mkSource = + spec: + assert spec ? type; + let + path = + if spec.type == "Git" then + mkGitSource spec + else if spec.type == "GitRelease" then + mkGitSource spec + else if spec.type == "PyPi" then + mkPyPiSource spec + else if spec.type == "Channel" then + mkChannelSource spec + else + builtins.throw "Unknown source type ${spec.type}"; + in + spec // { outPath = path; }; + + mkGitSource = + { + repository, + revision, + url ? null, + hash, + branch ? null, + ... + }: + assert repository ? type; + # At the moment, either it is a plain git repository (which has an url), or it is a GitHub/GitLab repository + # In the latter case, there we will always be an url to the tarball + if url != null then + (builtins.fetchTarball { + inherit url; + sha256 = hash; # FIXME: check nix version & use SRI hashes + }) + else + assert repository.type == "Git"; + let + urlToName = + url: rev: + let + matched = builtins.match "^.*/([^/]*)(\\.git)?$" repository.url; + + short = builtins.substring 0 7 rev; + + appendShort = if (builtins.match "[a-f0-9]*" rev) != null then "-${short}" else ""; + in + "${if matched == null then "source" else builtins.head matched}${appendShort}"; + name = urlToName repository.url revision; + in + builtins.fetchGit { + url = repository.url; + rev = revision; + inherit name; + # hash = hash; + }; + + mkPyPiSource = + { url, hash, ... }: + builtins.fetchurl { + inherit url; + sha256 = hash; + }; + + mkChannelSource = + { url, hash, ... }: + builtins.fetchTarball { + inherit url; + sha256 = hash; + }; +in +if version == 3 then + builtins.mapAttrs (_: mkSource) data.pins +else + throw "Unsupported format version ${toString version} in sources.json. Try running `npins upgrade`" diff --git a/npins/sources.json b/npins/sources.json new file mode 100644 index 0000000..f15de69 --- /dev/null +++ b/npins/sources.json @@ -0,0 +1,23 @@ +{ + "pins": { + "git-hooks": { + "type": "Git", + "repository": { + "type": "GitHub", + "owner": "cachix", + "repo": "git-hooks.nix" + }, + "branch": "master", + "revision": "3c3e88f0f544d6bb54329832616af7eb971b6be6", + "url": "https://github.com/cachix/git-hooks.nix/archive/3c3e88f0f544d6bb54329832616af7eb971b6be6.tar.gz", + "hash": "04pwjz423iq2nkazkys905gvsm5j39722ngavrnx42b8msr5k555" + }, + "nixpkgs": { + "type": "Channel", + "name": "nixpkgs-unstable", + "url": "https://releases.nixos.org/nixpkgs/nixpkgs-24.11pre694416.ccc0c2126893/nixexprs.tar.xz", + "hash": "0cn1z4wzps8nfqxzr6l5mbn81adcqy2cy2ic70z13fhzicmxfsbx" + } + }, + "version": 3 +} \ No newline at end of file diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..d6d21cf --- /dev/null +++ b/shell.nix @@ -0,0 +1 @@ +(import ./. { }).devShell From 13b84b4da1ac7f2434551b05c4f5e053793e2706 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Mon, 21 Oct 2024 12:55:54 +0200 Subject: [PATCH 148/148] feat: Run formatters --- netbox_agent/__init__.py | 3 +- netbox_agent/cli.py | 35 +- netbox_agent/config.py | 213 +++++++----- netbox_agent/dmidecode.py | 133 ++++---- netbox_agent/drivers/file.py | 2 +- netbox_agent/ethtool.py | 53 +-- netbox_agent/inventory.py | 401 +++++++++++----------- netbox_agent/ipmi.py | 40 ++- netbox_agent/lldp.py | 42 +-- netbox_agent/location.py | 72 ++-- netbox_agent/logging.py | 2 +- netbox_agent/lshw.py | 117 ++++--- netbox_agent/misc.py | 69 ++-- netbox_agent/network.py | 518 ++++++++++++++++------------- netbox_agent/power.py | 96 +++--- netbox_agent/raid/base.py | 4 +- netbox_agent/raid/hp.py | 147 ++++---- netbox_agent/raid/omreport.py | 105 +++--- netbox_agent/raid/storcli.py | 94 +++--- netbox_agent/server.py | 228 +++++++------ netbox_agent/vendors/dell.py | 28 +- netbox_agent/vendors/generic.py | 4 +- netbox_agent/vendors/hp.py | 25 +- netbox_agent/vendors/qct.py | 14 +- netbox_agent/vendors/supermicro.py | 46 +-- netbox_agent/virtualmachine.py | 69 ++-- pyproject.toml | 2 + setup.py | 48 ++- tests/conftest.py | 10 +- tests/network.py | 20 +- tests/server.py | 80 ++--- 31 files changed, 1454 insertions(+), 1266 deletions(-) create mode 100644 pyproject.toml diff --git a/netbox_agent/__init__.py b/netbox_agent/__init__.py index cb47dd4..8fe809c 100644 --- a/netbox_agent/__init__.py +++ b/netbox_agent/__init__.py @@ -1,4 +1,5 @@ -from importlib.metadata import version as _get_version, PackageNotFoundError +from importlib.metadata import PackageNotFoundError +from importlib.metadata import version as _get_version try: __version__ = _get_version(__name__) diff --git a/netbox_agent/cli.py b/netbox_agent/cli.py index 414e231..1293d18 100644 --- a/netbox_agent/cli.py +++ b/netbox_agent/cli.py @@ -1,4 +1,5 @@ from packaging import version + import netbox_agent.dmidecode as dmidecode from netbox_agent.config import config from netbox_agent.config import netbox_instance as nb @@ -11,12 +12,12 @@ from netbox_agent.vendors.supermicro import SupermicroHost from netbox_agent.virtualmachine import VirtualMachine, is_vm MANUFACTURERS = { - 'Dell Inc.': DellHost, - 'HP': HPHost, - 'HPE': HPHost, - 'Supermicro': SupermicroHost, - 'Quanta Cloud Technology Inc.': QCTHost, - 'Generic': GenericHost, + "Dell Inc.": DellHost, + "HP": HPHost, + "HPE": HPHost, + "Supermicro": SupermicroHost, + "Quanta Cloud Technology Inc.": QCTHost, + "Generic": GenericHost, } @@ -25,21 +26,29 @@ def run(config): if config.virtual.enabled or is_vm(dmi): if not config.virtual.cluster_name: - raise Exception('virtual.cluster_name parameter is mandatory because it\'s a VM') + raise Exception( + "virtual.cluster_name parameter is mandatory because it's a VM" + ) server = VirtualMachine(dmi=dmi) else: - manufacturer = dmidecode.get_by_type(dmi, 'Chassis')[0].get('Manufacturer') + manufacturer = dmidecode.get_by_type(dmi, "Chassis")[0].get("Manufacturer") try: server = MANUFACTURERS[manufacturer](dmi=dmi) except KeyError: server = GenericHost(dmi=dmi) - if version.parse(nb.version) < version.parse('3.7'): - print('netbox-agent is not compatible with Netbox prior to version 3.7') + if version.parse(nb.version) < version.parse("3.7"): + print("netbox-agent is not compatible with Netbox prior to version 3.7") return False - if config.register or config.update_all or config.update_network or \ - config.update_location or config.update_inventory or config.update_psu: + if ( + config.register + or config.update_all + or config.update_network + or config.update_location + or config.update_inventory + or config.update_psu + ): server.netbox_create_or_update(config) if config.debug: server.print_debug() @@ -50,5 +59,5 @@ def main(): return 0 if run(config) else 1 -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/netbox_agent/config.py b/netbox_agent/config.py index 4cd4d58..67c5e34 100644 --- a/netbox_agent/config.py +++ b/netbox_agent/config.py @@ -10,85 +10,148 @@ import urllib3 def get_config(): p = jsonargparse.ArgumentParser( default_config_files=[ - '/etc/netbox_agent.yaml', - '~/.config/netbox_agent.yaml', - '~/.netbox_agent.yaml', + "/etc/netbox_agent.yaml", + "~/.config/netbox_agent.yaml", + "~/.netbox_agent.yaml", ], - prog='netbox_agent', + prog="netbox_agent", description="Netbox agent to run on your infrastructure's servers", - env_prefix='NETBOX_AGENT_', - default_env=True + env_prefix="NETBOX_AGENT_", + default_env=True, ) - p.add_argument('-c', '--config', action=jsonargparse.ActionConfigFile) + p.add_argument("-c", "--config", action=jsonargparse.ActionConfigFile) - p.add_argument('-r', '--register', action='store_true', help='Register server to Netbox') - p.add_argument('-u', '--update-all', action='store_true', help='Update all infos in Netbox') - p.add_argument('-d', '--debug', action='store_true', help='Print debug infos') - p.add_argument('--update-network', action='store_true', help='Update network') - p.add_argument('--update-inventory', action='store_true', help='Update inventory') - p.add_argument('--update-location', action='store_true', help='Update location') - p.add_argument('--update-psu', action='store_true', help='Update PSU') - p.add_argument('--purge-old-devices', action='store_true', - help='Purge existing (old ?) devices having same name but different serial') - p.add_argument('--expansion-as-device', action='store_true', - help='Manage blade expansions as external devices') + p.add_argument( + "-r", "--register", action="store_true", help="Register server to Netbox" + ) + p.add_argument( + "-u", "--update-all", action="store_true", help="Update all infos in Netbox" + ) + p.add_argument("-d", "--debug", action="store_true", help="Print debug infos") + p.add_argument("--update-network", action="store_true", help="Update network") + p.add_argument("--update-inventory", action="store_true", help="Update inventory") + p.add_argument("--update-location", action="store_true", help="Update location") + p.add_argument("--update-psu", action="store_true", help="Update PSU") + p.add_argument( + "--purge-old-devices", + action="store_true", + help="Purge existing (old ?) devices having same name but different serial", + ) + p.add_argument( + "--expansion-as-device", + action="store_true", + help="Manage blade expansions as external devices", + ) - p.add_argument('--log_level', default='debug') - p.add_argument('--netbox.ssl_ca_certs_file', help='SSL CA certificates file') - p.add_argument('--netbox.url', help='Netbox URL') - p.add_argument('--netbox.token', help='Netbox API Token') - p.add_argument('--netbox.ssl_verify', default=True, action='store_true', - help='Disable SSL verification') - p.add_argument('--virtual.enabled', action='store_true', help='Is a virtual machine or not') - p.add_argument('--virtual.cluster_name', help='Cluster name of VM') - p.add_argument('--hostname_cmd', default=None, - help="Command to output hostname, used as Device's name in netbox") - p.add_argument('--device.platform', default=None, - help='Override device platform. Here we use OS distribution.') - p.add_argument('--device.tags', default=r'', - help='tags to use for a host') - p.add_argument('--preserve-tags', action='store_true', help='Append new unique tags, preserve those already present') - p.add_argument('--device.custom_fields', default=r'', - help='custom_fields to use for a host, eg: field1=v1,field2=v2') - p.add_argument('--device.blade_role', default=r'Blade', - help='role to use for a blade server') - p.add_argument('--device.chassis_role', default=r'Server Chassis', - help='role to use for a chassis') - p.add_argument('--device.server_role', default=r'Server', - help='role to use for a server') - p.add_argument('--tenant.driver', - help='tenant driver, ie cmd, file') - p.add_argument('--tenant.driver_file', - help='tenant driver custom driver file path') - p.add_argument('--tenant.regex', - help='tenant regex to extract Netbox tenant slug') - p.add_argument('--datacenter_location.driver', - help='Datacenter location driver, ie: cmd, file') - p.add_argument('--datacenter_location.driver_file', - help='Datacenter location custom driver file path') - p.add_argument('--datacenter_location.regex', - help='Datacenter location regex to extract Netbox DC slug') - p.add_argument('--rack_location.driver', help='Rack location driver, ie: cmd, file') - p.add_argument('--rack_location.driver_file', help='Rack location custom driver file path') - p.add_argument('--rack_location.regex', help='Rack location regex to extract Rack name') - p.add_argument('--slot_location.driver', help='Slot location driver, ie: cmd, file') - p.add_argument('--slot_location.driver_file', help='Slot location custom driver file path') - p.add_argument('--slot_location.regex', help='Slot location regex to extract slot name') - p.add_argument('--network.ignore_interfaces', default=r'(dummy.*|docker.*)', - help='Regex to ignore interfaces') - p.add_argument('--network.ignore_ips', default=r'^(127\.0\.0\..*|fe80.*|::1.*)', - help='Regex to ignore IPs') - p.add_argument('--network.ipmi', default=True, help='Enable gathering IPMI information') - p.add_argument('--network.lldp', help='Enable auto-cabling feature through LLDP infos') - p.add_argument('--inventory', action='store_true', - help='Enable HW inventory (CPU, Memory, RAID Cards, Disks) feature') - p.add_argument('--process-virtual-drives', action='store_true', - help='Process virtual drives information from RAID ' - 'controllers to fill disk custom_fields') - p.add_argument('--force-disk-refresh', action='store_true', - help='Forces disks detection reprocessing') - p.add_argument('--dump-disks-map', - help='File path to dump physical/virtual disks map') + p.add_argument("--log_level", default="debug") + p.add_argument("--netbox.ssl_ca_certs_file", help="SSL CA certificates file") + p.add_argument("--netbox.url", help="Netbox URL") + p.add_argument("--netbox.token", help="Netbox API Token") + p.add_argument( + "--netbox.ssl_verify", + default=True, + action="store_true", + help="Disable SSL verification", + ) + p.add_argument( + "--virtual.enabled", action="store_true", help="Is a virtual machine or not" + ) + p.add_argument("--virtual.cluster_name", help="Cluster name of VM") + p.add_argument( + "--hostname_cmd", + default=None, + help="Command to output hostname, used as Device's name in netbox", + ) + p.add_argument( + "--device.platform", + default=None, + help="Override device platform. Here we use OS distribution.", + ) + p.add_argument("--device.tags", default=r"", help="tags to use for a host") + p.add_argument( + "--preserve-tags", + action="store_true", + help="Append new unique tags, preserve those already present", + ) + p.add_argument( + "--device.custom_fields", + default=r"", + help="custom_fields to use for a host, eg: field1=v1,field2=v2", + ) + p.add_argument( + "--device.blade_role", default=r"Blade", help="role to use for a blade server" + ) + p.add_argument( + "--device.chassis_role", + default=r"Server Chassis", + help="role to use for a chassis", + ) + p.add_argument( + "--device.server_role", default=r"Server", help="role to use for a server" + ) + p.add_argument("--tenant.driver", help="tenant driver, ie cmd, file") + p.add_argument("--tenant.driver_file", help="tenant driver custom driver file path") + p.add_argument("--tenant.regex", help="tenant regex to extract Netbox tenant slug") + p.add_argument( + "--datacenter_location.driver", help="Datacenter location driver, ie: cmd, file" + ) + p.add_argument( + "--datacenter_location.driver_file", + help="Datacenter location custom driver file path", + ) + p.add_argument( + "--datacenter_location.regex", + help="Datacenter location regex to extract Netbox DC slug", + ) + p.add_argument("--rack_location.driver", help="Rack location driver, ie: cmd, file") + p.add_argument( + "--rack_location.driver_file", help="Rack location custom driver file path" + ) + p.add_argument( + "--rack_location.regex", help="Rack location regex to extract Rack name" + ) + p.add_argument("--slot_location.driver", help="Slot location driver, ie: cmd, file") + p.add_argument( + "--slot_location.driver_file", help="Slot location custom driver file path" + ) + p.add_argument( + "--slot_location.regex", help="Slot location regex to extract slot name" + ) + p.add_argument( + "--network.ignore_interfaces", + default=r"(dummy.*|docker.*)", + help="Regex to ignore interfaces", + ) + p.add_argument( + "--network.ignore_ips", + default=r"^(127\.0\.0\..*|fe80.*|::1.*)", + help="Regex to ignore IPs", + ) + p.add_argument( + "--network.ipmi", default=True, help="Enable gathering IPMI information" + ) + p.add_argument( + "--network.lldp", help="Enable auto-cabling feature through LLDP infos" + ) + p.add_argument( + "--inventory", + action="store_true", + help="Enable HW inventory (CPU, Memory, RAID Cards, Disks) feature", + ) + p.add_argument( + "--process-virtual-drives", + action="store_true", + help="Process virtual drives information from RAID " + "controllers to fill disk custom_fields", + ) + p.add_argument( + "--force-disk-refresh", + action="store_true", + help="Forces disks detection reprocessing", + ) + p.add_argument( + "--dump-disks-map", help="File path to dump physical/virtual disks map" + ) options = p.parse_args() return options @@ -99,7 +162,7 @@ config = get_config() def get_netbox_instance(): if config.netbox.url is None or config.netbox.token is None: - logging.error('Netbox URL and token are mandatory') + logging.error("Netbox URL and token are mandatory") sys.exit(1) nb = pynetbox.api( diff --git a/netbox_agent/dmidecode.py b/netbox_agent/dmidecode.py index 028a924..05f6eeb 100644 --- a/netbox_agent/dmidecode.py +++ b/netbox_agent/dmidecode.py @@ -5,55 +5,57 @@ import sys from netbox_agent.misc import is_tool -_handle_re = _re.compile('^Handle\\s+(.+),\\s+DMI\\s+type\\s+(\\d+),\\s+(\\d+)\\s+bytes$') -_in_block_re = _re.compile('^\\t\\t(.+)$') -_record_re = _re.compile('\\t(.+):\\s+(.+)$') -_record2_re = _re.compile('\\t(.+):$') +_handle_re = _re.compile( + "^Handle\\s+(.+),\\s+DMI\\s+type\\s+(\\d+),\\s+(\\d+)\\s+bytes$" +) +_in_block_re = _re.compile("^\\t\\t(.+)$") +_record_re = _re.compile("\\t(.+):\\s+(.+)$") +_record2_re = _re.compile("\\t(.+):$") _type2str = { - 0: 'BIOS', - 1: 'System', - 2: 'Baseboard', - 3: 'Chassis', - 4: 'Processor', - 5: 'Memory Controller', - 6: 'Memory Module', - 7: 'Cache', - 8: 'Port Connector', - 9: 'System Slots', - 10: ' On Board Devices', - 11: ' OEM Strings', - 12: ' System Configuration Options', - 13: ' BIOS Language', - 14: ' Group Associations', - 15: ' System Event Log', - 16: ' Physical Memory Array', - 17: ' Memory Device', - 18: ' 32-bit Memory Error', - 19: ' Memory Array Mapped Address', - 20: ' Memory Device Mapped Address', - 21: ' Built-in Pointing Device', - 22: ' Portable Battery', - 23: ' System Reset', - 24: ' Hardware Security', - 25: ' System Power Controls', - 26: ' Voltage Probe', - 27: ' Cooling Device', - 28: ' Temperature Probe', - 29: ' Electrical Current Probe', - 30: ' Out-of-band Remote Access', - 31: ' Boot Integrity Services', - 32: ' System Boot', - 33: ' 64-bit Memory Error', - 34: ' Management Device', - 35: ' Management Device Component', - 36: ' Management Device Threshold Data', - 37: ' Memory Channel', - 38: ' IPMI Device', - 39: ' Power Supply', - 40: ' Additional Information', - 41: ' Onboard Devices Extended Information', - 42: ' Management Controller Host Interface' + 0: "BIOS", + 1: "System", + 2: "Baseboard", + 3: "Chassis", + 4: "Processor", + 5: "Memory Controller", + 6: "Memory Module", + 7: "Cache", + 8: "Port Connector", + 9: "System Slots", + 10: " On Board Devices", + 11: " OEM Strings", + 12: " System Configuration Options", + 13: " BIOS Language", + 14: " Group Associations", + 15: " System Event Log", + 16: " Physical Memory Array", + 17: " Memory Device", + 18: " 32-bit Memory Error", + 19: " Memory Array Mapped Address", + 20: " Memory Device Mapped Address", + 21: " Built-in Pointing Device", + 22: " Portable Battery", + 23: " System Reset", + 24: " Hardware Security", + 25: " System Power Controls", + 26: " Voltage Probe", + 27: " Cooling Device", + 28: " Temperature Probe", + 29: " Electrical Current Probe", + 30: " Out-of-band Remote Access", + 31: " Boot Integrity Services", + 32: " System Boot", + 33: " 64-bit Memory Error", + 34: " Management Device", + 35: " Management Device Component", + 36: " Management Device Threshold Data", + 37: " Memory Channel", + 38: " IPMI Device", + 39: " Power Supply", + 40: " Additional Information", + 41: " Onboard Devices Extended Information", + 42: " Management Controller Host Interface", } _str2type = {} for type_id, type_str in _type2str.items(): @@ -70,7 +72,7 @@ def parse(output=None): else: buffer = _execute_cmd() if isinstance(buffer, bytes): - buffer = buffer.decode('utf-8') + buffer = buffer.decode("utf-8") _data = _parse(buffer) return _data @@ -129,24 +131,31 @@ def get_by_type(data, type_id): result = [] for entry in data.values(): - if entry['DMIType'] == type_id: + if entry["DMIType"] == type_id: result.append(entry) return result def _execute_cmd(): - if not is_tool('dmidecode'): - logging.error('Dmidecode does not seem to be present on your system. Add it your path or ' - 'check the compatibility of this project with your distro.') + if not is_tool("dmidecode"): + logging.error( + "Dmidecode does not seem to be present on your system. Add it your path or " + "check the compatibility of this project with your distro." + ) sys.exit(1) - return _subprocess.check_output(['dmidecode', ], stderr=_subprocess.PIPE) + return _subprocess.check_output( + [ + "dmidecode", + ], + stderr=_subprocess.PIPE, + ) def _parse(buffer): output_data = {} # Each record is separated by double newlines - split_output = buffer.split('\n\n') + split_output = buffer.split("\n\n") for record in split_output: record_element = record.splitlines() @@ -164,21 +173,21 @@ def _parse(buffer): dmi_handle = handle_data[0] output_data[dmi_handle] = {} - output_data[dmi_handle]['DMIType'] = int(handle_data[1]) - output_data[dmi_handle]['DMISize'] = int(handle_data[2]) + output_data[dmi_handle]["DMIType"] = int(handle_data[1]) + output_data[dmi_handle]["DMISize"] = int(handle_data[2]) # Okay, we know 2nd line == name - output_data[dmi_handle]['DMIName'] = record_element[1] + output_data[dmi_handle]["DMIName"] = record_element[1] - in_block_elemet = '' - in_block_list = '' + in_block_elemet = "" + in_block_list = "" # Loop over the rest of the record, gathering values for i in range(2, len(record_element), 1): if i >= len(record_element): break # Check whether we are inside a \t\t block - if in_block_elemet != '': + if in_block_elemet != "": in_block_data = _in_block_re.findall(record_element[i]) if in_block_data: @@ -192,7 +201,7 @@ def _parse(buffer): else: # We are out of the \t\t block; reset it again, and let # the parsing continue - in_block_elemet = '' + in_block_elemet = "" record_data = _record_re.findall(record_element[i]) @@ -208,7 +217,7 @@ def _parse(buffer): # This is an array of data - let the loop know we are inside # an array block in_block_elemet = record_data2[0] - in_block_list = '' + in_block_list = "" continue diff --git a/netbox_agent/drivers/file.py b/netbox_agent/drivers/file.py index 487a298..c099048 100644 --- a/netbox_agent/drivers/file.py +++ b/netbox_agent/drivers/file.py @@ -2,7 +2,7 @@ import re def get(value, regex): - for line in open(value, 'r'): + for line in open(value, "r"): r = re.search(regex, line) if r and len(r.groups()) > 0: return r.groups()[0] diff --git a/netbox_agent/ethtool.py b/netbox_agent/ethtool.py index 132cd09..cf08028 100644 --- a/netbox_agent/ethtool.py +++ b/netbox_agent/ethtool.py @@ -6,16 +6,16 @@ from shutil import which # mapping fields from ethtool output to simple names field_map = { - 'Supported ports': 'ports', - 'Supported link modes': 'sup_link_modes', - 'Supports auto-negotiation': 'sup_autoneg', - 'Advertised link modes': 'adv_link_modes', - 'Advertised auto-negotiation': 'adv_autoneg', - 'Speed': 'speed', - 'Duplex': 'duplex', - 'Port': 'port', - 'Auto-negotiation': 'autoneg', - 'Link detected': 'link', + "Supported ports": "ports", + "Supported link modes": "sup_link_modes", + "Supports auto-negotiation": "sup_autoneg", + "Advertised link modes": "adv_link_modes", + "Advertised auto-negotiation": "adv_autoneg", + "Speed": "speed", + "Duplex": "duplex", + "Port": "port", + "Auto-negotiation": "autoneg", + "Link detected": "link", } @@ -25,7 +25,7 @@ def merge_two_dicts(x, y): return z -class Ethtool(): +class Ethtool: """ This class aims to parse ethtool output There is several bindings to have something proper, but it requires @@ -40,39 +40,40 @@ class Ethtool(): parse ethtool output """ - output = subprocess.getoutput('ethtool {}'.format(self.interface)) + output = subprocess.getoutput("ethtool {}".format(self.interface)) fields = {} - field = '' - fields['speed'] = '-' - fields['link'] = '-' - fields['duplex'] = '-' - for line in output.split('\n')[1:]: + field = "" + fields["speed"] = "-" + fields["link"] = "-" + fields["duplex"] = "-" + for line in output.split("\n")[1:]: line = line.rstrip() - r = line.find(':') + r = line.find(":") if r > 0: field = line[:r].strip() if field not in field_map: continue field = field_map[field] - output = line[r + 1:].strip() + output = line[r + 1 :].strip() fields[field] = output else: - if len(field) > 0 and \ - field in field_map: - fields[field] += ' ' + line.strip() + if len(field) > 0 and field in field_map: + fields[field] += " " + line.strip() return fields def _parse_ethtool_module_output(self): - status, output = subprocess.getstatusoutput('ethtool -m {}'.format(self.interface)) + status, output = subprocess.getstatusoutput( + "ethtool -m {}".format(self.interface) + ) if status == 0: - r = re.search(r'Identifier.*\((\w+)\)', output) + r = re.search(r"Identifier.*\((\w+)\)", output) if r and len(r.groups()) > 0: - return {'form_factor': r.groups()[0]} + return {"form_factor": r.groups()[0]} return {} def parse(self): - if which('ethtool') is None: + if which("ethtool") is None: return None output = self._parse_ethtool_output() output.update(self._parse_ethtool_module_output()) diff --git a/netbox_agent/inventory.py b/netbox_agent/inventory.py index 40e73d4..8fdc359 100644 --- a/netbox_agent/inventory.py +++ b/netbox_agent/inventory.py @@ -1,3 +1,11 @@ +import json +import logging +import re +import sys +import traceback + +import pynetbox + from netbox_agent.config import config from netbox_agent.config import netbox_instance as nb from netbox_agent.lshw import LSHW @@ -5,25 +13,19 @@ from netbox_agent.misc import get_vendor, is_tool from netbox_agent.raid.hp import HPRaid from netbox_agent.raid.omreport import OmreportRaid from netbox_agent.raid.storcli import StorcliRaid -import traceback -import pynetbox -import logging -import json -import re - INVENTORY_TAG = { - 'cpu': {'name': 'hw:cpu', 'slug': 'hw-cpu'}, - 'gpu': {'name': 'hw:gpu', 'slug': 'hw-gpu'}, - 'disk': {'name': 'hw:disk', 'slug': 'hw-disk'}, - 'interface': {'name': 'hw:interface', 'slug': 'hw-interface'}, - 'memory': {'name': 'hw:memory', 'slug': 'hw-memory'}, - 'motherboard': {'name': 'hw:motherboard', 'slug': 'hw-motherboard'}, - 'raid_card': {'name': 'hw:raid_card', 'slug': 'hw-raid-card'}, + "cpu": {"name": "hw:cpu", "slug": "hw-cpu"}, + "gpu": {"name": "hw:gpu", "slug": "hw-gpu"}, + "disk": {"name": "hw:disk", "slug": "hw-disk"}, + "interface": {"name": "hw:interface", "slug": "hw-interface"}, + "memory": {"name": "hw:memory", "slug": "hw-memory"}, + "motherboard": {"name": "hw:motherboard", "slug": "hw-motherboard"}, + "raid_card": {"name": "hw:raid_card", "slug": "hw-raid-card"}, } -class Inventory(): +class Inventory: """ Better Inventory items coming, see: - https://github.com/netbox-community/netbox/issues/3087 @@ -62,14 +64,12 @@ class Inventory(): def create_netbox_tags(self): ret = [] for key, tag in INVENTORY_TAG.items(): - nb_tag = nb.extras.tags.get( - name=tag['name'] - ) + nb_tag = nb.extras.tags.get(name=tag["name"]) if not nb_tag: nb_tag = nb.extras.tags.create( - name=tag['name'], - slug=tag['slug'], - comments=tag['name'], + name=tag["name"], + slug=tag["slug"], + comments=tag["name"], ) ret.append(nb_tag) return ret @@ -82,29 +82,28 @@ class Inventory(): name=name, ) if not manufacturer: - logging.info('Creating missing manufacturer {name}'.format(name=name)) + logging.info("Creating missing manufacturer {name}".format(name=name)) manufacturer = nb.dcim.manufacturers.create( name=name, - slug=re.sub('[^A-Za-z0-9]+', '-', name).lower(), + slug=re.sub("[^A-Za-z0-9]+", "-", name).lower(), ) - logging.info('Creating missing manufacturer {name}'.format(name=name)) + logging.info("Creating missing manufacturer {name}".format(name=name)) return manufacturer def get_netbox_inventory(self, device_id, tag): try: - items = nb.dcim.inventory_items.filter( - device_id=device_id, - tag=tag - ) + items = nb.dcim.inventory_items.filter(device_id=device_id, tag=tag) except pynetbox.core.query.RequestError: - logging.info('Tag {tag} is missing, returning empty array.'.format(tag=tag)) + logging.info("Tag {tag} is missing, returning empty array.".format(tag=tag)) items = [] return list(items) - def create_netbox_inventory_item(self, device_id, tags, vendor, name, serial, description): + def create_netbox_inventory_item( + self, device_id, tags, vendor, name, serial, description + ): manufacturer = self.find_or_create_manufacturer(vendor) _ = nb.dcim.inventory_items.create( @@ -112,26 +111,25 @@ class Inventory(): manufacturer=manufacturer.id, discovered=True, tags=tags, - name='{}'.format(name), - serial='{}'.format(serial), - description=description + name="{}".format(name), + serial="{}".format(serial), + description=description, ) - logging.info('Creating inventory item {} {}/{} {} '.format( - vendor, - name, - serial, - description) + logging.info( + "Creating inventory item {} {}/{} {} ".format( + vendor, name, serial, description + ) ) def get_hw_motherboards(self): motherboards = [] m = {} - m['serial'] = self.lshw.motherboard_serial - m['vendor'] = self.lshw.vendor - m['name'] = '{} {}'.format(self.lshw.vendor, self.lshw.motherboard) - m['description'] = '{} Motherboard'.format(self.lshw.motherboard) + m["serial"] = self.lshw.motherboard_serial + m["vendor"] = self.lshw.vendor + m["name"] = "{} {}".format(self.lshw.vendor, self.lshw.motherboard) + m["description"] = "{} Motherboard".format(self.lshw.motherboard) motherboards.append(m) @@ -141,27 +139,29 @@ class Inventory(): motherboards = self.get_hw_motherboards() nb_motherboards = self.get_netbox_inventory( - device_id=self.device_id, - tag=INVENTORY_TAG['motherboard']['slug']) + device_id=self.device_id, tag=INVENTORY_TAG["motherboard"]["slug"] + ) for nb_motherboard in nb_motherboards: - if nb_motherboard.serial not in [x['serial'] for x in motherboards]: - logging.info('Deleting unknown motherboard {motherboard}/{serial}'.format( - motherboard=self.lshw.motherboard, - serial=nb_motherboard.serial, - )) + if nb_motherboard.serial not in [x["serial"] for x in motherboards]: + logging.info( + "Deleting unknown motherboard {motherboard}/{serial}".format( + motherboard=self.lshw.motherboard, + serial=nb_motherboard.serial, + ) + ) nb_motherboard.delete() # create interfaces that are not in netbox for motherboard in motherboards: - if motherboard.get('serial') not in [x.serial for x in nb_motherboards]: + if motherboard.get("serial") not in [x.serial for x in nb_motherboards]: self.create_netbox_inventory_item( device_id=self.device_id, - tags=[{'name': INVENTORY_TAG['motherboard']['name']}], - vendor='{}'.format(motherboard.get('vendor', 'N/A')), - serial='{}'.format(motherboard.get('serial', 'No SN')), - name='{}'.format(motherboard.get('name')), - description='{}'.format(motherboard.get('description')) + tags=[{"name": INVENTORY_TAG["motherboard"]["name"]}], + vendor="{}".format(motherboard.get("vendor", "N/A")), + serial="{}".format(motherboard.get("serial", "No SN")), + name="{}".format(motherboard.get("name")), + description="{}".format(motherboard.get("description")), ) def create_netbox_interface(self, iface): @@ -170,56 +170,57 @@ class Inventory(): device=self.device_id, manufacturer=manufacturer.id, discovered=True, - tags=[{'name': INVENTORY_TAG['interface']['name']}], - name="{}".format(iface['product']), - serial='{}'.format(iface['serial']), - description='{} {}'.format(iface['description'], iface['name']) + tags=[{"name": INVENTORY_TAG["interface"]["name"]}], + name="{}".format(iface["product"]), + serial="{}".format(iface["serial"]), + description="{} {}".format(iface["description"], iface["name"]), ) def do_netbox_interfaces(self): nb_interfaces = self.get_netbox_inventory( - device_id=self.device_id, - tag=INVENTORY_TAG['interface']['slug']) + device_id=self.device_id, tag=INVENTORY_TAG["interface"]["slug"] + ) interfaces = self.lshw.interfaces # delete interfaces that are in netbox but not locally # use the serial_number has the comparison element for nb_interface in nb_interfaces: - if nb_interface.serial not in [x['serial'] for x in interfaces]: - logging.info('Deleting unknown interface {serial}'.format( - serial=nb_interface.serial, - )) + if nb_interface.serial not in [x["serial"] for x in interfaces]: + logging.info( + "Deleting unknown interface {serial}".format( + serial=nb_interface.serial, + ) + ) nb_interface.delete() # create interfaces that are not in netbox for iface in interfaces: - if iface.get('serial') not in [x.serial for x in nb_interfaces]: + if iface.get("serial") not in [x.serial for x in nb_interfaces]: self.create_netbox_interface(iface) def create_netbox_cpus(self): - for cpu in self.lshw.get_hw_linux('cpu'): + for cpu in self.lshw.get_hw_linux("cpu"): manufacturer = self.find_or_create_manufacturer(cpu["vendor"]) _ = nb.dcim.inventory_items.create( device=self.device_id, manufacturer=manufacturer.id, discovered=True, - tags=[{'name': INVENTORY_TAG['cpu']['name']}], - name=cpu['product'], - description='CPU {}'.format(cpu['location']), + tags=[{"name": INVENTORY_TAG["cpu"]["name"]}], + name=cpu["product"], + description="CPU {}".format(cpu["location"]), # asset_tag=cpu['location'] ) - logging.info('Creating CPU model {}'.format(cpu['product'])) + logging.info("Creating CPU model {}".format(cpu["product"])) def do_netbox_cpus(self): - cpus = self.lshw.get_hw_linux('cpu') + cpus = self.lshw.get_hw_linux("cpu") nb_cpus = self.get_netbox_inventory( device_id=self.device_id, - tag=INVENTORY_TAG['cpu']['slug'], + tag=INVENTORY_TAG["cpu"]["slug"], ) - if not len(nb_cpus) or \ - len(nb_cpus) and len(cpus) != len(nb_cpus): + if not len(nb_cpus) or len(nb_cpus) and len(cpus) != len(nb_cpus): for x in nb_cpus: x.delete() @@ -227,13 +228,13 @@ class Inventory(): def get_raid_cards(self, filter_cards=False): raid_class = None - if self.server.manufacturer in ('Dell', 'Huawei'): - if is_tool('omreport'): + if self.server.manufacturer in ("Dell", "Huawei"): + if is_tool("omreport"): raid_class = OmreportRaid - if is_tool('storcli'): + if is_tool("storcli"): raid_class = StorcliRaid - elif self.server.manufacturer in ('HP', 'HPE'): - if is_tool('ssacli'): + elif self.server.manufacturer in ("HP", "HPE"): + if is_tool("ssacli"): raid_class = HPRaid if not raid_class: @@ -241,19 +242,21 @@ class Inventory(): self.raid = raid_class() - if filter_cards and config.expansion_as_device \ - and self.server.own_expansion_slot(): + if ( + filter_cards + and config.expansion_as_device + and self.server.own_expansion_slot() + ): return [ - c for c in self.raid.get_controllers() + c + for c in self.raid.get_controllers() if c.is_external() is self.update_expansion ] else: return self.raid.get_controllers() def create_netbox_raid_card(self, raid_card): - manufacturer = self.find_or_create_manufacturer( - raid_card.get_manufacturer() - ) + manufacturer = self.find_or_create_manufacturer(raid_card.get_manufacturer()) name = raid_card.get_product_name() serial = raid_card.get_serial_number() @@ -261,15 +264,17 @@ class Inventory(): device=self.device_id, discovered=True, manufacturer=manufacturer.id if manufacturer else None, - tags=[{'name': INVENTORY_TAG['raid_card']['name']}], - name='{}'.format(name), - serial='{}'.format(serial), - description='RAID Card', + tags=[{"name": INVENTORY_TAG["raid_card"]["name"]}], + name="{}".format(name), + serial="{}".format(serial), + description="RAID Card", + ) + logging.info( + "Creating RAID Card {name} (SN: {serial})".format( + name=name, + serial=serial, + ) ) - logging.info('Creating RAID Card {name} (SN: {serial})'.format( - name=name, - serial=serial, - )) return nb_raid_card def do_netbox_raid_cards(self): @@ -284,8 +289,7 @@ class Inventory(): """ nb_raid_cards = self.get_netbox_inventory( - device_id=self.device_id, - tag=[INVENTORY_TAG['raid_card']['slug']] + device_id=self.device_id, tag=[INVENTORY_TAG["raid_card"]["slug"]] ) raid_cards = self.get_raid_cards(filter_cards=True) @@ -293,9 +297,11 @@ class Inventory(): # use the serial_number has the comparison element for nb_raid_card in nb_raid_cards: if nb_raid_card.serial not in [x.get_serial_number() for x in raid_cards]: - logging.info('Deleting unknown locally RAID Card {serial}'.format( - serial=nb_raid_card.serial, - )) + logging.info( + "Deleting unknown locally RAID Card {serial}".format( + serial=nb_raid_card.serial, + ) + ) nb_raid_card.delete() # create card that are not in netbox @@ -304,25 +310,32 @@ class Inventory(): self.create_netbox_raid_card(raid_card) def is_virtual_disk(self, disk, raid_devices): - disk_type = disk.get('type') - logicalname = disk.get('logicalname') - description = disk.get('description') - size = disk.get('size') - product = disk.get('product') - if logicalname in raid_devices or disk_type is None or product is None or description is None: + disk_type = disk.get("type") + logicalname = disk.get("logicalname") + description = disk.get("description") + size = disk.get("size") + product = disk.get("product") + if ( + logicalname in raid_devices + or disk_type is None + or product is None + or description is None + ): return True non_raid_disks = [ - 'MR9361-8i', + "MR9361-8i", ] - if logicalname in raid_devices or \ - product in non_raid_disks or \ - 'virtual' in product.lower() or \ - 'logical' in product.lower() or \ - 'volume' in description.lower() or \ - 'dvd-ram' in description.lower() or \ - description == 'SCSI Enclosure' or \ - (size is None and logicalname is None): + if ( + logicalname in raid_devices + or product in non_raid_disks + or "virtual" in product.lower() + or "logical" in product.lower() + or "volume" in description.lower() + or "dvd-ram" in description.lower() + or description == "SCSI Enclosure" + or (size is None and logicalname is None) + ): return True return False @@ -333,9 +346,9 @@ class Inventory(): disks.extend(raid_card.get_physical_disks()) raid_devices = [ - d.get('custom_fields', {}).get('vd_device') + d.get("custom_fields", {}).get("vd_device") for d in disks - if d.get('custom_fields', {}).get('vd_device') + if d.get("custom_fields", {}).get("vd_device") ] for disk in self.lshw.get_hw_linux("storage"): @@ -344,22 +357,22 @@ class Inventory(): size = int(getattr(disk, "size", 0)) / 1073741824 d = { "name": "", - 'Size': '{} GB'.format(size), - 'logicalname': disk.get('logicalname'), - 'description': disk.get('description'), - 'SN': disk.get('serial'), - 'Model': disk.get('product'), - 'Type': disk.get('type'), + "Size": "{} GB".format(size), + "logicalname": disk.get("logicalname"), + "description": disk.get("description"), + "SN": disk.get("serial"), + "Model": disk.get("product"), + "Type": disk.get("type"), } - if disk.get('vendor'): - d['Vendor'] = disk['vendor'] + if disk.get("vendor"): + d["Vendor"] = disk["vendor"] else: - d['Vendor'] = get_vendor(disk['product']) + d["Vendor"] = get_vendor(disk["product"]) disks.append(d) # remove duplicate serials seen = set() - uniq = [x for x in disks if x['SN'] not in seen and not seen.add(x['SN'])] + uniq = [x for x in disks if x["SN"] not in seen and not seen.add(x["SN"])] return uniq def create_netbox_disk(self, disk): @@ -367,53 +380,45 @@ class Inventory(): if "Vendor" in disk: manufacturer = self.find_or_create_manufacturer(disk["Vendor"]) - logicalname = disk.get('logicalname') - desc = disk.get('description') - name = '{} ({})'.format(disk['Model'], disk['Size']) - description = disk['Type'] - sn = disk.get('SN', 'unknown') + name = "{} ({})".format(disk["Model"], disk["Size"]) + description = disk["Type"] + sn = disk.get("SN", "unknown") parms = { - 'device': self.device_id, - 'discovered': True, - 'tags': [{'name': INVENTORY_TAG['disk']['name']}], - 'name': name, - 'serial': sn, - 'part_id': disk['Model'], - 'description': description, - 'manufacturer': getattr(manufacturer, "id", None), + "device": self.device_id, + "discovered": True, + "tags": [{"name": INVENTORY_TAG["disk"]["name"]}], + "name": name, + "serial": sn, + "part_id": disk["Model"], + "description": description, + "manufacturer": getattr(manufacturer, "id", None), } if config.process_virtual_drives: - parms['custom_fields'] = disk.get("custom_fields", {}) + parms["custom_fields"] = disk.get("custom_fields", {}) _ = nb.dcim.inventory_items.create(**parms) - logging.info('Creating Disk {model} {serial}'.format( - model=disk['Model'], - serial=sn, - )) + logging.info( + "Creating Disk {model} {serial}".format( + model=disk["Model"], + serial=sn, + ) + ) def dump_disks_map(self, disks): - disk_map = [d['custom_fields'] for d in disks if 'custom_fields' in d] + disk_map = [d["custom_fields"] for d in disks if "custom_fields" in d] if config.dump_disks_map == "-": f = sys.stdout else: f = open(config.dump_disks_map, "w") - f.write( - json.dumps( - disk_map, - separators=(',', ':'), - indent=4, - sort_keys=True - ) - ) + f.write(json.dumps(disk_map, separators=(",", ":"), indent=4, sort_keys=True)) if config.dump_disks_map != "-": f.close() def do_netbox_disks(self): nb_disks = self.get_netbox_inventory( - device_id=self.device_id, - tag=INVENTORY_TAG['disk']['slug'] + device_id=self.device_id, tag=INVENTORY_TAG["disk"]["slug"] ) disks = self.get_hw_disks() if config.dump_disks_map: @@ -422,100 +427,108 @@ class Inventory(): except Exception as e: logging.error("Failed to dump disks map: {}".format(e)) logging.debug(traceback.format_exc()) - disk_serials = [d['SN'] for d in disks if 'SN' in d] + disk_serials = [d["SN"] for d in disks if "SN" in d] # delete disks that are in netbox but not locally # use the serial_number has the comparison element for nb_disk in nb_disks: - if nb_disk.serial not in disk_serials or \ - config.force_disk_refresh: - logging.info('Deleting unknown locally Disk {serial}'.format( - serial=nb_disk.serial, - )) + if nb_disk.serial not in disk_serials or config.force_disk_refresh: + logging.info( + "Deleting unknown locally Disk {serial}".format( + serial=nb_disk.serial, + ) + ) nb_disk.delete() if config.force_disk_refresh: nb_disks = self.get_netbox_inventory( - device_id=self.device_id, - tag=INVENTORY_TAG['disk']['slug'] + device_id=self.device_id, tag=INVENTORY_TAG["disk"]["slug"] ) # create disks that are not in netbox for disk in disks: - if disk.get('SN') not in [d.serial for d in nb_disks]: + if disk.get("SN") not in [d.serial for d in nb_disks]: self.create_netbox_disk(disk) def create_netbox_memory(self, memory): - manufacturer = self.find_or_create_manufacturer(memory['vendor']) - name = 'Slot {} ({}GB)'.format(memory['slot'], memory['size']) + manufacturer = self.find_or_create_manufacturer(memory["vendor"]) + name = "Slot {} ({}GB)".format(memory["slot"], memory["size"]) nb_memory = nb.dcim.inventory_items.create( device=self.device_id, discovered=True, manufacturer=manufacturer.id, - tags=[{'name': INVENTORY_TAG['memory']['name']}], + tags=[{"name": INVENTORY_TAG["memory"]["name"]}], name=name, - part_id=memory['product'], - serial=memory['serial'], - description=memory['description'], + part_id=memory["product"], + serial=memory["serial"], + description=memory["description"], ) - logging.info('Creating Memory {location} {type} {size}GB'.format( - location=memory['slot'], - type=memory['product'], - size=memory['size'], - )) + logging.info( + "Creating Memory {location} {type} {size}GB".format( + location=memory["slot"], + type=memory["product"], + size=memory["size"], + ) + ) return nb_memory def do_netbox_memories(self): memories = self.lshw.memories nb_memories = self.get_netbox_inventory( - device_id=self.device_id, - tag=INVENTORY_TAG['memory']['slug'] + device_id=self.device_id, tag=INVENTORY_TAG["memory"]["slug"] ) for nb_memory in nb_memories: - if nb_memory.serial not in [x['serial'] for x in memories]: - logging.info('Deleting unknown locally Memory {serial}'.format( - serial=nb_memory.serial, - )) + if nb_memory.serial not in [x["serial"] for x in memories]: + logging.info( + "Deleting unknown locally Memory {serial}".format( + serial=nb_memory.serial, + ) + ) nb_memory.delete() for memory in memories: - if memory.get('serial') not in [x.serial for x in nb_memories]: + if memory.get("serial") not in [x.serial for x in nb_memories]: self.create_netbox_memory(memory) def create_netbox_gpus(self, gpus): for gpu in gpus: - if 'product' in gpu and len(gpu['product']) > 50: - gpu['product'] = (gpu['product'][:48] + '..') + if "product" in gpu and len(gpu["product"]) > 50: + gpu["product"] = gpu["product"][:48] + ".." manufacturer = self.find_or_create_manufacturer(gpu["vendor"]) _ = nb.dcim.inventory_items.create( device=self.device_id, manufacturer=manufacturer.id, discovered=True, - tags=[{'name': INVENTORY_TAG['gpu']['name']}], - name=gpu['product'], - description=gpu['description'], + tags=[{"name": INVENTORY_TAG["gpu"]["name"]}], + name=gpu["product"], + description=gpu["description"], ) - logging.info('Creating GPU model {}'.format(gpu['product'])) + logging.info("Creating GPU model {}".format(gpu["product"])) def is_external_gpu(self, gpu): - is_3d_gpu = gpu['description'].startswith('3D') - return self.server.is_blade() and \ - self.server.own_gpu_expansion_slot() and is_3d_gpu + is_3d_gpu = gpu["description"].startswith("3D") + return ( + self.server.is_blade() + and self.server.own_gpu_expansion_slot() + and is_3d_gpu + ) def do_netbox_gpus(self): gpus = [] gpu_models = {} - for gpu in self.lshw.get_hw_linux('gpu'): + for gpu in self.lshw.get_hw_linux("gpu"): # Filters GPU if an expansion bay is detected: # The internal (VGA) GPU only goes into the blade inventory, # the external (3D) GPU goes into the expansion blade. - if config.expansion_as_device and \ - self.update_expansion ^ self.is_external_gpu(gpu): + if ( + config.expansion_as_device + and self.update_expansion ^ self.is_external_gpu(gpu) + ): continue gpus.append(gpu) gpu_models.setdefault(gpu["product"], 0) @@ -523,7 +536,7 @@ class Inventory(): nb_gpus = self.get_netbox_inventory( device_id=self.device_id, - tag=INVENTORY_TAG['gpu']['slug'], + tag=INVENTORY_TAG["gpu"]["slug"], ) nb_gpu_models = {} for gpu in nb_gpus: diff --git a/netbox_agent/ipmi.py b/netbox_agent/ipmi.py index ff2b42b..13f8425 100644 --- a/netbox_agent/ipmi.py +++ b/netbox_agent/ipmi.py @@ -4,7 +4,7 @@ import subprocess from netaddr import IPNetwork -class IPMI(): +class IPMI: """ Parse IPMI output ie: @@ -37,9 +37,9 @@ class IPMI(): """ def __init__(self): - self.ret, self.output = subprocess.getstatusoutput('ipmitool lan print') + self.ret, self.output = subprocess.getstatusoutput("ipmitool lan print") if self.ret != 0: - logging.error('Cannot get ipmi info: {}'.format(self.output)) + logging.error("Cannot get ipmi info: {}".format(self.output)) def parse(self): _ipmi = {} @@ -47,23 +47,31 @@ class IPMI(): return _ipmi for line in self.output.splitlines(): - key = line.split(':')[0].strip() - if key not in ['802.1q VLAN ID', 'IP Address', 'Subnet Mask', 'MAC Address']: + key = line.split(":")[0].strip() + if key not in [ + "802.1q VLAN ID", + "IP Address", + "Subnet Mask", + "MAC Address", + ]: continue - value = ':'.join(line.split(':')[1:]).strip() + value = ":".join(line.split(":")[1:]).strip() _ipmi[key] = value ret = {} - ret['name'] = 'IPMI' + ret["name"] = "IPMI" ret["mtu"] = 1500 - ret['bonding'] = False - ret['mac'] = _ipmi['MAC Address'] - ret['vlan'] = int(_ipmi['802.1q VLAN ID']) \ - if _ipmi['802.1q VLAN ID'] != 'Disabled' else None - ip = _ipmi['IP Address'] - netmask = _ipmi['Subnet Mask'] - address = str(IPNetwork('{}/{}'.format(ip, netmask))) + ret["bonding"] = False + ret["mac"] = _ipmi["MAC Address"] + ret["vlan"] = ( + int(_ipmi["802.1q VLAN ID"]) + if _ipmi["802.1q VLAN ID"] != "Disabled" + else None + ) + ip = _ipmi["IP Address"] + netmask = _ipmi["Subnet Mask"] + address = str(IPNetwork("{}/{}".format(ip, netmask))) - ret['ip'] = [address] - ret['ipmi'] = True + ret["ip"] = [address] + ret["ipmi"] = True return ret diff --git a/netbox_agent/lldp.py b/netbox_agent/lldp.py index 21b645f..34b4ac2 100644 --- a/netbox_agent/lldp.py +++ b/netbox_agent/lldp.py @@ -4,14 +4,14 @@ import subprocess from netbox_agent.misc import is_tool -class LLDP(): +class LLDP: def __init__(self, output=None): - if not is_tool('lldpctl'): - logging.debug('lldpd package seems to be missing or daemon not running.') + if not is_tool("lldpctl"): + logging.debug("lldpd package seems to be missing or daemon not running.") if output: self.output = output else: - self.output = subprocess.getoutput('lldpctl -f keyvalue') + self.output = subprocess.getoutput("lldpctl -f keyvalue") self.data = self.parse() def parse(self): @@ -19,7 +19,7 @@ class LLDP(): vlans = {} vid = None for entry in self.output.splitlines(): - if '=' not in entry: + if "=" not in entry: continue path, value = entry.strip().split("=", 1) split_path = path.split(".") @@ -34,38 +34,38 @@ class LLDP(): if not isinstance(current_dict.get(path_component), dict): current_dict[path_component] = {} current_dict = current_dict.get(path_component) - if 'vlan-id' in path: + if "vlan-id" in path: vid = value vlans[interface][value] = vlans[interface].get(vid, {}) - elif path.endswith('vlan'): - vid = value.replace('vlan-', '') + elif path.endswith("vlan"): + vid = value.replace("vlan-", "") vlans[interface][vid] = vlans[interface].get(vid, {}) - elif 'pvid' in path: - vlans[interface][vid]['pvid'] = True - if 'vlan' not in path: + elif "pvid" in path: + vlans[interface][vid]["pvid"] = True + if "vlan" not in path: current_dict[final] = value for interface, vlan in vlans.items(): - output_dict['lldp'][interface]['vlan'] = vlan + output_dict["lldp"][interface]["vlan"] = vlan if not output_dict: - logging.debug('No LLDP output, please check your network config.') + logging.debug("No LLDP output, please check your network config.") return output_dict def get_switch_ip(self, interface): # lldp.eth0.chassis.mgmt-ip=100.66.7.222 - if self.data['lldp'].get(interface) is None: + if self.data["lldp"].get(interface) is None: return None - return self.data['lldp'][interface]['chassis'].get('mgmt-ip') + return self.data["lldp"][interface]["chassis"].get("mgmt-ip") def get_switch_port(self, interface): # lldp.eth0.port.descr=GigabitEthernet1/0/1 - if self.data['lldp'].get(interface) is None: + if self.data["lldp"].get(interface) is None: return None - if self.data['lldp'][interface]['port'].get('ifname'): - return self.data['lldp'][interface]['port']['ifname'] - return self.data['lldp'][interface]['port']['descr'] + if self.data["lldp"][interface]["port"].get("ifname"): + return self.data["lldp"][interface]["port"]["ifname"] + return self.data["lldp"][interface]["port"]["descr"] def get_switch_vlan(self, interface): # lldp.eth0.vlan.vlan-id=296 - if self.data['lldp'].get(interface) is None: + if self.data["lldp"].get(interface) is None: return None - return self.data['lldp'][interface]['vlan'] + return self.data["lldp"][interface]["vlan"] diff --git a/netbox_agent/location.py b/netbox_agent/location.py index bb1fcfc..c046aaf 100644 --- a/netbox_agent/location.py +++ b/netbox_agent/location.py @@ -4,7 +4,7 @@ import importlib.machinery from netbox_agent.config import config -class LocationBase(): +class LocationBase: """ This class is used to guess the location in order to push the information in Netbox for a `Device` @@ -27,15 +27,19 @@ class LocationBase(): if self.driver_file: try: # FIXME: Works with Python 3.3+, support older version? - loader = importlib.machinery.SourceFileLoader('driver_file', self.driver_file) + loader = importlib.machinery.SourceFileLoader( + "driver_file", self.driver_file + ) self.driver = loader.load_module() except ImportError: - raise ImportError("Couldn't import {} as a module".format(self.driver_file)) + raise ImportError( + "Couldn't import {} as a module".format(self.driver_file) + ) else: if self.driver: try: self.driver = importlib.import_module( - 'netbox_agent.drivers.{}'.format(self.driver) + "netbox_agent.drivers.{}".format(self.driver) ) except ImportError: raise ImportError("Driver {} doesn't exists".format(self.driver)) @@ -43,19 +47,23 @@ class LocationBase(): def get(self): if self.driver is None: return None - if not hasattr(self.driver, 'get'): + if not hasattr(self.driver, "get"): raise Exception( - "Your driver {} doesn't have a get() function, please fix it".format(self.driver) + "Your driver {} doesn't have a get() function, please fix it".format( + self.driver + ) ) - return getattr(self.driver, 'get')(self.driver_value, self.regex) + return getattr(self.driver, "get")(self.driver_value, self.regex) class Tenant(LocationBase): def __init__(self): - driver = config.tenant.driver.split(':')[0] if \ - config.tenant.driver else None - driver_value = ':'.join(config.tenant.driver.split(':')[1:]) if \ - config.tenant.driver else None + driver = config.tenant.driver.split(":")[0] if config.tenant.driver else None + driver_value = ( + ":".join(config.tenant.driver.split(":")[1:]) + if config.tenant.driver + else None + ) driver_file = config.tenant.driver_file regex = config.tenant.regex super().__init__(driver, driver_value, driver_file, regex) @@ -63,10 +71,16 @@ class Tenant(LocationBase): class Datacenter(LocationBase): def __init__(self): - driver = config.datacenter_location.driver.split(':')[0] if \ - config.datacenter_location.driver else None - driver_value = ':'.join(config.datacenter_location.driver.split(':')[1:]) if \ - config.datacenter_location.driver else None + driver = ( + config.datacenter_location.driver.split(":")[0] + if config.datacenter_location.driver + else None + ) + driver_value = ( + ":".join(config.datacenter_location.driver.split(":")[1:]) + if config.datacenter_location.driver + else None + ) driver_file = config.datacenter_location.driver_file regex = config.datacenter_location.regex super().__init__(driver, driver_value, driver_file, regex) @@ -74,10 +88,16 @@ class Datacenter(LocationBase): class Rack(LocationBase): def __init__(self): - driver = config.rack_location.driver.split(':')[0] if \ - config.rack_location.driver else None - driver_value = ':'.join(config.rack_location.driver.split(':')[1:]) if \ - config.rack_location.driver else None + driver = ( + config.rack_location.driver.split(":")[0] + if config.rack_location.driver + else None + ) + driver_value = ( + ":".join(config.rack_location.driver.split(":")[1:]) + if config.rack_location.driver + else None + ) driver_file = config.rack_location.driver_file regex = config.rack_location.regex super().__init__(driver, driver_value, driver_file, regex) @@ -85,10 +105,16 @@ class Rack(LocationBase): class Slot(LocationBase): def __init__(self): - driver = config.slot_location.driver.split(':')[0] if \ - config.slot_location.driver else None - driver_value = ':'.join(config.slot_location.driver.split(':')[1:]) if \ - config.slot_location.driver else None + driver = ( + config.slot_location.driver.split(":")[0] + if config.slot_location.driver + else None + ) + driver_value = ( + ":".join(config.slot_location.driver.split(":")[1:]) + if config.slot_location.driver + else None + ) driver_file = config.slot_location.driver_file regex = config.slot_location.regex super().__init__(driver, driver_value, driver_file, regex) diff --git a/netbox_agent/logging.py b/netbox_agent/logging.py index a0f7e20..4032293 100644 --- a/netbox_agent/logging.py +++ b/netbox_agent/logging.py @@ -3,7 +3,7 @@ import logging from netbox_agent.config import config logger = logging.getLogger() -if config.log_level.lower() == 'debug': +if config.log_level.lower() == "debug": logger.setLevel(logging.DEBUG) else: logger.setLevel(logging.INFO) diff --git a/netbox_agent/lshw.py b/netbox_agent/lshw.py index 876df97..fe2cb48 100644 --- a/netbox_agent/lshw.py +++ b/netbox_agent/lshw.py @@ -1,19 +1,18 @@ -from netbox_agent.misc import is_tool -import subprocess -import logging import json +import logging +import subprocess import sys +from netbox_agent.misc import is_tool -class LSHW(): + +class LSHW: def __init__(self): - if not is_tool('lshw'): - logging.error('lshw does not seem to be installed') + if not is_tool("lshw"): + logging.error("lshw does not seem to be installed") sys.exit(1) - data = subprocess.getoutput( - 'lshw -quiet -json' - ) + data = subprocess.getoutput("lshw -quiet -json") json_data = json.loads(data) # Starting from version 02.18, `lshw -json` wraps its result in a list # rather than returning directly a dictionary @@ -63,9 +62,9 @@ class LSHW(): return self.gpus if hwclass == "network": return self.interfaces - if hwclass == 'storage': + if hwclass == "storage": return self.disks - if hwclass == 'memory': + if hwclass == "memory": return self.memories def find_network(self, obj): @@ -82,64 +81,70 @@ class LSHW(): for j in i["name"]: if j.startswith("unknown"): unkn_intfs.append(j) - + unkn_name = "unknown{}".format(len(unkn_intfs)) - self.interfaces.append({ - "name": obj.get("logicalname", unkn_name), - "macaddress": obj.get("serial", ""), - "serial": obj.get("serial", ""), - "product": obj.get("product", "Unknown NIC"), - "vendor": obj.get("vendor", "Unknown"), - "description": obj.get("description", ""), - }) + self.interfaces.append( + { + "name": obj.get("logicalname", unkn_name), + "macaddress": obj.get("serial", ""), + "serial": obj.get("serial", ""), + "product": obj.get("product", "Unknown NIC"), + "vendor": obj.get("vendor", "Unknown"), + "description": obj.get("description", ""), + } + ) def find_storage(self, obj): if "children" in obj: for device in obj["children"]: - self.disks.append({ - "logicalname": device.get("logicalname"), - "product": device.get("product"), - "serial": device.get("serial"), - "version": device.get("version"), - "size": device.get("size"), - "description": device.get("description"), - "type": device.get("description"), - }) + self.disks.append( + { + "logicalname": device.get("logicalname"), + "product": device.get("product"), + "serial": device.get("serial"), + "version": device.get("version"), + "size": device.get("size"), + "description": device.get("description"), + "type": device.get("description"), + } + ) elif "nvme" in obj["configuration"]["driver"]: - if not is_tool('nvme'): - logging.error('nvme-cli >= 1.0 does not seem to be installed') + if not is_tool("nvme"): + logging.error("nvme-cli >= 1.0 does not seem to be installed") return try: nvme = json.loads( subprocess.check_output( - ["nvme", '-list', '-o', 'json'], - encoding='utf8') + ["nvme", "-list", "-o", "json"], encoding="utf8" + ) ) for device in nvme["Devices"]: d = { - 'logicalname': device["DevicePath"], - 'product': device["ModelNumber"], - 'serial': device["SerialNumber"], + "logicalname": device["DevicePath"], + "product": device["ModelNumber"], + "serial": device["SerialNumber"], "version": device["Firmware"], - 'description': "NVME", - 'type': "NVME", + "description": "NVME", + "type": "NVME", } if "UsedSize" in device: - d['size'] = device["UsedSize"] + d["size"] = device["UsedSize"] if "UsedBytes" in device: - d['size'] = device["UsedBytes"] + d["size"] = device["UsedBytes"] self.disks.append(d) except Exception: pass def find_cpus(self, obj): if "product" in obj: - self.cpus.append({ - "product": obj.get("product", "Unknown CPU"), - "vendor": obj.get("vendor", "Unknown vendor"), - "description": obj.get("description", ""), - "location": obj.get("slot", ""), - }) + self.cpus.append( + { + "product": obj.get("product", "Unknown CPU"), + "vendor": obj.get("vendor", "Unknown vendor"), + "description": obj.get("description", ""), + "location": obj.get("slot", ""), + } + ) def find_memories(self, obj): if "children" not in obj: @@ -150,15 +155,17 @@ class LSHW(): if "empty" in dimm["description"]: continue - self.memories.append({ - "slot": dimm.get("slot"), - "description": dimm.get("description"), - "id": dimm.get("id"), - "serial": dimm.get("serial", 'N/A'), - "vendor": dimm.get("vendor", 'N/A'), - "product": dimm.get("product", 'N/A'), - "size": dimm.get("size", 0) / 2 ** 20 / 1024, - }) + self.memories.append( + { + "slot": dimm.get("slot"), + "description": dimm.get("description"), + "id": dimm.get("id"), + "serial": dimm.get("serial", "N/A"), + "vendor": dimm.get("vendor", "N/A"), + "product": dimm.get("product", "N/A"), + "size": dimm.get("size", 0) / 2**20 / 1024, + } + ) def find_gpus(self, obj): if "product" in obj: diff --git a/netbox_agent/misc.py b/netbox_agent/misc.py index 8672cad..4e23df7 100644 --- a/netbox_agent/misc.py +++ b/netbox_agent/misc.py @@ -1,29 +1,27 @@ -from netbox_agent.config import netbox_instance as nb -from slugify import slugify -from shutil import which -import subprocess -import socket import re +import socket +import subprocess +from shutil import which + +from slugify import slugify + +from netbox_agent.config import netbox_instance as nb def is_tool(name): - '''Check whether `name` is on PATH and marked as executable.''' + """Check whether `name` is on PATH and marked as executable.""" return which(name) is not None def get_device_role(role): - device_role = nb.dcim.device_roles.get( - name=role - ) + device_role = nb.dcim.device_roles.get(name=role) if device_role is None: raise Exception('DeviceRole "{}" does not exist, please create it'.format(role)) return device_role def get_device_type(type): - device_type = nb.dcim.device_types.get( - model=type - ) + device_type = nb.dcim.device_types.get(model=type) if device_type is None: raise Exception('DeviceType "{}" does not exist, please create it'.format(type)) return device_type @@ -35,9 +33,11 @@ def get_device_platform(device_platform): # Python 3.8+ moved linux_distribution() to distro try: import distro + linux_distribution = " ".join(distro.linux_distribution()) except ImportError: import platform + linux_distribution = " ".join(platform.linux_distribution()) if not linux_distribution: @@ -54,24 +54,25 @@ def get_device_platform(device_platform): ) return device_platform + def get_vendor(name): vendors = { - 'PERC': 'Dell', - 'SANDISK': 'SanDisk', - 'DELL': 'Dell', - 'ST': 'Seagate', - 'CRUCIAL': 'Crucial', - 'MICRON': 'Micron', - 'INTEL': 'Intel', - 'SAMSUNG': 'Samsung', - 'EH0': 'HP', - 'HGST': 'HGST', - 'HUH': 'HGST', - 'MB': 'Toshiba', - 'MC': 'Toshiba', - 'MD': 'Toshiba', - 'MG': 'Toshiba', - 'WD': 'WDC' + "PERC": "Dell", + "SANDISK": "SanDisk", + "DELL": "Dell", + "ST": "Seagate", + "CRUCIAL": "Crucial", + "MICRON": "Micron", + "INTEL": "Intel", + "SAMSUNG": "Samsung", + "EH0": "HP", + "HGST": "HGST", + "HUH": "HGST", + "MB": "Toshiba", + "MC": "Toshiba", + "MD": "Toshiba", + "MG": "Toshiba", + "WD": "WDC", } for key, value in vendors.items(): if name.upper().startswith(key): @@ -81,16 +82,14 @@ def get_vendor(name): def get_hostname(config): if config.hostname_cmd is None: - return '{}'.format(socket.gethostname()) + return "{}".format(socket.gethostname()) return subprocess.getoutput(config.hostname_cmd) def create_netbox_tags(tags): ret = [] for tag in tags: - nb_tag = nb.extras.tags.get( - name=tag - ) + nb_tag = nb.extras.tags.get(name=tag) if not nb_tag: nb_tag = nb.extras.tags.create( name=tag, @@ -102,15 +101,13 @@ def create_netbox_tags(tags): def get_mount_points(): mount_points = {} - output = subprocess.getoutput('mount') + output = subprocess.getoutput("mount") for r in output.split("\n"): if not r.startswith("/dev/"): continue mount_info = r.split() device = mount_info[0] - device = re.sub(r'\d+$', '', device) + device = re.sub(r"\d+$", "", device) mp = mount_info[2] mount_points.setdefault(device, []).append(mp) return mount_points - - diff --git a/netbox_agent/network.py b/netbox_agent/network.py index 8630cc7..6ae318c 100644 --- a/netbox_agent/network.py +++ b/netbox_agent/network.py @@ -26,42 +26,45 @@ class Network(object): self.dcim_choices = {} dcim_c = nb.dcim.interfaces.choices() for _choice_type in dcim_c: - key = 'interface:{}'.format(_choice_type) + key = "interface:{}".format(_choice_type) self.dcim_choices[key] = {} for choice in dcim_c[_choice_type]: - self.dcim_choices[key][choice['display_name']] = choice['value'] + self.dcim_choices[key][choice["display_name"]] = choice["value"] self.ipam_choices = {} ipam_c = nb.ipam.ip_addresses.choices() for _choice_type in ipam_c: - key = 'ip-address:{}'.format(_choice_type) + key = "ip-address:{}".format(_choice_type) self.ipam_choices[key] = {} for choice in ipam_c[_choice_type]: - self.ipam_choices[key][choice['display_name']] = choice['value'] + self.ipam_choices[key][choice["display_name"]] = choice["value"] def get_network_type(): return NotImplementedError def scan(self): nics = [] - for interface in os.listdir('/sys/class/net/'): + for interface in os.listdir("/sys/class/net/"): # ignore if it's not a link (ie: bonding_masters etc) - if not os.path.islink('/sys/class/net/{}'.format(interface)): + if not os.path.islink("/sys/class/net/{}".format(interface)): continue - if config.network.ignore_interfaces and \ - re.match(config.network.ignore_interfaces, interface): - logging.debug('Ignore interface {interface}'.format(interface=interface)) + if config.network.ignore_interfaces and re.match( + config.network.ignore_interfaces, interface + ): + logging.debug( + "Ignore interface {interface}".format(interface=interface) + ) continue ip_addr = netifaces.ifaddresses(interface).get(netifaces.AF_INET, []) ip6_addr = netifaces.ifaddresses(interface).get(netifaces.AF_INET6, []) if config.network.ignore_ips: for i, ip in enumerate(ip_addr): - if re.match(config.network.ignore_ips, ip['addr']): + if re.match(config.network.ignore_ips, ip["addr"]): ip_addr.pop(i) for i, ip in enumerate(ip6_addr): - if re.match(config.network.ignore_ips, ip['addr']): + if re.match(config.network.ignore_ips, ip["addr"]): ip6_addr.pop(i) # netifaces returns a ipv6 netmask that netaddr does not understand. @@ -80,63 +83,72 @@ class Network(object): # } # for addr in ip6_addr: - addr["addr"] = addr["addr"].replace('%{}'.format(interface), '') - addr["mask"] = addr["mask"].split('/')[0] + addr["addr"] = addr["addr"].replace("%{}".format(interface), "") + addr["mask"] = addr["mask"].split("/")[0] ip_addr.append(addr) - mac = open('/sys/class/net/{}/address'.format(interface), 'r').read().strip() - mtu = int(open('/sys/class/net/{}/mtu'.format(interface), 'r').read().strip()) + mac = ( + open("/sys/class/net/{}/address".format(interface), "r").read().strip() + ) + mtu = int( + open("/sys/class/net/{}/mtu".format(interface), "r").read().strip() + ) vlan = None - if len(interface.split('.')) > 1: - vlan = int(interface.split('.')[1]) + if len(interface.split(".")) > 1: + vlan = int(interface.split(".")[1]) bonding = False bonding_slaves = [] - if os.path.isdir('/sys/class/net/{}/bonding'.format(interface)): + if os.path.isdir("/sys/class/net/{}/bonding".format(interface)): bonding = True - bonding_slaves = open( - '/sys/class/net/{}/bonding/slaves'.format(interface) - ).read().split() + bonding_slaves = ( + open("/sys/class/net/{}/bonding/slaves".format(interface)) + .read() + .split() + ) # Tun and TAP support - virtual = os.path.isfile( - '/sys/class/net/{}/tun_flags'.format(interface) - ) + virtual = os.path.isfile("/sys/class/net/{}/tun_flags".format(interface)) nic = { - 'name': interface, - 'mac': mac if mac != '00:00:00:00:00:00' else None, - 'ip': [ - '{}/{}'.format( - x['addr'], - IPAddress(x['mask']).netmask_bits() - ) for x in ip_addr - ] if ip_addr else None, # FIXME: handle IPv6 addresses - 'ethtool': Ethtool(interface).parse(), - 'virtual': virtual, - 'vlan': vlan, - 'mtu': mtu, - 'bonding': bonding, - 'bonding_slaves': bonding_slaves, + "name": interface, + "mac": mac if mac != "00:00:00:00:00:00" else None, + "ip": ( + [ + "{}/{}".format(x["addr"], IPAddress(x["mask"]).netmask_bits()) + for x in ip_addr + ] + if ip_addr + else None + ), # FIXME: handle IPv6 addresses + "ethtool": Ethtool(interface).parse(), + "virtual": virtual, + "vlan": vlan, + "mtu": mtu, + "bonding": bonding, + "bonding_slaves": bonding_slaves, } nics.append(nic) return nics def _set_bonding_interfaces(self): - bonding_nics = (x for x in self.nics if x['bonding']) + bonding_nics = (x for x in self.nics if x["bonding"]) for nic in bonding_nics: bond_int = self.get_netbox_network_card(nic) - logging.debug('Setting slave interface for {name}'.format( - name=bond_int.name - )) + logging.debug( + "Setting slave interface for {name}".format(name=bond_int.name) + ) for slave_int in ( - self.get_netbox_network_card(slave_nic) - for slave_nic in self.nics - if slave_nic['name'] in nic['bonding_slaves']): + self.get_netbox_network_card(slave_nic) + for slave_nic in self.nics + if slave_nic["name"] in nic["bonding_slaves"] + ): if slave_int.lag is None or slave_int.lag.id != bond_int.id: - logging.debug('Settting interface {name} as slave of {master}'.format( - name=slave_int.name, master=bond_int.name - )) + logging.debug( + "Settting interface {name} as slave of {master}".format( + name=slave_int.name, master=bond_int.name + ) + ) slave_int.lag = bond_int slave_int.save() else: @@ -147,55 +159,50 @@ class Network(object): return self.nics def get_netbox_network_card(self, nic): - if nic['mac'] is None: + if nic["mac"] is None: interface = self.nb_net.interfaces.get( - name=nic['name'], - **self.custom_arg_id + name=nic["name"], **self.custom_arg_id ) else: interface = self.nb_net.interfaces.get( - mac_address=nic['mac'], - name=nic['name'], - **self.custom_arg_id + mac_address=nic["mac"], name=nic["name"], **self.custom_arg_id ) return interface def get_netbox_network_cards(self): - return self.nb_net.interfaces.filter( - **self.custom_arg_id - ) + return self.nb_net.interfaces.filter(**self.custom_arg_id) def get_netbox_type_for_nic(self, nic): - if self.get_network_type() == 'virtual': - return self.dcim_choices['interface:type']['Virtual'] + if self.get_network_type() == "virtual": + return self.dcim_choices["interface:type"]["Virtual"] - if nic.get('bonding'): - return self.dcim_choices['interface:type']['Link Aggregation Group (LAG)'] + if nic.get("bonding"): + return self.dcim_choices["interface:type"]["Link Aggregation Group (LAG)"] - if nic.get('bonding'): - return self.dcim_choices['interface:type']['Link Aggregation Group (LAG)'] + if nic.get("bonding"): + return self.dcim_choices["interface:type"]["Link Aggregation Group (LAG)"] - if nic.get('virtual'): - return self.dcim_choices['interface:type']['Virtual'] + if nic.get("virtual"): + return self.dcim_choices["interface:type"]["Virtual"] - if nic.get('ethtool') is None: - return self.dcim_choices['interface:type']['Other'] + if nic.get("ethtool") is None: + return self.dcim_choices["interface:type"]["Other"] - if nic['ethtool']['speed'] == '10000Mb/s': - if nic['ethtool']['port'] in ('FIBRE', 'Direct Attach Copper'): - return self.dcim_choices['interface:type']['SFP+ (10GE)'] - return self.dcim_choices['interface:type']['10GBASE-T (10GE)'] + if nic["ethtool"]["speed"] == "10000Mb/s": + if nic["ethtool"]["port"] in ("FIBRE", "Direct Attach Copper"): + return self.dcim_choices["interface:type"]["SFP+ (10GE)"] + return self.dcim_choices["interface:type"]["10GBASE-T (10GE)"] - elif nic['ethtool']['speed'] == '25000Mb/s': - if nic['ethtool']['port'] in ('FIBRE', 'Direct Attach Copper'): - return self.dcim_choices['interface:type']['SFP28 (25GE)'] + elif nic["ethtool"]["speed"] == "25000Mb/s": + if nic["ethtool"]["port"] in ("FIBRE", "Direct Attach Copper"): + return self.dcim_choices["interface:type"]["SFP28 (25GE)"] - elif nic['ethtool']['speed'] == '1000Mb/s': - if nic['ethtool']['port'] in ('FIBRE', 'Direct Attach Copper'): - return self.dcim_choices['interface:type']['SFP (1GE)'] - return self.dcim_choices['interface:type']['1000BASE-T (1GE)'] + elif nic["ethtool"]["speed"] == "1000Mb/s": + if nic["ethtool"]["port"] in ("FIBRE", "Direct Attach Copper"): + return self.dcim_choices["interface:type"]["SFP (1GE)"] + return self.dcim_choices["interface:type"]["1000BASE-T (1GE)"] - return self.dcim_choices['interface:type']['Other'] + return self.dcim_choices["interface:type"]["Other"] def get_or_create_vlan(self, vlan_id): # FIXME: we may need to specify the datacenter @@ -205,25 +212,35 @@ class Network(object): ) if vlan is None: vlan = nb.ipam.vlans.create( - name='VLAN {}'.format(vlan_id), + name="VLAN {}".format(vlan_id), vid=vlan_id, ) return vlan def reset_vlan_on_interface(self, nic, interface): update = False - vlan_id = nic['vlan'] - lldp_vlan = self.lldp.get_switch_vlan(nic['name']) if config.network.lldp and isinstance(self, ServerNetwork) else None + vlan_id = nic["vlan"] + lldp_vlan = ( + self.lldp.get_switch_vlan(nic["name"]) + if config.network.lldp and isinstance(self, ServerNetwork) + else None + ) # For strange reason, we need to get the object from scratch # The object returned by pynetbox's save isn't always working (since pynetbox 6) interface = self.nb_net.interfaces.get(id=interface.id) # Handle the case were the local interface isn't an interface vlan as reported by Netbox # and that LLDP doesn't report a vlan-id - if vlan_id is None and lldp_vlan is None and \ - (interface.mode is not None or len(interface.tagged_vlans) > 0): - logging.info('Interface {interface} is not tagged, reseting mode'.format( - interface=interface)) + if ( + vlan_id is None + and lldp_vlan is None + and (interface.mode is not None or len(interface.tagged_vlans) > 0) + ): + logging.info( + "Interface {interface} is not tagged, reseting mode".format( + interface=interface + ) + ) update = True interface.mode = None interface.tagged_vlans = [] @@ -232,71 +249,87 @@ class Network(object): # if mode is either not set or not correctly configured or vlan are not # correctly configured, we reset the vlan elif vlan_id and ( - interface.mode is None or - type(interface.mode) is not int and ( - hasattr(interface.mode, 'value') and - interface.mode.value == self.dcim_choices['interface:mode']['Access'] or - len(interface.tagged_vlans) != 1 or - int(interface.tagged_vlans[0].vid) != int(vlan_id))): - logging.info('Resetting tagged VLAN(s) on interface {interface}'.format( - interface=interface)) + interface.mode is None + or not isinstance(interface.mode, int) + and ( + hasattr(interface.mode, "value") + and interface.mode.value + == self.dcim_choices["interface:mode"]["Access"] + or len(interface.tagged_vlans) != 1 + or int(interface.tagged_vlans[0].vid) != int(vlan_id) + ) + ): + logging.info( + "Resetting tagged VLAN(s) on interface {interface}".format( + interface=interface + ) + ) update = True nb_vlan = self.get_or_create_vlan(vlan_id) - interface.mode = self.dcim_choices['interface:mode']['Tagged'] + interface.mode = self.dcim_choices["interface:mode"]["Tagged"] interface.tagged_vlans = [nb_vlan] if nb_vlan else [] interface.untagged_vlan = None # Finally if LLDP reports a vlan-id with the pvid attribute elif lldp_vlan: - pvid_vlan = [key for (key, value) in lldp_vlan.items() if value['pvid']] + pvid_vlan = [key for (key, value) in lldp_vlan.items() if value["pvid"]] if len(pvid_vlan) > 0 and ( - interface.mode is None or - interface.mode.value != self.dcim_choices['interface:mode']['Access'] or - interface.untagged_vlan is None or - interface.untagged_vlan.vid != int(pvid_vlan[0])): - logging.info('Resetting access VLAN on interface {interface}'.format( - interface=interface)) + interface.mode is None + or interface.mode.value != self.dcim_choices["interface:mode"]["Access"] + or interface.untagged_vlan is None + or interface.untagged_vlan.vid != int(pvid_vlan[0]) + ): + logging.info( + "Resetting access VLAN on interface {interface}".format( + interface=interface + ) + ) update = True nb_vlan = self.get_or_create_vlan(pvid_vlan[0]) - interface.mode = self.dcim_choices['interface:mode']['Access'] + interface.mode = self.dcim_choices["interface:mode"]["Access"] interface.untagged_vlan = nb_vlan.id return update, interface def create_netbox_nic(self, nic, mgmt=False): # TODO: add Optic Vendor, PN and Serial nic_type = self.get_netbox_type_for_nic(nic) - logging.info('Creating NIC {name} ({mac}) on {device}'.format( - name=nic['name'], mac=nic['mac'], device=self.device.name)) + logging.info( + "Creating NIC {name} ({mac}) on {device}".format( + name=nic["name"], mac=nic["mac"], device=self.device.name + ) + ) nb_vlan = None params = dict(self.custom_arg) - params.update({ - 'name': nic['name'], - 'type': nic_type, - 'mgmt_only': mgmt, - }) - if nic['mac']: - params['mac_address'] = nic['mac'] + params.update( + { + "name": nic["name"], + "type": nic_type, + "mgmt_only": mgmt, + } + ) + if nic["mac"]: + params["mac_address"] = nic["mac"] - if nic['mtu']: - params['mtu'] = nic['mtu'] + if nic["mtu"]: + params["mtu"] = nic["mtu"] interface = self.nb_net.interfaces.create(**params) - if nic['vlan']: - nb_vlan = self.get_or_create_vlan(nic['vlan']) - interface.mode = self.dcim_choices['interface:mode']['Tagged'] + if nic["vlan"]: + nb_vlan = self.get_or_create_vlan(nic["vlan"]) + interface.mode = self.dcim_choices["interface:mode"]["Tagged"] interface.tagged_vlans = [nb_vlan.id] interface.save() - elif config.network.lldp and self.lldp.get_switch_vlan(nic['name']) is not None: + elif config.network.lldp and self.lldp.get_switch_vlan(nic["name"]) is not None: # if lldp reports a vlan on an interface, tag the interface in access and set the vlan # report only the interface which has `pvid=yes` (ie: lldp.eth3.vlan.pvid=yes) # if pvid is not present, it'll be processed as a vlan tagged interface - vlans = self.lldp.get_switch_vlan(nic['name']) + vlans = self.lldp.get_switch_vlan(nic["name"]) for vid, vlan_infos in vlans.items(): nb_vlan = self.get_or_create_vlan(vid) - if vlan_infos.get('vid'): - interface.mode = self.dcim_choices['interface:mode']['Access'] + if vlan_infos.get("vid"): + interface.mode = self.dcim_choices["interface:mode"]["Access"] interface.untagged_vlan = nb_vlan.id interface.save() @@ -314,7 +347,7 @@ class Network(object): return interface def create_or_update_netbox_ip_on_interface(self, ip, interface): - ''' + """ Two behaviors: - Anycast IP * If IP exists and is in Anycast, create a new Anycast one @@ -325,69 +358,78 @@ class Network(object): * If IP doesn't exist, create it * If IP exists and isn't assigned, take it * If IP exists and interface is wrong, change interface - ''' + """ netbox_ips = nb.ipam.ip_addresses.filter( address=ip, ) if not netbox_ips: - logging.info('Create new IP {ip} on {interface}'.format( - ip=ip, interface=interface)) + logging.info( + "Create new IP {ip} on {interface}".format(ip=ip, interface=interface) + ) query_params = { - 'address': ip, - 'status': "active", - 'assigned_object_type': self.assigned_object_type, - 'assigned_object_id': interface.id + "address": ip, + "status": "active", + "assigned_object_type": self.assigned_object_type, + "assigned_object_id": interface.id, } - netbox_ip = nb.ipam.ip_addresses.create( - **query_params - ) + netbox_ip = nb.ipam.ip_addresses.create(**query_params) return netbox_ip netbox_ip = list(netbox_ips)[0] # If IP exists in anycast - if netbox_ip.role and netbox_ip.role.label == 'Anycast': - logging.debug('IP {} is Anycast..'.format(ip)) + if netbox_ip.role and netbox_ip.role.label == "Anycast": + logging.debug("IP {} is Anycast..".format(ip)) unassigned_anycast_ip = [x for x in netbox_ips if x.interface is None] - assigned_anycast_ip = [x for x in netbox_ips if - x.interface and x.interface.id == interface.id] + assigned_anycast_ip = [ + x for x in netbox_ips if x.interface and x.interface.id == interface.id + ] # use the first available anycast ip if len(unassigned_anycast_ip): - logging.info('Assigning existing Anycast IP {} to interface'.format(ip)) + logging.info("Assigning existing Anycast IP {} to interface".format(ip)) netbox_ip = unassigned_anycast_ip[0] netbox_ip.interface = interface netbox_ip.save() # or if everything is assigned to other servers elif not len(assigned_anycast_ip): - logging.info('Creating Anycast IP {} and assigning it to interface'.format(ip)) + logging.info( + "Creating Anycast IP {} and assigning it to interface".format(ip) + ) query_params = { "address": ip, "status": "active", - "role": self.ipam_choices['ip-address:role']['Anycast'], + "role": self.ipam_choices["ip-address:role"]["Anycast"], "tenant": self.tenant.id if self.tenant else None, "assigned_object_type": self.assigned_object_type, - "assigned_object_id": interface.id + "assigned_object_id": interface.id, } netbox_ip = nb.ipam.ip_addresses.create(**query_params) return netbox_ip else: - ip_interface = getattr(netbox_ip, 'interface', None) - assigned_object = getattr(netbox_ip, 'assigned_object', None) + ip_interface = getattr(netbox_ip, "interface", None) + assigned_object = getattr(netbox_ip, "assigned_object", None) if not ip_interface or not assigned_object: - logging.info('Assigning existing IP {ip} to {interface}'.format( - ip=ip, interface=interface)) - elif (ip_interface and ip_interface.id != interface.id) or \ - (assigned_object and assigned_object_id != interface.id): + logging.info( + "Assigning existing IP {ip} to {interface}".format( + ip=ip, interface=interface + ) + ) + elif (ip_interface and ip_interface.id != interface.id) or ( + assigned_object and assigned_object.id != interface.id + ): old_interface = getattr(netbox_ip, "assigned_object", "n/a") logging.info( - 'Detected interface change for ip {ip}: old interface is ' - '{old_interface} (id: {old_id}), new interface is {new_interface} ' - ' (id: {new_id})' - .format( - old_interface=old_interface, new_interface=interface, - old_id=netbox_ip.id, new_id=interface.id, ip=netbox_ip.address - )) + "Detected interface change for ip {ip}: old interface is " + "{old_interface} (id: {old_id}), new interface is {new_interface} " + " (id: {new_id})".format( + old_interface=old_interface, + new_interface=interface, + old_id=netbox_ip.id, + new_id=interface.id, + ip=netbox_ip.address, + ) + ) else: return netbox_ip @@ -398,82 +440,93 @@ class Network(object): def create_or_update_netbox_network_cards(self): if config.update_all is None or config.update_network is None: return None - logging.debug('Creating/Updating NIC...') + logging.debug("Creating/Updating NIC...") # delete unknown interface nb_nics = list(self.get_netbox_network_cards()) - local_nics = [x['name'] for x in self.nics] + local_nics = [x["name"] for x in self.nics] for nic in nb_nics: if nic.name not in local_nics: - logging.info('Deleting netbox interface {name} because not present locally'.format( - name=nic.name - )) + logging.info( + "Deleting netbox interface {name} because not present locally".format( + name=nic.name + ) + ) nb_nics.remove(nic) nic.delete() # delete IP on netbox that are not known on this server if len(nb_nics): + def batched(it, n): - while batch := tuple(islice(it, n)): - yield batch + while batch := tuple(islice(it, n)): + yield batch netbox_ips = [] for ids in batched((x.id for x in nb_nics), 25): - netbox_ips += list( - nb.ipam.ip_addresses.filter(**{self.intf_type: ids}) - ) + netbox_ips += list(nb.ipam.ip_addresses.filter(**{self.intf_type: ids})) - all_local_ips = list(chain.from_iterable([ - x['ip'] for x in self.nics if x['ip'] is not None - ])) + all_local_ips = list( + chain.from_iterable([x["ip"] for x in self.nics if x["ip"] is not None]) + ) for netbox_ip in netbox_ips: if netbox_ip.address not in all_local_ips: - logging.info('Unassigning IP {ip} from {interface}'.format( - ip=netbox_ip.address, interface=netbox_ip.assigned_object)) + logging.info( + "Unassigning IP {ip} from {interface}".format( + ip=netbox_ip.address, interface=netbox_ip.assigned_object + ) + ) netbox_ip.assigned_object_type = None - netbox_ip.assigned_object_id = None + netbox_ip.ssigned_object_id = None netbox_ip.save() # update each nic for nic in self.nics: interface = self.get_netbox_network_card(nic) if not interface: - logging.info('Interface {mac_address} not found, creating..'.format( - mac_address=nic['mac']) + logging.info( + "Interface {mac_address} not found, creating..".format( + mac_address=nic["mac"] + ) ) interface = self.create_netbox_nic(nic) nic_update = 0 - if nic['name'] != interface.name: - logging.info('Updating interface {interface} name to: {name}'.format( - interface=interface, name=nic['name'])) - interface.name = nic['name'] + if nic["name"] != interface.name: + logging.info( + "Updating interface {interface} name to: {name}".format( + interface=interface, name=nic["name"] + ) + ) + interface.name = nic["name"] nic_update += 1 ret, interface = self.reset_vlan_on_interface(nic, interface) nic_update += ret - if hasattr(interface, 'mtu'): - if nic['mtu'] != interface.mtu: - logging.info('Interface mtu is wrong, updating to: {mtu}'.format( - mtu=nic['mtu'])) - interface.mtu = nic['mtu'] + if hasattr(interface, "mtu"): + if nic["mtu"] != interface.mtu: + logging.info( + "Interface mtu is wrong, updating to: {mtu}".format( + mtu=nic["mtu"] + ) + ) + interface.mtu = nic["mtu"] nic_update += 1 - if hasattr(interface, 'type'): + if hasattr(interface, "type"): _type = self.get_netbox_type_for_nic(nic) - if not interface.type or \ - _type != interface.type.value: - logging.info('Interface type is wrong, resetting') + if not interface.type or _type != interface.type.value: + logging.info("Interface type is wrong, resetting") interface.type = _type nic_update += 1 - if hasattr(interface, 'lag') and interface.lag is not None: + if hasattr(interface, "lag") and interface.lag is not None: local_lag_int = next( - item for item in self.nics if item['name'] == interface.lag.name + item for item in self.nics if item["name"] == interface.lag.name ) - if nic['name'] not in local_lag_int['bonding_slaves']: - logging.info('Interface has no LAG, resetting') + if nic["name"] not in local_lag_int["bonding_slaves"]: + logging.info("Interface has no LAG, resetting") nic_update += 1 interface.lag = None @@ -487,15 +540,15 @@ class Network(object): ) nic_update += ret - if nic['ip']: + if nic["ip"]: # sync local IPs - for ip in nic['ip']: + for ip in nic["ip"]: self.create_or_update_netbox_ip_on_interface(ip, interface) if nic_update > 0: interface.save() self._set_bonding_interfaces() - logging.debug('Finished updating NIC!') + logging.debug("Finished updating NIC!") class ServerNetwork(Network): @@ -510,38 +563,43 @@ class ServerNetwork(Network): self.server = server self.device = self.server.get_netbox_server() self.nb_net = nb.dcim - self.custom_arg = {'device': getattr(self.device, "id", None)} - self.custom_arg_id = {'device_id': getattr(self.device, "id", None)} + self.custom_arg = {"device": getattr(self.device, "id", None)} + self.custom_arg_id = {"device_id": getattr(self.device, "id", None)} self.intf_type = "interface_id" self.assigned_object_type = "dcim.interface" def get_network_type(self): - return 'server' + return "server" def get_ipmi(self): ipmi = IPMI().parse() return ipmi - def connect_interface_to_switch(self, switch_ip, switch_interface, nb_server_interface): - logging.info('Interface {} is not connected to switch, trying to connect..'.format( - nb_server_interface.name - )) + def connect_interface_to_switch( + self, switch_ip, switch_interface, nb_server_interface + ): + logging.info( + "Interface {} is not connected to switch, trying to connect..".format( + nb_server_interface.name + ) + ) nb_mgmt_ip = nb.ipam.ip_addresses.get( address=switch_ip, ) if not nb_mgmt_ip: - logging.error('Switch IP {} cannot be found in Netbox'.format(switch_ip)) + logging.error("Switch IP {} cannot be found in Netbox".format(switch_ip)) return nb_server_interface try: nb_switch = nb_mgmt_ip.assigned_object.device - logging.info('Found a switch in Netbox based on LLDP infos: {} (id: {})'.format( - switch_ip, - nb_switch.id - )) + logging.info( + "Found a switch in Netbox based on LLDP infos: {} (id: {})".format( + switch_ip, nb_switch.id + ) + ) except KeyError: logging.error( - 'Switch IP {} is found but not associated to a Netbox Switch Device'.format( + "Switch IP {} is found but not associated to a Netbox Switch Device".format( switch_ip ) ) @@ -553,13 +611,17 @@ class ServerNetwork(Network): name=switch_interface, ) if nb_switch_interface is None: - logging.error('Switch interface {} cannot be found'.format(switch_interface)) + logging.error( + "Switch interface {} cannot be found".format(switch_interface) + ) return nb_server_interface - logging.info('Found interface {} on switch {}'.format( - switch_interface, - switch_ip, - )) + logging.info( + "Found interface {} on switch {}".format( + switch_interface, + switch_ip, + ) + ) cable = nb.dcim.cables.create( a_terminations=[ {"object_type": "dcim.interface", "object_id": nb_server_interface.id}, @@ -570,7 +632,7 @@ class ServerNetwork(Network): ) nb_server_interface.cable = cable logging.info( - 'Connected interface {interface} with {switch_interface} of {switch_ip}'.format( + "Connected interface {interface} with {switch_interface} of {switch_ip}".format( interface=nb_server_interface.name, switch_interface=switch_interface, switch_ip=switch_ip, @@ -588,38 +650,30 @@ class ServerNetwork(Network): else: nb_sw_int = nb_server_interface.cable.b_terminations[0] nb_sw = nb_sw_int.device - nb_mgmt_int = nb.dcim.interfaces.get( - device_id=nb_sw.id, - mgmt_only=True - ) - nb_mgmt_ip = nb.ipam.ip_addresses.get( - interface_id=nb_mgmt_int.id - ) + nb_mgmt_int = nb.dcim.interfaces.get(device_id=nb_sw.id, mgmt_only=True) + nb_mgmt_ip = nb.ipam.ip_addresses.get(interface_id=nb_mgmt_int.id) if nb_mgmt_ip is None: logging.error( - 'Switch {switch_ip} does not have IP on its management interface'.format( + "Switch {switch_ip} does not have IP on its management interface".format( switch_ip=switch_ip, ) ) return update, nb_server_interface # Netbox IP is always IP/Netmask - nb_mgmt_ip = nb_mgmt_ip.address.split('/')[0] - if nb_mgmt_ip != switch_ip or \ - nb_sw_int.name != switch_interface: - logging.info('Netbox cable is not connected to correct ports, fixing..') + nb_mgmt_ip = nb_mgmt_ip.address.split("/")[0] + if nb_mgmt_ip != switch_ip or nb_sw_int.name != switch_interface: + logging.info("Netbox cable is not connected to correct ports, fixing..") logging.info( - 'Deleting cable {cable_id} from {interface} to {switch_interface} of ' - '{switch_ip}'.format( + "Deleting cable {cable_id} from {interface} to {switch_interface} of " + "{switch_ip}".format( cable_id=nb_server_interface.cable.id, interface=nb_server_interface.name, switch_interface=nb_sw_int.name, switch_ip=nb_mgmt_ip, ) ) - cable = nb.dcim.cables.get( - nb_server_interface.cable.id - ) + cable = nb.dcim.cables.get(nb_server_interface.cable.id) cable.delete() update = True nb_server_interface = self.connect_interface_to_switch( @@ -634,17 +688,17 @@ class VirtualNetwork(Network): self.server = server self.device = self.server.get_netbox_vm() self.nb_net = nb.virtualization - self.custom_arg = {'virtual_machine': getattr(self.device, "id", None)} - self.custom_arg_id = {'virtual_machine_id': getattr(self.device, "id", None)} + self.custom_arg = {"virtual_machine": getattr(self.device, "id", None)} + self.custom_arg_id = {"virtual_machine_id": getattr(self.device, "id", None)} self.intf_type = "vminterface_id" self.assigned_object_type = "virtualization.vminterface" dcim_c = nb.virtualization.interfaces.choices() for _choice_type in dcim_c: - key = 'interface:{}'.format(_choice_type) + key = "interface:{}".format(_choice_type) self.dcim_choices[key] = {} for choice in dcim_c[_choice_type]: - self.dcim_choices[key][choice['display_name']] = choice['value'] + self.dcim_choices[key][choice["display_name"]] = choice["value"] def get_network_type(self): - return 'virtual' + return "virtual" diff --git a/netbox_agent/power.py b/netbox_agent/power.py index 477c00e..e20bed9 100644 --- a/netbox_agent/power.py +++ b/netbox_agent/power.py @@ -6,49 +6,51 @@ from netbox_agent.config import netbox_instance as nb PSU_DMI_TYPE = 39 -class PowerSupply(): +class PowerSupply: def __init__(self, server=None): self.server = server self.netbox_server = self.server.get_netbox_server() if self.server.is_blade(): - self.device_id = self.netbox_server.parent_device.id if self.netbox_server else None + self.device_id = ( + self.netbox_server.parent_device.id if self.netbox_server else None + ) else: self.device_id = self.netbox_server.id if self.netbox_server else None def get_power_supply(self): power_supply = [] for psu in dmidecode.get_by_type(self.server.dmi, PSU_DMI_TYPE): - if 'Present' not in psu['Status'] or psu['Status'] == 'Not Present': + if "Present" not in psu["Status"] or psu["Status"] == "Not Present": continue try: - max_power = int(psu.get('Max Power Capacity').split()[0]) + max_power = int(psu.get("Max Power Capacity").split()[0]) except ValueError: max_power = None - desc = '{} - {}'.format( - psu.get('Manufacturer', 'No Manufacturer').strip(), - psu.get('Name', 'No name').strip(), + desc = "{} - {}".format( + psu.get("Manufacturer", "No Manufacturer").strip(), + psu.get("Name", "No name").strip(), ) - sn = psu.get('Serial Number', '').strip() + sn = psu.get("Serial Number", "").strip() # Let's assume that if no serial and no power reported we skip it - if sn == '' and max_power is None: + if sn == "" and max_power is None: continue - if sn == '': - sn = 'N/A' - power_supply.append({ - 'name': sn, - 'description': desc, - 'allocated_draw': None, - 'maximum_draw': max_power, - 'device': self.device_id, - }) + if sn == "": + sn = "N/A" + power_supply.append( + { + "name": sn, + "description": desc, + "allocated_draw": None, + "maximum_draw": max_power, + "device": self.device_id, + } + ) return power_supply def get_netbox_power_supply(self): - return nb.dcim.power_ports.filter( - device_id=self.device_id - ) + return nb.dcim.power_ports.filter(device_id=self.device_id) def create_or_update_power_supply(self): nb_psus = list(self.get_netbox_power_supply()) @@ -57,10 +59,10 @@ class PowerSupply(): # Delete unknown PSU delete = False for nb_psu in nb_psus: - if nb_psu.name not in [x['name'] for x in psus]: - logging.info('Deleting unknown locally PSU {name}'.format( - name=nb_psu.name - )) + if nb_psu.name not in [x["name"] for x in psus]: + logging.info( + "Deleting unknown locally PSU {name}".format(name=nb_psu.name) + ) nb_psu.delete() delete = True @@ -69,27 +71,23 @@ class PowerSupply(): # sync existing Netbox PSU with local infos for nb_psu in nb_psus: - local_psu = next( - item for item in psus if item['name'] == nb_psu.name - ) + local_psu = next(item for item in psus if item["name"] == nb_psu.name) update = False - if nb_psu.description != local_psu['description']: + if nb_psu.description != local_psu["description"]: update = True - nb_psu.description = local_psu['description'] - if nb_psu.maximum_draw != local_psu['maximum_draw']: + nb_psu.description = local_psu["description"] + if nb_psu.maximum_draw != local_psu["maximum_draw"]: update = True - nb_psu.maximum_draw = local_psu['maximum_draw'] + nb_psu.maximum_draw = local_psu["maximum_draw"] if update: nb_psu.save() for psu in psus: - if psu['name'] not in [x.name for x in nb_psus]: - logging.info('Creating PSU {name} ({description}), {maximum_draw}W'.format( - **psu - )) - nb_psu = nb.dcim.power_ports.create( - **psu + if psu["name"] not in [x.name for x in nb_psus]: + logging.info( + "Creating PSU {name} ({description}), {maximum_draw}W".format(**psu) ) + nb_psu = nb.dcim.power_ports.create(**psu) return True @@ -97,7 +95,7 @@ class PowerSupply(): try: psu_cons = self.server.get_power_consumption() except NotImplementedError: - logging.error('Cannot report power consumption for this vendor') + logging.error("Cannot report power consumption for this vendor") return False nb_psus = self.get_netbox_power_supply() @@ -107,25 +105,25 @@ class PowerSupply(): # find power feeds for rack or dc pwr_feeds = None if self.netbox_server.rack: - pwr_feeds = nb.dcim.power_feeds.filter( - rack=self.netbox_server.rack.id - ) + pwr_feeds = nb.dcim.power_feeds.filter(rack=self.netbox_server.rack.id) if pwr_feeds: - voltage = [p['voltage'] for p in pwr_feeds] + voltage = [p["voltage"] for p in pwr_feeds] else: - logging.info('Could not find power feeds for Rack, defaulting value to 230') + logging.info("Could not find power feeds for Rack, defaulting value to 230") voltage = [230 for _ in nb_psus] for i, nb_psu in enumerate(nb_psus): nb_psu.allocated_draw = int(float(psu_cons[i]) * voltage[i]) if nb_psu.allocated_draw < 1: - logging.info('PSU is not connected or in standby mode') + logging.info("PSU is not connected or in standby mode") continue nb_psu.save() - logging.info('Updated power consumption for PSU {}: {}W'.format( - nb_psu.name, - nb_psu.allocated_draw, - )) + logging.info( + "Updated power consumption for PSU {}: {}W".format( + nb_psu.name, + nb_psu.allocated_draw, + ) + ) return True diff --git a/netbox_agent/raid/base.py b/netbox_agent/raid/base.py index 97b8274..40834d1 100644 --- a/netbox_agent/raid/base.py +++ b/netbox_agent/raid/base.py @@ -1,4 +1,4 @@ -class RaidController(): +class RaidController: def get_product_name(self): raise NotImplementedError @@ -19,6 +19,6 @@ class RaidController(): return False -class Raid(): +class Raid: def get_controllers(self): raise NotImplementedError diff --git a/netbox_agent/raid/hp.py b/netbox_agent/raid/hp.py index aff1f2f..612d970 100644 --- a/netbox_agent/raid/hp.py +++ b/netbox_agent/raid/hp.py @@ -1,45 +1,50 @@ -from netbox_agent.raid.base import Raid, RaidController -from netbox_agent.misc import get_vendor -from netbox_agent.config import config -import subprocess import logging import re +import subprocess + +from netbox_agent.misc import get_vendor +from netbox_agent.raid.base import Raid, RaidController + +REGEXP_CONTROLLER_HP = re.compile(r"Smart Array ([a-zA-Z0-9- ]+) in Slot ([0-9]+)") -REGEXP_CONTROLLER_HP = re.compile(r'Smart Array ([a-zA-Z0-9- ]+) in Slot ([0-9]+)') class HPRaidControllerError(Exception): pass + def ssacli(sub_command): command = ["ssacli"] command.extend(sub_command.split()) - p = subprocess.Popen( - command, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT - ) + p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) stdout, stderr = p.communicate() stdout = stdout.decode("utf-8") if p.returncode != 0: - mesg = "Failed to execute command '{}':\n{}".format( - " ".join(command), stdout - ) + mesg = "Failed to execute command '{}':\n{}".format(" ".join(command), stdout) raise HPRaidControllerError(mesg) - if 'does not have any physical' in stdout: + if "does not have any physical" in stdout: return list() else: - lines = stdout.split('\n') + lines = stdout.split("\n") lines = list(filter(None, lines)) return lines + def _test_if_valid_line(line): - ignore_patterns = ['Note:', 'Error:', 'is not loaded', 'README', ' failure', ' cache'] + ignore_patterns = [ + "Note:", + "Error:", + "is not loaded", + "README", + " failure", + " cache", + ] for pattern in ignore_patterns: if not line or pattern in line: return None return line + def _parse_ctrl_output(lines): controllers = {} current_ctrl = None @@ -53,14 +58,14 @@ def _parse_ctrl_output(lines): if ctrl is not None: slot = ctrl.group(2) current_ctrl = "{} - Slot {}".format(ctrl.group(1), slot) - controllers[current_ctrl] = {'Slot': slot} - if 'Embedded' not in line: - controllers[current_ctrl]['External'] = True + controllers[current_ctrl] = {"Slot": slot} + if "Embedded" not in line: + controllers[current_ctrl]["External"] = True continue - if ': ' not in line: + if ": " not in line: continue - attr, val = line.split(': ', 1) + attr, val = line.split(": ", 1) attr = attr.strip() val = val.strip() controllers[current_ctrl][attr] = val @@ -78,25 +83,24 @@ def _parse_pd_output(lines): if line is None: continue # Parses the Array the drives are in - if line.startswith('Array'): + if line.startswith("Array"): current_array = line.split(None, 1)[1] # Detects new physical drive - if line.startswith('physicaldrive'): + if line.startswith("physicaldrive"): current_drv = line.split(None, 1)[1] drives[current_drv] = {} if current_array is not None: - drives[current_drv]['Array'] = current_array + drives[current_drv]["Array"] = current_array continue - if ': ' not in line: + if ": " not in line: continue - attr, val = line.split(': ', 1) + attr, val = line.split(": ", 1) attr = attr.strip() val = val.strip() drives.setdefault(current_drv, {})[attr] = val return drives - def _parse_ld_output(lines): drives = {} current_array = None @@ -108,17 +112,17 @@ def _parse_ld_output(lines): if line is None: continue # Parses the Array the drives are in - if line.startswith('Array'): + if line.startswith("Array"): current_array = line.split(None, 1)[1] drives[current_array] = {} # Detects new physical drive - if line.startswith('Logical Drive'): - current_drv = line.split(': ', 1)[1] - drives.setdefault(current_array, {})['LogicalDrive'] = current_drv + if line.startswith("Logical Drive"): + current_drv = line.split(": ", 1)[1] + drives.setdefault(current_array, {})["LogicalDrive"] = current_drv continue - if ': ' not in line: + if ": " not in line: continue - attr, val = line.split(': ', 1) + attr, val = line.split(": ", 1) drives.setdefault(current_array, {})[attr] = val return drives @@ -128,7 +132,7 @@ class HPRaidController(RaidController): self.controller_name = controller_name self.data = data self.pdrives = self._get_physical_disks() - arrays = [d['Array'] for d in self.pdrives.values() if d.get('Array')] + arrays = [d["Array"] for d in self.pdrives.values() if d.get("Array")] if arrays: self.ldrives = self._get_logical_drives() self._get_virtual_drives_map() @@ -137,64 +141,69 @@ class HPRaidController(RaidController): return self.controller_name def get_manufacturer(self): - return 'HP' + return "HP" def get_serial_number(self): - return self.data['Serial Number'] + return self.data["Serial Number"] def get_firmware_version(self): - return self.data['Firmware Version'] + return self.data["Firmware Version"] def is_external(self): - return self.data.get('External', False) + return self.data.get("External", False) def _get_physical_disks(self): - lines = ssacli('ctrl slot={} pd all show detail'.format(self.data['Slot'])) + lines = ssacli("ctrl slot={} pd all show detail".format(self.data["Slot"])) pdrives = _parse_pd_output(lines) ret = {} for name, attrs in pdrives.items(): - array = attrs.get('Array', '') - model = attrs.get('Model', '').strip() + array = attrs.get("Array", "") + model = attrs.get("Model", "").strip() vendor = None - if model.startswith('HP'): - vendor = 'HP' + if model.startswith("HP"): + vendor = "HP" elif len(model.split()) > 1: vendor = get_vendor(model.split()[1]) else: vendor = get_vendor(model) ret[name] = { - 'Array': array, - 'Model': model, - 'Vendor': vendor, - 'SN': attrs.get('Serial Number', '').strip(), - 'Size': attrs.get('Size', '').strip(), - 'Type': 'SSD' if attrs.get('Interface Type') == 'Solid State SATA' - else 'HDD', - '_src': self.__class__.__name__, - 'custom_fields': { - 'pd_identifier': name, - 'mount_point': attrs.get('Mount Points', '').strip(), - 'vd_device': attrs.get('Disk Name', '').strip(), - 'vd_size': attrs.get('Size', '').strip(), - } + "Array": array, + "Model": model, + "Vendor": vendor, + "SN": attrs.get("Serial Number", "").strip(), + "Size": attrs.get("Size", "").strip(), + "Type": ( + "SSD" + if attrs.get("Interface Type") == "Solid State SATA" + else "HDD" + ), + "_src": self.__class__.__name__, + "custom_fields": { + "pd_identifier": name, + "mount_point": attrs.get("Mount Points", "").strip(), + "vd_device": attrs.get("Disk Name", "").strip(), + "vd_size": attrs.get("Size", "").strip(), + }, } return ret def _get_logical_drives(self): - lines = ssacli('ctrl slot={} ld all show detail'.format(self.data['Slot'])) + lines = ssacli("ctrl slot={} ld all show detail".format(self.data["Slot"])) ldrives = _parse_ld_output(lines) ret = {} for array, attrs in ldrives.items(): ret[array] = { - 'vd_array': array, - 'vd_size': attrs.get('Size', '').strip(), - 'vd_consistency': attrs.get('Status', '').strip(), - 'vd_raid_type': 'RAID {}'.format(attrs.get('Fault Tolerance', 'N/A').strip()), - 'vd_device': attrs.get('LogicalDrive', '').strip(), - 'mount_point': attrs.get('Mount Points', '').strip() + "vd_array": array, + "vd_size": attrs.get("Size", "").strip(), + "vd_consistency": attrs.get("Status", "").strip(), + "vd_raid_type": "RAID {}".format( + attrs.get("Fault Tolerance", "N/A").strip() + ), + "vd_device": attrs.get("LogicalDrive", "").strip(), + "mount_point": attrs.get("Mount Points", "").strip(), } return ret @@ -208,7 +217,7 @@ class HPRaidController(RaidController): " Ignoring.".format(name) ) continue - attrs['custom_fields'].update(ld) + attrs["custom_fields"].update(ld) def get_physical_disks(self): return list(self.pdrives.values()) @@ -216,18 +225,16 @@ class HPRaidController(RaidController): class HPRaid(Raid): def __init__(self): - self.output = subprocess.getoutput('ssacli ctrl all show detail') + self.output = subprocess.getoutput("ssacli ctrl all show detail") self.controllers = [] self.convert_to_dict() def convert_to_dict(self): - lines = self.output.split('\n') + lines = self.output.split("\n") lines = list(filter(None, lines)) controllers = _parse_ctrl_output(lines) for controller, attrs in controllers.items(): - self.controllers.append( - HPRaidController(controller, attrs) - ) + self.controllers.append(HPRaidController(controller, attrs)) def get_controllers(self): return self.controllers diff --git a/netbox_agent/raid/omreport.py b/netbox_agent/raid/omreport.py index 811761b..d3be39f 100644 --- a/netbox_agent/raid/omreport.py +++ b/netbox_agent/raid/omreport.py @@ -1,9 +1,9 @@ -from netbox_agent.raid.base import Raid, RaidController -from netbox_agent.misc import get_vendor, get_mount_points -from netbox_agent.config import config -import subprocess import logging import re +import subprocess + +from netbox_agent.misc import get_mount_points, get_vendor +from netbox_agent.raid.base import Raid, RaidController class OmreportControllerError(Exception): @@ -13,30 +13,24 @@ class OmreportControllerError(Exception): def omreport(sub_command): command = ["omreport"] command.extend(sub_command.split()) - p = subprocess.Popen( - command, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT - ) + p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) p.wait() stdout = p.stdout.read().decode("utf-8") if p.returncode != 0: - mesg = "Failed to execute command '{}':\n{}".format( - " ".join(command), stdout - ) + mesg = "Failed to execute command '{}':\n{}".format(" ".join(command), stdout) raise OmreportControllerError(mesg) res = {} - section_re = re.compile('^[A-Z]') + section_re = re.compile("^[A-Z]") current_section = None current_obj = None - for line in stdout.split('\n'): - if ': ' in line: - attr, value = line.split(': ', 1) + for line in stdout.split("\n"): + if ": " in line: + attr, value = line.split(": ", 1) attr = attr.strip() value = value.strip() - if attr == 'ID': + if attr == "ID": obj = {} res.setdefault(current_section, []).append(obj) current_obj = obj @@ -52,60 +46,57 @@ class OmreportController(RaidController): self.controller_index = controller_index def get_product_name(self): - return self.data['Name'] + return self.data["Name"] def get_manufacturer(self): return None def get_serial_number(self): - return self.data.get('DeviceSerialNumber') + return self.data.get("DeviceSerialNumber") def get_firmware_version(self): - return self.data.get('Firmware Version') + return self.data.get("Firmware Version") def _get_physical_disks(self): pds = {} - res = omreport('storage pdisk controller={}'.format( - self.controller_index - )) + res = omreport("storage pdisk controller={}".format(self.controller_index)) for pdisk in [d for d in list(res.values())[0]]: - disk_id = pdisk['ID'] - size = re.sub('B .*$', 'B', pdisk['Capacity']) + disk_id = pdisk["ID"] + size = re.sub("B .*$", "B", pdisk["Capacity"]) pds[disk_id] = { - 'Vendor': get_vendor(pdisk['Vendor ID']), - 'Model': pdisk['Product ID'], - 'SN': pdisk['Serial No.'], - 'Size': size, - 'Type': pdisk['Media'], - '_src': self.__class__.__name__, + "Vendor": get_vendor(pdisk["Vendor ID"]), + "Model": pdisk["Product ID"], + "SN": pdisk["Serial No."], + "Size": size, + "Type": pdisk["Media"], + "_src": self.__class__.__name__, } return pds def _get_virtual_drives_map(self): pds = {} - res = omreport('storage vdisk controller={}'.format( - self.controller_index - )) + res = omreport("storage vdisk controller={}".format(self.controller_index)) for vdisk in [d for d in list(res.values())[0]]: - vdisk_id = vdisk['ID'] - device = vdisk['Device Name'] + vdisk_id = vdisk["ID"] + device = vdisk["Device Name"] mount_points = get_mount_points() - mp = mount_points.get(device, 'n/a') - size = re.sub('B .*$', 'B', vdisk['Size']) + mp = mount_points.get(device, "n/a") + size = re.sub("B .*$", "B", vdisk["Size"]) vd = { - 'vd_array': vdisk_id, - 'vd_size': size, - 'vd_consistency': vdisk['State'], - 'vd_raid_type': vdisk['Layout'], - 'vd_device': vdisk['Device Name'], - 'mount_point': ', '.join(sorted(mp)), + "vd_array": vdisk_id, + "vd_size": size, + "vd_consistency": vdisk["State"], + "vd_raid_type": vdisk["Layout"], + "vd_device": vdisk["Device Name"], + "mount_point": ", ".join(sorted(mp)), } drives_res = omreport( - 'storage pdisk controller={} vdisk={}'.format( + "storage pdisk controller={} vdisk={}".format( self.controller_index, vdisk_id - )) + ) + ) for pdisk in [d for d in list(drives_res.values())[0]]: - pds[pdisk['ID']] = vd + pds[pdisk["ID"]] = vd return pds def get_physical_disks(self): @@ -114,27 +105,23 @@ class OmreportController(RaidController): for pd_identifier, vd in vds.items(): if pd_identifier not in pds: logging.error( - 'Physical drive {} listed in virtual drive {} not ' - 'found in drives list'.format( - pd_identifier, vd['vd_array'] - ) + "Physical drive {} listed in virtual drive {} not " + "found in drives list".format(pd_identifier, vd["vd_array"]) ) continue - pds[pd_identifier].setdefault('custom_fields', {}).update(vd) - pds[pd_identifier]['custom_fields']['pd_identifier'] = pd_identifier + pds[pd_identifier].setdefault("custom_fields", {}).update(vd) + pds[pd_identifier]["custom_fields"]["pd_identifier"] = pd_identifier return list(pds.values()) class OmreportRaid(Raid): def __init__(self): self.controllers = [] - res = omreport('storage controller') + res = omreport("storage controller") - for controller in res['Controller']: - ctrl_index = controller['ID'] - self.controllers.append( - OmreportController(ctrl_index, controller) - ) + for controller in res["Controller"]: + ctrl_index = controller["ID"] + self.controllers.append(OmreportController(ctrl_index, controller)) def get_controllers(self): return self.controllers diff --git a/netbox_agent/raid/storcli.py b/netbox_agent/raid/storcli.py index 8eacae6..3cf42f2 100644 --- a/netbox_agent/raid/storcli.py +++ b/netbox_agent/raid/storcli.py @@ -1,11 +1,12 @@ -from netbox_agent.raid.base import Raid, RaidController -from netbox_agent.misc import get_vendor, get_mount_points -from netbox_agent.config import config -import subprocess -import logging import json -import re +import logging import os +import re +import subprocess + +from netbox_agent.config import config +from netbox_agent.misc import get_mount_points, get_vendor +from netbox_agent.raid.base import Raid, RaidController class StorcliControllerError(Exception): @@ -16,29 +17,23 @@ def storecli(sub_command): command = ["storcli"] command.extend(sub_command.split()) command.append("J") - p = subprocess.Popen( - command, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT - ) + p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) stdout, stderr = p.communicate() if stderr: - mesg = "Failed to execute command '{}':\n{}".format( - " ".join(command), stdout - ) + mesg = "Failed to execute command '{}':\n{}".format(" ".join(command), stdout) raise StorcliControllerError(mesg) stdout = stdout.decode("utf-8") data = json.loads(stdout) - controllers = dict([ - ( - c['Command Status']['Controller'], - c['Response Data'] - ) for c in data['Controllers'] - if c['Command Status']['Status'] == 'Success' - ]) + controllers = dict( + [ + (c["Command Status"]["Controller"], c["Response Data"]) + for c in data["Controllers"] + if c["Command Status"]["Status"] == "Success" + ] + ) if not controllers: logging.error( "Failed to execute command '{}'. " @@ -54,23 +49,23 @@ class StorcliController(RaidController): self.controller_index = controller_index def get_product_name(self): - return self.data['Product Name'] + return self.data["Product Name"] def get_manufacturer(self): return None def get_serial_number(self): - return self.data['Serial Number'] + return self.data["Serial Number"] def get_firmware_version(self): - return self.data['FW Package Build'] + return self.data["FW Package Build"] def _get_physical_disks(self): pds = {} - cmd = '/c{}/eall/sall show all'.format(self.controller_index) + cmd = "/c{}/eall/sall show all".format(self.controller_index) controllers = storecli(cmd) pd_info = controllers[self.controller_index] - pd_re = re.compile(r'^Drive (/c\d+/e\d+/s\d+)$') + pd_re = re.compile(r"^Drive (/c\d+/e\d+/s\d+)$") for section, attrs in pd_info.items(): reg = pd_re.search(section) @@ -78,28 +73,28 @@ class StorcliController(RaidController): continue pd_name = reg.group(1) pd_attr = attrs[0] - pd_identifier = pd_attr['EID:Slt'] - size = pd_attr.get('Size', '').strip() - media_type = pd_attr.get('Med', '').strip() - pd_details = pd_info['{} - Detailed Information'.format(section)] - pd_dev_attr = pd_details['{} Device attributes'.format(section)] - model = pd_dev_attr.get('Model Number', '').strip() + pd_identifier = pd_attr["EID:Slt"] + size = pd_attr.get("Size", "").strip() + media_type = pd_attr.get("Med", "").strip() + pd_details = pd_info["{} - Detailed Information".format(section)] + pd_dev_attr = pd_details["{} Device attributes".format(section)] + model = pd_dev_attr.get("Model Number", "").strip() pd = { - 'Model': model, - 'Vendor': get_vendor(model), - 'SN': pd_dev_attr.get('SN', '').strip(), - 'Size': size, - 'Type': media_type, - '_src': self.__class__.__name__, + "Model": model, + "Vendor": get_vendor(model), + "SN": pd_dev_attr.get("SN", "").strip(), + "Size": size, + "Type": media_type, + "_src": self.__class__.__name__, } if config.process_virtual_drives: - pd.setdefault('custom_fields', {})['pd_identifier'] = pd_name + pd.setdefault("custom_fields", {})["pd_identifier"] = pd_name pds[pd_identifier] = pd return pds def _get_virtual_drives_map(self): vds = {} - cmd = '/c{}/vall show all'.format(self.controller_index) + cmd = "/c{}/vall show all".format(self.controller_index) controllers = storecli(cmd) vd_info = controllers[self.controller_index] mount_points = get_mount_points() @@ -109,9 +104,9 @@ class StorcliController(RaidController): continue volume = vd_identifier.split("/")[-1].lstrip("v") vd_attr = vd_attrs[0] - vd_pd_identifier = 'PDs for VD {}'.format(volume) + vd_pd_identifier = "PDs for VD {}".format(volume) vd_pds = vd_info[vd_pd_identifier] - vd_prop_identifier = 'VD{} Properties'.format(volume) + vd_prop_identifier = "VD{} Properties".format(volume) vd_properties = vd_info[vd_prop_identifier] for pd in vd_pds: pd_identifier = pd["EID:Slt"] @@ -125,7 +120,7 @@ class StorcliController(RaidController): "vd_consistency": vd_attr["Consist"], "vd_raid_type": vd_attr["TYPE"], "vd_device": device, - "mount_point": ", ".join(sorted(mp)) + "mount_point": ", ".join(sorted(mp)), } return vds @@ -139,9 +134,7 @@ class StorcliController(RaidController): if pd_identifier not in pds: logging.error( "Physical drive {} listed in virtual drive {} not " - "found in drives list".format( - pd_identifier, vd["vd_array"] - ) + "found in drives list".format(pd_identifier, vd["vd_array"]) ) continue pds[pd_identifier].setdefault("custom_fields", {}).update(vd) @@ -152,14 +145,9 @@ class StorcliController(RaidController): class StorcliRaid(Raid): def __init__(self): self.controllers = [] - controllers = storecli('/call show') + controllers = storecli("/call show") for controller_id, controller_data in controllers.items(): - self.controllers.append( - StorcliController( - controller_id, - controller_data - ) - ) + self.controllers.append(StorcliController(controller_id, controller_data)) def get_controllers(self): return self.controllers diff --git a/netbox_agent/server.py b/netbox_agent/server.py index 5077b7f..8775de6 100644 --- a/netbox_agent/server.py +++ b/netbox_agent/server.py @@ -1,46 +1,57 @@ +import logging +import socket +import subprocess +import sys +from pprint import pprint + import netbox_agent.dmidecode as dmidecode from netbox_agent.config import config from netbox_agent.config import netbox_instance as nb from netbox_agent.inventory import Inventory from netbox_agent.location import Datacenter, Rack, Tenant -from netbox_agent.misc import create_netbox_tags, get_device_role, get_device_type, get_device_platform +from netbox_agent.misc import ( + create_netbox_tags, + get_device_platform, + get_device_role, + get_device_type, +) from netbox_agent.network import ServerNetwork from netbox_agent.power import PowerSupply -from pprint import pprint -import subprocess -import logging -import socket -import sys -class ServerBase(): +class ServerBase: def __init__(self, dmi=None): if dmi: self.dmi = dmi else: self.dmi = dmidecode.parse() - self.baseboard = dmidecode.get_by_type(self.dmi, 'Baseboard') - self.bios = dmidecode.get_by_type(self.dmi, 'BIOS') - self.chassis = dmidecode.get_by_type(self.dmi, 'Chassis') - self.system = dmidecode.get_by_type(self.dmi, 'System') + self.baseboard = dmidecode.get_by_type(self.dmi, "Baseboard") + self.bios = dmidecode.get_by_type(self.dmi, "BIOS") + self.chassis = dmidecode.get_by_type(self.dmi, "Chassis") + self.system = dmidecode.get_by_type(self.dmi, "System") self.device_platform = get_device_platform(config.device.platform) self.network = None - self.tags = list(set([ - x.strip() for x in config.device.tags.split(',') if x.strip() - ])) if config.device.tags else [] + self.tags = ( + list(set([x.strip() for x in config.device.tags.split(",") if x.strip()])) + if config.device.tags + else [] + ) self.nb_tags = list(create_netbox_tags(self.tags)) - config_cf = set([ - f.strip() for f in config.device.custom_fields.split(",") - if f.strip() - ]) + config_cf = set( + [f.strip() for f in config.device.custom_fields.split(",") if f.strip()] + ) self.custom_fields = {} - self.custom_fields.update(dict([ - (k.strip(), v.strip()) for k, v in - [f.split("=", 1) for f in config_cf] - ])) + self.custom_fields.update( + dict( + [ + (k.strip(), v.strip()) + for k, v in [f.split("=", 1) for f in config_cf] + ] + ) + ) def get_tenant(self): tenant = Tenant() @@ -50,9 +61,7 @@ class ServerBase(): tenant = self.get_tenant() if tenant is None: return None - nb_tenant = nb.tenancy.tenants.get( - slug=self.get_tenant() - ) + nb_tenant = nb.tenancy.tenants.get(slug=self.get_tenant()) return nb_tenant def get_datacenter(self): @@ -81,22 +90,22 @@ class ServerBase(): update = False if dc and server.site and server.site.slug != nb_dc.slug: - logging.info('Datacenter location has changed from {} to {}, updating'.format( - server.site.slug, - nb_dc.slug, - )) + logging.info( + "Datacenter location has changed from {} to {}, updating".format( + server.site.slug, + nb_dc.slug, + ) + ) update = True server.site = nb_dc.id - if ( - server.rack - and nb_rack - and server.rack.id != nb_rack.id - ): - logging.info('Rack location has changed from {} to {}, updating'.format( - server.rack, - nb_rack, - )) + if server.rack and nb_rack and server.rack.id != nb_rack.id: + logging.info( + "Rack location has changed from {} to {}, updating".format( + server.rack, + nb_rack, + ) + ) update = True server.rack = nb_rack if nb_rack is None: @@ -139,24 +148,24 @@ class ServerBase(): """ Return the Chassis Name from dmidecode info """ - return self.system[0]['Product Name'].strip() + return self.system[0]["Product Name"].strip() def get_service_tag(self): """ Return the Service Tag from dmidecode info """ - return self.system[0]['Serial Number'].strip() + return self.system[0]["Serial Number"].strip() def get_expansion_service_tag(self): """ Return the virtual Service Tag from dmidecode info host with 'expansion' """ - return self.system[0]['Serial Number'].strip() + " expansion" + return self.system[0]["Serial Number"].strip() + " expansion" def get_hostname(self): if config.hostname_cmd is None: - return '{}'.format(socket.gethostname()) + return "{}".format(socket.gethostname()) return subprocess.getoutput(config.hostname_cmd) def is_blade(self): @@ -193,8 +202,7 @@ class ServerBase(): device_type = get_device_type(self.get_chassis()) device_role = get_device_role(config.device.chassis_role) serial = self.get_chassis_service_tag() - logging.info('Creating chassis blade (serial: {serial})'.format( - serial=serial)) + logging.info("Creating chassis blade (serial: {serial})".format(serial=serial)) new_chassis = nb.dcim.devices.create( name=self.get_chassis_name(), device_type=device_type.id, @@ -203,7 +211,7 @@ class ServerBase(): site=datacenter.id if datacenter else None, tenant=tenant.id if tenant else None, rack=rack.id if rack else None, - tags=[{'name': x} for x in self.tags], + tags=[{"name": x} for x in self.tags], custom_fields=self.custom_fields, ) return new_chassis @@ -214,9 +222,10 @@ class ServerBase(): serial = self.get_service_tag() hostname = self.get_hostname() logging.info( - 'Creating blade (serial: {serial}) {hostname} on chassis {chassis_serial}'.format( + "Creating blade (serial: {serial}) {hostname} on chassis {chassis_serial}".format( serial=serial, hostname=hostname, chassis_serial=chassis.serial - )) + ) + ) new_blade = nb.dcim.devices.create( name=hostname, serial=serial, @@ -226,7 +235,7 @@ class ServerBase(): site=datacenter.id if datacenter else None, tenant=tenant.id if tenant else None, rack=rack.id if rack else None, - tags=[{'name': x} for x in self.tags], + tags=[{"name": x} for x in self.tags], custom_fields=self.custom_fields, ) return new_blade @@ -237,9 +246,10 @@ class ServerBase(): serial = self.get_expansion_service_tag() hostname = self.get_hostname() + " expansion" logging.info( - 'Creating expansion (serial: {serial}) {hostname} on chassis {chassis_serial}'.format( + "Creating expansion (serial: {serial}) {hostname} on chassis {chassis_serial}".format( serial=serial, hostname=hostname, chassis_serial=chassis.serial - )) + ) + ) new_blade = nb.dcim.devices.create( name=hostname, serial=serial, @@ -249,7 +259,7 @@ class ServerBase(): site=datacenter.id if datacenter else None, tenant=tenant.id if tenant else None, rack=rack.id if rack else None, - tags=[{'name': x} for x in self.tags], + tags=[{"name": x} for x in self.tags], ) return new_blade @@ -267,8 +277,11 @@ class ServerBase(): raise Exception('Chassis "{}" doesn\'t exist'.format(self.get_chassis())) serial = self.get_service_tag() hostname = self.get_hostname() - logging.info('Creating server (serial: {serial}) {hostname}'.format( - serial=serial, hostname=hostname)) + logging.info( + "Creating server (serial: {serial}) {hostname}".format( + serial=serial, hostname=hostname + ) + ) new_server = nb.dcim.devices.create( name=hostname, serial=serial, @@ -278,7 +291,7 @@ class ServerBase(): site=datacenter.id if datacenter else None, tenant=tenant.id if tenant else None, rack=rack.id if rack else None, - tags=[{'name': x} for x in self.tags], + tags=[{"name": x} for x in self.tags], ) return new_server @@ -290,14 +303,16 @@ class ServerBase(): def _netbox_set_or_update_blade_slot(self, server, chassis, datacenter): # before everything check if right chassis - actual_device_bay = server.parent_device.device_bay \ - if server.parent_device else None - actual_chassis = actual_device_bay.device \ - if actual_device_bay else None + actual_device_bay = ( + server.parent_device.device_bay if server.parent_device else None + ) + actual_chassis = actual_device_bay.device if actual_device_bay else None slot = self.get_blade_slot() - if actual_chassis and \ - actual_chassis.serial == chassis.serial and \ - actual_device_bay.name == slot: + if ( + actual_chassis + and actual_chassis.serial == chassis.serial + and actual_device_bay.name == slot + ): return real_device_bays = nb.dcim.device_bays.filter( @@ -310,10 +325,11 @@ class ServerBase(): ) if real_device_bays: logging.info( - 'Setting device ({serial}) new slot on {slot} ' - '(Chassis {chassis_serial})..'.format( + "Setting device ({serial}) new slot on {slot} " + "(Chassis {chassis_serial})..".format( serial=server.serial, slot=slot, chassis_serial=chassis.serial - )) + ) + ) # reset actual device bay if set if actual_device_bay: # Forces the evaluation of the installed_device attribute to @@ -327,18 +343,22 @@ class ServerBase(): real_device_bay.installed_device = server real_device_bay.save() else: - logging.error('Could not find slot {slot} for chassis'.format( - slot=slot - )) + logging.error("Could not find slot {slot} for chassis".format(slot=slot)) - def _netbox_set_or_update_blade_expansion_slot(self, expansion, chassis, datacenter): + def _netbox_set_or_update_blade_expansion_slot( + self, expansion, chassis, datacenter + ): # before everything check if right chassis - actual_device_bay = expansion.parent_device.device_bay if expansion.parent_device else None + actual_device_bay = ( + expansion.parent_device.device_bay if expansion.parent_device else None + ) actual_chassis = actual_device_bay.device if actual_device_bay else None slot = self.get_blade_expansion_slot() - if actual_chassis and \ - actual_chassis.serial == chassis.serial and \ - actual_device_bay.name == slot: + if ( + actual_chassis + and actual_chassis.serial == chassis.serial + and actual_device_bay.name == slot + ): return real_device_bays = nb.dcim.device_bays.filter( @@ -346,15 +366,16 @@ class ServerBase(): name=slot, ) if not real_device_bays: - logging.error('Could not find slot {slot} expansion for chassis'.format( - slot=slot - )) + logging.error( + "Could not find slot {slot} expansion for chassis".format(slot=slot) + ) return logging.info( - 'Setting device expansion ({serial}) new slot on {slot} ' - '(Chassis {chassis_serial})..'.format( + "Setting device expansion ({serial}) new slot on {slot} " + "(Chassis {chassis_serial})..".format( serial=expansion.serial, slot=slot, chassis_serial=chassis.serial - )) + ) + ) # reset actual device bay if set if actual_device_bay: # Forces the evaluation of the installed_device attribute to @@ -388,9 +409,7 @@ class ServerBase(): self._netbox_deduplicate_server() if self.is_blade(): - chassis = nb.dcim.devices.get( - serial=self.get_chassis_service_tag() - ) + chassis = nb.dcim.devices.get(serial=self.get_chassis_service_tag()) # Chassis does not exist if not chassis: chassis = self._netbox_create_chassis(datacenter, tenant, rack) @@ -406,13 +425,14 @@ class ServerBase(): if not server: server = self._netbox_create_server(datacenter, tenant, rack) - logging.debug('Updating Server...') + logging.debug("Updating Server...") # check network cards if config.register or config.update_all or config.update_network: self.network = ServerNetwork(server=self) self.network.create_or_update_netbox_network_cards() - update_inventory = config.inventory and (config.register or - config.update_all or config.update_inventory) + update_inventory = config.inventory and ( + config.register or config.update_all or config.update_inventory + ) # update inventory if feature is enabled self.inventory = Inventory(server=self) if update_inventory: @@ -425,12 +445,16 @@ class ServerBase(): expansion = nb.dcim.devices.get(serial=self.get_expansion_service_tag()) if self.own_expansion_slot() and config.expansion_as_device: - logging.debug('Update Server expansion...') + logging.debug("Update Server expansion...") if not expansion: - expansion = self._netbox_create_blade_expansion(chassis, datacenter, tenant, rack) + expansion = self._netbox_create_blade_expansion( + chassis, datacenter, tenant, rack + ) # set slot for blade expansion - self._netbox_set_or_update_blade_expansion_slot(expansion, chassis, datacenter) + self._netbox_set_or_update_blade_expansion_slot( + expansion, chassis, datacenter + ) if update_inventory: # Updates expansion inventory inventory = Inventory(server=self, update_expansion=True) @@ -474,7 +498,7 @@ class ServerBase(): if expansion: update = 0 - expansion_name = server.name + ' expansion' + expansion_name = server.name + " expansion" if expansion.name != expansion_name: expansion.name = expansion_name update += 1 @@ -482,22 +506,24 @@ class ServerBase(): update += 1 if update: expansion.save() - logging.debug('Finished updating Server!') + logging.debug("Finished updating Server!") def print_debug(self): self.network = ServerNetwork(server=self) - print('Datacenter:', self.get_datacenter()) - print('Netbox Datacenter:', self.get_netbox_datacenter()) - print('Rack:', self.get_rack()) - print('Netbox Rack:', self.get_netbox_rack()) - print('Is blade:', self.is_blade()) - print('Got expansion:', self.own_expansion_slot()) - print('Product Name:', self.get_product_name()) - print('Platform:', self.device_platform) - print('Chassis:', self.get_chassis()) - print('Chassis service tag:', self.get_chassis_service_tag()) - print('Service tag:', self.get_service_tag()) - print('NIC:',) + print("Datacenter:", self.get_datacenter()) + print("Netbox Datacenter:", self.get_netbox_datacenter()) + print("Rack:", self.get_rack()) + print("Netbox Rack:", self.get_netbox_rack()) + print("Is blade:", self.is_blade()) + print("Got expansion:", self.own_expansion_slot()) + print("Product Name:", self.get_product_name()) + print("Platform:", self.device_platform) + print("Chassis:", self.get_chassis()) + print("Chassis service tag:", self.get_chassis_service_tag()) + print("Service tag:", self.get_service_tag()) + print( + "NIC:", + ) pprint(self.network.get_network_cards()) pass diff --git a/netbox_agent/vendors/dell.py b/netbox_agent/vendors/dell.py index dddb67a..104c702 100644 --- a/netbox_agent/vendors/dell.py +++ b/netbox_agent/vendors/dell.py @@ -8,10 +8,10 @@ from netbox_agent.server import ServerBase class DellHost(ServerBase): def __init__(self, *args, **kwargs): super(DellHost, self).__init__(*args, **kwargs) - self.manufacturer = 'Dell' + self.manufacturer = "Dell" def is_blade(self): - return self.get_product_name().startswith('PowerEdge M') + return self.get_product_name().startswith("PowerEdge M") def get_blade_slot(self): """ @@ -20,48 +20,48 @@ class DellHost(ServerBase): ` Location In Chassis: Slot 03` """ if self.is_blade(): - return self.baseboard[0].get('Location In Chassis').strip() + return self.baseboard[0].get("Location In Chassis").strip() return None def get_chassis_name(self): if not self.is_blade(): return None - return 'Chassis {}'.format(self.get_service_tag()) + return "Chassis {}".format(self.get_service_tag()) def get_chassis(self): if self.is_blade(): - return self.chassis[0]['Version'].strip() + return self.chassis[0]["Version"].strip() return self.get_product_name() def get_chassis_service_tag(self): if self.is_blade(): - return self.chassis[0]['Serial Number'].strip() + return self.chassis[0]["Serial Number"].strip() return self.get_service_tag() def get_power_consumption(self): - ''' + """ Parse omreport output like this Amperage PS1 Current 1 : 1.8 A PS2 Current 2 : 1.4 A - ''' + """ value = [] - if not is_tool('omreport'): - logging.error('omreport does not seem to be installed, please debug') + if not is_tool("omreport"): + logging.error("omreport does not seem to be installed, please debug") return value - data = subprocess.getoutput('omreport chassis pwrmonitoring') + data = subprocess.getoutput("omreport chassis pwrmonitoring") amperage = False for line in data.splitlines(): - if line.startswith('Amperage'): + if line.startswith("Amperage"): amperage = True continue if amperage: - if line.startswith('PS'): - amp_value = line.split(':')[1].split()[0] + if line.startswith("PS"): + amp_value = line.split(":")[1].split()[0] value.append(amp_value) else: break diff --git a/netbox_agent/vendors/generic.py b/netbox_agent/vendors/generic.py index c57d2d3..a5a71d2 100644 --- a/netbox_agent/vendors/generic.py +++ b/netbox_agent/vendors/generic.py @@ -5,7 +5,9 @@ from netbox_agent.server import ServerBase class GenericHost(ServerBase): def __init__(self, *args, **kwargs): super(GenericHost, self).__init__(*args, **kwargs) - self.manufacturer = dmidecode.get_by_type(self.dmi, 'Baseboard')[0].get('Manufacturer') + self.manufacturer = dmidecode.get_by_type(self.dmi, "Baseboard")[0].get( + "Manufacturer" + ) def is_blade(self): return False diff --git a/netbox_agent/vendors/hp.py b/netbox_agent/vendors/hp.py index 78b3092..7b299fd 100644 --- a/netbox_agent/vendors/hp.py +++ b/netbox_agent/vendors/hp.py @@ -1,6 +1,6 @@ import netbox_agent.dmidecode as dmidecode -from netbox_agent.server import ServerBase from netbox_agent.inventory import Inventory +from netbox_agent.server import ServerBase class HPHost(ServerBase): @@ -13,8 +13,9 @@ class HPHost(ServerBase): def is_blade(self): blade = self.product.startswith("ProLiant BL") - blade |= self.product.startswith("ProLiant m") and \ - self.product.endswith("Server Cartridge") + blade |= self.product.startswith("ProLiant m") and self.product.endswith( + "Server Cartridge" + ) return blade def _find_rack_locator(self): @@ -36,7 +37,9 @@ class HPHost(ServerBase): } # HP ProLiant m750, m710x, m510 Server Cartridge - if self.product.startswith("ProLiant m") and self.product.endswith("Server Cartridge"): + if self.product.startswith("ProLiant m") and self.product.endswith( + "Server Cartridge" + ): locator = dmidecode.get_by_type(self.dmi, 2) chassis = dmidecode.get_by_type(self.dmi, 3) return { @@ -72,10 +75,14 @@ class HPHost(ServerBase): """ Expansion slot are always the compute bay number + 1 """ - if self.is_blade() and self.own_gpu_expansion_slot() or \ - self.own_disk_expansion_slot() or True: - return 'Bay {}'.format( - str(int(self.hp_rack_locator['Server Bay'].strip()) + 1) + if ( + self.is_blade() + and self.own_gpu_expansion_slot() + or self.own_disk_expansion_slot() + or True + ): + return "Bay {}".format( + str(int(self.hp_rack_locator["Server Bay"].strip()) + 1) ) return None @@ -102,7 +109,7 @@ class HPHost(ServerBase): Indicates if the device hosts a GPU expansion card based on the product name """ - return self.get_product_name().endswith('Graphics Exp') + return self.get_product_name().endswith("Graphics Exp") def own_disk_expansion_slot(self): """ diff --git a/netbox_agent/vendors/qct.py b/netbox_agent/vendors/qct.py index 5582d11..4e65ac7 100644 --- a/netbox_agent/vendors/qct.py +++ b/netbox_agent/vendors/qct.py @@ -4,29 +4,29 @@ from netbox_agent.server import ServerBase class QCTHost(ServerBase): def __init__(self, *args, **kwargs): super(QCTHost, self).__init__(*args, **kwargs) - self.manufacturer = 'QCT' + self.manufacturer = "QCT" def is_blade(self): - return 'Location In Chassis' in self.baseboard[0].keys() + return "Location In Chassis" in self.baseboard[0].keys() def get_blade_slot(self): if self.is_blade(): - return 'Slot {}'.format( - self.baseboard[0].get('Location In Chassis').strip() + return "Slot {}".format( + self.baseboard[0].get("Location In Chassis").strip() ) return None def get_chassis_name(self): if not self.is_blade(): return None - return 'Chassis {}'.format(self.get_service_tag()) + return "Chassis {}".format(self.get_service_tag()) def get_chassis(self): if self.is_blade(): - return self.chassis[0]['Version'].strip() + return self.chassis[0]["Version"].strip() return self.get_product_name() def get_chassis_service_tag(self): if self.is_blade(): - return self.chassis[0]['Serial Number'].strip() + return self.chassis[0]["Serial Number"].strip() return self.get_service_tag() diff --git a/netbox_agent/vendors/supermicro.py b/netbox_agent/vendors/supermicro.py index d27b343..97a0dc2 100644 --- a/netbox_agent/vendors/supermicro.py +++ b/netbox_agent/vendors/supermicro.py @@ -4,36 +4,36 @@ from netbox_agent.server import ServerBase class SupermicroHost(ServerBase): """ - Supermicro DMI can be messed up. They depend on the vendor - to set the correct values. The endusers cannot - change them without buying a license from Supermicro. + Supermicro DMI can be messed up. They depend on the vendor + to set the correct values. The endusers cannot + change them without buying a license from Supermicro. - There are 3 serial numbers in the system + There are 3 serial numbers in the system - 1) System - this is used for the chassis information. - 2) Baseboard - this is used for the blade. - 3) Chassis - this is ignored. + 1) System - this is used for the chassis information. + 2) Baseboard - this is used for the blade. + 3) Chassis - this is ignored. """ def __init__(self, *args, **kwargs): super(SupermicroHost, self).__init__(*args, **kwargs) - self.manufacturer = 'Supermicro' + self.manufacturer = "Supermicro" def is_blade(self): - product_name = self.system[0]['Product Name'].strip() + product_name = self.system[0]["Product Name"].strip() # Blades - blade = product_name.startswith('SBI') - blade |= product_name.startswith('SBA') + blade = product_name.startswith("SBI") + blade |= product_name.startswith("SBA") # Twin - blade |= 'TR-' in product_name + blade |= "TR-" in product_name # TwinPro - blade |= 'TP-' in product_name + blade |= "TP-" in product_name # BigTwin - blade |= 'BT-' in product_name + blade |= "BT-" in product_name # Microcloud - blade |= product_name.startswith('SYS-5039') - blade |= product_name.startswith('SYS-5038') + blade |= product_name.startswith("SYS-5039") + blade |= product_name.startswith("SYS-5038") return blade def get_blade_slot(self): @@ -47,28 +47,28 @@ class SupermicroHost(ServerBase): def get_service_tag(self): if self.is_blade(): - return self.baseboard[0]['Serial Number'].strip() - return self.system[0]['Serial Number'].strip() + return self.baseboard[0]["Serial Number"].strip() + return self.system[0]["Serial Number"].strip() def get_product_name(self): if self.is_blade(): - return self.baseboard[0]['Product Name'].strip() - return self.system[0]['Product Name'].strip() + return self.baseboard[0]["Product Name"].strip() + return self.system[0]["Product Name"].strip() def get_chassis(self): if self.is_blade(): - return self.system[0]['Product Name'].strip() + return self.system[0]["Product Name"].strip() return self.get_product_name() def get_chassis_service_tag(self): if self.is_blade(): - return self.system[0]['Serial Number'].strip() + return self.system[0]["Serial Number"].strip() return self.get_service_tag() def get_chassis_name(self): if not self.is_blade(): return None - return 'Chassis {}'.format(self.get_chassis_service_tag()) + return "Chassis {}".format(self.get_chassis_service_tag()) def get_expansion_product(self): """ diff --git a/netbox_agent/virtualmachine.py b/netbox_agent/virtualmachine.py index 314c2c3..640635e 100644 --- a/netbox_agent/virtualmachine.py +++ b/netbox_agent/virtualmachine.py @@ -1,35 +1,32 @@ import os +from pprint import pprint import netbox_agent.dmidecode as dmidecode from netbox_agent.config import config from netbox_agent.config import netbox_instance as nb from netbox_agent.location import Tenant from netbox_agent.logging import logging # NOQA -from netbox_agent.misc import create_netbox_tags, get_hostname, get_device_platform +from netbox_agent.misc import create_netbox_tags, get_device_platform, get_hostname from netbox_agent.network import VirtualNetwork -from pprint import pprint def is_vm(dmi): - bios = dmidecode.get_by_type(dmi, 'BIOS')[0] - system = dmidecode.get_by_type(dmi, 'System')[0] + bios = dmidecode.get_by_type(dmi, "BIOS")[0] + system = dmidecode.get_by_type(dmi, "System")[0] return ( + "Hyper-V" in bios["Version"] + or "Xen" in bios["Version"] + or "Google Compute Engine" in system["Product Name"] + ) or ( ( - 'Hyper-V' in bios['Version'] or - 'Xen' in bios['Version'] or - 'Google Compute Engine' in system['Product Name'] - ) or - ( - ( - 'Amazon EC2' in system['Manufacturer'] and - not system['Product Name'].endswith('.metal') - ) or - 'RHEV Hypervisor' in system['Product Name'] or - 'QEMU' in system['Manufacturer'] or - 'VirtualBox' in bios['Version'] or - 'VMware' in system['Manufacturer'] + "Amazon EC2" in system["Manufacturer"] + and not system["Product Name"].endswith(".metal") ) + or "RHEV Hypervisor" in system["Product Name"] + or "QEMU" in system["Manufacturer"] + or "VirtualBox" in bios["Version"] + or "VMware" in system["Manufacturer"] ) @@ -42,12 +39,16 @@ class VirtualMachine(object): self.network = None self.device_platform = get_device_platform(config.device.platform) - self.tags = list(set(config.device.tags.split(','))) if config.device.tags else [] + self.tags = ( + list(set(config.device.tags.split(","))) if config.device.tags else [] + ) self.nb_tags = create_netbox_tags(self.tags) def get_memory(self): - mem_bytes = os.sysconf('SC_PAGE_SIZE') * os.sysconf('SC_PHYS_PAGES') # e.g. 4015976448 - mem_gib = mem_bytes / (1024.**2) # e.g. 3.74 + mem_bytes = os.sysconf("SC_PAGE_SIZE") * os.sysconf( + "SC_PHYS_PAGES" + ) # e.g. 4015976448 + mem_gib = mem_bytes / (1024.0**2) # e.g. 3.74 return int(mem_gib) def get_vcpus(self): @@ -55,9 +56,7 @@ class VirtualMachine(object): def get_netbox_vm(self): hostname = get_hostname(config) - vm = nb.virtualization.virtual_machines.get( - name=hostname - ) + vm = nb.virtualization.virtual_machines.get(name=hostname) return vm def get_netbox_cluster(self, name): @@ -80,13 +79,11 @@ class VirtualMachine(object): tenant = self.get_tenant() if tenant is None: return None - nb_tenant = nb.tenancy.tenants.get( - slug=self.get_tenant() - ) + nb_tenant = nb.tenancy.tenants.get(slug=self.get_tenant()) return nb_tenant def netbox_create_or_update(self, config): - logging.debug('It\'s a virtual machine') + logging.debug("It's a virtual machine") created = False updated = 0 @@ -97,7 +94,7 @@ class VirtualMachine(object): memory = self.get_memory() tenant = self.get_netbox_tenant() if not vm: - logging.debug('Creating Virtual machine..') + logging.debug("Creating Virtual machine..") cluster = self.get_netbox_cluster(config.virtual.cluster_name) vm = nb.virtualization.virtual_machines.create( @@ -107,7 +104,7 @@ class VirtualMachine(object): vcpus=vcpus, memory=memory, tenant=tenant.id if tenant else None, - tags=[{'name': x} for x in self.tags], + tags=[{"name": x} for x in self.tags], ) created = True @@ -142,11 +139,13 @@ class VirtualMachine(object): def print_debug(self): self.network = VirtualNetwork(server=self) - print('Cluster:', self.get_netbox_cluster(config.virtual.cluster_name)) - print('Platform:', self.device_platform) - print('VM:', self.get_netbox_vm()) - print('vCPU:', self.get_vcpus()) - print('Memory:', f"{self.get_memory()} MB") - print('NIC:',) + print("Cluster:", self.get_netbox_cluster(config.virtual.cluster_name)) + print("Platform:", self.device_platform) + print("VM:", self.get_netbox_vm()) + print("vCPU:", self.get_vcpus()) + print("Memory:", f"{self.get_memory()} MB") + print( + "NIC:", + ) pprint(self.network.get_network_cards()) pass diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..5d7bf33 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,2 @@ +[tool.isort] +profile = "black" diff --git a/setup.py b/setup.py index 3233cb1..bf28cd1 100644 --- a/setup.py +++ b/setup.py @@ -1,42 +1,38 @@ -from setuptools import find_packages, setup import os +from setuptools import find_packages, setup + + def get_requirements(): - reqs_path = os.path.join( - os.path.dirname(__file__), - 'requirements.txt' - ) - with open(reqs_path, 'r') as f: - reqs = [ - r.strip() for r in f - if r.strip() - ] + reqs_path = os.path.join(os.path.dirname(__file__), "requirements.txt") + with open(reqs_path, "r") as f: + reqs = [r.strip() for r in f if r.strip()] return reqs setup( - name='netbox_agent', - version='0.7.1', - description='NetBox agent for server', - long_description=open('README.md', encoding="utf-8").read(), - long_description_content_type='text/markdown', - url='https://github.com/solvik/netbox_agent', - author='Solvik Blum', - author_email='solvik@solvik.fr', - license='Apache2', + name="netbox_agent", + version="0.7.1", + description="NetBox agent for server", + long_description=open("README.md", encoding="utf-8").read(), + long_description_content_type="text/markdown", + url="https://github.com/solvik/netbox_agent", + author="Solvik Blum", + author_email="solvik@solvik.fr", + license="Apache2", include_package_data=True, packages=find_packages(exclude=["*.tests", "*.tests.*", "tests.*", "tests"]), use_scm_version=True, install_requires=get_requirements(), zip_safe=False, - keywords=['netbox'], + keywords=["netbox"], classifiers=[ - 'Intended Audience :: Developers', - 'Development Status :: 5 - Production/Stable', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.6', + "Intended Audience :: Developers", + "Development Status :: 5 - Production/Stable", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.6", ], entry_points={ - 'console_scripts': ['netbox_agent=netbox_agent.cli:main'], - } + "console_scripts": ["netbox_agent=netbox_agent.cli:main"], + }, ) diff --git a/tests/conftest.py b/tests/conftest.py index 4a97a8c..e5f0ae3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,14 +14,15 @@ def get_fixture_paths(path): return fixture_paths -def parametrize_with_fixtures(path, base_path='tests/fixtures', - argname='fixture', only_filenames=None): +def parametrize_with_fixtures( + path, base_path="tests/fixtures", argname="fixture", only_filenames=None +): path = os.path.join(base_path, path) fixture_paths = get_fixture_paths(path) argvalues = [] for path in fixture_paths: - with open(path, 'r') as f: - content = ''.join(f.readlines()) + with open(path, "r") as f: + content = "".join(f.readlines()) filename = os.path.basename(path) if only_filenames and filename not in only_filenames: continue @@ -30,4 +31,5 @@ def parametrize_with_fixtures(path, base_path='tests/fixtures', def _decorator(test_function): return pytest.mark.parametrize(argname, argvalues)(test_function) + return _decorator diff --git a/tests/network.py b/tests/network.py index 7b341ed..e6ddcee 100644 --- a/tests/network.py +++ b/tests/network.py @@ -3,18 +3,22 @@ from tests.conftest import parametrize_with_fixtures @parametrize_with_fixtures( - 'lldp/', only_filenames=[ - 'dedibox1.txt', - ]) + "lldp/", + only_filenames=[ + "dedibox1.txt", + ], +) def test_lldp_parse_with_port_desc(fixture): lldp = LLDP(fixture) - assert lldp.get_switch_port('enp1s0f0') == 'RJ-9' + assert lldp.get_switch_port("enp1s0f0") == "RJ-9" @parametrize_with_fixtures( - 'lldp/', only_filenames=[ - 'qfx.txt', - ]) + "lldp/", + only_filenames=[ + "qfx.txt", + ], +) def test_lldp_parse_without_ifname(fixture): lldp = LLDP(fixture) - assert lldp.get_switch_port('eth0') == 'xe-0/0/1' + assert lldp.get_switch_port("eth0") == "xe-0/0/1" diff --git a/tests/server.py b/tests/server.py index 541e2f7..bbf205f 100644 --- a/tests/server.py +++ b/tests/server.py @@ -6,7 +6,7 @@ from netbox_agent.vendors.supermicro import SupermicroHost from tests.conftest import parametrize_with_fixtures -@parametrize_with_fixtures('dmidecode/') +@parametrize_with_fixtures("dmidecode/") def test_init(fixture): dmi = parse(fixture) server = ServerBase(dmi) @@ -14,96 +14,78 @@ def test_init(fixture): @parametrize_with_fixtures( - 'dmidecode/', only_filenames=[ - 'HP_SL4540_Gen8', - 'HP_BL460c_Gen9', - 'HP_DL380p_Gen8', - 'HP_SL4540_Gen8' - 'HP_ProLiant_BL460c_Gen10_Graphics_Exp' - ]) + "dmidecode/", + only_filenames=[ + "HP_SL4540_Gen8", + "HP_BL460c_Gen9", + "HP_DL380p_Gen8", + "HP_SL4540_Gen8" "HP_ProLiant_BL460c_Gen10_Graphics_Exp", + ], +) def test_hp_service_tag(fixture): dmi = parse(fixture) server = HPHost(dmi) - assert server.get_service_tag() == '4242' + assert server.get_service_tag() == "4242" -@parametrize_with_fixtures( - 'dmidecode/', only_filenames=[ - 'HP_ProLiant_m710x' - ]) +@parametrize_with_fixtures("dmidecode/", only_filenames=["HP_ProLiant_m710x"]) def test_moonshot_blade(fixture): dmi = parse(fixture) server = HPHost(dmi) - assert server.get_service_tag() == 'CN66480BLA' - assert server.get_chassis_service_tag() == 'CZ3702MD5K' + assert server.get_service_tag() == "CN66480BLA" + assert server.get_chassis_service_tag() == "CZ3702MD5K" assert server.is_blade() is True assert server.own_expansion_slot() is False -@parametrize_with_fixtures( - 'dmidecode/', only_filenames=[ - 'SYS-5039MS-H12TRF-OS012.txt' - ]) +@parametrize_with_fixtures("dmidecode/", only_filenames=["SYS-5039MS-H12TRF-OS012.txt"]) def test_supermicro_blade(fixture): dmi = parse(fixture) server = SupermicroHost(dmi) - assert server.get_service_tag() == 'E235735X6B01665' - assert server.get_chassis_service_tag() == 'C9390AF40A20098' - assert server.get_chassis() == 'SYS-5039MS-H12TRF-OS012' + assert server.get_service_tag() == "E235735X6B01665" + assert server.get_chassis_service_tag() == "C9390AF40A20098" + assert server.get_chassis() == "SYS-5039MS-H12TRF-OS012" assert server.is_blade() is True -@parametrize_with_fixtures( - 'dmidecode/', only_filenames=[ - 'SM_SYS-6018R' - ]) +@parametrize_with_fixtures("dmidecode/", only_filenames=["SM_SYS-6018R"]) def test_supermicro_pizza(fixture): dmi = parse(fixture) server = SupermicroHost(dmi) - assert server.get_service_tag() == 'A177950X7709591' - assert server.get_chassis() == 'SYS-6018R-TDTPR' + assert server.get_service_tag() == "A177950X7709591" + assert server.get_chassis() == "SYS-6018R-TDTPR" assert server.is_blade() is False -@parametrize_with_fixtures( - 'dmidecode/', only_filenames=[ - 'QCT_X10E-9N' - ]) +@parametrize_with_fixtures("dmidecode/", only_filenames=["QCT_X10E-9N"]) def test_qct_x10(fixture): dmi = parse(fixture) server = QCTHost(dmi) - assert server.get_service_tag() == 'QTFCQ57140285' + assert server.get_service_tag() == "QTFCQ57140285" -@parametrize_with_fixtures( - 'dmidecode/', only_filenames=[ - 'unknown.txt' - ]) +@parametrize_with_fixtures("dmidecode/", only_filenames=["unknown.txt"]) def test_generic_host_service_tag(fixture): dmi = parse(fixture) server = ServerBase(dmi) - assert server.get_service_tag() == '42' + assert server.get_service_tag() == "42" -@parametrize_with_fixtures( - 'dmidecode/', only_filenames=[ - 'unknown.txt' - ]) +@parametrize_with_fixtures("dmidecode/", only_filenames=["unknown.txt"]) def test_generic_host_product_name(fixture): dmi = parse(fixture) server = ServerBase(dmi) - assert server.get_product_name() == 'SR' + assert server.get_product_name() == "SR" @parametrize_with_fixtures( - 'dmidecode/', only_filenames=[ - 'HP_ProLiant_BL460c_Gen10_Graphics_Exp' - ]) + "dmidecode/", only_filenames=["HP_ProLiant_BL460c_Gen10_Graphics_Exp"] +) def test_hp_blade_with_gpu_expansion(fixture): dmi = parse(fixture) server = HPHost(dmi) - assert server.get_service_tag() == '4242' - assert server.get_chassis_service_tag() == '4343' + assert server.get_service_tag() == "4242" + assert server.get_chassis_service_tag() == "4343" assert server.is_blade() is True assert server.own_expansion_slot() is True - assert server.get_expansion_service_tag() == '4242 expansion' + assert server.get_expansion_service_tag() == "4242 expansion"