Merge pull request #3 from TheNetworkGuy/Dynamic-interface-parameters

Dynamic interface parameters
This commit is contained in:
Twan K 2021-04-19 21:36:24 +02:00 committed by GitHub
commit 6d9698c40c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 233 additions and 28 deletions

View File

@ -21,7 +21,7 @@ A script to sync the Netbox device inventory to Zabbix.
#### Logging #### 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 #### Hostgroups: manual mode
@ -52,6 +52,63 @@ And this field for the Zabbix template
* Default: null * Default: null
* Object: dcim > device_type * 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 #### Permissions
Make sure that the user has proper permissions for device read and modify (modify to set the Zabbix HostID custom field) operations. Make sure that the user has proper permissions for device read and modify (modify to set the Zabbix HostID custom field) operations.

View File

@ -23,6 +23,7 @@ logger.addHandler(lgout)
logger.addHandler(lgfile) logger.addHandler(lgfile)
logger.setLevel(logging.WARNING) logger.setLevel(logging.WARNING)
# Set template and device Netbox "custom field" names
template_cf = "zabbix_template" template_cf = "zabbix_template"
device_cf = "zabbix_hostid" device_cf = "zabbix_hostid"
@ -138,11 +139,17 @@ class EnvironmentVarError(SyncError):
pass pass
class InterfaceConfigError(SyncError):
pass
class NetworkDevice(): class NetworkDevice():
""" """
Represents Network device. Represents Network device.
INPUT: (Netbox device class, ZabbixAPI class) INPUT: (Netbox device class, ZabbixAPI class)
""" """
def __init__(self, nb, zabbix): def __init__(self, nb, zabbix):
self.nb = nb self.nb = nb
self.id = nb.id self.id = nb.id
@ -265,8 +272,6 @@ class NetworkDevice():
if(group['name'] == self.hostgroup): if(group['name'] == self.hostgroup):
self.group_id = group['groupid'] self.group_id = group['groupid']
e = (f"Found group {group['name']} for host {self.name}.") 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) logger.debug(e)
return True return True
else: else:
@ -302,6 +307,28 @@ class NetworkDevice():
else: else:
return False 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, def createInZabbix(self, groups, templates,
description="Host added by Netbox sync script."): description="Host added by Netbox sync script."):
""" """
@ -314,10 +341,7 @@ class NetworkDevice():
raise SyncInventoryError() raise SyncInventoryError()
self.getZabbixTemplate(templates) self.getZabbixTemplate(templates)
# Set interface, group and template configuration # Set interface, group and template configuration
interfaces = [{"type": 2, "main": 1, "useip": 1, interfaces = self.setInterfaceDetails()
"ip": self.ip, "dns": "", "port": 161,
"details": {"version": 2, "bulk": 0,
"community": "{$SNMP_COMMUNITY}"}}]
groups = [{"groupid": self.group_id}] groups = [{"groupid": self.group_id}]
templates = [{"templateid": self.template_id}] templates = [{"templateid": self.template_id}]
# Add host to Zabbix # Add host to Zabbix
@ -373,7 +397,9 @@ class NetworkDevice():
self.getZabbixGroup(groups) self.getZabbixGroup(groups)
self.getZabbixTemplate(templates) self.getZabbixTemplate(templates)
host = self.zabbix.host.get(filter={'hostid': self.zabbix_id}, host = self.zabbix.host.get(filter={'hostid': self.zabbix_id},
selectInterfaces=["interfaceid", "ip"], selectInterfaces=['type', 'ip',
'port', 'details',
'interfaceid'],
selectGroups=["id"], selectGroups=["id"],
selectParentTemplates=["id"]) selectParentTemplates=["id"])
if(len(host) > 1): if(len(host) > 1):
@ -413,29 +439,151 @@ class NetworkDevice():
logger.warning(f"Device {self.name}: hostgroup OUT of sync.") logger.warning(f"Device {self.name}: hostgroup OUT of sync.")
self.updateZabbixHost(groups={'groupid': self.group_id}) self.updateZabbixHost(groups={'groupid': self.group_id})
for interface in host["interfaces"]: # If only 1 interface has been found
if(interface["ip"] == self.ip): if(len(host['interfaces']) == 1):
logger.debug(f"Device {self.name}: IP address in-sync.") updates = {}
break # Go through each key / item and check if it matches Zabbix
else: for key, item in self.setInterfaceDetails()[0].items():
if(len(host['interfaces']) == 1): # Check if Netbox value is found in Zabbix
logger.warning(f"Device {self.name}: IP address OUT of sync.") if(key in host["interfaces"][0]):
int_id = host["interfaces"][0]['interfaceid'] # 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: try:
self.zabbix.hostinterface.update(interfaceid=int_id, # API call to Zabbix
ip=self.ip) self.zabbix.hostinterface.update(updates)
e = f"Updated host {self.name} with IP {self.ip}." e = f"Solved {self.name} interface conflict."
logger.warning(e) logger.info(e)
except ZabbixAPIException as e: except ZabbixAPIException as e:
e = f"Zabbix returned the following error: {str(e)}." e = f"Zabbix returned the following error: {str(e)}."
logger.error(e) logger.error(e)
raise SyncExternalError(e) raise SyncExternalError(e)
else: else:
e = (f"Device {self.name} has conflicting IP. Host has total " # If no updates are found, Zabbix interface is in-sync
f"of {len(host['interfaces'])} interfaces. Manual " e = f"Device {self.name}: interface in-sync."
"interfention required.") logger.debug(e)
logger.error(e) else:
SyncInventoryError(e) 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__"): if(__name__ == "__main__"):
@ -447,14 +595,14 @@ if(__name__ == "__main__"):
action="store_true") action="store_true")
parser.add_argument("-c", "--cluster", action="store_true", parser.add_argument("-c", "--cluster", action="store_true",
help=("Only add the primary node of a cluster " help=("Only add the primary node of a cluster "
"to Zabbix. Usefull when a shared virtual IP is " "to Zabbix. Usefull when a shared virtual IP is "
"used for the control plane.")) "used for the control plane."))
parser.add_argument("-H", "--hostgroups", parser.add_argument("-H", "--hostgroups",
help="Create Zabbix hostgroups if not present", help="Create Zabbix hostgroups if not present",
action="store_true") action="store_true")
parser.add_argument("-t", "--tenant", action="store_true", parser.add_argument("-t", "--tenant", action="store_true",
help=("Add Tenant name to the Zabbix " help=("Add Tenant name to the Zabbix "
"hostgroup name scheme.")) "hostgroup name scheme."))
args = parser.parse_args() args = parser.parse_args()
main(args) main(args)