mirror of
https://github.com/TheNetworkGuy/netbox-zabbix-sync.git
synced 2025-07-14 01:41:25 -06:00
Added support for syncing Zabbix Inventory, this is also a fix for https://github.com/TheNetworkGuy/netbox-zabbix-sync/issues/44
This commit is contained in:
parent
583d845c40
commit
5b08d27a5e
18
README.md
18
README.md
@ -170,6 +170,24 @@ You can modify this behaviour by changing the following list variables in the sc
|
|||||||
- `zabbix_device_removal`
|
- `zabbix_device_removal`
|
||||||
- `zabbix_device_disable`
|
- `zabbix_device_disable`
|
||||||
|
|
||||||
|
### Zabbix Inventory
|
||||||
|
This script allows you to enable the inventory on managed Zabbix hosts and sync NetBox device properties to the specified inventory fields.
|
||||||
|
To enable, set `inventory_sync` to `True`.
|
||||||
|
Set `inventory_automatic` to `False` to use manual inventory, or `True` for automatic.
|
||||||
|
See [Zabix Manual](https://www.zabbix.com/documentation/current/en/manual/config/hosts/inventory#building-inventory) for more information about the modes.
|
||||||
|
|
||||||
|
Use the `inventory_map` variable to map which NetBox properties are used in which Zabbix Inventory fields.
|
||||||
|
For nested properties, you can use the '/' seperator.
|
||||||
|
For example, the following map will assign the custom field 'mycustomfield' to the 'alias' Zabbix inventory field:
|
||||||
|
```
|
||||||
|
inventory_sync = True
|
||||||
|
inventory_automatic = True
|
||||||
|
inventory_map = { "custom_fields/mycustomfield/name": "alias"}
|
||||||
|
```
|
||||||
|
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.
|
||||||
|
|
||||||
### Template source
|
### Template source
|
||||||
You can either use a Netbox device type custom field or Netbox config context for the Zabbix template information.
|
You can either use a Netbox device type custom field or Netbox config context for the Zabbix template information.
|
||||||
|
|
||||||
|
@ -56,3 +56,32 @@ traverse_site_groups = False
|
|||||||
|
|
||||||
# Default device filter, only get devices which have a name in Netbox:
|
# Default device filter, only get devices which have a name in Netbox:
|
||||||
nb_device_filter = {"name__n": "null"}
|
nb_device_filter = {"name__n": "null"}
|
||||||
|
|
||||||
|
## Inventory
|
||||||
|
# To allow syncing of NetBox device properties, set inventory_sync to True
|
||||||
|
inventory_sync = False
|
||||||
|
|
||||||
|
# Set inventory_automatic to False to use manual inventory, True for automatic
|
||||||
|
# See https://www.zabbix.com/documentation/current/en/manual/config/hosts/inventory#building-inventory
|
||||||
|
inventory_automatic = True
|
||||||
|
|
||||||
|
# inventory_map is used to map NetBox properties to Zabbix Inventory fields.
|
||||||
|
# For nested properties, you can use the '/' seperator.
|
||||||
|
# For example, the following map will assign the custom field 'mycustomfield' to the 'alias' Zabbix inventory field:
|
||||||
|
#
|
||||||
|
# inventory_map = { "custom_fields/mycustomfield/name": "alias"}
|
||||||
|
#
|
||||||
|
# The following map should provide some nice defaults:
|
||||||
|
inventory_map = { "asset_tag": "asset_tag",
|
||||||
|
"virtual_chassis/name": "chassis",
|
||||||
|
"status/label": "deployment_status",
|
||||||
|
"location/name": "location",
|
||||||
|
"latitude": "location_lat",
|
||||||
|
"longitude": "location_lon",
|
||||||
|
"comments": "notes",
|
||||||
|
"name": "name",
|
||||||
|
"rack/name": "site_rack",
|
||||||
|
"serial": "serialno_a",
|
||||||
|
"device_type/model": "type",
|
||||||
|
"device_type/manufacturer/name": "vendor",
|
||||||
|
"oob_ip/address": "oob_ip" }
|
||||||
|
@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
|
|
||||||
"""Netbox to Zabbix sync script."""
|
"""Netbox to Zabbix sync script."""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import argparse
|
import argparse
|
||||||
from os import environ, path, sys
|
from os import environ, path, sys
|
||||||
@ -20,6 +19,11 @@ try:
|
|||||||
zabbix_device_removal,
|
zabbix_device_removal,
|
||||||
zabbix_device_disable,
|
zabbix_device_disable,
|
||||||
hostgroup_format,
|
hostgroup_format,
|
||||||
|
traverse_site_groups,
|
||||||
|
traverse_regions,
|
||||||
|
inventory_sync,
|
||||||
|
inventory_automatic,
|
||||||
|
inventory_map,
|
||||||
nb_device_filter
|
nb_device_filter
|
||||||
)
|
)
|
||||||
except ModuleNotFoundError:
|
except ModuleNotFoundError:
|
||||||
@ -45,6 +49,30 @@ logger.addHandler(lgfile)
|
|||||||
logger.setLevel(logging.WARNING)
|
logger.setLevel(logging.WARNING)
|
||||||
|
|
||||||
|
|
||||||
|
def convert_recordset(recordset):
|
||||||
|
""" Converts netbox RedcordSet to list of dicts. """
|
||||||
|
recordlist = []
|
||||||
|
for record in recordset:
|
||||||
|
recordlist.append(record.__dict__)
|
||||||
|
return recordlist
|
||||||
|
|
||||||
|
def build_path(endpoint, list_of_dicts):
|
||||||
|
"""
|
||||||
|
Builds a path list of related parent/child items.
|
||||||
|
This can be used to generate a joinable list to
|
||||||
|
be used in hostgroups.
|
||||||
|
"""
|
||||||
|
path = []
|
||||||
|
itemlist = [i for i in list_of_dicts if i['name'] == endpoint]
|
||||||
|
item = itemlist[0] if len(itemlist) == 1 else None
|
||||||
|
path.append(item['name'])
|
||||||
|
while item['_depth'] > 0:
|
||||||
|
itemlist = [i for i in list_of_dicts if i['name'] == str(item['parent'])]
|
||||||
|
item = itemlist[0] if len(itemlist) == 1 else None
|
||||||
|
path.append(item['name'])
|
||||||
|
path.reverse()
|
||||||
|
return(path)
|
||||||
|
|
||||||
def main(arguments):
|
def main(arguments):
|
||||||
"""Run the sync process."""
|
"""Run the sync process."""
|
||||||
# pylint: disable=too-many-branches, too-many-statements
|
# pylint: disable=too-many-branches, too-many-statements
|
||||||
@ -110,6 +138,8 @@ def main(arguments):
|
|||||||
proxy_name = "name"
|
proxy_name = "name"
|
||||||
# Get all Zabbix and Netbox data
|
# Get all Zabbix and Netbox data
|
||||||
netbox_devices = netbox.dcim.devices.filter(**nb_device_filter)
|
netbox_devices = netbox.dcim.devices.filter(**nb_device_filter)
|
||||||
|
netbox_site_groups = convert_recordset((netbox.dcim.site_groups.all()))
|
||||||
|
netbox_regions = convert_recordset(netbox.dcim.regions.all())
|
||||||
netbox_journals = netbox.extras.journal_entries
|
netbox_journals = netbox.extras.journal_entries
|
||||||
zabbix_groups = zabbix.hostgroup.get(output=['groupid', 'name'])
|
zabbix_groups = zabbix.hostgroup.get(output=['groupid', 'name'])
|
||||||
zabbix_templates = zabbix.template.get(output=['templateid', 'name'])
|
zabbix_templates = zabbix.template.get(output=['templateid', 'name'])
|
||||||
@ -125,8 +155,9 @@ def main(arguments):
|
|||||||
try:
|
try:
|
||||||
device = NetworkDevice(nb_device, zabbix, netbox_journals,
|
device = NetworkDevice(nb_device, zabbix, netbox_journals,
|
||||||
create_journal)
|
create_journal)
|
||||||
device.set_hostgroup(hostgroup_format)
|
device.set_hostgroup(hostgroup_format,netbox_site_groups,netbox_regions)
|
||||||
device.set_template(templates_config_context, templates_config_context_overrule)
|
device.set_template(templates_config_context, templates_config_context_overrule)
|
||||||
|
device.set_inventory(nb_device)
|
||||||
# Checks if device is part of cluster.
|
# Checks if device is part of cluster.
|
||||||
# Requires clustering variable
|
# Requires clustering variable
|
||||||
if device.isCluster() and clustering:
|
if device.isCluster() and clustering:
|
||||||
@ -236,6 +267,8 @@ class NetworkDevice():
|
|||||||
self.zabbix_state = 0
|
self.zabbix_state = 0
|
||||||
self.journal = journal
|
self.journal = journal
|
||||||
self.nb_journals = nb_journal_class
|
self.nb_journals = nb_journal_class
|
||||||
|
self.inventory_mode = -1
|
||||||
|
self.inventory = {}
|
||||||
self._setBasics()
|
self._setBasics()
|
||||||
|
|
||||||
def _setBasics(self):
|
def _setBasics(self):
|
||||||
@ -248,7 +281,7 @@ class NetworkDevice():
|
|||||||
self.ip = self.cidr.split("/")[0]
|
self.ip = self.cidr.split("/")[0]
|
||||||
else:
|
else:
|
||||||
e = f"Device {self.name}: no primary IP."
|
e = f"Device {self.name}: no primary IP."
|
||||||
logger.warning(e)
|
logger.info(e)
|
||||||
raise SyncInventoryError(e)
|
raise SyncInventoryError(e)
|
||||||
|
|
||||||
# Check if device has custom field for ZBX ID
|
# Check if device has custom field for ZBX ID
|
||||||
@ -259,7 +292,7 @@ class NetworkDevice():
|
|||||||
logger.warning(e)
|
logger.warning(e)
|
||||||
raise SyncInventoryError(e)
|
raise SyncInventoryError(e)
|
||||||
|
|
||||||
def set_hostgroup(self, hg_format):
|
def set_hostgroup(self, hg_format, nb_site_groups, nb_regions):
|
||||||
"""Set the hostgroup for this device"""
|
"""Set the hostgroup for this device"""
|
||||||
# Get all variables from the NB data
|
# Get all variables from the NB data
|
||||||
dev_location = str(self.nb.location) if self.nb.location else None
|
dev_location = str(self.nb.location) if self.nb.location else None
|
||||||
@ -274,7 +307,7 @@ class NetworkDevice():
|
|||||||
hostgroup_vars = {"dev_location": dev_location, "dev_role": dev_role,
|
hostgroup_vars = {"dev_location": dev_location, "dev_role": dev_role,
|
||||||
"manufacturer": manufacturer, "region": region,
|
"manufacturer": manufacturer, "region": region,
|
||||||
"site": site, "site_group": site_group,
|
"site": site, "site_group": site_group,
|
||||||
"tenant": tenant, "tenant_group": tenant_group}
|
"tenant": tenant, "tenant_group": tenant_group}
|
||||||
# Generate list based off string input format
|
# Generate list based off string input format
|
||||||
hg_items = hg_format.split("/")
|
hg_items = hg_format.split("/")
|
||||||
hostgroup = ""
|
hostgroup = ""
|
||||||
@ -293,7 +326,14 @@ class NetworkDevice():
|
|||||||
# the variable is invalid. Skip regardless.
|
# the variable is invalid. Skip regardless.
|
||||||
continue
|
continue
|
||||||
# Add value of predefined variable to hostgroup format
|
# Add value of predefined variable to hostgroup format
|
||||||
hostgroup += hostgroup_vars[item] + "/"
|
if item == "site_group" and nb_site_groups and traverse_site_groups:
|
||||||
|
path = build_path(site_group, nb_site_groups)
|
||||||
|
hostgroup += "/".join(path) + "/"
|
||||||
|
elif item == "region" and nb_regions and traverse_regions:
|
||||||
|
path = build_path(region, nb_regions)
|
||||||
|
hostgroup += "/".join(path) + "/"
|
||||||
|
else:
|
||||||
|
hostgroup += hostgroup_vars[item] + "/"
|
||||||
# If the final hostgroup variable is empty
|
# If the final hostgroup variable is empty
|
||||||
if not hostgroup:
|
if not hostgroup:
|
||||||
e = (f"{self.name} has no reliable hostgroup. This is"
|
e = (f"{self.name} has no reliable hostgroup. This is"
|
||||||
@ -353,6 +393,32 @@ class NetworkDevice():
|
|||||||
raise TemplateError(e)
|
raise TemplateError(e)
|
||||||
return self.config_context["zabbix"]["templates"]
|
return self.config_context["zabbix"]["templates"]
|
||||||
|
|
||||||
|
def set_inventory(self, nbdevice):
|
||||||
|
""" Set host inventory """
|
||||||
|
self.inventory_mode = -1
|
||||||
|
self.inventory = {}
|
||||||
|
if inventory_sync:
|
||||||
|
self.inventory_mode = 1 if inventory_automatic else 0
|
||||||
|
for nb_inv_field, zbx_inv_field in inventory_map.items():
|
||||||
|
field_list = nb_inv_field.split("/")
|
||||||
|
fieldstr = "nbdevice"
|
||||||
|
for field in field_list:
|
||||||
|
fieldstr += "['" + field + "']"
|
||||||
|
try:
|
||||||
|
nb_value = eval(fieldstr)
|
||||||
|
except:
|
||||||
|
nb_value = None
|
||||||
|
if nb_value and isinstance(nb_value, int | float | str ):
|
||||||
|
self.inventory[zbx_inv_field] = str(nb_value)
|
||||||
|
elif not nb_value:
|
||||||
|
logger.debug('Inventory lookup for "%s" returned an empty value' % nb_inv_field)
|
||||||
|
self.inventory[zbx_inv_field] = ""
|
||||||
|
else:
|
||||||
|
# Value is not a string or numeral, probably not what the user expected.
|
||||||
|
logger.error('Inventory lookup for "%s" returned an unexpected type,'
|
||||||
|
' it will be skipped.' % nb_inv_field)
|
||||||
|
return True
|
||||||
|
|
||||||
def isCluster(self):
|
def isCluster(self):
|
||||||
"""
|
"""
|
||||||
Checks if device is part of cluster.
|
Checks if device is part of cluster.
|
||||||
@ -537,7 +603,9 @@ class NetworkDevice():
|
|||||||
groups=groups,
|
groups=groups,
|
||||||
templates=templateids,
|
templates=templateids,
|
||||||
proxy_hostid=self.zbxproxy,
|
proxy_hostid=self.zbxproxy,
|
||||||
description=description)
|
description=description,
|
||||||
|
inventory_mode=self.inventory_mode,
|
||||||
|
inventory=self.inventory)
|
||||||
else:
|
else:
|
||||||
host = self.zabbix.host.create(host=self.name,
|
host = self.zabbix.host.create(host=self.name,
|
||||||
status=self.zabbix_state,
|
status=self.zabbix_state,
|
||||||
@ -545,7 +613,9 @@ class NetworkDevice():
|
|||||||
groups=groups,
|
groups=groups,
|
||||||
templates=templateids,
|
templates=templateids,
|
||||||
proxyid=self.zbxproxy,
|
proxyid=self.zbxproxy,
|
||||||
description=description)
|
description=description,
|
||||||
|
inventory_mode=self.inventory_mode,
|
||||||
|
inventory=self.inventory)
|
||||||
self.zabbix_id = host["hostids"][0]
|
self.zabbix_id = host["hostids"][0]
|
||||||
except ZabbixAPIException as e:
|
except ZabbixAPIException as e:
|
||||||
e = f"Couldn't add {self.name}, Zabbix returned {str(e)}."
|
e = f"Couldn't add {self.name}, Zabbix returned {str(e)}."
|
||||||
@ -603,7 +673,8 @@ class NetworkDevice():
|
|||||||
'port', 'details',
|
'port', 'details',
|
||||||
'interfaceid'],
|
'interfaceid'],
|
||||||
selectGroups=["groupid"],
|
selectGroups=["groupid"],
|
||||||
selectParentTemplates=["templateid"])
|
selectParentTemplates=["templateid"],
|
||||||
|
selectInventory=list(inventory_map.values()))
|
||||||
if len(host) > 1:
|
if len(host) > 1:
|
||||||
e = (f"Got {len(host)} results for Zabbix hosts "
|
e = (f"Got {len(host)} results for Zabbix hosts "
|
||||||
f"with ID {self.zabbix_id} - hostname {self.name}.")
|
f"with ID {self.zabbix_id} - hostname {self.name}.")
|
||||||
@ -616,7 +687,6 @@ class NetworkDevice():
|
|||||||
logger.error(e)
|
logger.error(e)
|
||||||
raise SyncInventoryError(e)
|
raise SyncInventoryError(e)
|
||||||
host = host[0]
|
host = host[0]
|
||||||
|
|
||||||
if host["host"] == self.name:
|
if host["host"] == self.name:
|
||||||
logger.debug(f"Device {self.name}: hostname in-sync.")
|
logger.debug(f"Device {self.name}: hostname in-sync.")
|
||||||
else:
|
else:
|
||||||
@ -678,6 +748,19 @@ class NetworkDevice():
|
|||||||
f"with proxy in Zabbix but not in Netbox. The"
|
f"with proxy in Zabbix but not in Netbox. The"
|
||||||
" -p flag was ommited: no "
|
" -p flag was ommited: no "
|
||||||
"changes have been made.")
|
"changes have been made.")
|
||||||
|
# Check host inventory
|
||||||
|
if inventory_sync:
|
||||||
|
if str(host['inventory_mode']) == str(self.inventory_mode):
|
||||||
|
logger.debug(f"Device {self.name}: inventory_mode in-sync.")
|
||||||
|
else:
|
||||||
|
logger.warning(f"Device {self.name}: inventory_mode OUT of sync.")
|
||||||
|
self.updateZabbixHost(inventory_mode=str(self.inventory_mode))
|
||||||
|
if host['inventory'] == self.inventory:
|
||||||
|
logger.debug(f"Device {self.name}: inventory in-sync.")
|
||||||
|
else:
|
||||||
|
logger.warning(f"Device {self.name}: inventory OUT of sync.")
|
||||||
|
self.updateZabbixHost(inventory=self.inventory)
|
||||||
|
|
||||||
# If only 1 interface has been found
|
# If only 1 interface has been found
|
||||||
# pylint: disable=too-many-nested-blocks
|
# pylint: disable=too-many-nested-blocks
|
||||||
if len(host['interfaces']) == 1:
|
if len(host['interfaces']) == 1:
|
||||||
|
Loading…
Reference in New Issue
Block a user