From 229d6f085398beb02e68d5ecfa1859e7b266b9f4 Mon Sep 17 00:00:00 2001 From: Charles Luzzato Date: Sun, 15 Jun 2025 21:05:00 +0200 Subject: [PATCH 01/69] Added a character limit to the name of inventory items --- netbox_agent/inventory.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/netbox_agent/inventory.py b/netbox_agent/inventory.py index 57b0de53..c9a96761 100644 --- a/netbox_agent/inventory.py +++ b/netbox_agent/inventory.py @@ -159,12 +159,14 @@ def do_netbox_motherboard(self): def create_netbox_interface(self, iface): manufacturer = self.find_or_create_manufacturer(iface["vendor"]) + # Netbox name character limit is 64 characters + char_limit = 64 _ = nb.dcim.inventory_items.create( device=self.device_id, manufacturer=manufacturer.id, discovered=True, tags=[{"name": INVENTORY_TAG["interface"]["name"]}], - name="{}".format(iface["product"]), + name="{}".format((iface["product"][:char_limit-3] + '..') if (len(iface["product"]) > char_limit) else iface["product"]), serial="{}".format(iface["serial"]), description="{} {}".format(iface["description"], iface["name"]), ) From 0c14ece18e8ce43a5b0f4cf753cdf3b2a8cd9e88 Mon Sep 17 00:00:00 2001 From: Charles Luzzato Date: Sun, 15 Jun 2025 21:08:24 +0200 Subject: [PATCH 02/69] Certain Netgear switches have the interface name set as local, and descr could not be set --- netbox_agent/lldp.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/netbox_agent/lldp.py b/netbox_agent/lldp.py index 7b6fa39f..c117f9bd 100644 --- a/netbox_agent/lldp.py +++ b/netbox_agent/lldp.py @@ -62,7 +62,12 @@ def get_switch_port(self, interface): 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("local"): + return self.data["lldp"][interface]["port"]["local"] + if self.data["lldp"][interface]["port"].get("descr"): + return self.data["lldp"][interface]["port"]["descr"] + return None + def get_switch_vlan(self, interface): # lldp.eth0.vlan.vlan-id=296 From a730d9e30bac2cb8e8848d42f4264a052d135edd Mon Sep 17 00:00:00 2001 From: Charles Luzzato Date: Sun, 15 Jun 2025 23:12:15 +0200 Subject: [PATCH 03/69] Add the ability to auto create manufacturer --- netbox_agent/config.py | 1 + netbox_agent/misc.py | 9 ++++++++- netbox_agent/server.py | 21 ++++++++++++++++++++- 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/netbox_agent/config.py b/netbox_agent/config.py index 48a910de..2eb003d8 100644 --- a/netbox_agent/config.py +++ b/netbox_agent/config.py @@ -90,6 +90,7 @@ def get_config(): "--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("--device.autocreate_device_type", default=True, help="Define whether a device type should be create when it doesnt exist.") 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") diff --git a/netbox_agent/misc.py b/netbox_agent/misc.py index ccfae61b..1419d04c 100644 --- a/netbox_agent/misc.py +++ b/netbox_agent/misc.py @@ -1,9 +1,11 @@ from contextlib import suppress +from netbox_agent.config import config from netbox_agent.config import netbox_instance as nb from slugify import slugify from shutil import which import distro import subprocess +import logging import socket import re @@ -23,7 +25,12 @@ def get_device_role(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)) + if config.device.autocreate_device_type: + logging.info( + 'DeviceType "{}" does not yet exist, it will be created'.format(type) + ) + else: + raise Exception('DeviceType "{}" does not exist, please create it, or set device.autocreate_device_type to true'.format(type)) return device_type diff --git a/netbox_agent/server.py b/netbox_agent/server.py index c25be6ff..c46e6ddd 100644 --- a/netbox_agent/server.py +++ b/netbox_agent/server.py @@ -143,6 +143,13 @@ def get_product_name(self): """ return self.system[0]["Product Name"].strip() + def get_manufacturer(self): + """ + Return the Manufacturer from dmidecode info, and create it if needed + """ + manufacturer = Inventory.find_or_create_manufacturer(self.system[0]["Manufacturer"].strip()) + return manufacturer + def get_service_tag(self): """ Return the Service Tag from dmidecode info @@ -271,7 +278,10 @@ 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())) + if config.device.autocreate_device_type: + + else: + raise Exception('Chassis "{}" doesn\'t exist'.format(self.get_chassis())) serial = self.get_service_tag() hostname = self.get_hostname() logging.info( @@ -292,6 +302,15 @@ def _netbox_create_server(self, datacenter, tenant, rack): ) return new_server + def _netbox_create_device_type(self): + manufacturer = self.get_manufacturer() + model = self.get_product_name() + new_device_type = nb.dcim.devices.create( + manufacturer = netbox.dcim.manufacturers.get(name=manufacturer).id, + model = model + ) + return new_device_type + def get_netbox_server(self, expansion=False): if expansion is False: return nb.dcim.devices.get(serial=self.get_service_tag()) From 044ba8022e9c2d4831f881c8bfb3ff4b03e842f2 Mon Sep 17 00:00:00 2001 From: Charles Luzzato Date: Sun, 15 Jun 2025 23:17:15 +0200 Subject: [PATCH 04/69] Fixed missing code --- netbox_agent/server.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/netbox_agent/server.py b/netbox_agent/server.py index c46e6ddd..4baeb620 100644 --- a/netbox_agent/server.py +++ b/netbox_agent/server.py @@ -279,7 +279,8 @@ def _netbox_create_server(self, datacenter, tenant, rack): device_type = get_device_type(self.get_product_name()) if not device_type: if config.device.autocreate_device_type: - + device_type_create = _netbox_create_device_type() + device_type = get_device_type(self.get_product_name()) else: raise Exception('Chassis "{}" doesn\'t exist'.format(self.get_chassis())) serial = self.get_service_tag() From b24f83bc296d81474bb7d23d3508b737c81a8621 Mon Sep 17 00:00:00 2001 From: Charles Luzzato Date: Sun, 15 Jun 2025 23:25:01 +0200 Subject: [PATCH 05/69] Added logging and fixed order of functions --- netbox_agent/server.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/netbox_agent/server.py b/netbox_agent/server.py index 4baeb620..2b85d13d 100644 --- a/netbox_agent/server.py +++ b/netbox_agent/server.py @@ -274,6 +274,19 @@ def _netbox_deduplicate_server(self, purge): server.serial = serial server.save() + def _netbox_create_device_type(self): + manufacturer = self.get_manufacturer() + model = self.get_product_name() + logging.info( + "Creating device type {model}.".format( + model=model + ) + new_device_type = nb.dcim.devices.create( + manufacturer = netbox.dcim.manufacturers.get(name=manufacturer).id, + model = model + ) + return new_device_type + 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()) @@ -303,15 +316,6 @@ def _netbox_create_server(self, datacenter, tenant, rack): ) return new_server - def _netbox_create_device_type(self): - manufacturer = self.get_manufacturer() - model = self.get_product_name() - new_device_type = nb.dcim.devices.create( - manufacturer = netbox.dcim.manufacturers.get(name=manufacturer).id, - model = model - ) - return new_device_type - def get_netbox_server(self, expansion=False): if expansion is False: return nb.dcim.devices.get(serial=self.get_service_tag()) From be418c6e32e1b31c44b095b505b696f714f9a2fb Mon Sep 17 00:00:00 2001 From: Charles Luzzato Date: Sun, 15 Jun 2025 23:26:09 +0200 Subject: [PATCH 06/69] Missing Parenthesis --- netbox_agent/server.py | 1 + 1 file changed, 1 insertion(+) diff --git a/netbox_agent/server.py b/netbox_agent/server.py index 2b85d13d..8a467025 100644 --- a/netbox_agent/server.py +++ b/netbox_agent/server.py @@ -281,6 +281,7 @@ def _netbox_create_device_type(self): "Creating device type {model}.".format( model=model ) + ) new_device_type = nb.dcim.devices.create( manufacturer = netbox.dcim.manufacturers.get(name=manufacturer).id, model = model From b5f0af205962423d42a70b1f806be888a5670dc1 Mon Sep 17 00:00:00 2001 From: Charles Luzzato Date: Sun, 15 Jun 2025 23:27:08 +0200 Subject: [PATCH 07/69] Missing Self --- 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 8a467025..4737db4d 100644 --- a/netbox_agent/server.py +++ b/netbox_agent/server.py @@ -293,7 +293,7 @@ def _netbox_create_server(self, datacenter, tenant, rack): device_type = get_device_type(self.get_product_name()) if not device_type: if config.device.autocreate_device_type: - device_type_create = _netbox_create_device_type() + device_type_create = self._netbox_create_device_type() device_type = get_device_type(self.get_product_name()) else: raise Exception('Chassis "{}" doesn\'t exist'.format(self.get_chassis())) From 348f2314a3040b045347c0aefcdad09077c8123c Mon Sep 17 00:00:00 2001 From: Charles Luzzato Date: Sun, 15 Jun 2025 23:29:11 +0200 Subject: [PATCH 08/69] Print debug --- netbox_agent/server.py | 1 + 1 file changed, 1 insertion(+) diff --git a/netbox_agent/server.py b/netbox_agent/server.py index 4737db4d..d604ada4 100644 --- a/netbox_agent/server.py +++ b/netbox_agent/server.py @@ -147,6 +147,7 @@ def get_manufacturer(self): """ Return the Manufacturer from dmidecode info, and create it if needed """ + print(self.system[0]) manufacturer = Inventory.find_or_create_manufacturer(self.system[0]["Manufacturer"].strip()) return manufacturer From 78e25b0716f0d1a30b97a09aed071f7ba1d71cb8 Mon Sep 17 00:00:00 2001 From: Charles Luzzato Date: Sun, 15 Jun 2025 23:48:06 +0200 Subject: [PATCH 09/69] Fixed manufacturer creation --- netbox_agent/server.py | 69 +++++++++++++++++++++++++++++++----------- 1 file changed, 52 insertions(+), 17 deletions(-) diff --git a/netbox_agent/server.py b/netbox_agent/server.py index d604ada4..31b15090 100644 --- a/netbox_agent/server.py +++ b/netbox_agent/server.py @@ -119,6 +119,24 @@ def update_netbox_expansion_location(self, server, expansion): update = True return update + def find_or_create_manufacturer(self, name): + if name is None: + return None + + manufacturer = nb.dcim.manufacturers.get( + name=name, + ) + if not manufacturer: + 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(), + ) + + logging.info("Creating missing manufacturer {name}".format(name=name)) + + return manufacturer + def get_rack(self): rack = Rack() return rack.get() @@ -147,8 +165,7 @@ def get_manufacturer(self): """ Return the Manufacturer from dmidecode info, and create it if needed """ - print(self.system[0]) - manufacturer = Inventory.find_or_create_manufacturer(self.system[0]["Manufacturer"].strip()) + manufacturer = self.find_or_create_manufacturer(self.system[0]["Manufacturer"].strip()) return manufacturer def get_service_tag(self): @@ -199,8 +216,28 @@ def get_power_consumption(self): def get_expansion_product(self): raise NotImplementedError + def _netbox_create_device_type(self): + manufacturer = self.get_manufacturer() + model = self.get_product_name() + logging.info( + "Creating device type {model}.".format( + model=model + ) + ) + new_device_type = nb.dcim.devices.create( + manufacturer = netbox.dcim.manufacturers.get(name=manufacturer).id, + model = model + ) + return new_device_type + def _netbox_create_chassis(self, datacenter, tenant, rack): device_type = get_device_type(self.get_chassis()) + if not device_type: + if config.device.autocreate_device_type: + device_type_create = self._netbox_create_device_type() + device_type = get_device_type(self.get_chassis()) + else: + raise Exception('Chassis "{}" doesn\'t exist'.format(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)) @@ -220,6 +257,12 @@ def _netbox_create_chassis(self, datacenter, tenant, rack): 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()) + if not device_type: + if config.device.autocreate_device_type: + device_type_create = self._netbox_create_device_type() + device_type = get_device_type(self.get_product_name()) + else: + raise Exception('Blade "{}" type doesn\'t exist'.format(self.get_product_name())) serial = self.get_service_tag() hostname = self.get_hostname() logging.info( @@ -244,6 +287,12 @@ def _netbox_create_blade(self, chassis, datacenter, tenant, rack): 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()) + if not device_type: + if config.device.autocreate_device_type: + device_type_create = self._netbox_create_device_type() + device_type = get_device_type(self.get_expansion_product()) + else: + raise Exception('Blade expansion type"{}" doesn\'t exist'.format(self.get_expansion_product())) serial = self.get_expansion_service_tag() hostname = self.get_hostname() + " expansion" logging.info( @@ -275,20 +324,6 @@ def _netbox_deduplicate_server(self, purge): server.serial = serial server.save() - def _netbox_create_device_type(self): - manufacturer = self.get_manufacturer() - model = self.get_product_name() - logging.info( - "Creating device type {model}.".format( - model=model - ) - ) - new_device_type = nb.dcim.devices.create( - manufacturer = netbox.dcim.manufacturers.get(name=manufacturer).id, - model = model - ) - return new_device_type - 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()) @@ -297,7 +332,7 @@ def _netbox_create_server(self, datacenter, tenant, rack): device_type_create = self._netbox_create_device_type() device_type = get_device_type(self.get_product_name()) else: - raise Exception('Chassis "{}" doesn\'t exist'.format(self.get_chassis())) + raise Exception('Server type "{}" doesn\'t exist'.format(self.get_chassis())) serial = self.get_service_tag() hostname = self.get_hostname() logging.info( From c10504f14291617a9f418ed8614e7960ebc645b5 Mon Sep 17 00:00:00 2001 From: Charles Luzzato Date: Sun, 15 Jun 2025 23:49:00 +0200 Subject: [PATCH 10/69] fixed error --- 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 31b15090..54a10c2d 100644 --- a/netbox_agent/server.py +++ b/netbox_agent/server.py @@ -225,7 +225,7 @@ def _netbox_create_device_type(self): ) ) new_device_type = nb.dcim.devices.create( - manufacturer = netbox.dcim.manufacturers.get(name=manufacturer).id, + manufacturer = nb.dcim.manufacturers.get(name=manufacturer).id, model = model ) return new_device_type From 939bf097cc32f5cb7358e2deb1d92d52b52083b1 Mon Sep 17 00:00:00 2001 From: Charles Luzzato Date: Mon, 16 Jun 2025 00:02:40 +0200 Subject: [PATCH 11/69] Added slug --- netbox_agent/server.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/netbox_agent/server.py b/netbox_agent/server.py index 54a10c2d..7238cb61 100644 --- a/netbox_agent/server.py +++ b/netbox_agent/server.py @@ -13,12 +13,12 @@ from netbox_agent.network import ServerNetwork from netbox_agent.power import PowerSupply from pprint import pprint +from slugify import slugify import subprocess import logging import socket import sys - class ServerBase: def __init__(self, dmi=None): if dmi: @@ -224,9 +224,10 @@ def _netbox_create_device_type(self): model=model ) ) - new_device_type = nb.dcim.devices.create( + new_device_type = nb.dcim.device_types.create( manufacturer = nb.dcim.manufacturers.get(name=manufacturer).id, - model = model + model = model, + slug=slugify(model) ) return new_device_type From bba07405d5e949198ef49cbccf6a08484400e6f1 Mon Sep 17 00:00:00 2001 From: Charles Luzzato Date: Mon, 16 Jun 2025 00:15:51 +0200 Subject: [PATCH 12/69] Added attribute error for when assigned_object is None --- 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 c2706108..5282f0ac 100644 --- a/netbox_agent/network.py +++ b/netbox_agent/network.py @@ -659,7 +659,7 @@ def connect_interface_to_switch(self, switch_ip, switch_interface, nb_server_int switch_ip, nb_switch.id ) ) - except KeyError: + except KeyError, AttributeError: logging.error( "Switch IP {} is found but not associated to a Netbox Switch Device".format( switch_ip From a4076344d14f1986ddbd6e5bef858956f81152f5 Mon Sep 17 00:00:00 2001 From: Charles Luzzato Date: Mon, 16 Jun 2025 00:15:51 +0200 Subject: [PATCH 13/69] Added attribute error for when assigned_object is None --- 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 5282f0ac..4cf89668 100644 --- a/netbox_agent/network.py +++ b/netbox_agent/network.py @@ -659,7 +659,7 @@ def connect_interface_to_switch(self, switch_ip, switch_interface, nb_server_int switch_ip, nb_switch.id ) ) - except KeyError, AttributeError: + except (KeyError, AttributeError): logging.error( "Switch IP {} is found but not associated to a Netbox Switch Device".format( switch_ip From c6c4cbd3bc762160665e3efd3146f4ad18f13ccc Mon Sep 17 00:00:00 2001 From: Charles Luzzato Date: Mon, 16 Jun 2025 23:45:41 +0200 Subject: [PATCH 14/69] Need to specify the attached interface id to be able to get the correct MAC when multiple interfaces have the same max --- 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 4cf89668..18e7418a 100644 --- a/netbox_agent/network.py +++ b/netbox_agent/network.py @@ -551,7 +551,7 @@ def batched(it, n): if version.parse(nb.version) < version.parse("4.2"): interface.mac_address = nic["mac"] else: - interface.primary_mac_address = {"mac_address": nic["mac"]} + interface.primary_mac_address = {"mac_address": nic["mac"], "assigned_object_id": interface.id} nic_update += 1 if hasattr(interface, "mtu"): From 6bab34c248ad81648a063519e5395dcae4f6513c Mon Sep 17 00:00:00 2001 From: Charles Luzzato Date: Tue, 17 Jun 2025 00:11:50 +0200 Subject: [PATCH 15/69] Added set option for VRF --- netbox_agent/config.py | 1 + netbox_agent/network.py | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/netbox_agent/config.py b/netbox_agent/config.py index 2eb003d8..155f4e2f 100644 --- a/netbox_agent/config.py +++ b/netbox_agent/config.py @@ -127,6 +127,7 @@ def get_config(): default="name", help="What property to use as NIC identifier", ) + p.add_argument("--network.vrf", default=None, help="Set VRF for network interfaces") p.add_argument( "--network.primary_mac", choices=("permanent", "temp"), diff --git a/netbox_agent/network.py b/netbox_agent/network.py index 18e7418a..4d8411fa 100644 --- a/netbox_agent/network.py +++ b/netbox_agent/network.py @@ -223,6 +223,10 @@ def get_or_create_vlan(self, vlan_id): ) return vlan + def get_vrf_id(sef, vrf_name): + vrf_id = self.nb.ipam.vrfs.get(name=vrf_name, **self.custom_arg_id).id + return vrf_id + def reset_vlan_on_interface(self, nic, interface): update = False vlan_id = nic["vlan"] @@ -542,6 +546,16 @@ def batched(it, n): if nic["mac"]: self.update_interface_macs(interface, [nic["mac"]]) + if config.network.vrf and config.network.vrf != interface.vrf: + logging.info( + "Updating interface {interface} VRF to: {vrf}".format( + interface=interface, vrf=config.network.vrf + ) + ) + vrf = get_vrf_id(config.network.vrf) + interface.vrf = vrf + nic_update += 1 + if nic["mac"] and nic["mac"] != interface.mac_address: logging.info( "Updating interface {interface} mac to: {mac}".format( From 6cf8555b7bbdac6ba40139755e79de7be159e668 Mon Sep 17 00:00:00 2001 From: Charles Luzzato Date: Tue, 17 Jun 2025 00:12:36 +0200 Subject: [PATCH 16/69] Fixed error --- 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 4d8411fa..a938c009 100644 --- a/netbox_agent/network.py +++ b/netbox_agent/network.py @@ -223,7 +223,7 @@ def get_or_create_vlan(self, vlan_id): ) return vlan - def get_vrf_id(sef, vrf_name): + def get_vrf_id(self, vrf_name): vrf_id = self.nb.ipam.vrfs.get(name=vrf_name, **self.custom_arg_id).id return vrf_id From c3dee8baba3287895054e16b3c2003857c9b6535 Mon Sep 17 00:00:00 2001 From: Charles Luzzato Date: Tue, 17 Jun 2025 00:13:26 +0200 Subject: [PATCH 17/69] Fixed typo --- 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 a938c009..1b03f46d 100644 --- a/netbox_agent/network.py +++ b/netbox_agent/network.py @@ -552,7 +552,7 @@ def batched(it, n): interface=interface, vrf=config.network.vrf ) ) - vrf = get_vrf_id(config.network.vrf) + vrf = self.get_vrf_id(config.network.vrf) interface.vrf = vrf nic_update += 1 From dc21a4eac3a7989cf35462582bf73bdd43c93e07 Mon Sep 17 00:00:00 2001 From: Charles Luzzato Date: Tue, 17 Jun 2025 00:15:12 +0200 Subject: [PATCH 18/69] Fix typo --- 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 1b03f46d..bdaf5ef4 100644 --- a/netbox_agent/network.py +++ b/netbox_agent/network.py @@ -224,7 +224,7 @@ def get_or_create_vlan(self, vlan_id): return vlan def get_vrf_id(self, vrf_name): - vrf_id = self.nb.ipam.vrfs.get(name=vrf_name, **self.custom_arg_id).id + vrf_id = nb.ipam.vrfs.get(name=vrf_name, **self.custom_arg_id).id return vrf_id def reset_vlan_on_interface(self, nic, interface): From 878df4ab73f951e5c080863a4062dc33cfb683c5 Mon Sep 17 00:00:00 2001 From: Charles Luzzato Date: Tue, 17 Jun 2025 00:17:15 +0200 Subject: [PATCH 19/69] Fixed typoi --- netbox_agent/network.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/netbox_agent/network.py b/netbox_agent/network.py index bdaf5ef4..4354b6f8 100644 --- a/netbox_agent/network.py +++ b/netbox_agent/network.py @@ -224,8 +224,11 @@ def get_or_create_vlan(self, vlan_id): return vlan def get_vrf_id(self, vrf_name): - vrf_id = nb.ipam.vrfs.get(name=vrf_name, **self.custom_arg_id).id - return vrf_id + vrf = nb.ipam.vrfs.get(name=vrf_name, **self.custom_arg_id) + if vrf: + return vrf.id + else: + return None def reset_vlan_on_interface(self, nic, interface): update = False From adae0ad0b7887aff1736481a06f668bddf29f179 Mon Sep 17 00:00:00 2001 From: Charles Luzzato Date: Tue, 17 Jun 2025 00:26:44 +0200 Subject: [PATCH 20/69] Fixed final bug --- 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 4354b6f8..195ce41a 100644 --- a/netbox_agent/network.py +++ b/netbox_agent/network.py @@ -549,7 +549,7 @@ def batched(it, n): if nic["mac"]: self.update_interface_macs(interface, [nic["mac"]]) - if config.network.vrf and config.network.vrf != interface.vrf: + if config.network.vrf and config.network.vrf != str(interface.vrf): logging.info( "Updating interface {interface} VRF to: {vrf}".format( interface=interface, vrf=config.network.vrf From 4d42b4d686066419f39d1723e02a7c84c237f54e Mon Sep 17 00:00:00 2001 From: Charles Luzzato Date: Tue, 17 Jun 2025 07:35:40 +0200 Subject: [PATCH 21/69] Added brctl command --- netbox_agent/network.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/netbox_agent/network.py b/netbox_agent/network.py index 195ce41a..d226490d 100644 --- a/netbox_agent/network.py +++ b/netbox_agent/network.py @@ -16,6 +16,20 @@ VIRTUAL_NET_FOLDER = Path("/sys/devices/virtual/net") +def _execute_brctl_cmd(interface_name): + if not is_tool("brctl"): + logging.error( + "Brctl 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( + [ + "brctl", + interface_name + ], + stderr=_subprocess.PIPE, + ) class Network(object): def __init__(self, server, *args, **kwargs): @@ -116,6 +130,12 @@ def scan(self): open("/sys/class/net/{}/bonding/slaves".format(interface)).read().split() ) + bridging = False + bridge_parent = [] + outputbrctl = _execute_brctl_cmd(interface) + print("Running brctl") + print(outputbrctl) + virtual = Path(f"/sys/class/net/{interface}").resolve().parent == VIRTUAL_NET_FOLDER nic = { From b9e9391899f3b030197853c9eb8e1d30cd35ca7f Mon Sep 17 00:00:00 2001 From: Charles Luzzato Date: Tue, 17 Jun 2025 07:44:15 +0200 Subject: [PATCH 22/69] Added import --- netbox_agent/network.py | 1 + 1 file changed, 1 insertion(+) diff --git a/netbox_agent/network.py b/netbox_agent/network.py index d226490d..f792accd 100644 --- a/netbox_agent/network.py +++ b/netbox_agent/network.py @@ -13,6 +13,7 @@ from netbox_agent.ethtool import Ethtool from netbox_agent.ipmi import IPMI from netbox_agent.lldp import LLDP +from netbox_agent.misc import is_tool VIRTUAL_NET_FOLDER = Path("/sys/devices/virtual/net") From fba05b0fe3b600877c13d647f8e548bd12a082aa Mon Sep 17 00:00:00 2001 From: Charles Luzzato Date: Tue, 17 Jun 2025 07:46:14 +0200 Subject: [PATCH 23/69] Added subprocess --- netbox_agent/network.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/netbox_agent/network.py b/netbox_agent/network.py index f792accd..6728a2d3 100644 --- a/netbox_agent/network.py +++ b/netbox_agent/network.py @@ -5,6 +5,7 @@ from pathlib import Path import netifaces +import subprocess from netaddr import IPAddress from packaging import version @@ -24,7 +25,7 @@ def _execute_brctl_cmd(interface_name): "check the compatibility of this project with your distro." ) sys.exit(1) - return _subprocess.check_output( + return subprocess.getoutput( [ "brctl", interface_name From 4e4a600b8253e06a39e7620342aa90523ccc788b Mon Sep 17 00:00:00 2001 From: Charles Luzzato Date: Tue, 17 Jun 2025 22:45:56 +0200 Subject: [PATCH 24/69] Bridging automation --- netbox_agent/network.py | 55 ++++++++++++++++++++++++++++++++++------- 1 file changed, 46 insertions(+), 9 deletions(-) diff --git a/netbox_agent/network.py b/netbox_agent/network.py index 6728a2d3..5eb65f86 100644 --- a/netbox_agent/network.py +++ b/netbox_agent/network.py @@ -26,12 +26,8 @@ def _execute_brctl_cmd(interface_name): ) sys.exit(1) return subprocess.getoutput( - [ - "brctl", - interface_name - ], - stderr=_subprocess.PIPE, - ) + "brctl show " + str(interface_name) + ) class Network(object): def __init__(self, server, *args, **kwargs): @@ -133,10 +129,22 @@ def scan(self): ) bridging = False - bridge_parent = [] + bridge_parents = [] outputbrctl = _execute_brctl_cmd(interface) - print("Running brctl") - print(outputbrctl) + lineparse_output = outputbrctl.splitlines() + if len(lineparse_output)>1: + headers = lineparse_output[0].replace("\t\t", "\t").split("\t") + brctl = dict((key, []) for key in headers) + # Interface is a bridge + bridging = True + for spec_line in lineparse_output[1:]: + cleaned_spec_line = spec_line.replace("\t\t\t\t\t\t\t", "\t\t\t\t\t\t") + cleaned_spec_line = cleaned_spec_line.replace("\t\t", "\t").split("\t") + for col, val in enumerate(cleaned_spec_line): + if val: + brctl[headers[col]].append(val) + + bridge_parents = brctl[headers[-1]] virtual = Path(f"/sys/class/net/{interface}").resolve().parent == VIRTUAL_NET_FOLDER @@ -154,6 +162,8 @@ def scan(self): "mtu": mtu, "bonding": bonding, "bonding_slaves": bonding_slaves, + "bridged": bridging, + "bridge_parents": bridge_parents } nics.append(nic) return nics @@ -180,6 +190,32 @@ def _set_bonding_interfaces(self): return False return True + def _set_bridged_interfaces(self): + bridged_nics = (x for x in self.nics if x["bridged"]) + for nic in bridged_nics: + bridged_int = self.get_netbox_network_card(nic) + logging.debug("Setting bridg parent interface for {name}".format(name=bridged_int.name)) + bridged_int.type = { "value": "bridge", "label": "Bridge"} + for parent_num, parent_int in enumerate( + self.get_netbox_network_card(parent_bridge_nic) + for parent_bridge_nic in self.nics + if parent_bridge_nic["name"] in nic["bridge_parents"] + ): + if parent_num == 0: + # First parent, set the parent interface + bridged_int.parent = parent_int + logging.debug( + "Setting interface {parent} as a parent of bridge {name}".format( + name=bridged_int.name, master=parent_int.name + ) + ) + parent_int.bridge = bridged_int + parent_int.save() + bridged_int.save() + else: + return False + return True + def get_network_cards(self): return self.nics @@ -651,6 +687,7 @@ def batched(it, n): interface.save() self._set_bonding_interfaces() + self._set_bridged_interfaces() logging.debug("Finished updating NIC!") From f27c2c050f8e345ab3f06427f73a37a01a7e6930 Mon Sep 17 00:00:00 2001 From: Charles Luzzato Date: Tue, 17 Jun 2025 23:01:17 +0200 Subject: [PATCH 25/69] Fixed a few issues --- netbox_agent/network.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/netbox_agent/network.py b/netbox_agent/network.py index 5eb65f86..d48a79d7 100644 --- a/netbox_agent/network.py +++ b/netbox_agent/network.py @@ -194,19 +194,16 @@ def _set_bridged_interfaces(self): bridged_nics = (x for x in self.nics if x["bridged"]) for nic in bridged_nics: bridged_int = self.get_netbox_network_card(nic) - logging.debug("Setting bridg parent interface for {name}".format(name=bridged_int.name)) + logging.debug("Setting bridge interface and properties for {name}".format(name=bridged_int.name)) bridged_int.type = { "value": "bridge", "label": "Bridge"} - for parent_num, parent_int in enumerate( + for parent_int in ( self.get_netbox_network_card(parent_bridge_nic) for parent_bridge_nic in self.nics if parent_bridge_nic["name"] in nic["bridge_parents"] ): - if parent_num == 0: - # First parent, set the parent interface - bridged_int.parent = parent_int logging.debug( "Setting interface {parent} as a parent of bridge {name}".format( - name=bridged_int.name, master=parent_int.name + name=bridged_int.name, parent=parent_int.name ) ) parent_int.bridge = bridged_int From 4ce4b2e85f1a7bfe50495bb668d3767df370ea7a Mon Sep 17 00:00:00 2001 From: Charles Luzzato Date: Tue, 17 Jun 2025 23:20:51 +0200 Subject: [PATCH 26/69] Added parent interface settings for virtual int --- netbox_agent/network.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/netbox_agent/network.py b/netbox_agent/network.py index d48a79d7..cea7b257 100644 --- a/netbox_agent/network.py +++ b/netbox_agent/network.py @@ -29,6 +29,11 @@ def _execute_brctl_cmd(interface_name): "brctl show " + str(interface_name) ) +def _execute_basename_cmd(interface_name): + return subprocess.getoutput( + "echo $(basename $(readlink /sys/class/net/"+ str(interface_name) + "/lower_*)) + ) + class Network(object): def __init__(self, server, *args, **kwargs): self.nics = [] @@ -147,7 +152,10 @@ def scan(self): bridge_parents = brctl[headers[-1]] virtual = Path(f"/sys/class/net/{interface}").resolve().parent == VIRTUAL_NET_FOLDER - + if virtual: + parent = _execute_basename_cmd(interface) + if not parent: + parent = None nic = { "name": interface, "mac": mac, @@ -162,6 +170,7 @@ def scan(self): "mtu": mtu, "bonding": bonding, "bonding_slaves": bonding_slaves, + "parent": parent, "bridged": bridging, "bridge_parents": bridge_parents } @@ -195,7 +204,7 @@ def _set_bridged_interfaces(self): for nic in bridged_nics: bridged_int = self.get_netbox_network_card(nic) logging.debug("Setting bridge interface and properties for {name}".format(name=bridged_int.name)) - bridged_int.type = { "value": "bridge", "label": "Bridge"} + bridged_int.type = "bridge" for parent_int in ( self.get_netbox_network_card(parent_bridge_nic) for parent_bridge_nic in self.nics @@ -634,6 +643,15 @@ def batched(it, n): interface.mtu = nic["mtu"] nic_update += 1 + if hasattr(interface, "parent") and interface.parent is not None: + if nic["parent"] != interface.parent: + logging.info( + "Interface parent is wrong, updating to: {parent}".format(parent=nic["parent"]) + ) + int_parent = get_netbox_network_card(interface.parent) + interface.parent = {"name": int_parent.name, "id": int_parent.id} + nic_update += 1 + if not isinstance(self, VirtualNetwork) and nic.get("ethtool"): if ( nic["ethtool"]["duplex"] != "-" From b058c8f36b0c1a90d3c0a3fbe57458bd307d22e4 Mon Sep 17 00:00:00 2001 From: Charles Luzzato Date: Tue, 17 Jun 2025 23:25:43 +0200 Subject: [PATCH 27/69] Fixed indents --- netbox_agent/network.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/netbox_agent/network.py b/netbox_agent/network.py index cea7b257..f0edfc2a 100644 --- a/netbox_agent/network.py +++ b/netbox_agent/network.py @@ -31,7 +31,7 @@ def _execute_brctl_cmd(interface_name): def _execute_basename_cmd(interface_name): return subprocess.getoutput( - "echo $(basename $(readlink /sys/class/net/"+ str(interface_name) + "/lower_*)) + "echo $(basename $(readlink /sys/class/net/"+ str(interface_name) + "/lower_*))" ) class Network(object): @@ -138,18 +138,18 @@ def scan(self): outputbrctl = _execute_brctl_cmd(interface) lineparse_output = outputbrctl.splitlines() if len(lineparse_output)>1: - headers = lineparse_output[0].replace("\t\t", "\t").split("\t") - brctl = dict((key, []) for key in headers) - # Interface is a bridge - bridging = True - for spec_line in lineparse_output[1:]: - cleaned_spec_line = spec_line.replace("\t\t\t\t\t\t\t", "\t\t\t\t\t\t") - cleaned_spec_line = cleaned_spec_line.replace("\t\t", "\t").split("\t") - for col, val in enumerate(cleaned_spec_line): - if val: - brctl[headers[col]].append(val) - - bridge_parents = brctl[headers[-1]] + headers = lineparse_output[0].replace("\t\t", "\t").split("\t") + brctl = dict((key, []) for key in headers) + # Interface is a bridge + bridging = True + for spec_line in lineparse_output[1:]: + cleaned_spec_line = spec_line.replace("\t\t\t\t\t\t\t", "\t\t\t\t\t\t") + cleaned_spec_line = cleaned_spec_line.replace("\t\t", "\t").split("\t") + for col, val in enumerate(cleaned_spec_line): + if val: + brctl[headers[col]].append(val) + + bridge_parents = brctl[headers[-1]] virtual = Path(f"/sys/class/net/{interface}").resolve().parent == VIRTUAL_NET_FOLDER if virtual: From 51771de3ba64ee3555da27efac078e342779fdf1 Mon Sep 17 00:00:00 2001 From: Charles Luzzato Date: Tue, 17 Jun 2025 23:26:45 +0200 Subject: [PATCH 28/69] Fixed indetns2 --- netbox_agent/network.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/netbox_agent/network.py b/netbox_agent/network.py index f0edfc2a..b28de46b 100644 --- a/netbox_agent/network.py +++ b/netbox_agent/network.py @@ -143,11 +143,11 @@ def scan(self): # Interface is a bridge bridging = True for spec_line in lineparse_output[1:]: - cleaned_spec_line = spec_line.replace("\t\t\t\t\t\t\t", "\t\t\t\t\t\t") - cleaned_spec_line = cleaned_spec_line.replace("\t\t", "\t").split("\t") - for col, val in enumerate(cleaned_spec_line): - if val: - brctl[headers[col]].append(val) + cleaned_spec_line = spec_line.replace("\t\t\t\t\t\t\t", "\t\t\t\t\t\t") + cleaned_spec_line = cleaned_spec_line.replace("\t\t", "\t").split("\t") + for col, val in enumerate(cleaned_spec_line): + if val: + brctl[headers[col]].append(val) bridge_parents = brctl[headers[-1]] From ecc67fbf26c2cfb83409d506c3af932bffae9605 Mon Sep 17 00:00:00 2001 From: Charles Luzzato Date: Tue, 17 Jun 2025 23:28:28 +0200 Subject: [PATCH 29/69] Fixed indent3 --- 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 b28de46b..c418e4f7 100644 --- a/netbox_agent/network.py +++ b/netbox_agent/network.py @@ -152,10 +152,10 @@ def scan(self): bridge_parents = brctl[headers[-1]] virtual = Path(f"/sys/class/net/{interface}").resolve().parent == VIRTUAL_NET_FOLDER - if virtual: - parent = _execute_basename_cmd(interface) - if not parent: - parent = None + if virtual: + parent = _execute_basename_cmd(interface) + if not parent: + parent = None nic = { "name": interface, "mac": mac, From b6ec1e66ed510c1ce7b801c7c518b88b7acdcdad Mon Sep 17 00:00:00 2001 From: Charles Luzzato Date: Tue, 17 Jun 2025 23:29:25 +0200 Subject: [PATCH 30/69] Added default parent value --- netbox_agent/network.py | 1 + 1 file changed, 1 insertion(+) diff --git a/netbox_agent/network.py b/netbox_agent/network.py index c418e4f7..bf3fd2f8 100644 --- a/netbox_agent/network.py +++ b/netbox_agent/network.py @@ -152,6 +152,7 @@ def scan(self): bridge_parents = brctl[headers[-1]] virtual = Path(f"/sys/class/net/{interface}").resolve().parent == VIRTUAL_NET_FOLDER + parent = None if virtual: parent = _execute_basename_cmd(interface) if not parent: From 4c6372868ec30feb0c5a8d69f3aa7d8273f09e78 Mon Sep 17 00:00:00 2001 From: Charles Luzzato Date: Tue, 17 Jun 2025 23:40:43 +0200 Subject: [PATCH 31/69] Fixed parent settings --- netbox_agent/network.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/netbox_agent/network.py b/netbox_agent/network.py index bf3fd2f8..7e984ee3 100644 --- a/netbox_agent/network.py +++ b/netbox_agent/network.py @@ -31,7 +31,7 @@ def _execute_brctl_cmd(interface_name): def _execute_basename_cmd(interface_name): return subprocess.getoutput( - "echo $(basename $(readlink /sys/class/net/"+ str(interface_name) + "/lower_*))" + 'echo $(basename $(readlink /sys/class/net/' + str(interface_name) + '/lower_*))' ) class Network(object): @@ -212,12 +212,13 @@ def _set_bridged_interfaces(self): if parent_bridge_nic["name"] in nic["bridge_parents"] ): logging.debug( - "Setting interface {parent} as a parent of bridge {name}".format( + "Setting interface {parent} as a bridge to {name}".format( name=bridged_int.name, parent=parent_int.name ) ) parent_int.bridge = bridged_int parent_int.save() + bridged_int.bridge = parent_int bridged_int.save() else: return False @@ -644,14 +645,13 @@ def batched(it, n): interface.mtu = nic["mtu"] nic_update += 1 - if hasattr(interface, "parent") and interface.parent is not None: - if nic["parent"] != interface.parent: - logging.info( - "Interface parent is wrong, updating to: {parent}".format(parent=nic["parent"]) - ) - int_parent = get_netbox_network_card(interface.parent) - interface.parent = {"name": int_parent.name, "id": int_parent.id} - nic_update += 1 + if nic["parent"] and nic["parent"] != interface.parent: + logging.info( + "Interface parent is wrong, updating to: {parent}".format(parent=nic["parent"]) + ) + int_parent = get_netbox_network_card(nic["parent"]) + interface.parent = {"name": int_parent.name, "id": int_parent.id} + nic_update += 1 if not isinstance(self, VirtualNetwork) and nic.get("ethtool"): if ( From d9d52e837381fc622c3d2a8ee5d9e6a997e426e5 Mon Sep 17 00:00:00 2001 From: Charles Luzzato Date: Tue, 17 Jun 2025 23:49:08 +0200 Subject: [PATCH 32/69] Fixes for symmetric bridge --- 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 7e984ee3..8e237a39 100644 --- a/netbox_agent/network.py +++ b/netbox_agent/network.py @@ -31,7 +31,7 @@ def _execute_brctl_cmd(interface_name): def _execute_basename_cmd(interface_name): return subprocess.getoutput( - 'echo $(basename $(readlink /sys/class/net/' + str(interface_name) + '/lower_*))' + ['echo', '$(basename $(readlink /sys/class/net/' + str(interface_name) + '/lower_*))'] ) class Network(object): @@ -218,7 +218,7 @@ def _set_bridged_interfaces(self): ) parent_int.bridge = bridged_int parent_int.save() - bridged_int.bridge = parent_int + bridged_int.bridge = {"name": parent_int.name, "id": parent_int.id} bridged_int.save() else: return False @@ -649,7 +649,7 @@ def batched(it, n): logging.info( "Interface parent is wrong, updating to: {parent}".format(parent=nic["parent"]) ) - int_parent = get_netbox_network_card(nic["parent"]) + int_parent = self.get_netbox_network_card(nic["parent"]) interface.parent = {"name": int_parent.name, "id": int_parent.id} nic_update += 1 From ffe44b345667ccacb44578686da51538a89e24db Mon Sep 17 00:00:00 2001 From: Charles Luzzato Date: Wed, 18 Jun 2025 00:00:02 +0200 Subject: [PATCH 33/69] Fix parent detection --- 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 8e237a39..a558c012 100644 --- a/netbox_agent/network.py +++ b/netbox_agent/network.py @@ -1,6 +1,7 @@ import logging import os import re +import glob from itertools import chain, islice from pathlib import Path @@ -30,9 +31,8 @@ def _execute_brctl_cmd(interface_name): ) def _execute_basename_cmd(interface_name): - return subprocess.getoutput( - ['echo', '$(basename $(readlink /sys/class/net/' + str(interface_name) + '/lower_*))'] - ) + parent_int = os.path.basename(glob.glob("/sys/class/net/" + str(interface_name) + "/lower_*")).split("_")[1] + return parent_int class Network(object): def __init__(self, server, *args, **kwargs): From 5b19065559c39443753bdee059c8c45540d8c775 Mon Sep 17 00:00:00 2001 From: Charles Luzzato Date: Wed, 18 Jun 2025 00:08:51 +0200 Subject: [PATCH 34/69] Fixed paretn nix def --- 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 a558c012..b9b4b8b1 100644 --- a/netbox_agent/network.py +++ b/netbox_agent/network.py @@ -31,7 +31,10 @@ def _execute_brctl_cmd(interface_name): ) def _execute_basename_cmd(interface_name): - parent_int = os.path.basename(glob.glob("/sys/class/net/" + str(interface_name) + "/lower_*")).split("_")[1] + parent_int = None + parent_list = glob.glob("/sys/class/net/" + str(interface_name) + "/lower_*") + if len(parent_list)>0: + parent_int = os.path.basename(parent_list[0]).split("_")[1] return parent_int class Network(object): @@ -649,7 +652,9 @@ def batched(it, n): logging.info( "Interface parent is wrong, updating to: {parent}".format(parent=nic["parent"]) ) - int_parent = self.get_netbox_network_card(nic["parent"]) + nic_parent = { self.get_netbox_network_card(parent_bridge_nic) for parent_bridge_nic in self.nics + if parent_bridge_nic["name"] in nic["bridge_parents"] } [0] + int_parent = self.get_netbox_network_card(nic_parent) interface.parent = {"name": int_parent.name, "id": int_parent.id} nic_update += 1 From be43afe59eccd4d75bcc5d7ebfebb49b3d952504 Mon Sep 17 00:00:00 2001 From: Charles Luzzato Date: Wed, 18 Jun 2025 00:11:33 +0200 Subject: [PATCH 35/69] Fix again --- netbox_agent/network.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/netbox_agent/network.py b/netbox_agent/network.py index b9b4b8b1..8718bcc6 100644 --- a/netbox_agent/network.py +++ b/netbox_agent/network.py @@ -652,8 +652,10 @@ def batched(it, n): logging.info( "Interface parent is wrong, updating to: {parent}".format(parent=nic["parent"]) ) - nic_parent = { self.get_netbox_network_card(parent_bridge_nic) for parent_bridge_nic in self.nics - if parent_bridge_nic["name"] in nic["bridge_parents"] } [0] + for parent_nic in self.nics : + if parent_nic["name"] in nic["parent"] + nic_parent = self.get_netbox_network_card(parent_nic) + break int_parent = self.get_netbox_network_card(nic_parent) interface.parent = {"name": int_parent.name, "id": int_parent.id} nic_update += 1 From 1ae646645e5f5417f7fe490c64e7611dc0a974f4 Mon Sep 17 00:00:00 2001 From: Charles Luzzato Date: Wed, 18 Jun 2025 00:17:40 +0200 Subject: [PATCH 36/69] Parents can only be set for virtaul interfaces --- netbox_agent/network.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/netbox_agent/network.py b/netbox_agent/network.py index 8718bcc6..dab348eb 100644 --- a/netbox_agent/network.py +++ b/netbox_agent/network.py @@ -136,6 +136,8 @@ def scan(self): open("/sys/class/net/{}/bonding/slaves".format(interface)).read().split() ) + virtual = Path(f"/sys/class/net/{interface}").resolve().parent == VIRTUAL_NET_FOLDER + bridging = False bridge_parents = [] outputbrctl = _execute_brctl_cmd(interface) @@ -145,6 +147,7 @@ def scan(self): brctl = dict((key, []) for key in headers) # Interface is a bridge bridging = True + virtual = False for spec_line in lineparse_output[1:]: cleaned_spec_line = spec_line.replace("\t\t\t\t\t\t\t", "\t\t\t\t\t\t") cleaned_spec_line = cleaned_spec_line.replace("\t\t", "\t").split("\t") @@ -154,7 +157,6 @@ def scan(self): bridge_parents = brctl[headers[-1]] - virtual = Path(f"/sys/class/net/{interface}").resolve().parent == VIRTUAL_NET_FOLDER parent = None if virtual: parent = _execute_basename_cmd(interface) @@ -648,12 +650,12 @@ def batched(it, n): interface.mtu = nic["mtu"] nic_update += 1 - if nic["parent"] and nic["parent"] != interface.parent: + if nic["virtual"] and nic["parent"] and nic["parent"] != interface.parent: logging.info( "Interface parent is wrong, updating to: {parent}".format(parent=nic["parent"]) ) for parent_nic in self.nics : - if parent_nic["name"] in nic["parent"] + if parent_nic["name"] in nic["parent"]: nic_parent = self.get_netbox_network_card(parent_nic) break int_parent = self.get_netbox_network_card(nic_parent) From baeda4e3ec4c1947c29d688dc96663cc840e13c6 Mon Sep 17 00:00:00 2001 From: Charles Luzzato Date: Wed, 18 Jun 2025 07:12:51 +0200 Subject: [PATCH 37/69] Add the VRF to IP addresses --- 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 dab348eb..4bb4a410 100644 --- a/netbox_agent/network.py +++ b/netbox_agent/network.py @@ -447,7 +447,7 @@ def create_netbox_nic(self, nic, mgmt=False): interface.save() return interface - def create_or_update_netbox_ip_on_interface(self, ip, interface): + def create_or_update_netbox_ip_on_interface(self, ip, interface, vrf): """ Two behaviors: - Anycast IP @@ -470,6 +470,7 @@ def create_or_update_netbox_ip_on_interface(self, ip, interface): "status": "active", "assigned_object_type": self.assigned_object_type, "assigned_object_id": interface.id, + "vrf": vrf, } netbox_ip = nb.ipam.ip_addresses.create(**query_params) @@ -488,6 +489,7 @@ def create_or_update_netbox_ip_on_interface(self, ip, interface): logging.info("Assigning existing Anycast IP {} to interface".format(ip)) netbox_ip = unassigned_anycast_ip[0] netbox_ip.interface = interface + netbox_ip.vrf = vrf netbox_ip.save() # or if everything is assigned to other servers elif not len(assigned_anycast_ip): @@ -499,6 +501,7 @@ def create_or_update_netbox_ip_on_interface(self, ip, interface): "tenant": self.tenant.id if self.tenant else None, "assigned_object_type": self.assigned_object_type, "assigned_object_id": interface.id, + "vrf": vrf, } netbox_ip = nb.ipam.ip_addresses.create(**query_params) return netbox_ip @@ -526,6 +529,7 @@ def create_or_update_netbox_ip_on_interface(self, ip, interface): netbox_ip.assigned_object_type = self.assigned_object_type netbox_ip.assigned_object_id = interface.id + netbox_ip.vrf = vrf netbox_ip.save() def _nic_identifier(self, nic): @@ -620,6 +624,7 @@ def batched(it, n): if nic["mac"]: self.update_interface_macs(interface, [nic["mac"]]) + vrf = None if config.network.vrf and config.network.vrf != str(interface.vrf): logging.info( "Updating interface {interface} VRF to: {vrf}".format( @@ -707,7 +712,7 @@ def batched(it, n): if nic["ip"]: # sync local IPs for ip in nic["ip"]: - self.create_or_update_netbox_ip_on_interface(ip, interface) + self.create_or_update_netbox_ip_on_interface(ip, interface, vrf) if nic_update > 0: interface.save() From 0092e032feb64140a75d3b893df292d840b9b8af Mon Sep 17 00:00:00 2001 From: Charles Luzzato Date: Wed, 18 Jun 2025 07:40:58 +0200 Subject: [PATCH 38/69] Added IP status --- netbox_agent/config.py | 1 + netbox_agent/network.py | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/netbox_agent/config.py b/netbox_agent/config.py index 155f4e2f..3cf535d9 100644 --- a/netbox_agent/config.py +++ b/netbox_agent/config.py @@ -128,6 +128,7 @@ def get_config(): help="What property to use as NIC identifier", ) p.add_argument("--network.vrf", default=None, help="Set VRF for network interfaces") + p.add_argument("--network.status", default="Active", help="Set the status for IP Addresses") p.add_argument( "--network.primary_mac", choices=("permanent", "temp"), diff --git a/netbox_agent/network.py b/netbox_agent/network.py index 4bb4a410..98e879a4 100644 --- a/netbox_agent/network.py +++ b/netbox_agent/network.py @@ -471,6 +471,7 @@ def create_or_update_netbox_ip_on_interface(self, ip, interface, vrf): "assigned_object_type": self.assigned_object_type, "assigned_object_id": interface.id, "vrf": vrf, + "status": config.network.status, } netbox_ip = nb.ipam.ip_addresses.create(**query_params) @@ -490,6 +491,7 @@ def create_or_update_netbox_ip_on_interface(self, ip, interface, vrf): netbox_ip = unassigned_anycast_ip[0] netbox_ip.interface = interface netbox_ip.vrf = vrf + netbox_ip.status = config.network.status netbox_ip.save() # or if everything is assigned to other servers elif not len(assigned_anycast_ip): @@ -502,6 +504,7 @@ def create_or_update_netbox_ip_on_interface(self, ip, interface, vrf): "assigned_object_type": self.assigned_object_type, "assigned_object_id": interface.id, "vrf": vrf, + "status": config.network.status, } netbox_ip = nb.ipam.ip_addresses.create(**query_params) return netbox_ip @@ -530,6 +533,7 @@ def create_or_update_netbox_ip_on_interface(self, ip, interface, vrf): netbox_ip.assigned_object_type = self.assigned_object_type netbox_ip.assigned_object_id = interface.id netbox_ip.vrf = vrf + netbox_ip.status = config.network.status netbox_ip.save() def _nic_identifier(self, nic): From e2de80a9fee6d67edb3981412cee5483606c694c Mon Sep 17 00:00:00 2001 From: Charles Luzzato Date: Wed, 18 Jun 2025 08:03:20 +0200 Subject: [PATCH 39/69] Added required sys import --- netbox_agent/network.py | 1 + 1 file changed, 1 insertion(+) diff --git a/netbox_agent/network.py b/netbox_agent/network.py index 98e879a4..5b534eac 100644 --- a/netbox_agent/network.py +++ b/netbox_agent/network.py @@ -2,6 +2,7 @@ import os import re import glob +import sys from itertools import chain, islice from pathlib import Path From a8f9173de68eeb33b55455922df68d5af6dfcf16 Mon Sep 17 00:00:00 2001 From: Charles Luzzato Date: Wed, 18 Jun 2025 20:36:27 +0200 Subject: [PATCH 40/69] Changes to parent managemnt --- netbox_agent/network.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/netbox_agent/network.py b/netbox_agent/network.py index 5b534eac..199d5049 100644 --- a/netbox_agent/network.py +++ b/netbox_agent/network.py @@ -600,7 +600,7 @@ def batched(it, n): netbox_ip.assigned_object_id = None netbox_ip.save() - # update each nic + # create each nic for nic in self.nics: interface = self.get_netbox_network_card(nic) @@ -610,6 +610,8 @@ def batched(it, n): ) interface = self.create_netbox_nic(nic) + #restart loop once everything has been create for updates + for nic in self.nics: nic_update = 0 ret, interface = self.reset_vlan_on_interface(nic, interface) @@ -666,6 +668,8 @@ def batched(it, n): ) for parent_nic in self.nics : if parent_nic["name"] in nic["parent"]: + print(parent_nic.name) + print(parent_nic) nic_parent = self.get_netbox_network_card(parent_nic) break int_parent = self.get_netbox_network_card(nic_parent) From 8268a4e30a65252b84434dc34cac66086ead481f Mon Sep 17 00:00:00 2001 From: Charles Luzzato Date: Wed, 18 Jun 2025 21:04:00 +0200 Subject: [PATCH 41/69] Changes to the loop onm interfaces --- 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 199d5049..88516eb3 100644 --- a/netbox_agent/network.py +++ b/netbox_agent/network.py @@ -129,16 +129,17 @@ def scan(self): if len(interface.split(".")) > 1: vlan = int(interface.split(".")[1]) + virtual = Path(f"/sys/class/net/{interface}").resolve().parent == VIRTUAL_NET_FOLDER + bonding = False bonding_slaves = [] if os.path.isdir("/sys/class/net/{}/bonding".format(interface)): bonding = True + virtual = False bonding_slaves = ( open("/sys/class/net/{}/bonding/slaves".format(interface)).read().split() ) - virtual = Path(f"/sys/class/net/{interface}").resolve().parent == VIRTUAL_NET_FOLDER - bridging = False bridge_parents = [] outputbrctl = _execute_brctl_cmd(interface) @@ -253,6 +254,9 @@ def get_netbox_type_for_nic(self, nic): if nic.get("virtual"): return self.dcim_choices["interface:type"]["Virtual"] + if nic.get("bridge"): + return self.dcim_choices["interface:type"]["Bridge"] + if nic.get("ethtool") is None: return self.dcim_choices["interface:type"]["Other"] @@ -445,6 +449,7 @@ def create_netbox_nic(self, nic, mgmt=False): switch_ip, switch_interface, interface ) if nic_update: + logging.debug("Saving changes to interface {interface}".format(interface=interface)) interface.save() return interface @@ -601,6 +606,7 @@ def batched(it, n): netbox_ip.save() # create each nic + interfaces = [] for nic in self.nics: interface = self.get_netbox_network_card(nic) @@ -609,9 +615,10 @@ def batched(it, n): "Interface {nic} not found, creating..".format(nic=self._nic_identifier(nic)) ) interface = self.create_netbox_nic(nic) + interfaces.append(interface) #restart loop once everything has been create for updates - for nic in self.nics: + for interface in interfaces: nic_update = 0 ret, interface = self.reset_vlan_on_interface(nic, interface) @@ -668,8 +675,6 @@ def batched(it, n): ) for parent_nic in self.nics : if parent_nic["name"] in nic["parent"]: - print(parent_nic.name) - print(parent_nic) nic_parent = self.get_netbox_network_card(parent_nic) break int_parent = self.get_netbox_network_card(nic_parent) From ec29aa03366bd8c8a63a8cc8df7f4154d05c6598 Mon Sep 17 00:00:00 2001 From: Charles Luzzato Date: Wed, 18 Jun 2025 21:06:22 +0200 Subject: [PATCH 42/69] Corrected loop --- netbox_agent/network.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/netbox_agent/network.py b/netbox_agent/network.py index 88516eb3..05bef3e0 100644 --- a/netbox_agent/network.py +++ b/netbox_agent/network.py @@ -618,7 +618,8 @@ def batched(it, n): interfaces.append(interface) #restart loop once everything has been create for updates - for interface in interfaces: + for index, nic in enumerate(self.nics): + interface = interfaces[index] nic_update = 0 ret, interface = self.reset_vlan_on_interface(nic, interface) From eed894a7da8a7694b5a1fa9faefe094ee62a8c71 Mon Sep 17 00:00:00 2001 From: Charles Luzzato Date: Wed, 18 Jun 2025 21:14:01 +0200 Subject: [PATCH 43/69] added test for virtual field --- netbox_agent/network.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/netbox_agent/network.py b/netbox_agent/network.py index 05bef3e0..e1ef7198 100644 --- a/netbox_agent/network.py +++ b/netbox_agent/network.py @@ -670,17 +670,18 @@ def batched(it, n): interface.mtu = nic["mtu"] nic_update += 1 - if nic["virtual"] and nic["parent"] and nic["parent"] != interface.parent: - logging.info( - "Interface parent is wrong, updating to: {parent}".format(parent=nic["parent"]) - ) - for parent_nic in self.nics : - if parent_nic["name"] in nic["parent"]: - nic_parent = self.get_netbox_network_card(parent_nic) - break - int_parent = self.get_netbox_network_card(nic_parent) - interface.parent = {"name": int_parent.name, "id": int_parent.id} - nic_update += 1 + if hasattr(interface, "virtual"): + if nic["virtual"] and nic["parent"] and nic["parent"] != interface.parent: + logging.info( + "Interface parent is wrong, updating to: {parent}".format(parent=nic["parent"]) + ) + for parent_nic in self.nics : + if parent_nic["name"] in nic["parent"]: + nic_parent = self.get_netbox_network_card(parent_nic) + break + int_parent = self.get_netbox_network_card(nic_parent) + interface.parent = {"name": int_parent.name, "id": int_parent.id} + nic_update += 1 if not isinstance(self, VirtualNetwork) and nic.get("ethtool"): if ( From 513c35e67b16df676b4dfb0ecf17eaac1e6139a2 Mon Sep 17 00:00:00 2001 From: Charles Luzzato Date: Wed, 18 Jun 2025 21:40:16 +0200 Subject: [PATCH 44/69] Added DNS name from fqdn --- netbox_agent/config.py | 5 +++++ netbox_agent/misc.py | 4 ++++ netbox_agent/network.py | 7 ++++++- 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/netbox_agent/config.py b/netbox_agent/config.py index 3cf535d9..6537973d 100644 --- a/netbox_agent/config.py +++ b/netbox_agent/config.py @@ -69,6 +69,11 @@ def get_config(): default=None, help="Command to output hostname, used as Device's name in netbox", ) + p.add_argument( + "--fqdn_cmd", + default=None, + help="Command to output fully qualified domain name, used as Device's DNS name in netbox", + ) p.add_argument( "--device.platform", default=None, diff --git a/netbox_agent/misc.py b/netbox_agent/misc.py index 1419d04c..7c790fb5 100644 --- a/netbox_agent/misc.py +++ b/netbox_agent/misc.py @@ -88,6 +88,10 @@ def get_hostname(config): return "{}".format(socket.gethostname()) return subprocess.getoutput(config.hostname_cmd) +def get_fqdn(config): + if config.fqdn_cmd is None: + return "{}".format(socket.socket.getfqdn()) + return subprocess.getoutput(config.fqdn_cmd) def create_netbox_tags(tags): ret = [] diff --git a/netbox_agent/network.py b/netbox_agent/network.py index e1ef7198..43bbc0c7 100644 --- a/netbox_agent/network.py +++ b/netbox_agent/network.py @@ -16,7 +16,7 @@ from netbox_agent.ethtool import Ethtool from netbox_agent.ipmi import IPMI from netbox_agent.lldp import LLDP -from netbox_agent.misc import is_tool +from netbox_agent.misc import is_tool,get_fqdn VIRTUAL_NET_FOLDER = Path("/sys/devices/virtual/net") @@ -208,6 +208,9 @@ def _set_bonding_interfaces(self): return True def _set_bridged_interfaces(self): + for runner in self.nics: + print(runner["name"]) + print(runner["bridged"]) bridged_nics = (x for x in self.nics if x["bridged"]) for nic in bridged_nics: bridged_int = self.get_netbox_network_card(nic) @@ -478,6 +481,7 @@ def create_or_update_netbox_ip_on_interface(self, ip, interface, vrf): "assigned_object_id": interface.id, "vrf": vrf, "status": config.network.status, + "dns_name": get_fqdn(), } netbox_ip = nb.ipam.ip_addresses.create(**query_params) @@ -511,6 +515,7 @@ def create_or_update_netbox_ip_on_interface(self, ip, interface, vrf): "assigned_object_id": interface.id, "vrf": vrf, "status": config.network.status, + "dns_name": get_fqdn, } netbox_ip = nb.ipam.ip_addresses.create(**query_params) return netbox_ip From 3462d6115980601978f620316a1aa85168be3fad Mon Sep 17 00:00:00 2001 From: Charles Luzzato Date: Wed, 18 Jun 2025 21:42:54 +0200 Subject: [PATCH 45/69] Fix for bridged indicator in IIPMI interface --- netbox_agent/ipmi.py | 1 + netbox_agent/network.py | 3 --- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/netbox_agent/ipmi.py b/netbox_agent/ipmi.py index 8f394b7d..96cdcb04 100644 --- a/netbox_agent/ipmi.py +++ b/netbox_agent/ipmi.py @@ -55,6 +55,7 @@ def parse(self): ret["name"] = "IPMI" ret["mtu"] = 1500 ret["bonding"] = False + ret["bridged"] = False try: ret["mac"] = _ipmi["MAC Address"] if ret["mac"]: diff --git a/netbox_agent/network.py b/netbox_agent/network.py index 43bbc0c7..160a378e 100644 --- a/netbox_agent/network.py +++ b/netbox_agent/network.py @@ -208,9 +208,6 @@ def _set_bonding_interfaces(self): return True def _set_bridged_interfaces(self): - for runner in self.nics: - print(runner["name"]) - print(runner["bridged"]) bridged_nics = (x for x in self.nics if x["bridged"]) for nic in bridged_nics: bridged_int = self.get_netbox_network_card(nic) From b4d5f167e048bafabc936cf7b80c54e73381e765 Mon Sep 17 00:00:00 2001 From: Charles Luzzato Date: Wed, 18 Jun 2025 21:46:44 +0200 Subject: [PATCH 46/69] Missed the IP dns assignement for update --- netbox_agent/network.py | 1 + 1 file changed, 1 insertion(+) diff --git a/netbox_agent/network.py b/netbox_agent/network.py index 160a378e..6084c54e 100644 --- a/netbox_agent/network.py +++ b/netbox_agent/network.py @@ -498,6 +498,7 @@ def create_or_update_netbox_ip_on_interface(self, ip, interface, vrf): netbox_ip = unassigned_anycast_ip[0] netbox_ip.interface = interface netbox_ip.vrf = vrf + netbox_ip.dns_name = get_fqdn() netbox_ip.status = config.network.status netbox_ip.save() # or if everything is assigned to other servers From 9798c62587b86b7fce82b83ddd27deb571fd179c Mon Sep 17 00:00:00 2001 From: Charles Luzzato Date: Wed, 18 Jun 2025 21:52:21 +0200 Subject: [PATCH 47/69] updated dns --- netbox_agent/network.py | 1 + 1 file changed, 1 insertion(+) diff --git a/netbox_agent/network.py b/netbox_agent/network.py index 6084c54e..9a4a7054 100644 --- a/netbox_agent/network.py +++ b/netbox_agent/network.py @@ -542,6 +542,7 @@ def create_or_update_netbox_ip_on_interface(self, ip, interface, vrf): netbox_ip.assigned_object_type = self.assigned_object_type netbox_ip.assigned_object_id = interface.id netbox_ip.vrf = vrf + netbox_ip.dns_name = get_fqdn() netbox_ip.status = config.network.status netbox_ip.save() From cbf63c71c4c8aa6f665659bc8b5812c33f77676d Mon Sep 17 00:00:00 2001 From: Charles Luzzato Date: Wed, 18 Jun 2025 21:58:16 +0200 Subject: [PATCH 48/69] fix socket --- netbox_agent/misc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox_agent/misc.py b/netbox_agent/misc.py index 7c790fb5..1fd15735 100644 --- a/netbox_agent/misc.py +++ b/netbox_agent/misc.py @@ -90,7 +90,7 @@ def get_hostname(config): def get_fqdn(config): if config.fqdn_cmd is None: - return "{}".format(socket.socket.getfqdn()) + return "{}".format(socket.getfqdn()) return subprocess.getoutput(config.fqdn_cmd) def create_netbox_tags(tags): From 4bad9731cd039ae52a1a7ea0adad6aadd80efd14 Mon Sep 17 00:00:00 2001 From: Charles Luzzato Date: Wed, 18 Jun 2025 22:03:51 +0200 Subject: [PATCH 49/69] fix fqdn --- 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 9a4a7054..a06375c4 100644 --- a/netbox_agent/network.py +++ b/netbox_agent/network.py @@ -478,7 +478,7 @@ def create_or_update_netbox_ip_on_interface(self, ip, interface, vrf): "assigned_object_id": interface.id, "vrf": vrf, "status": config.network.status, - "dns_name": get_fqdn(), + "dns_name": get_fqdn(config), } netbox_ip = nb.ipam.ip_addresses.create(**query_params) @@ -498,7 +498,7 @@ def create_or_update_netbox_ip_on_interface(self, ip, interface, vrf): netbox_ip = unassigned_anycast_ip[0] netbox_ip.interface = interface netbox_ip.vrf = vrf - netbox_ip.dns_name = get_fqdn() + netbox_ip.dns_name = get_fqdn(config) netbox_ip.status = config.network.status netbox_ip.save() # or if everything is assigned to other servers @@ -513,7 +513,7 @@ def create_or_update_netbox_ip_on_interface(self, ip, interface, vrf): "assigned_object_id": interface.id, "vrf": vrf, "status": config.network.status, - "dns_name": get_fqdn, + "dns_name": get_fqdn(config), } netbox_ip = nb.ipam.ip_addresses.create(**query_params) return netbox_ip @@ -542,7 +542,7 @@ def create_or_update_netbox_ip_on_interface(self, ip, interface, vrf): netbox_ip.assigned_object_type = self.assigned_object_type netbox_ip.assigned_object_id = interface.id netbox_ip.vrf = vrf - netbox_ip.dns_name = get_fqdn() + netbox_ip.dns_name = get_fqdn(config) netbox_ip.status = config.network.status netbox_ip.save() From 017b184946e66733aafffaa92f8adae61b84598e Mon Sep 17 00:00:00 2001 From: Charles Luzzato Date: Sat, 21 Jun 2025 14:59:05 +0200 Subject: [PATCH 50/69] Updated the prmary mac ssignmenet --- netbox_agent/network.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/netbox_agent/network.py b/netbox_agent/network.py index a06375c4..6b3b8f17 100644 --- a/netbox_agent/network.py +++ b/netbox_agent/network.py @@ -663,7 +663,8 @@ def batched(it, n): if version.parse(nb.version) < version.parse("4.2"): interface.mac_address = nic["mac"] else: - interface.primary_mac_address = {"mac_address": nic["mac"], "assigned_object_id": interface.id} + nb_macs = list(self.nb_net.mac_addresses.filter(interface_id=nic.id)) + interface.primary_mac_address = {"mac_address": nic["mac"], "assigned_object_id": nb_macs[0].id} nic_update += 1 if hasattr(interface, "mtu"): From c84fab5e2e29919b5ce637f745e35727f068cd6b Mon Sep 17 00:00:00 2001 From: Charles Luzzato Date: Sat, 21 Jun 2025 15:02:27 +0200 Subject: [PATCH 51/69] Changed ID --- 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 6b3b8f17..325ff0ba 100644 --- a/netbox_agent/network.py +++ b/netbox_agent/network.py @@ -663,7 +663,7 @@ def batched(it, n): if version.parse(nb.version) < version.parse("4.2"): interface.mac_address = nic["mac"] else: - nb_macs = list(self.nb_net.mac_addresses.filter(interface_id=nic.id)) + nb_macs = list(self.nb_net.mac_addresses.filter(interface_id=interface.id)) interface.primary_mac_address = {"mac_address": nic["mac"], "assigned_object_id": nb_macs[0].id} nic_update += 1 From e3dd8e564d6eabe586ff4971984cc70ee93ce872 Mon Sep 17 00:00:00 2001 From: Charles Luzzato Date: Sat, 21 Jun 2025 16:08:18 +0200 Subject: [PATCH 52/69] Strange error with hasattr virtual. Must be a reserved keyword --- netbox_agent/network.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/netbox_agent/network.py b/netbox_agent/network.py index 325ff0ba..2bbcb4b2 100644 --- a/netbox_agent/network.py +++ b/netbox_agent/network.py @@ -675,7 +675,7 @@ def batched(it, n): interface.mtu = nic["mtu"] nic_update += 1 - if hasattr(interface, "virtual"): + try: if nic["virtual"] and nic["parent"] and nic["parent"] != interface.parent: logging.info( "Interface parent is wrong, updating to: {parent}".format(parent=nic["parent"]) @@ -687,6 +687,8 @@ def batched(it, n): int_parent = self.get_netbox_network_card(nic_parent) interface.parent = {"name": int_parent.name, "id": int_parent.id} nic_update += 1 + except: + print("Failed to set parent interface {parent} for {nic}".format(parent=nic["parent"],nic=nic['name'])) if not isinstance(self, VirtualNetwork) and nic.get("ethtool"): if ( From a992404163b97db924b4d4390946bba06090927e Mon Sep 17 00:00:00 2001 From: Charles Luzzato Date: Sat, 21 Jun 2025 16:09:46 +0200 Subject: [PATCH 53/69] Corrected the print --- 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 2bbcb4b2..4c4bfa41 100644 --- a/netbox_agent/network.py +++ b/netbox_agent/network.py @@ -688,7 +688,7 @@ def batched(it, n): interface.parent = {"name": int_parent.name, "id": int_parent.id} nic_update += 1 except: - print("Failed to set parent interface {parent} for {nic}".format(parent=nic["parent"],nic=nic['name'])) + print("Failed to set parent interface for {nic}".format(nic=nic['name'])) if not isinstance(self, VirtualNetwork) and nic.get("ethtool"): if ( From 781b3616a37f8e397fd1e6352281fccef36aedc6 Mon Sep 17 00:00:00 2001 From: Charles Luzzato Date: Sat, 21 Jun 2025 16:11:10 +0200 Subject: [PATCH 54/69] Fixed primary mac assignment --- 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 4c4bfa41..3f6725d3 100644 --- a/netbox_agent/network.py +++ b/netbox_agent/network.py @@ -664,7 +664,7 @@ def batched(it, n): interface.mac_address = nic["mac"] else: nb_macs = list(self.nb_net.mac_addresses.filter(interface_id=interface.id)) - interface.primary_mac_address = {"mac_address": nic["mac"], "assigned_object_id": nb_macs[0].id} + interface.primary_mac_address = {"assigned_object_id": nb_macs[0].id} nic_update += 1 if hasattr(interface, "mtu"): From 1cc0d1deb1317f26617db2abf2af693e276cfbe4 Mon Sep 17 00:00:00 2001 From: Charles Luzzato Date: Sat, 21 Jun 2025 16:12:55 +0200 Subject: [PATCH 55/69] Typo --- 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 3f6725d3..8c33d34f 100644 --- a/netbox_agent/network.py +++ b/netbox_agent/network.py @@ -664,7 +664,7 @@ def batched(it, n): interface.mac_address = nic["mac"] else: nb_macs = list(self.nb_net.mac_addresses.filter(interface_id=interface.id)) - interface.primary_mac_address = {"assigned_object_id": nb_macs[0].id} + interface.primary_mac_address = {"mac_address": nic["mac"], "id": nb_macs[0].id} nic_update += 1 if hasattr(interface, "mtu"): From 8f50f2fe0993be3206b8ffd0905637f83086d60c Mon Sep 17 00:00:00 2001 From: Charles Luzzato Date: Sat, 21 Jun 2025 16:22:38 +0200 Subject: [PATCH 56/69] Set primary ipv4 and ipv6 with first available ip --- netbox_agent/server.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/netbox_agent/server.py b/netbox_agent/server.py index 7238cb61..f0480c43 100644 --- a/netbox_agent/server.py +++ b/netbox_agent/server.py @@ -568,11 +568,24 @@ def netbox_create_or_update(self, config): myips = nb.ipam.ip_addresses.filter(device_id=server.id) update = 0 + # Deal with IPMI for ip in myips: if ip.assigned_object.display == "IPMI" and ip != server.oob_ip: server.oob_ip = ip.id update += 1 break + # Deal with iPV4 + for ip in myips: + if ip.family.value == 4 and ip != server.primary_ip4: + server.primary_ip4 = ip.id + update += 1 + break + # Deal with iPV6 + for ip in myips: + if ip.family.value == 6 and ip != server.primary_ip6: + server.primary_ip6 = ip.id + update += 1 + break if update: server.save() From 13f1282e6832c39b4bfbbbfb141e08b22acbf8b6 Mon Sep 17 00:00:00 2001 From: Charles Luzzato Date: Sat, 21 Jun 2025 16:28:22 +0200 Subject: [PATCH 57/69] fixed ip assignement --- netbox_agent/server.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/netbox_agent/server.py b/netbox_agent/server.py index f0480c43..db9a533c 100644 --- a/netbox_agent/server.py +++ b/netbox_agent/server.py @@ -576,14 +576,14 @@ def netbox_create_or_update(self, config): break # Deal with iPV4 for ip in myips: - if ip.family.value == 4 and ip != server.primary_ip4: - server.primary_ip4 = ip.id + if ip.assigned_object.display != "IPMI" and ip.family.value == 4 and ip != server.primary_ip4: + server.primary_ip4 = {ip.id} update += 1 break # Deal with iPV6 for ip in myips: - if ip.family.value == 6 and ip != server.primary_ip6: - server.primary_ip6 = ip.id + if ip.assigned_object.display != "IPMI" and ip.family.value == 6 and ip != server.primary_ip6: + server.primary_ip6 = {ip.id} update += 1 break From 4474668ec66d0f6cef301952fcd7502a806fd439 Mon Sep 17 00:00:00 2001 From: Charles Luzzato Date: Sat, 21 Jun 2025 16:42:59 +0200 Subject: [PATCH 58/69] Fixed primary ipv4 and ipv6 assignmeent --- netbox_agent/server.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/netbox_agent/server.py b/netbox_agent/server.py index db9a533c..1c51751a 100644 --- a/netbox_agent/server.py +++ b/netbox_agent/server.py @@ -575,15 +575,19 @@ def netbox_create_or_update(self, config): update += 1 break # Deal with iPV4 - for ip in myips: + myip4s = nb.ipam.ip_addresses.filter(device_id=server.id) + for ip in myip4s: + print(ip.family.value) + print(ip.assigned_object.display) if ip.assigned_object.display != "IPMI" and ip.family.value == 4 and ip != server.primary_ip4: - server.primary_ip4 = {ip.id} + server.primary_ip4 = ip.id update += 1 break # Deal with iPV6 - for ip in myips: + myip6s = nb.ipam.ip_addresses.filter(device_id=server.id) + for ip in myip6s: if ip.assigned_object.display != "IPMI" and ip.family.value == 6 and ip != server.primary_ip6: - server.primary_ip6 = {ip.id} + server.primary_ip6 = ip.id update += 1 break From 5b744b40458cae93d6efd4c8f750739bf7c1fe14 Mon Sep 17 00:00:00 2001 From: Charles Luzzato Date: Sat, 21 Jun 2025 17:38:39 +0200 Subject: [PATCH 59/69] Fix possible lack of mgmt interface on switch --- netbox_agent/network.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/netbox_agent/network.py b/netbox_agent/network.py index 8c33d34f..da75f754 100644 --- a/netbox_agent/network.py +++ b/netbox_agent/network.py @@ -675,7 +675,7 @@ def batched(it, n): interface.mtu = nic["mtu"] nic_update += 1 - try: + if hasattr(nic, 'virtual'): if nic["virtual"] and nic["parent"] and nic["parent"] != interface.parent: logging.info( "Interface parent is wrong, updating to: {parent}".format(parent=nic["parent"]) @@ -687,8 +687,6 @@ def batched(it, n): int_parent = self.get_netbox_network_card(nic_parent) interface.parent = {"name": int_parent.name, "id": int_parent.id} nic_update += 1 - except: - print("Failed to set parent interface for {nic}".format(nic=nic['name'])) if not isinstance(self, VirtualNetwork) and nic.get("ethtool"): if ( @@ -840,7 +838,10 @@ def create_or_update_cable(self, switch_ip, switch_interface, nb_server_interfac 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) + if hasattr(nb_mgmt_int, "id"): + nb_mgmt_ip = nb.ipam.ip_addresses.get(interface_id=nb_mgmt_int.id) + else: + nb_mgmt_ip = None if nb_mgmt_ip is None: logging.error( "Switch {switch_ip} does not have IP on its management interface".format( From 85b0730f11e706029c06d36393427281daefcf6b Mon Sep 17 00:00:00 2001 From: Charles Luzzato Date: Sat, 21 Jun 2025 18:04:06 +0200 Subject: [PATCH 60/69] Some PSU names come back as all, so removing this case --- 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 c574ca27..acce49d9 100644 --- a/netbox_agent/power.py +++ b/netbox_agent/power.py @@ -79,7 +79,7 @@ def create_or_update_power_supply(self): nb_psu.save() for psu in psus: - if psu["name"] not in [x.name for x in nb_psus]: + if (psu["name"] not in [x.name for x in nb_psus]) and ( psu["name"] not in "__all__") : logging.info("Creating PSU {name} ({description}), {maximum_draw}W".format(**psu)) nb_psu = nb.dcim.power_ports.create(**psu) From e4c3464bf886c7d8cebfe3c089d1bffae91f1496 Mon Sep 17 00:00:00 2001 From: Charles Luzzato Date: Mon, 23 Jun 2025 20:29:42 +0200 Subject: [PATCH 61/69] added the cat proc option --- netbox_agent/dmidecode.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/netbox_agent/dmidecode.py b/netbox_agent/dmidecode.py index a6fc8eb1..746ee91a 100644 --- a/netbox_agent/dmidecode.py +++ b/netbox_agent/dmidecode.py @@ -62,7 +62,8 @@ def parse(output=None): """ - parse the full output of the dmidecode + parse the full output of the dmidecode on normal systems, + or cat /proc/cpuinfo for raspberry pi command and return a dic containing the parsed information """ if output: @@ -149,6 +150,13 @@ def _execute_cmd(): stderr=_subprocess.PIPE, ) +def _execute_cmd_pi(): + return _subprocess.check_output([ + "cat", + "/proc/cpuinfo" + ], + stderr=_subprocess.PIPE, + ) def _parse(buffer): output_data = {} From 0f3cde6cb7ecb870f0cc6453b0977a04e1b6b5f0 Mon Sep 17 00:00:00 2001 From: Charles Luzzato Date: Sat, 28 Jun 2025 18:26:16 +0200 Subject: [PATCH 62/69] Added support for RPI --- netbox_agent/dmidecode.py | 102 +++++++++++++++++++++++++++++++++-- netbox_agent/lshw.py | 111 +++++++++++++++++++++++++++++--------- netbox_agent/server.py | 1 + 3 files changed, 186 insertions(+), 28 deletions(-) diff --git a/netbox_agent/dmidecode.py b/netbox_agent/dmidecode.py index 746ee91a..887e8791 100644 --- a/netbox_agent/dmidecode.py +++ b/netbox_agent/dmidecode.py @@ -63,7 +63,7 @@ def parse(output=None): """ parse the full output of the dmidecode on normal systems, - or cat /proc/cpuinfo for raspberry pi + or raspinfo for raspberry pi command and return a dic containing the parsed information """ if output: @@ -72,7 +72,14 @@ def parse(output=None): buffer = _execute_cmd() if isinstance(buffer, bytes): buffer = buffer.decode("utf-8") - _data = _parse(buffer) + if buffer.splitlines()[1] == "# No SMBIOS nor DMI entry point found, sorry.": + logging.info( + "Dmidecode does not seem to be finding information on your system. " + "Using raspinfo instead." + ) + _data = _rpi_parse(buffer) + else: + _data = _parse(buffer) return _data @@ -151,9 +158,47 @@ def _execute_cmd(): ) def _execute_cmd_pi(): + return _subprocess.check_output([ + "raspinfo", + ], + stderr=_subprocess.PIPE, + ) + +def _execute_cmd_os_pi(): return _subprocess.check_output([ "cat", - "/proc/cpuinfo" + "/etc/os-release", + "|", + "head", + "-4", + ], + stderr=_subprocess.PIPE, shell=True + ) + +def _execute_cmd_serial_pi(): + output_var = str( _subprocess.check_output([ + "cat", + "/proc/cpuinfo", + ], + stderr=_subprocess.PIPE + ), 'utf-8' ) + return output_var.splitlines()[-3:] + +def _execute_cmd_ram_pi(): + return _subprocess.check_output([ + "cat", + "/proc/meminfo", + "|", + "head", + "-1" + ], + stderr=_subprocess.PIPE, shell=True + ) + +def _execute_cmd_bios_pi(): + return _subprocess.check_output([ + "vcgencmd", + "version", ], stderr=_subprocess.PIPE, ) @@ -232,6 +277,57 @@ def _parse(buffer): return output_data +def _rpi_parse(buffer): + # 0 BIOS + # 1 System + # 2 Baseboard + # 3 Chassis + + handles = [ + "0x001A", + "0x0001", + "0x0000", + "0x001D", + ] + + names = [ + "BIOS", + "System", + "Baseboard", + "Chassis", + ] + + output_data = {} + + for index,cur_name in enumerate(names): + output_data[handles[index]]= {} + output_data[handles[index]]["DMIType"] = int(index) + output_data[handles[index]]["DMISize"] = int(28) + output_data[handles[index]]["DMIName"] = names[index] + + # Define BIOS info + output_data[handles[0]][names[0]] = _execute_cmd_bios_pi() + output_data[handles[0]]["Version"] = str(_execute_cmd_bios_pi(),"utf-8").splitlines()[-1].split(" ")[1].strip() + + # Define System info + serial_data = _execute_cmd_serial_pi() + revision_number = serial_data[0].split(":")[1].strip() + serial_number = serial_data[1].split(":")[1].strip() + model = serial_data[2].split(":")[1].strip() + output_data[handles[1]]["Product Name"] = model + output_data[handles[1]]["Serial Number"] = serial_number + output_data[handles[1]]["Manufacturer"] = "Raspberry Pi Foundation" + + + # Define Chassis INfo + output_data[handles[3]]["Manufacturer"] = "Raspberry Pi Foundation" + + + if not output_data: + raise ParseError("Unable to parse 'dmidecode' output") + + return output_data + class ParseError(Exception): pass diff --git a/netbox_agent/lshw.py b/netbox_agent/lshw.py index 381c5414..7cf601cb 100644 --- a/netbox_agent/lshw.py +++ b/netbox_agent/lshw.py @@ -3,6 +3,7 @@ import logging import json import sys +import re class LSHW: @@ -26,7 +27,11 @@ def __init__(self): self.power = [] self.disks = [] self.gpus = [] - self.vendor = self.hw_info["vendor"] + if hasattr(self.hw_info, "vendor"): + vendor = self.hw_info["vendor"] + else: + vendor = "Not Specified" + self.vendor = vendor self.product = self.hw_info["product"] self.chassis_serial = self.hw_info["serial"] self.motherboard_serial = self.hw_info["children"][0].get("serial", "No S/N") @@ -94,20 +99,10 @@ def find_network(self, obj): ) 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"), - } - ) - elif "driver" in obj["configuration"] and "nvme" in obj["configuration"]["driver"]: + if "driver" in obj["configuration"] and "nvme" in obj["configuration"]["driver"]: + print("found NVME device") + print("found NVME device") + if not is_tool("nvme"): logging.error("nvme-cli >= 1.0 does not seem to be installed") return @@ -131,22 +126,88 @@ def find_storage(self, obj): self.disks.append(d) except Exception: pass + elif "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"), + } + ) 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", ""), - } - ) + if (bool(re.search(r'cpu\:\d', obj.get("id"))) and obj.get("product") == "cpu"): + # First trey to get more information with lscpu + vendor_name = "Unknown vendor" + cpu_name = "Unknown CPU" + description_detail = "" + if not is_tool("lscpu"): + logging.error("lscpu does not seem to be installed") + try: + lscpu = json.loads( + subprocess.check_output(["lscpu", "-J"], encoding="utf8") + ) + for device in lscpu["lscpu"]: + if device["field"] == "Vendor ID:": + vendor_name = device["data"] + if device["field"] == "Model name:": + cpu_name = device["data"] + if device["field"] == "Architecture:": + description_detail = description_detail + "Architecture: " + device["data"] + " " + if device["field"] == "Flags:": + description_detail = description_detail + "Flags: " + device["data"] + except Exception: + pass + + # In this case each CPU core is counted as a separate entity; overwrite cputoappend entity + temp_cpu_name = obj.get("product", cpu_name) + temp_description = obj.get("description", description_detail), + if temp_cpu_name == "cpu" and cpu_name != "Unknown CPU": + # cpu this is the default name, dont use it if better data is available + temp_cpu_name = cpu_name + if temp_description[0] == "CPU" and description_detail != "": + # CPU this is the default description, dont use it if better data is available + temp_description = description_detail + self.cpus = [ { + "product": temp_cpu_name + " (" + str(int(obj.get("id").split(":")[1])+1) + " Core, " + str(obj.get("size")/1000000) + " MHz)", + "vendor": obj.get("vendor", vendor_name), + "description": temp_description, + "location": obj.get("slot", ""), + }] + else: + 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: - # print("not a DIMM memory.") - return + if obj.get("description") == "System memory": + self.memories.append( + { + # This is probably embedded memory as for a Raspberry pi + "slot": obj.get("slot", "Integrated Memory"), + "description": obj.get("description"), + "id": obj.get("id"), + "serial": obj.get("serial", "N/A"), + "vendor": obj.get("vendor", "N/A"), + "product": obj.get("product", "N/A"), + "size": obj.get("size", 0) / 2**20 / 1024, + }) + return + else: + # print("not a DIMM memory or integrated memory.") + return for dimm in obj["children"]: if "empty" in dimm["description"]: diff --git a/netbox_agent/server.py b/netbox_agent/server.py index 1c51751a..322c1c33 100644 --- a/netbox_agent/server.py +++ b/netbox_agent/server.py @@ -18,6 +18,7 @@ import logging import socket import sys +import re class ServerBase: def __init__(self, dmi=None): From 9fa322621a668b62172361e80b8b17e788871f06 Mon Sep 17 00:00:00 2001 From: Charles Luzzato Date: Sat, 28 Jun 2025 18:33:58 +0200 Subject: [PATCH 63/69] Remove uneeded printing --- netbox_agent/lshw.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/netbox_agent/lshw.py b/netbox_agent/lshw.py index 7cf601cb..6d13b2bd 100644 --- a/netbox_agent/lshw.py +++ b/netbox_agent/lshw.py @@ -100,9 +100,6 @@ def find_network(self, obj): def find_storage(self, obj): if "driver" in obj["configuration"] and "nvme" in obj["configuration"]["driver"]: - print("found NVME device") - print("found NVME device") - if not is_tool("nvme"): logging.error("nvme-cli >= 1.0 does not seem to be installed") return From 07e60f00db25725b35d7cf6df510e077a2826964 Mon Sep 17 00:00:00 2001 From: Charles Luzzato Date: Sun, 15 Jun 2025 23:12:15 +0200 Subject: [PATCH 64/69] Add the ability to auto create manufacturer --- netbox_agent/config.py | 1 + netbox_agent/misc.py | 9 +++++- netbox_agent/server.py | 66 ++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 73 insertions(+), 3 deletions(-) diff --git a/netbox_agent/config.py b/netbox_agent/config.py index 48a910de..2eb003d8 100644 --- a/netbox_agent/config.py +++ b/netbox_agent/config.py @@ -90,6 +90,7 @@ def get_config(): "--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("--device.autocreate_device_type", default=True, help="Define whether a device type should be create when it doesnt exist.") 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") diff --git a/netbox_agent/misc.py b/netbox_agent/misc.py index ccfae61b..1419d04c 100644 --- a/netbox_agent/misc.py +++ b/netbox_agent/misc.py @@ -1,9 +1,11 @@ from contextlib import suppress +from netbox_agent.config import config from netbox_agent.config import netbox_instance as nb from slugify import slugify from shutil import which import distro import subprocess +import logging import socket import re @@ -23,7 +25,12 @@ def get_device_role(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)) + if config.device.autocreate_device_type: + logging.info( + 'DeviceType "{}" does not yet exist, it will be created'.format(type) + ) + else: + raise Exception('DeviceType "{}" does not exist, please create it, or set device.autocreate_device_type to true'.format(type)) return device_type diff --git a/netbox_agent/server.py b/netbox_agent/server.py index c25be6ff..7238cb61 100644 --- a/netbox_agent/server.py +++ b/netbox_agent/server.py @@ -13,12 +13,12 @@ from netbox_agent.network import ServerNetwork from netbox_agent.power import PowerSupply from pprint import pprint +from slugify import slugify import subprocess import logging import socket import sys - class ServerBase: def __init__(self, dmi=None): if dmi: @@ -119,6 +119,24 @@ def update_netbox_expansion_location(self, server, expansion): update = True return update + def find_or_create_manufacturer(self, name): + if name is None: + return None + + manufacturer = nb.dcim.manufacturers.get( + name=name, + ) + if not manufacturer: + 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(), + ) + + logging.info("Creating missing manufacturer {name}".format(name=name)) + + return manufacturer + def get_rack(self): rack = Rack() return rack.get() @@ -143,6 +161,13 @@ def get_product_name(self): """ return self.system[0]["Product Name"].strip() + def get_manufacturer(self): + """ + Return the Manufacturer from dmidecode info, and create it if needed + """ + manufacturer = self.find_or_create_manufacturer(self.system[0]["Manufacturer"].strip()) + return manufacturer + def get_service_tag(self): """ Return the Service Tag from dmidecode info @@ -191,8 +216,29 @@ def get_power_consumption(self): def get_expansion_product(self): raise NotImplementedError + def _netbox_create_device_type(self): + manufacturer = self.get_manufacturer() + model = self.get_product_name() + logging.info( + "Creating device type {model}.".format( + model=model + ) + ) + new_device_type = nb.dcim.device_types.create( + manufacturer = nb.dcim.manufacturers.get(name=manufacturer).id, + model = model, + slug=slugify(model) + ) + return new_device_type + def _netbox_create_chassis(self, datacenter, tenant, rack): device_type = get_device_type(self.get_chassis()) + if not device_type: + if config.device.autocreate_device_type: + device_type_create = self._netbox_create_device_type() + device_type = get_device_type(self.get_chassis()) + else: + raise Exception('Chassis "{}" doesn\'t exist'.format(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)) @@ -212,6 +258,12 @@ def _netbox_create_chassis(self, datacenter, tenant, rack): 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()) + if not device_type: + if config.device.autocreate_device_type: + device_type_create = self._netbox_create_device_type() + device_type = get_device_type(self.get_product_name()) + else: + raise Exception('Blade "{}" type doesn\'t exist'.format(self.get_product_name())) serial = self.get_service_tag() hostname = self.get_hostname() logging.info( @@ -236,6 +288,12 @@ def _netbox_create_blade(self, chassis, datacenter, tenant, rack): 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()) + if not device_type: + if config.device.autocreate_device_type: + device_type_create = self._netbox_create_device_type() + device_type = get_device_type(self.get_expansion_product()) + else: + raise Exception('Blade expansion type"{}" doesn\'t exist'.format(self.get_expansion_product())) serial = self.get_expansion_service_tag() hostname = self.get_hostname() + " expansion" logging.info( @@ -271,7 +329,11 @@ 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())) + if config.device.autocreate_device_type: + device_type_create = self._netbox_create_device_type() + device_type = get_device_type(self.get_product_name()) + else: + raise Exception('Server type "{}" doesn\'t exist'.format(self.get_chassis())) serial = self.get_service_tag() hostname = self.get_hostname() logging.info( From f26692d911f58a5f8a141e2a4c0f82666b8b92b2 Mon Sep 17 00:00:00 2001 From: Charles Luzzato Date: Mon, 16 Jun 2025 23:45:41 +0200 Subject: [PATCH 65/69] Need to specify the attached interface id to be able to get the correct MAC when multiple interfaces have the same max --- 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 4cf89668..18e7418a 100644 --- a/netbox_agent/network.py +++ b/netbox_agent/network.py @@ -551,7 +551,7 @@ def batched(it, n): if version.parse(nb.version) < version.parse("4.2"): interface.mac_address = nic["mac"] else: - interface.primary_mac_address = {"mac_address": nic["mac"]} + interface.primary_mac_address = {"mac_address": nic["mac"], "assigned_object_id": interface.id} nic_update += 1 if hasattr(interface, "mtu"): From 3d6c1fa0f263148bf3a251db94d83acc12d211a8 Mon Sep 17 00:00:00 2001 From: Charles Luzzato Date: Tue, 17 Jun 2025 00:11:50 +0200 Subject: [PATCH 66/69] Added set option for VRF --- netbox_agent/config.py | 1 + netbox_agent/network.py | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/netbox_agent/config.py b/netbox_agent/config.py index 2eb003d8..155f4e2f 100644 --- a/netbox_agent/config.py +++ b/netbox_agent/config.py @@ -127,6 +127,7 @@ def get_config(): default="name", help="What property to use as NIC identifier", ) + p.add_argument("--network.vrf", default=None, help="Set VRF for network interfaces") p.add_argument( "--network.primary_mac", choices=("permanent", "temp"), diff --git a/netbox_agent/network.py b/netbox_agent/network.py index 18e7418a..195ce41a 100644 --- a/netbox_agent/network.py +++ b/netbox_agent/network.py @@ -223,6 +223,13 @@ def get_or_create_vlan(self, vlan_id): ) return vlan + def get_vrf_id(self, vrf_name): + vrf = nb.ipam.vrfs.get(name=vrf_name, **self.custom_arg_id) + if vrf: + return vrf.id + else: + return None + def reset_vlan_on_interface(self, nic, interface): update = False vlan_id = nic["vlan"] @@ -542,6 +549,16 @@ def batched(it, n): if nic["mac"]: self.update_interface_macs(interface, [nic["mac"]]) + if config.network.vrf and config.network.vrf != str(interface.vrf): + logging.info( + "Updating interface {interface} VRF to: {vrf}".format( + interface=interface, vrf=config.network.vrf + ) + ) + vrf = self.get_vrf_id(config.network.vrf) + interface.vrf = vrf + nic_update += 1 + if nic["mac"] and nic["mac"] != interface.mac_address: logging.info( "Updating interface {interface} mac to: {mac}".format( From ea22e34a2e5f006e22d2512fe4a5c1a1c2690dce Mon Sep 17 00:00:00 2001 From: Charles Luzzato Date: Tue, 17 Jun 2025 07:35:40 +0200 Subject: [PATCH 67/69] Properly manage bridge virtual interfaces --- netbox_agent/config.py | 1 + netbox_agent/ipmi.py | 1 + netbox_agent/network.py | 115 ++++++++++++++++++++++++++++++++++++++-- 3 files changed, 112 insertions(+), 5 deletions(-) diff --git a/netbox_agent/config.py b/netbox_agent/config.py index 155f4e2f..3cf535d9 100644 --- a/netbox_agent/config.py +++ b/netbox_agent/config.py @@ -128,6 +128,7 @@ def get_config(): help="What property to use as NIC identifier", ) p.add_argument("--network.vrf", default=None, help="Set VRF for network interfaces") + p.add_argument("--network.status", default="Active", help="Set the status for IP Addresses") p.add_argument( "--network.primary_mac", choices=("permanent", "temp"), diff --git a/netbox_agent/ipmi.py b/netbox_agent/ipmi.py index 8f394b7d..96cdcb04 100644 --- a/netbox_agent/ipmi.py +++ b/netbox_agent/ipmi.py @@ -55,6 +55,7 @@ def parse(self): ret["name"] = "IPMI" ret["mtu"] = 1500 ret["bonding"] = False + ret["bridged"] = False try: ret["mac"] = _ipmi["MAC Address"] if ret["mac"]: diff --git a/netbox_agent/network.py b/netbox_agent/network.py index 195ce41a..e1ef7198 100644 --- a/netbox_agent/network.py +++ b/netbox_agent/network.py @@ -1,10 +1,13 @@ import logging import os import re +import glob +import sys from itertools import chain, islice from pathlib import Path import netifaces +import subprocess from netaddr import IPAddress from packaging import version @@ -13,9 +16,27 @@ from netbox_agent.ethtool import Ethtool from netbox_agent.ipmi import IPMI from netbox_agent.lldp import LLDP +from netbox_agent.misc import is_tool VIRTUAL_NET_FOLDER = Path("/sys/devices/virtual/net") +def _execute_brctl_cmd(interface_name): + if not is_tool("brctl"): + logging.error( + "Brctl 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.getoutput( + "brctl show " + str(interface_name) + ) + +def _execute_basename_cmd(interface_name): + parent_int = None + parent_list = glob.glob("/sys/class/net/" + str(interface_name) + "/lower_*") + if len(parent_list)>0: + parent_int = os.path.basename(parent_list[0]).split("_")[1] + return parent_int class Network(object): def __init__(self, server, *args, **kwargs): @@ -108,16 +129,41 @@ def scan(self): if len(interface.split(".")) > 1: vlan = int(interface.split(".")[1]) + virtual = Path(f"/sys/class/net/{interface}").resolve().parent == VIRTUAL_NET_FOLDER + bonding = False bonding_slaves = [] if os.path.isdir("/sys/class/net/{}/bonding".format(interface)): bonding = True + virtual = False bonding_slaves = ( open("/sys/class/net/{}/bonding/slaves".format(interface)).read().split() ) - virtual = Path(f"/sys/class/net/{interface}").resolve().parent == VIRTUAL_NET_FOLDER - + bridging = False + bridge_parents = [] + outputbrctl = _execute_brctl_cmd(interface) + lineparse_output = outputbrctl.splitlines() + if len(lineparse_output)>1: + headers = lineparse_output[0].replace("\t\t", "\t").split("\t") + brctl = dict((key, []) for key in headers) + # Interface is a bridge + bridging = True + virtual = False + for spec_line in lineparse_output[1:]: + cleaned_spec_line = spec_line.replace("\t\t\t\t\t\t\t", "\t\t\t\t\t\t") + cleaned_spec_line = cleaned_spec_line.replace("\t\t", "\t").split("\t") + for col, val in enumerate(cleaned_spec_line): + if val: + brctl[headers[col]].append(val) + + bridge_parents = brctl[headers[-1]] + + parent = None + if virtual: + parent = _execute_basename_cmd(interface) + if not parent: + parent = None nic = { "name": interface, "mac": mac, @@ -132,6 +178,9 @@ def scan(self): "mtu": mtu, "bonding": bonding, "bonding_slaves": bonding_slaves, + "parent": parent, + "bridged": bridging, + "bridge_parents": bridge_parents } nics.append(nic) return nics @@ -158,6 +207,30 @@ def _set_bonding_interfaces(self): return False return True + def _set_bridged_interfaces(self): + bridged_nics = (x for x in self.nics if x["bridged"]) + for nic in bridged_nics: + bridged_int = self.get_netbox_network_card(nic) + logging.debug("Setting bridge interface and properties for {name}".format(name=bridged_int.name)) + bridged_int.type = "bridge" + for parent_int in ( + self.get_netbox_network_card(parent_bridge_nic) + for parent_bridge_nic in self.nics + if parent_bridge_nic["name"] in nic["bridge_parents"] + ): + logging.debug( + "Setting interface {parent} as a bridge to {name}".format( + name=bridged_int.name, parent=parent_int.name + ) + ) + parent_int.bridge = bridged_int + parent_int.save() + bridged_int.bridge = {"name": parent_int.name, "id": parent_int.id} + bridged_int.save() + else: + return False + return True + def get_network_cards(self): return self.nics @@ -181,6 +254,9 @@ def get_netbox_type_for_nic(self, nic): if nic.get("virtual"): return self.dcim_choices["interface:type"]["Virtual"] + if nic.get("bridge"): + return self.dcim_choices["interface:type"]["Bridge"] + if nic.get("ethtool") is None: return self.dcim_choices["interface:type"]["Other"] @@ -373,10 +449,11 @@ def create_netbox_nic(self, nic, mgmt=False): switch_ip, switch_interface, interface ) if nic_update: + logging.debug("Saving changes to interface {interface}".format(interface=interface)) interface.save() return interface - def create_or_update_netbox_ip_on_interface(self, ip, interface): + def create_or_update_netbox_ip_on_interface(self, ip, interface, vrf): """ Two behaviors: - Anycast IP @@ -399,6 +476,8 @@ def create_or_update_netbox_ip_on_interface(self, ip, interface): "status": "active", "assigned_object_type": self.assigned_object_type, "assigned_object_id": interface.id, + "vrf": vrf, + "status": config.network.status, } netbox_ip = nb.ipam.ip_addresses.create(**query_params) @@ -417,6 +496,8 @@ def create_or_update_netbox_ip_on_interface(self, ip, interface): logging.info("Assigning existing Anycast IP {} to interface".format(ip)) netbox_ip = unassigned_anycast_ip[0] netbox_ip.interface = interface + netbox_ip.vrf = vrf + netbox_ip.status = config.network.status netbox_ip.save() # or if everything is assigned to other servers elif not len(assigned_anycast_ip): @@ -428,6 +509,8 @@ def create_or_update_netbox_ip_on_interface(self, ip, interface): "tenant": self.tenant.id if self.tenant else None, "assigned_object_type": self.assigned_object_type, "assigned_object_id": interface.id, + "vrf": vrf, + "status": config.network.status, } netbox_ip = nb.ipam.ip_addresses.create(**query_params) return netbox_ip @@ -455,6 +538,8 @@ def create_or_update_netbox_ip_on_interface(self, ip, interface): netbox_ip.assigned_object_type = self.assigned_object_type netbox_ip.assigned_object_id = interface.id + netbox_ip.vrf = vrf + netbox_ip.status = config.network.status netbox_ip.save() def _nic_identifier(self, nic): @@ -520,7 +605,8 @@ def batched(it, n): netbox_ip.assigned_object_id = None netbox_ip.save() - # update each nic + # create each nic + interfaces = [] for nic in self.nics: interface = self.get_netbox_network_card(nic) @@ -529,7 +615,11 @@ def batched(it, n): "Interface {nic} not found, creating..".format(nic=self._nic_identifier(nic)) ) interface = self.create_netbox_nic(nic) + interfaces.append(interface) + #restart loop once everything has been create for updates + for index, nic in enumerate(self.nics): + interface = interfaces[index] nic_update = 0 ret, interface = self.reset_vlan_on_interface(nic, interface) @@ -549,6 +639,7 @@ def batched(it, n): if nic["mac"]: self.update_interface_macs(interface, [nic["mac"]]) + vrf = None if config.network.vrf and config.network.vrf != str(interface.vrf): logging.info( "Updating interface {interface} VRF to: {vrf}".format( @@ -579,6 +670,19 @@ def batched(it, n): interface.mtu = nic["mtu"] nic_update += 1 + if hasattr(interface, "virtual"): + if nic["virtual"] and nic["parent"] and nic["parent"] != interface.parent: + logging.info( + "Interface parent is wrong, updating to: {parent}".format(parent=nic["parent"]) + ) + for parent_nic in self.nics : + if parent_nic["name"] in nic["parent"]: + nic_parent = self.get_netbox_network_card(parent_nic) + break + int_parent = self.get_netbox_network_card(nic_parent) + interface.parent = {"name": int_parent.name, "id": int_parent.id} + nic_update += 1 + if not isinstance(self, VirtualNetwork) and nic.get("ethtool"): if ( nic["ethtool"]["duplex"] != "-" @@ -624,11 +728,12 @@ def batched(it, n): if nic["ip"]: # sync local IPs for ip in nic["ip"]: - self.create_or_update_netbox_ip_on_interface(ip, interface) + self.create_or_update_netbox_ip_on_interface(ip, interface, vrf) if nic_update > 0: interface.save() self._set_bonding_interfaces() + self._set_bridged_interfaces() logging.debug("Finished updating NIC!") From d1dce0d072841c4a99537304176bb5aa6e4ffd51 Mon Sep 17 00:00:00 2001 From: Charles Luzzato Date: Wed, 18 Jun 2025 21:40:16 +0200 Subject: [PATCH 68/69] Added DNS name from fqdn --- netbox_agent/config.py | 5 +++++ netbox_agent/misc.py | 4 ++++ netbox_agent/network.py | 9 ++++++++- 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/netbox_agent/config.py b/netbox_agent/config.py index 3cf535d9..6537973d 100644 --- a/netbox_agent/config.py +++ b/netbox_agent/config.py @@ -69,6 +69,11 @@ def get_config(): default=None, help="Command to output hostname, used as Device's name in netbox", ) + p.add_argument( + "--fqdn_cmd", + default=None, + help="Command to output fully qualified domain name, used as Device's DNS name in netbox", + ) p.add_argument( "--device.platform", default=None, diff --git a/netbox_agent/misc.py b/netbox_agent/misc.py index 1419d04c..1fd15735 100644 --- a/netbox_agent/misc.py +++ b/netbox_agent/misc.py @@ -88,6 +88,10 @@ def get_hostname(config): return "{}".format(socket.gethostname()) return subprocess.getoutput(config.hostname_cmd) +def get_fqdn(config): + if config.fqdn_cmd is None: + return "{}".format(socket.getfqdn()) + return subprocess.getoutput(config.fqdn_cmd) def create_netbox_tags(tags): ret = [] diff --git a/netbox_agent/network.py b/netbox_agent/network.py index e1ef7198..e58b68c3 100644 --- a/netbox_agent/network.py +++ b/netbox_agent/network.py @@ -16,7 +16,7 @@ from netbox_agent.ethtool import Ethtool from netbox_agent.ipmi import IPMI from netbox_agent.lldp import LLDP -from netbox_agent.misc import is_tool +from netbox_agent.misc import is_tool,get_fqdn VIRTUAL_NET_FOLDER = Path("/sys/devices/virtual/net") @@ -208,6 +208,9 @@ def _set_bonding_interfaces(self): return True def _set_bridged_interfaces(self): + for runner in self.nics: + print(runner["name"]) + print(runner["bridged"]) bridged_nics = (x for x in self.nics if x["bridged"]) for nic in bridged_nics: bridged_int = self.get_netbox_network_card(nic) @@ -478,6 +481,7 @@ def create_or_update_netbox_ip_on_interface(self, ip, interface, vrf): "assigned_object_id": interface.id, "vrf": vrf, "status": config.network.status, + "dns_name": get_fqdn(config), } netbox_ip = nb.ipam.ip_addresses.create(**query_params) @@ -497,6 +501,7 @@ def create_or_update_netbox_ip_on_interface(self, ip, interface, vrf): netbox_ip = unassigned_anycast_ip[0] netbox_ip.interface = interface netbox_ip.vrf = vrf + netbox_ip.dns_name = get_fqdn(config) netbox_ip.status = config.network.status netbox_ip.save() # or if everything is assigned to other servers @@ -511,6 +516,7 @@ def create_or_update_netbox_ip_on_interface(self, ip, interface, vrf): "assigned_object_id": interface.id, "vrf": vrf, "status": config.network.status, + "dns_name": get_fqdn(config), } netbox_ip = nb.ipam.ip_addresses.create(**query_params) return netbox_ip @@ -539,6 +545,7 @@ def create_or_update_netbox_ip_on_interface(self, ip, interface, vrf): netbox_ip.assigned_object_type = self.assigned_object_type netbox_ip.assigned_object_id = interface.id netbox_ip.vrf = vrf + netbox_ip.dns_name = get_fqdn(config) netbox_ip.status = config.network.status netbox_ip.save() From 8e09b133d97d3795e6ad0faa3b3655866281361e Mon Sep 17 00:00:00 2001 From: Charles Luzzato Date: Sat, 21 Jun 2025 14:59:05 +0200 Subject: [PATCH 69/69] Bugfixes on : Primary mac adress assignment, primary IPv4 and IPv6 assignment, issues with possible lack of mgmt interface on switch --- netbox_agent/network.py | 10 +++++++--- netbox_agent/server.py | 17 +++++++++++++++++ 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/netbox_agent/network.py b/netbox_agent/network.py index a06375c4..da75f754 100644 --- a/netbox_agent/network.py +++ b/netbox_agent/network.py @@ -663,7 +663,8 @@ def batched(it, n): if version.parse(nb.version) < version.parse("4.2"): interface.mac_address = nic["mac"] else: - interface.primary_mac_address = {"mac_address": nic["mac"], "assigned_object_id": interface.id} + nb_macs = list(self.nb_net.mac_addresses.filter(interface_id=interface.id)) + interface.primary_mac_address = {"mac_address": nic["mac"], "id": nb_macs[0].id} nic_update += 1 if hasattr(interface, "mtu"): @@ -674,7 +675,7 @@ def batched(it, n): interface.mtu = nic["mtu"] nic_update += 1 - if hasattr(interface, "virtual"): + if hasattr(nic, 'virtual'): if nic["virtual"] and nic["parent"] and nic["parent"] != interface.parent: logging.info( "Interface parent is wrong, updating to: {parent}".format(parent=nic["parent"]) @@ -837,7 +838,10 @@ def create_or_update_cable(self, switch_ip, switch_interface, nb_server_interfac 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) + if hasattr(nb_mgmt_int, "id"): + nb_mgmt_ip = nb.ipam.ip_addresses.get(interface_id=nb_mgmt_int.id) + else: + nb_mgmt_ip = None if nb_mgmt_ip is None: logging.error( "Switch {switch_ip} does not have IP on its management interface".format( diff --git a/netbox_agent/server.py b/netbox_agent/server.py index 7238cb61..1c51751a 100644 --- a/netbox_agent/server.py +++ b/netbox_agent/server.py @@ -568,11 +568,28 @@ def netbox_create_or_update(self, config): myips = nb.ipam.ip_addresses.filter(device_id=server.id) update = 0 + # Deal with IPMI for ip in myips: if ip.assigned_object.display == "IPMI" and ip != server.oob_ip: server.oob_ip = ip.id update += 1 break + # Deal with iPV4 + myip4s = nb.ipam.ip_addresses.filter(device_id=server.id) + for ip in myip4s: + print(ip.family.value) + print(ip.assigned_object.display) + if ip.assigned_object.display != "IPMI" and ip.family.value == 4 and ip != server.primary_ip4: + server.primary_ip4 = ip.id + update += 1 + break + # Deal with iPV6 + myip6s = nb.ipam.ip_addresses.filter(device_id=server.id) + for ip in myip6s: + if ip.assigned_object.display != "IPMI" and ip.family.value == 6 and ip != server.primary_ip6: + server.primary_ip6 = ip.id + update += 1 + break if update: server.save()