diff --git a/README.md b/README.md index 007e509..3e72926 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ A script to sync the Netbox device inventory to Zabbix. #### Logging -Logs are generated under sync.log, set the script for debugging / info options etc. +Logs are generated under sync.log, use -v for debugging. #### Hostgroups: manual mode @@ -52,6 +52,63 @@ And this field for the Zabbix template * Default: null * Object: dcim > device_type +#### Set interface parameters within Netbox +When adding a new device, you can set the interface type with custom context. +Due to Zabbix limitations of changing interface type with a linked template, changing the interface type from within Netbox is not supported and the script will generate an error. + +For example when changing a SNMP interface to an Agent interface: +``` +Netbox-Zabbix-sync - WARNING - Device: Interface OUT of sync. +Netbox-Zabbix-sync - ERROR - Device: changing interface type to 1 is not supported. +``` + +To configure the interface parameters you'll need to use custom context. Custom context was used to make this script as customizable as posible for each environment. For example, you could: + * Set the custom context directly on a device + * Set the custom context on a label, which you would add to a device (for instance, SNMPv3) + * Set the custom context on a device role + * Set the custom context on a site or region + +##### Agent interface configuration example +```json +{ + "zabbix": { + "interface_port": 1500, + "interface_type": 1 + } +} +``` +##### SNMPv2 interface configuration example +```json +{ + "zabbix": { + "interface_port": 161, + "interface_type": 2, + "snmp": { + "bulk": 1, + "community": "SecretCommunity", + "version": 2 + } + } +} +``` +##### SNMPv3 interface configuration example +```json +{ + "zabbix": { + "interface_port": 1610, + "interface_type": 2, + "snmp": { + "authpassphrase": "SecretAuth", + "bulk": 1, + "securitylevel": 1, + "securityname": "MySecurityName", + "version": 3 + } + } +} +``` +Note: Not all SNMP data is required for a working configuration. [The following parameters are allowed ](https://www.zabbix.com/documentation/current/manual/api/reference/hostinterface/object#details_tag "The following parameters are allowed ")but are not all required, depending on your environment. + #### Permissions Make sure that the user has proper permissions for device read and modify (modify to set the Zabbix HostID custom field) operations. diff --git a/netbox_zabbix_sync b/netbox_zabbix_sync.py similarity index 64% rename from netbox_zabbix_sync rename to netbox_zabbix_sync.py index 9e7d46c..3a9e299 100755 --- a/netbox_zabbix_sync +++ b/netbox_zabbix_sync.py @@ -23,6 +23,7 @@ logger.addHandler(lgout) logger.addHandler(lgfile) logger.setLevel(logging.WARNING) +# Set template and device Netbox "custom field" names template_cf = "zabbix_template" device_cf = "zabbix_hostid" @@ -138,11 +139,17 @@ class EnvironmentVarError(SyncError): pass +class InterfaceConfigError(SyncError): + pass + + class NetworkDevice(): + """ Represents Network device. INPUT: (Netbox device class, ZabbixAPI class) """ + def __init__(self, nb, zabbix): self.nb = nb self.id = nb.id @@ -265,8 +272,6 @@ class NetworkDevice(): if(group['name'] == self.hostgroup): self.group_id = group['groupid'] e = (f"Found group {group['name']} for host {self.name}.") - #e = (f"Found group ID {str(group['groupid'])} " - # f"for host {self.name}.") logger.debug(e) return True else: @@ -302,6 +307,28 @@ class NetworkDevice(): else: return False + def setInterfaceDetails(self): + """ + Checks interface parameters from Netbox and + creates a model for the interface to be used in Zabbix. + """ + try: + # Initiate interface class + interface = ZabbixInterface(self.nb.config_context, self.ip) + # Check if Netbox has device context. + # If not fall back to old config. + if(interface.get_context()): + # If device is SNMP type, add aditional information. + if(interface.type == 2): + interface.set_snmp() + else: + interface.set_default() + return [interface.interface] + except InterfaceConfigError as e: + e = f"{self.name}: {e}" + logger.warning(e) + raise SyncInventoryError(e) + def createInZabbix(self, groups, templates, description="Host added by Netbox sync script."): """ @@ -314,10 +341,7 @@ class NetworkDevice(): raise SyncInventoryError() self.getZabbixTemplate(templates) # Set interface, group and template configuration - interfaces = [{"type": 2, "main": 1, "useip": 1, - "ip": self.ip, "dns": "", "port": 161, - "details": {"version": 2, "bulk": 0, - "community": "{$SNMP_COMMUNITY}"}}] + interfaces = self.setInterfaceDetails() groups = [{"groupid": self.group_id}] templates = [{"templateid": self.template_id}] # Add host to Zabbix @@ -373,7 +397,9 @@ class NetworkDevice(): self.getZabbixGroup(groups) self.getZabbixTemplate(templates) host = self.zabbix.host.get(filter={'hostid': self.zabbix_id}, - selectInterfaces=["interfaceid", "ip"], + selectInterfaces=['type', 'ip', + 'port', 'details', + 'interfaceid'], selectGroups=["id"], selectParentTemplates=["id"]) if(len(host) > 1): @@ -413,29 +439,151 @@ class NetworkDevice(): logger.warning(f"Device {self.name}: hostgroup OUT of sync.") self.updateZabbixHost(groups={'groupid': self.group_id}) - for interface in host["interfaces"]: - if(interface["ip"] == self.ip): - logger.debug(f"Device {self.name}: IP address in-sync.") - break - else: - if(len(host['interfaces']) == 1): - logger.warning(f"Device {self.name}: IP address OUT of sync.") - int_id = host["interfaces"][0]['interfaceid'] + # If only 1 interface has been found + if(len(host['interfaces']) == 1): + updates = {} + # Go through each key / item and check if it matches Zabbix + for key, item in self.setInterfaceDetails()[0].items(): + # Check if Netbox value is found in Zabbix + if(key in host["interfaces"][0]): + # If SNMP is used, go through nested dict + # to compare SNMP parameters + if(type(item) == dict and key == "details"): + for k, i in item.items(): + if(k in host["interfaces"][0][key]): + # Set update if values don't match + if(host["interfaces"][0][key][k] != str(i)): + # If dict has not been created, add it + if(key not in updates): + updates[key] = {} + updates[key][k] = str(i) + # If SNMP version has been changed + # break loop and force full SNMP update + if(k == "version"): + break + # Force full SNMP config update + # when version has changed. + if(key in updates): + if("version" in updates[key]): + for k, i in item.items(): + updates[key][k] = str(i) + continue + # Set update if values don't match + if(host["interfaces"][0][key] != str(item)): + updates[key] = item + if(updates): + # If interface updates have been found: push to Zabbix + logger.warning(f"Device {self.name}: Interface OUT of sync.") + if("type" in updates): + # Changing interface type not supported. Raise exception. + e = (f"Device {self.name}: changing interface type to " + f"{str(updates['type'])} is not supported.") + logger.error(e) + raise InterfaceConfigError(e) + # Set interfaceID for Zabbix config + updates["interfaceid"] = host["interfaces"][0]['interfaceid'] + logger.debug(f"{self.name}: Updating interface with " + f"config {updates}") try: - self.zabbix.hostinterface.update(interfaceid=int_id, - ip=self.ip) - e = f"Updated host {self.name} with IP {self.ip}." - logger.warning(e) + # API call to Zabbix + self.zabbix.hostinterface.update(updates) + e = f"Solved {self.name} interface conflict." + logger.info(e) except ZabbixAPIException as e: e = f"Zabbix returned the following error: {str(e)}." logger.error(e) raise SyncExternalError(e) else: - e = (f"Device {self.name} has conflicting IP. Host has total " - f"of {len(host['interfaces'])} interfaces. Manual " - "interfention required.") - logger.error(e) - SyncInventoryError(e) + # If no updates are found, Zabbix interface is in-sync + e = f"Device {self.name}: interface in-sync." + logger.debug(e) + else: + e = (f"Device {self.name} has unsupported interface configuration." + f" Host has total of {len(host['interfaces'])} interfaces. " + "Manual interfention required.") + logger.error(e) + SyncInventoryError(e) + + +class ZabbixInterface(): + def __init__(self, context, ip): + self.context = context + self.type = None + self.ip = ip + self.skelet = {"main": "1", "useip": "1", "dns": "", "ip": self.ip} + self.interface = self.skelet + + def get_context(self): + # check if Netbox custom context has been defined. + if("zabbix" in self.context): + try: + zabbix = self.context["zabbix"] + self.interface["type"] = zabbix["interface_type"] + self.interface["port"] = zabbix["interface_port"] + self.type = zabbix["interface_type"] + except KeyError: + e = ("Interface port or type is not defined under " + "config context 'zabbix'.") + raise InterfaceConfigError(e) + return True + else: + return False + + def set_snmp(self): + # Check if interface is type SNMP + if(self.interface["type"] == 2): + # Checks if SNMP settings are defined in Netbox + if("snmp" in self.context["zabbix"]): + snmp = self.context["zabbix"]["snmp"] + self.interface["details"] = {} + # Checks if bulk config has been defined + if(snmp.get("bulk")): + self.interface["details"]["bulk"] = str(snmp.pop("bulk")) + else: + # Fallback to bulk enabled if not specified + self.interface["details"]["bulk"] = "1" + # SNMP Version config is required in Netbox config context + if(snmp.get("version")): + self.interface["details"]["version"] = str(snmp.pop("version")) + else: + e = "SNMP version option is not defined." + raise InterfaceConfigError(e) + # If version 2 is used, get community string + if(self.interface["details"]["version"] == '2'): + if("community" in snmp): + community = snmp["community"] + self.interface["details"]["community"] = str(community) + else: + e = ("No SNMP community string " + "defined in custom context.") + raise InterfaceConfigError(e) + # If version 3 has been used, get all + # SNMPv3 Netbox related configs + elif(self.interface["details"]["version"] == '3'): + items = ["securityname", "securitylevel", "authpassphrase", + "privpassphrase", "authprotocol", "privprotocol", + "contextname"] + for key, item in snmp.items(): + if(key in items): + self.interface["details"][key] = str(item) + else: + e = "Unsupported SNMP version." + raise InterfaceConfigError(e) + else: + e = "Interface type SNMP but no parameters provided." + raise InterfaceConfigError(e) + else: + e = "Interface type is not SNMP, unable to set SNMP details" + raise InterfaceConfigError(e) + + def set_default(self): + # Set default config to SNMPv2,port 161 and community macro. + self.interface = self.skelet + self.interface["type"] = "2" + self.interface["port"] = "161" + self.interface["details"] = {"version": "2", + "community": "{$SNMP_COMMUNITY}", + "bulk": "1"} if(__name__ == "__main__"): @@ -447,14 +595,14 @@ if(__name__ == "__main__"): action="store_true") parser.add_argument("-c", "--cluster", action="store_true", help=("Only add the primary node of a cluster " - "to Zabbix. Usefull when a shared virtual IP is " - "used for the control plane.")) + "to Zabbix. Usefull when a shared virtual IP is " + "used for the control plane.")) parser.add_argument("-H", "--hostgroups", help="Create Zabbix hostgroups if not present", action="store_true") parser.add_argument("-t", "--tenant", action="store_true", help=("Add Tenant name to the Zabbix " - "hostgroup name scheme.")) + "hostgroup name scheme.")) args = parser.parse_args() main(args)