This commit is contained in:
Raymond Kuiper 2025-11-28 07:10:20 +00:00 committed by GitHub
commit eda652cd67
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 1125 additions and 1125 deletions

1584
README.md

File diff suppressed because it is too large Load Diff

View File

@ -1,333 +1,333 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# pylint: disable=invalid-name, logging-not-lazy, too-many-locals, logging-fstring-interpolation # pylint: disable=invalid-name, logging-not-lazy, too-many-locals, logging-fstring-interpolation
"""NetBox to Zabbix sync script.""" """NetBox to Zabbix sync script."""
import argparse import argparse
import logging import logging
import ssl import ssl
from os import environ, sys from os import environ, sys
from pynetbox import api from pynetbox import api
from pynetbox.core.query import RequestError as NBRequestError from pynetbox.core.query import RequestError as NBRequestError
from requests.exceptions import ConnectionError as RequestsConnectionError from requests.exceptions import ConnectionError as RequestsConnectionError
from zabbix_utils import APIRequestError, ProcessingError, ZabbixAPI from zabbix_utils import APIRequestError, ProcessingError, ZabbixAPI
from modules.config import load_config from modules.config import load_config
from modules.device import PhysicalDevice from modules.device import PhysicalDevice
from modules.exceptions import EnvironmentVarError, SyncError from modules.exceptions import EnvironmentVarError, SyncError
from modules.logging import get_logger, set_log_levels, setup_logger from modules.logging import get_logger, set_log_levels, setup_logger
from modules.tools import convert_recordset, proxy_prepper, verify_hg_format from modules.tools import convert_recordset, proxy_prepper, verify_hg_format
from modules.virtual_machine import VirtualMachine from modules.virtual_machine import VirtualMachine
config = load_config() config = load_config()
setup_logger() setup_logger()
logger = get_logger() logger = get_logger()
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
# set environment variables # set environment variables
if arguments.verbose: if arguments.verbose:
set_log_levels(logging.WARNING, logging.INFO) set_log_levels(logging.WARNING, logging.INFO)
if arguments.debug: if arguments.debug:
set_log_levels(logging.WARNING, logging.DEBUG) set_log_levels(logging.WARNING, logging.DEBUG)
if arguments.debug_all: if arguments.debug_all:
set_log_levels(logging.DEBUG, logging.DEBUG) set_log_levels(logging.DEBUG, logging.DEBUG)
if arguments.quiet: if arguments.quiet:
set_log_levels(logging.ERROR, logging.ERROR) set_log_levels(logging.ERROR, logging.ERROR)
env_vars = ["ZABBIX_HOST", "NETBOX_HOST", "NETBOX_TOKEN"] env_vars = ["ZABBIX_HOST", "NETBOX_HOST", "NETBOX_TOKEN"]
if "ZABBIX_TOKEN" in environ: if "ZABBIX_TOKEN" in environ:
env_vars.append("ZABBIX_TOKEN") env_vars.append("ZABBIX_TOKEN")
else: else:
env_vars.append("ZABBIX_USER") env_vars.append("ZABBIX_USER")
env_vars.append("ZABBIX_PASS") env_vars.append("ZABBIX_PASS")
for var in env_vars: for var in env_vars:
if var not in environ: if var not in environ:
e = f"Environment variable {var} has not been defined." e = f"Environment variable {var} has not been defined."
logger.error(e) logger.error(e)
raise EnvironmentVarError(e) raise EnvironmentVarError(e)
# Get all virtual environment variables # Get all virtual environment variables
if "ZABBIX_TOKEN" in env_vars: if "ZABBIX_TOKEN" in env_vars:
zabbix_user = None zabbix_user = None
zabbix_pass = None zabbix_pass = None
zabbix_token = environ.get("ZABBIX_TOKEN") zabbix_token = environ.get("ZABBIX_TOKEN")
else: else:
zabbix_user = environ.get("ZABBIX_USER") zabbix_user = environ.get("ZABBIX_USER")
zabbix_pass = environ.get("ZABBIX_PASS") zabbix_pass = environ.get("ZABBIX_PASS")
zabbix_token = None zabbix_token = None
zabbix_host = environ.get("ZABBIX_HOST") zabbix_host = environ.get("ZABBIX_HOST")
netbox_host = environ.get("NETBOX_HOST") netbox_host = environ.get("NETBOX_HOST")
netbox_token = environ.get("NETBOX_TOKEN") netbox_token = environ.get("NETBOX_TOKEN")
# Set NetBox API # Set NetBox API
netbox = api(netbox_host, token=netbox_token, threading=True) netbox = api(netbox_host, token=netbox_token, threading=True)
# Create API call to get all custom fields which are on the device objects # Create API call to get all custom fields which are on the device objects
try: try:
# Get NetBox version # Get NetBox version
nb_version = netbox.version nb_version = netbox.version
logger.debug("NetBox version is %s.", nb_version) logger.debug("NetBox version is %s.", nb_version)
except RequestsConnectionError: except RequestsConnectionError:
logger.error( logger.error(
"Unable to connect to NetBox with URL %s. Please check the URL and status of NetBox.", "Unable to connect to NetBox with URL %s. Please check the URL and status of NetBox.",
netbox_host, netbox_host,
) )
sys.exit(1) sys.exit(1)
except NBRequestError as e: except NBRequestError as e:
logger.error("NetBox error: %s", e) logger.error("NetBox error: %s", e)
sys.exit(1) sys.exit(1)
# Check if the provided Hostgroup layout is valid # Check if the provided Hostgroup layout is valid
device_cfs = [] device_cfs = []
vm_cfs = [] vm_cfs = []
device_cfs = list( device_cfs = list(
netbox.extras.custom_fields.filter( netbox.extras.custom_fields.filter(
type=["text", "object", "select"], content_types="dcim.device" type=["text", "object", "select"], content_types="dcim.device"
) )
) )
verify_hg_format( verify_hg_format(
config["hostgroup_format"], device_cfs=device_cfs, hg_type="dev", logger=logger config["hostgroup_format"], device_cfs=device_cfs, hg_type="dev", logger=logger
) )
if config["sync_vms"]: if config["sync_vms"]:
vm_cfs = list( vm_cfs = list(
netbox.extras.custom_fields.filter( netbox.extras.custom_fields.filter(
type=["text", "object", "select"], type=["text", "object", "select"],
content_types="virtualization.virtualmachine", content_types="virtualization.virtualmachine",
) )
) )
verify_hg_format( verify_hg_format(
config["vm_hostgroup_format"], vm_cfs=vm_cfs, hg_type="vm", logger=logger config["vm_hostgroup_format"], vm_cfs=vm_cfs, hg_type="vm", logger=logger
) )
# Set Zabbix API # Set Zabbix API
try: try:
ssl_ctx = ssl.create_default_context() ssl_ctx = ssl.create_default_context()
# If a custom CA bundle is set for pynetbox (requests), also use it for the Zabbix API # If a custom CA bundle is set for pynetbox (requests), also use it for the Zabbix API
if environ.get("REQUESTS_CA_BUNDLE", None): if environ.get("REQUESTS_CA_BUNDLE", None):
ssl_ctx.load_verify_locations(environ["REQUESTS_CA_BUNDLE"]) ssl_ctx.load_verify_locations(environ["REQUESTS_CA_BUNDLE"])
if not zabbix_token: if not zabbix_token:
zabbix = ZabbixAPI( zabbix = ZabbixAPI(
zabbix_host, user=zabbix_user, password=zabbix_pass, ssl_context=ssl_ctx zabbix_host, user=zabbix_user, password=zabbix_pass, ssl_context=ssl_ctx
) )
else: else:
zabbix = ZabbixAPI(zabbix_host, token=zabbix_token, ssl_context=ssl_ctx) zabbix = ZabbixAPI(zabbix_host, token=zabbix_token, ssl_context=ssl_ctx)
zabbix.check_auth() zabbix.check_auth()
except (APIRequestError, ProcessingError) as e: except (APIRequestError, ProcessingError) 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)
sys.exit(1) sys.exit(1)
# Set API parameter mapping based on API version # Set API parameter mapping based on API version
if not str(zabbix.version).startswith("7"): if not str(zabbix.version).startswith("7"):
proxy_name = "host" proxy_name = "host"
else: else:
proxy_name = "name" proxy_name = "name"
# Get all Zabbix and NetBox data # Get all Zabbix and NetBox data
netbox_devices = list(netbox.dcim.devices.filter(**config["nb_device_filter"])) netbox_devices = list(netbox.dcim.devices.filter(**config["nb_device_filter"]))
netbox_vms = [] netbox_vms = []
if config["sync_vms"]: if config["sync_vms"]:
netbox_vms = list( netbox_vms = list(
netbox.virtualization.virtual_machines.filter(**config["nb_vm_filter"]) netbox.virtualization.virtual_machines.filter(**config["nb_vm_filter"])
) )
netbox_site_groups = convert_recordset((netbox.dcim.site_groups.all())) netbox_site_groups = convert_recordset((netbox.dcim.site_groups.all()))
netbox_regions = convert_recordset(netbox.dcim.regions.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"])
zabbix_proxies = zabbix.proxy.get(output=["proxyid", proxy_name]) zabbix_proxies = zabbix.proxy.get(output=["proxyid", proxy_name])
# Set empty list for proxy processing Zabbix <= 6 # Set empty list for proxy processing Zabbix <= 6
zabbix_proxygroups = [] zabbix_proxygroups = []
if str(zabbix.version).startswith("7"): if str(zabbix.version).startswith("7"):
zabbix_proxygroups = zabbix.proxygroup.get(output=["proxy_groupid", "name"]) zabbix_proxygroups = zabbix.proxygroup.get(output=["proxy_groupid", "name"])
# Sanitize proxy data # Sanitize proxy data
if proxy_name == "host": if proxy_name == "host":
for proxy in zabbix_proxies: for proxy in zabbix_proxies:
proxy["name"] = proxy.pop("host") proxy["name"] = proxy.pop("host")
# Prepare list of all proxy and proxy_groups # Prepare list of all proxy and proxy_groups
zabbix_proxy_list = proxy_prepper(zabbix_proxies, zabbix_proxygroups) zabbix_proxy_list = proxy_prepper(zabbix_proxies, zabbix_proxygroups)
# Go through all NetBox devices # Go through all NetBox devices
for nb_vm in netbox_vms: for nb_vm in netbox_vms:
try: try:
vm = VirtualMachine( vm = VirtualMachine(
nb_vm, nb_vm,
zabbix, zabbix,
netbox_journals, netbox_journals,
nb_version, nb_version,
config["create_journal"], config["create_journal"],
logger, logger,
) )
logger.debug("Host %s: Started operations on VM.", vm.name) logger.debug("Host %s: Started operations on VM.", vm.name)
vm.set_vm_template() vm.set_vm_template()
# Check if a valid template has been found for this VM. # Check if a valid template has been found for this VM.
if not vm.zbx_template_names: if not vm.zbx_template_names:
continue continue
vm.set_hostgroup( vm.set_hostgroup(
config["vm_hostgroup_format"], netbox_site_groups, netbox_regions config["vm_hostgroup_format"], netbox_site_groups, netbox_regions
) )
# Check if a valid hostgroup has been found for this VM. # Check if a valid hostgroup has been found for this VM.
if not vm.hostgroups: if not vm.hostgroups:
continue continue
if config["extended_site_properties"] and nb_vm.site: if config["extended_site_properties"] and nb_vm.site:
logger.debug("VM %s: extending site information.", vm.name) logger.debug("VM %s: extending site information.", vm.name)
vm.site = convert_recordset(netbox.dcim.sites.filter(id=nb_vm.site.id)) vm.site = convert_recordset(netbox.dcim.sites.filter(id=nb_vm.site.id))
vm.set_inventory(nb_vm) vm.set_inventory(nb_vm)
vm.set_usermacros() vm.set_usermacros()
vm.set_tags() vm.set_tags()
# Checks if device is in cleanup state # Checks if device is in cleanup state
if vm.status in config["zabbix_device_removal"]: if vm.status in config["zabbix_device_removal"]:
if vm.zabbix_id: if vm.zabbix_id:
# Delete device from Zabbix # Delete device from Zabbix
# and remove hostID from NetBox. # and remove hostID from NetBox.
vm.cleanup() vm.cleanup()
logger.info("VM %s: cleanup complete", vm.name) logger.info("VM %s: cleanup complete", vm.name)
continue continue
# Device has been added to NetBox # Device has been added to NetBox
# but is not in Activate state # but is not in Activate state
logger.info( logger.info(
"VM %s: Skipping since this VM is not in the active state.", vm.name "VM %s: Skipping since this VM is not in the active state.", vm.name
) )
continue continue
# Check if the VM is in the disabled state # Check if the VM is in the disabled state
if vm.status in config["zabbix_device_disable"]: if vm.status in config["zabbix_device_disable"]:
vm.zabbix_state = 1 vm.zabbix_state = 1
# Add hostgroup if config is set # Add hostgroup if config is set
if config["create_hostgroups"]: if config["create_hostgroups"]:
# Create new hostgroup. Potentially multiple groups if nested # Create new hostgroup. Potentially multiple groups if nested
hostgroups = vm.createZabbixHostgroup(zabbix_groups) hostgroups = vm.createZabbixHostgroup(zabbix_groups)
# go through all newly created hostgroups # go through all newly created hostgroups
for group in hostgroups: for group in hostgroups:
# Add new hostgroups to zabbix group list # Add new hostgroups to zabbix group list
zabbix_groups.append(group) zabbix_groups.append(group)
# Check if VM is already in Zabbix # Check if VM is already in Zabbix
if vm.zabbix_id: if vm.zabbix_id:
vm.ConsistencyCheck( vm.ConsistencyCheck(
zabbix_groups, zabbix_groups,
zabbix_templates, zabbix_templates,
zabbix_proxy_list, zabbix_proxy_list,
config["full_proxy_sync"], config["full_proxy_sync"],
config["create_hostgroups"], config["create_hostgroups"],
) )
continue continue
# Add VM to Zabbix # Add VM to Zabbix
vm.createInZabbix(zabbix_groups, zabbix_templates, zabbix_proxy_list) vm.createInZabbix(zabbix_groups, zabbix_templates, zabbix_proxy_list)
except SyncError: except SyncError:
pass pass
for nb_device in netbox_devices: for nb_device in netbox_devices:
try: try:
# Set device instance set data such as hostgroup and template information. # Set device instance set data such as hostgroup and template information.
device = PhysicalDevice( device = PhysicalDevice(
nb_device, nb_device,
zabbix, zabbix,
netbox_journals, netbox_journals,
nb_version, nb_version,
config["create_journal"], config["create_journal"],
logger, logger,
) )
logger.debug("Host %s: Started operations on device.", device.name) logger.debug("Host %s: Started operations on device.", device.name)
device.set_template( device.set_template(
config["templates_config_context"], config["templates_config_context"],
config["templates_config_context_overrule"], config["templates_config_context_overrule"],
) )
# Check if a valid template has been found for this VM. # Check if a valid template has been found for this VM.
if not device.zbx_template_names: if not device.zbx_template_names:
continue continue
device.set_hostgroup( device.set_hostgroup(
config["hostgroup_format"], netbox_site_groups, netbox_regions config["hostgroup_format"], netbox_site_groups, netbox_regions
) )
# Check if a valid hostgroup has been found for this VM. # Check if a valid hostgroup has been found for this VM.
if not device.hostgroups: if not device.hostgroups:
logger.warning( logger.warning(
"Host %s: Host has no valid hostgroups, Skipping this host...", "Host %s: Host has no valid hostgroups, Skipping this host...",
device.name, device.name,
) )
continue continue
if config["extended_site_properties"] and nb_device.site: if config["extended_site_properties"] and nb_device.site:
logger.debug("Device %s: extending site information.", device.name) logger.debug("Device %s: extending site information.", device.name)
device.site = convert_recordset( device.site = convert_recordset(
netbox.dcim.sites.filter(id=nb_device.site.id) netbox.dcim.sites.filter(id=nb_device.site.id)
) )
device.set_inventory(nb_device) device.set_inventory(nb_device)
device.set_usermacros() device.set_usermacros()
device.set_tags() device.set_tags()
# Checks if device is part of cluster. # Checks if device is part of cluster.
# Requires clustering variable # Requires clustering variable
if device.isCluster() and config["clustering"]: if device.isCluster() and config["clustering"]:
# Check if device is primary or secondary # Check if device is primary or secondary
if device.promoteMasterDevice(): if device.promoteMasterDevice():
logger.info( logger.info(
"Device %s: is part of cluster and primary.", device.name "Device %s: is part of cluster and primary.", device.name
) )
else: else:
# Device is secondary in cluster. # Device is secondary in cluster.
# Don't continue with this device. # Don't continue with this device.
logger.info( logger.info(
"Device %s: Is part of cluster but not primary. Skipping this host...", "Device %s: Is part of cluster but not primary. Skipping this host...",
device.name, device.name,
) )
continue continue
# Checks if device is in cleanup state # Checks if device is in cleanup state
if device.status in config["zabbix_device_removal"]: if device.status in config["zabbix_device_removal"]:
if device.zabbix_id: if device.zabbix_id:
# Delete device from Zabbix # Delete device from Zabbix
# and remove hostID from NetBox. # and remove hostID from NetBox.
device.cleanup() device.cleanup()
logger.info("Device %s: cleanup complete", device.name) logger.info("Device %s: cleanup complete", device.name)
continue continue
# Device has been added to NetBox # Device has been added to NetBox
# but is not in Activate state # but is not in Activate state
logger.info( logger.info(
"Device %s: Skipping since this device is not in the active state.", "Device %s: Skipping since this device is not in the active state.",
device.name, device.name,
) )
continue continue
# Check if the device is in the disabled state # Check if the device is in the disabled state
if device.status in config["zabbix_device_disable"]: if device.status in config["zabbix_device_disable"]:
device.zabbix_state = 1 device.zabbix_state = 1
# Add hostgroup is config is set # Add hostgroup is config is set
if config["create_hostgroups"]: if config["create_hostgroups"]:
# Create new hostgroup. Potentially multiple groups if nested # Create new hostgroup. Potentially multiple groups if nested
hostgroups = device.createZabbixHostgroup(zabbix_groups) hostgroups = device.createZabbixHostgroup(zabbix_groups)
# go through all newly created hostgroups # go through all newly created hostgroups
for group in hostgroups: for group in hostgroups:
# Add new hostgroups to zabbix group list # Add new hostgroups to zabbix group list
zabbix_groups.append(group) zabbix_groups.append(group)
# Check if device is already in Zabbix # Check if device is already in Zabbix
if device.zabbix_id: if device.zabbix_id:
device.ConsistencyCheck( device.ConsistencyCheck(
zabbix_groups, zabbix_groups,
zabbix_templates, zabbix_templates,
zabbix_proxy_list, zabbix_proxy_list,
config["full_proxy_sync"], config["full_proxy_sync"],
config["create_hostgroups"], config["create_hostgroups"],
) )
continue continue
# Add device to Zabbix # Add device to Zabbix
device.createInZabbix(zabbix_groups, zabbix_templates, zabbix_proxy_list) device.createInZabbix(zabbix_groups, zabbix_templates, zabbix_proxy_list)
except SyncError: except SyncError:
pass pass
zabbix.logout() zabbix.logout()
if __name__ == "__main__": if __name__ == "__main__":
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description="A script to sync Zabbix with NetBox device data." description="A script to sync Zabbix with NetBox device data."
) )
parser.add_argument( parser.add_argument(
"-v", "--verbose", help="Turn on verbose logging.", action="store_true" "-v", "--verbose", help="Turn on verbose logging.", action="store_true"
) )
parser.add_argument( parser.add_argument(
"-vv", "--debug", help="Turn on debugging.", action="store_true" "-vv", "--debug", help="Turn on debugging.", action="store_true"
) )
parser.add_argument( parser.add_argument(
"-vvv", "-vvv",
"--debug-all", "--debug-all",
help="Turn on debugging for all modules.", help="Turn on debugging for all modules.",
action="store_true", action="store_true",
) )
parser.add_argument("-q", "--quiet", help="Turn off warnings.", action="store_true") parser.add_argument("-q", "--quiet", help="Turn off warnings.", action="store_true")
args = parser.parse_args() args = parser.parse_args()
main(args) main(args)