diff --git a/README.md b/README.md index dfb540d..57ddb62 100644 --- a/README.md +++ b/README.md @@ -70,18 +70,26 @@ ZABBIX_TOKEN=othersecrettoken ### Netbox custom fields Use the following custom fields in Netbox (if you are using config context for the template information then the zabbix_template field is not required): ``` -* Type: Integer * Name: zabbix_hostid +* Type: Integer * Required: False * Default: null -* Object: dcim > device +* Content types: DCIM > Device, DCIM > Virtual Chassis (optional) ``` ``` -* Type: Text * Name: zabbix_template +* Type: Text * Required: False * Default: null -* Object: dcim > device_type +* Content types: DCIM > Device Type +``` +``` +* Name: primary_ip +* Type: Object +* Object type: IPAM > IP Address +* Required: False +* Default: null +* Content types: DCIM > Virtual Chassis ``` You can make the `zabbix_hostid` field hidden or read-only to prevent human intervention. @@ -191,6 +199,13 @@ See `config.py.example` for an extensive example map. Any Zabix Inventory fields that are not included in the map will not be touched by the script, so you can safely add manual values or use items to automatically add values to other fields. +### Clustering +When `clustering` is set to `True`, the script will monitor devices within a a Virtual Chassis through the master device. +If needed, when `clusterip` is set to `True`, the script will try to use the custom field name defined in `clusterip_cf` on the Virtual Chassis +as the primary IP for the Master device. This is only true when the master device has no primary ip set. + +Setting this up allows you to use FHRP group assigned floating IPs to monitor your clusters which can be useful for a variety of vendors. + ### Template source You can either use a Netbox device type custom field or Netbox config context for the Zabbix template information. diff --git a/config.py.example b/config.py.example index b856989..fb53ea6 100644 --- a/config.py.example +++ b/config.py.example @@ -15,6 +15,11 @@ device_cf = "zabbix_hostid" ## Enable clustering of devices with virtual chassis setup clustering = False +# Allow monitoring on shared cluster IP if no primary IP has been set on the master device. +# clusterip_cf specifies which custom field contains the IP address object in the Virtual Chassis. +clusterip = False +clusterip_cf = "primary_ip" + ## Enable hostgroup generation. Requires permissions in Zabbix create_hostgroups = True diff --git a/modules/device.py b/modules/device.py index df8918d..b41fa90 100644 --- a/modules/device.py +++ b/modules/device.py @@ -13,6 +13,7 @@ from modules.tools import build_path try: from config import ( template_cf, device_cf, + clustering, clusterip, clusterip_cf, traverse_site_groups, traverse_regions, inventory_sync, @@ -31,7 +32,7 @@ class NetworkDevice(): INPUT: (Netbox device class, ZabbixAPI class, journal flag, NB journal class) """ - def __init__(self, nb, zabbix, nb_journal_class, nb_version, journal=None, logger=None): + def __init__(self, nb, zabbix, nb_journal_class, nb_version, nb_vcs, journal=None, logger=None): self.nb = nb self.id = nb.id self.name = nb.name @@ -53,18 +54,27 @@ class NetworkDevice(): self.inventory_mode = -1 self.inventory = {} self.logger = logger if logger else getLogger(__name__) - self._setBasics() + self._setBasics(nb_vcs) - def _setBasics(self): + def _setBasics(self,nb_vcs): """ Sets basic information like IP address. """ - # Return error if device does not have primary IP. + self.ip = None if self.nb.primary_ip: self.cidr = self.nb.primary_ip.address self.ip = self.cidr.split("/")[0] - else: - e = f"Device {self.name}: no primary IP." + elif bool(self.nb.virtual_chassis) and clustering and clusterip: + devcluster = next((vc for vc in nb_vcs if vc['id'] == self.nb.virtual_chassis.id), None) + if (devcluster and clusterip_cf in devcluster['custom_fields'] and + devcluster['custom_fields'][clusterip_cf]): + self.ip = devcluster['custom_fields'][clusterip_cf]['address'].split("/")[0] + self.logger.debug(f"Device {self.name} is cluster member, " + f"using primary cluster ip address {self.ip}.") + + if not self.ip: + # Return error if device does not have primary IP. + e = f"Device {self.name} no primary IP, skipping this device." self.logger.info(e) raise SyncInventoryError(e) @@ -262,7 +272,7 @@ class NetworkDevice(): raise SyncInventoryError(e) return self.nb.virtual_chassis.master.id - def promoteMasterDevice(self): + def promoteMasterDevice(self,vcs): """ If device is Primary in cluster, promote device name to the cluster name. diff --git a/netbox_zabbix_sync.py b/netbox_zabbix_sync.py index ea95bce..1b5be83 100755 --- a/netbox_zabbix_sync.py +++ b/netbox_zabbix_sync.py @@ -19,7 +19,8 @@ try: zabbix_device_removal, zabbix_device_disable, hostgroup_format, - nb_device_filter + nb_device_filter, + device_cf ) except ModuleNotFoundError: print("Configuration file config.py not found in main directory." @@ -107,6 +108,7 @@ def main(arguments): # Get all Zabbix and Netbox data netbox_devices = netbox.dcim.devices.filter(**nb_device_filter) netbox_site_groups = convert_recordset((netbox.dcim.site_groups.all())) + netbox_virtual_chassis = convert_recordset((netbox.dcim.virtual_chassis.all())) netbox_regions = convert_recordset(netbox.dcim.regions.all()) netbox_journals = netbox.extras.journal_entries zabbix_groups = zabbix.hostgroup.get(output=['groupid', 'name']) @@ -130,21 +132,44 @@ def main(arguments): for nb_device in netbox_devices: try: # Set device instance set data such as hostgroup and template information. + vcobj = None device = NetworkDevice(nb_device, zabbix, netbox_journals, nb_version, - create_journal, logger) + netbox_virtual_chassis, create_journal, logger) device.set_hostgroup(hostgroup_format,netbox_site_groups,netbox_regions) device.set_template(templates_config_context, templates_config_context_overrule) device.set_inventory(nb_device) # Checks if device is part of cluster. # Requires clustering variable if device.isCluster() and clustering: + vcobj = netbox.dcim.virtual_chassis.get(id=device.nb.virtual_chassis.id) # Check if device is primary or secondary - if device.promoteMasterDevice(): + if device.promoteMasterDevice(netbox_virtual_chassis): e = (f"Device {device.name}: is " f"part of cluster and primary.") logger.info(e) + # If the device has a Zabbix host ID, make sure to assign it + # to the Virtual Chassis if that is not yet the case. + if device.zabbix_id and not vcobj.custom_fields[device_cf]: + logger.debug(f"Assigning Zabbix host id {device.zabbix_id}" + f" to Virtual Chassis {device.nb.virtual_chassis['name']}.") + vcobj.custom_fields[device_cf] = int(device.zabbix_id) + vcobj.save() + # If the device does not have a Zabbix host ID, use the Virtual Cluster Zabbix ID + elif vcobj.custom_fields[device_cf] and not device.zabbix_id: + logger.debug(f"Assigning Zabbix host id {vcobj.custom_fields[device_cf]}" + f" to device {device.nb.name} based on cluster membership.") + device.zabbix_id = int(vcobj.custom_fields[device_cf]) + device.nb.custom_fields[device_cf] = device.zabbix_id + device.nb.save() else: - # Device is secondary in cluster. + # if the device has a Zabbix host ID but is no longer master of the Virtual Chassis, + # make sure to cleanup the host ID. + if vcobj.custom_fields[device_cf] and device.zabbix_id: + logger.debug(f"Device {device.name} cleanup old Zabbix host ID" + "{device.zabbix_id} artefact in NetBox.") + device.nb.custom_fields[device_cf] = None + device.nb.save() + # Device is not primary in cluster. # Don't continue with this device. e = (f"Device {device.name}: is part of cluster " f"but not primary. Skipping this host...") @@ -183,6 +208,12 @@ def main(arguments): # Add device to Zabbix device.createInZabbix(zabbix_groups, zabbix_templates, zabbix_proxy_list) + # Make sure to save hostid in Virtual Chassis as well + if device.isCluster() and clustering and device.zabbix_id: + logger.debug(f"Assigning Zabbix host id {device.zabbix_id}" + f" to Virtual Cluster {vcobj.name}") + vcobj.custom_fields[device_cf] = int(device.zabbix_id) + vcobj.save() except SyncError: pass