mirror of
https://github.com/TheNetworkGuy/netbox-zabbix-sync.git
synced 2025-07-13 15:24:48 -06:00
commit
48a04c58e3
81
.github/workflows/publish-image.yml
vendored
81
.github/workflows/publish-image.yml
vendored
@ -1,46 +1,53 @@
|
||||
name: Publish Docker image to GHCR on a new version
|
||||
name: Build and Push Docker Image
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- dockertest
|
||||
# tags:
|
||||
# - [0-9]+.*
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
release:
|
||||
types: [published]
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
|
||||
jobs:
|
||||
test_quality:
|
||||
uses: ./.github/workflows/quality.yml
|
||||
build_and_publish:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v4
|
||||
- name: Log in to the container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GHCR_PAT }}
|
||||
- name: Extract metadata (tags, labels)
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
type=semver,pattern={{ version }}
|
||||
type=ref,event=branch
|
||||
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'master') }}
|
||||
type=sha
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@369eb591f429131d6889c46b94e711f089e6ca96
|
||||
with:
|
||||
images: ghcr.io/${{ github.repository }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=ref,event=pr
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@ca877d9245402d1537745e0e356eab47c3520991
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
annotations: |
|
||||
index:org.opencontainers.image.description=Python script to synchronise NetBox devices to Zabbix.
|
||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,6 +1,8 @@
|
||||
*.log
|
||||
.venv
|
||||
config.py
|
||||
Pipfile
|
||||
Pipfile.lock
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
|
227
README.md
227
README.md
@ -7,7 +7,7 @@ A script to create, update and delete Zabbix hosts using NetBox device objects.
|
||||
To pull the latest stable version to your local cache, use the following docker
|
||||
pull command:
|
||||
|
||||
```
|
||||
```bash
|
||||
docker pull ghcr.io/thenetworkguy/netbox-zabbix-sync:main
|
||||
```
|
||||
|
||||
@ -15,7 +15,7 @@ Make sure to specify the needed environment variables for the script to work
|
||||
(see [here](#set-environment-variables)) on the command line or use an
|
||||
[env file](https://docs.docker.com/reference/cli/docker/container/run/#env).
|
||||
|
||||
```
|
||||
```bash
|
||||
docker run -d -t -i -e ZABBIX_HOST='https://zabbix.local' \
|
||||
-e ZABBIX_TOKEN='othersecrettoken' \
|
||||
-e NETBOX_HOST='https://netbox.local' \
|
||||
@ -30,7 +30,7 @@ The image uses the default `config.py` for it's configuration, you can use a
|
||||
volume mount in the docker run command to override with your own config file if
|
||||
needed (see [config file](#config-file)):
|
||||
|
||||
```
|
||||
```bash
|
||||
docker run -d -t -i -v $(pwd)/config.py:/opt/netbox-zabbix/config.py ...
|
||||
```
|
||||
|
||||
@ -38,7 +38,7 @@ docker run -d -t -i -v $(pwd)/config.py:/opt/netbox-zabbix/config.py ...
|
||||
|
||||
### Cloning the repository
|
||||
|
||||
```
|
||||
```bash
|
||||
git clone https://github.com/TheNetworkGuy/netbox-zabbix-sync.git
|
||||
```
|
||||
|
||||
@ -66,7 +66,7 @@ cp config.py.example config.py
|
||||
|
||||
Set the following environment variables:
|
||||
|
||||
```
|
||||
```bash
|
||||
ZABBIX_HOST="https://zabbix.local"
|
||||
ZABBIX_USER="username"
|
||||
ZABBIX_PASS="Password"
|
||||
@ -77,7 +77,7 @@ NETBOX_TOKEN="secrettoken"
|
||||
Or, you can use a Zabbix API token to login instead of using a username and
|
||||
password. In that case `ZABBIX_USER` and `ZABBIX_PASS` will be ignored.
|
||||
|
||||
```
|
||||
```bash
|
||||
ZABBIX_TOKEN=othersecrettoken
|
||||
```
|
||||
|
||||
@ -183,9 +183,9 @@ used:
|
||||
| cluster | VM cluster name |
|
||||
| cluster_type | VM cluster type |
|
||||
|
||||
You can specify the value sperated by a "/" like so:
|
||||
You can specify the value seperated by a "/" like so:
|
||||
|
||||
```
|
||||
```python
|
||||
hostgroup_format = "tenant/site/dev_location/role"
|
||||
```
|
||||
|
||||
@ -232,7 +232,7 @@ have a relationship with a tenant.
|
||||
- Device_role: PDU
|
||||
- Site: HQ-AMS
|
||||
|
||||
```
|
||||
```python
|
||||
hostgroup_format = "site/tenant/device_role"
|
||||
```
|
||||
|
||||
@ -245,7 +245,7 @@ generated for both hosts:
|
||||
|
||||
The same logic applies to custom fields being used in the HG format:
|
||||
|
||||
```
|
||||
```python
|
||||
hostgroup_format = "site/mycustomfieldname"
|
||||
```
|
||||
|
||||
@ -292,15 +292,18 @@ You can set the inventory mode to "disabled", "manual" or "automatic" with the
|
||||
[Zabbix 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
|
||||
Use the `device_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:
|
||||
|
||||
```
|
||||
For Virtual Machines, use `vm_inventory_map`.
|
||||
|
||||
```python
|
||||
inventory_sync = True
|
||||
inventory_mode = "manual"
|
||||
inventory_map = { "custom_fields/mycustomfield/name": "alias"}
|
||||
device_inventory_map = {"custom_fields/mycustomfield/name": "alias"}
|
||||
vm_inventory_map = {"custom_fields/mycustomfield/name": "alias"}
|
||||
```
|
||||
|
||||
See `config.py.example` for an extensive example map. Any Zabix Inventory fields
|
||||
@ -321,14 +324,14 @@ sticking to the custom field.
|
||||
You can change the behaviour in the config file. By default this setting is
|
||||
false but you can set it to true to use config context:
|
||||
|
||||
```
|
||||
```python
|
||||
templates_config_context = True
|
||||
```
|
||||
|
||||
After that make sure that for each host there is at least one template defined
|
||||
in the config context in this format:
|
||||
|
||||
```
|
||||
```json
|
||||
{
|
||||
"zabbix": {
|
||||
"templates": [
|
||||
@ -346,10 +349,196 @@ added benefit of overwriting the template should a device in NetBox have a
|
||||
device specific context defined. In this case the device specific context
|
||||
template(s) will take priority over the device type custom field template.
|
||||
|
||||
```
|
||||
```python
|
||||
templates_config_context_overrule = True
|
||||
```
|
||||
|
||||
### Tags
|
||||
|
||||
This script can sync host tags to your Zabbix hosts for use in filtering,
|
||||
SLA calculations and event correlation.
|
||||
|
||||
Tags can be synced from the following sources:
|
||||
|
||||
1. NetBox device/vm tags
|
||||
2. NetBox config ontext
|
||||
3. NetBox fields
|
||||
|
||||
Syncing tags will override any tags that were set manually on the host,
|
||||
making NetBox the single source-of-truth for managing tags.
|
||||
|
||||
To enable syncing, turn on tag_sync in the config file.
|
||||
By default, this script will modify tag names and tag values to lowercase.
|
||||
You can change this behaviour by setting tag_lower to False.
|
||||
|
||||
```python
|
||||
tag_sync = True
|
||||
tag_lower = True
|
||||
```
|
||||
|
||||
#### Device tags
|
||||
|
||||
As NetBox doesn't follow the tag/value pattern for tags, we will need a tag
|
||||
name set to register the netwbox tags.
|
||||
|
||||
By default the tag name is "NetBox", but you can change this to whatever you want.
|
||||
The value for the tag can be choosen from 'name', 'display' or 'slug'.
|
||||
|
||||
```python
|
||||
tag_name = 'NetBox'
|
||||
tag_value = 'name'
|
||||
```
|
||||
|
||||
#### Config context
|
||||
|
||||
You can supply custom tags via config context by adding the following:
|
||||
|
||||
```json
|
||||
{
|
||||
"zabbix": {
|
||||
"tags": [
|
||||
{
|
||||
"MyTagName": "MyTagValue"
|
||||
},
|
||||
{
|
||||
"environment": "production"
|
||||
}
|
||||
],
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This will allow you to assign tags based on the config context rules.
|
||||
|
||||
#### NetBox Field
|
||||
|
||||
NetBox field can also be used as input for tags, just like inventory and usermacros.
|
||||
To enable syncing from fields, make sure to configure a `device_tag_map` and/or a `vm_tag_map`.
|
||||
|
||||
```python
|
||||
device_tag_map = {"site/name": "site",
|
||||
"rack/name": "rack",
|
||||
"platform/name": "target"}
|
||||
|
||||
vm_tag_map = {"site/name": "site",
|
||||
"cluster/name": "cluster",
|
||||
"platform/name": "target"}
|
||||
```
|
||||
|
||||
To turn off field syncing, set the maps to empty dictionaries:
|
||||
|
||||
```python
|
||||
device_tag_map = {}
|
||||
vm_tag_map = {}
|
||||
```
|
||||
|
||||
|
||||
### Usermacros
|
||||
|
||||
You can choose to use NetBox as a source for Host usermacros by
|
||||
enabling the following option in the configuration file:
|
||||
|
||||
```python
|
||||
usermacro_sync = True
|
||||
```
|
||||
|
||||
Please be advised that enabling this option will _clear_ any usermacros
|
||||
manually set on the managed hosts and override them with the usermacros
|
||||
from NetBox.
|
||||
|
||||
There are two NetBox sources that can be used to populate usermacros:
|
||||
|
||||
1. NetBox config context
|
||||
2. NetBox fields
|
||||
|
||||
#### Config context
|
||||
|
||||
By defining a dictionary `usermacros` within the `zabbix` key in
|
||||
config context, you can dynamically assign usermacro values based on
|
||||
anything that you can target based on
|
||||
[config contexts](https://netboxlabs.com/docs/netbox/en/stable/features/context-data/)
|
||||
within NetBox.
|
||||
|
||||
Through this method, it is possible to define the following types of usermacros:
|
||||
|
||||
1. Text
|
||||
2. Secret
|
||||
3. Vault
|
||||
|
||||
The default macro type is text if no `type` and `value` have been set.
|
||||
It is also possible to create usermacros with
|
||||
[context](https://www.zabbix.com/documentation/7.0/en/manual/config/macros/user_macros_context).
|
||||
|
||||
Examples:
|
||||
|
||||
```json
|
||||
{
|
||||
"zabbix": {
|
||||
"usermacros": {
|
||||
"{$USER_MACRO}": "test value",
|
||||
"{$CONTEXT_MACRO:\"test\"}": "test value",
|
||||
"{$CONTEXT_REGEX_MACRO:regex:\".*\"}": "test value",
|
||||
"{$SECRET_MACRO}": {
|
||||
"type": "secret",
|
||||
"value": "PaSsPhRaSe"
|
||||
},
|
||||
"{$VAULT_MACRO}": {
|
||||
"type": "vault",
|
||||
"value": "secret/vmware:password"
|
||||
},
|
||||
"{$USER_MACRO2}": {
|
||||
"type": "text",
|
||||
"value": "another test value"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
Please be aware that secret usermacros are only synced _once_ by default.
|
||||
This is the default behaviour because Zabbix API won't return the value of
|
||||
secrets so the script cannot compare the values with the ones set in NetBox.
|
||||
|
||||
If you update a secret usermacro value, just remove the value from the host
|
||||
in Zabbix and the new value will be synced during the next run.
|
||||
|
||||
Alternatively, you can set the following option in the config file:
|
||||
|
||||
```python
|
||||
usermacro_sync = "full"
|
||||
```
|
||||
|
||||
This will force a full usermacro sync on every run on hosts that have secret usermacros set.
|
||||
That way, you will know for sure the secret values are always up to date.
|
||||
|
||||
Keep in mind that NetBox (and the log output of this script) will show your secrets
|
||||
in plain text. If true secrecy is required, consider switching to
|
||||
[vault](https://www.zabbix.com/documentation/current/en/manual/config/macros/secret_macros#vault-secret)
|
||||
usermacros.
|
||||
|
||||
#### Netbox Fields
|
||||
|
||||
To use NetBox fields as a source for usermacros, you will need to set up usermacro maps
|
||||
for devices and/or virtual machines in the configuration file.
|
||||
This method only supports `text` type usermacros.
|
||||
|
||||
For example:
|
||||
|
||||
```python
|
||||
usermacro_sync = True
|
||||
device_usermacro_map = {"serial": "{$HW_SERIAL}",
|
||||
"role/name": "{$DEV_ROLE}",
|
||||
"url": "{$NB_URL}",
|
||||
"id": "{$NB_ID}"}
|
||||
vm_usermacro_map = {"memory": "{$TOTAL_MEMORY}",
|
||||
"role/name": "{$DEV_ROLE}",
|
||||
"url": "{$NB_URL}",
|
||||
"id": "{$NB_ID}"}
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Permissions
|
||||
|
||||
### NetBox
|
||||
@ -518,9 +707,13 @@ environment. For example, you could:
|
||||
}
|
||||
```
|
||||
|
||||
I would recommend using macros for sensitive data such as community strings
|
||||
I would recommend using usermacros for sensitive data such as community strings
|
||||
since the data in NetBox is plain-text.
|
||||
|
||||
> **_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.
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -80,19 +80,74 @@ inventory_sync = False
|
||||
# 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"}
|
||||
# device_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" }
|
||||
# The following maps should provide some nice defaults:
|
||||
device_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" }
|
||||
|
||||
# We also support inventory mapping on Virtual Machines.
|
||||
vm_inventory_map = { "status/label": "deployment_status",
|
||||
"comments": "notes",
|
||||
"name": "name" }
|
||||
|
||||
# To allow syncing of usermacros from NetBox, set to True.
|
||||
# this will enable both field mapping and config context usermacros.
|
||||
#
|
||||
# If set to "full", it will force the update of secret usermacros every run.
|
||||
# Please see the README.md for more information.
|
||||
usermacro_sync = False
|
||||
|
||||
# device usermacro_map to map NetBox fields to usermacros.
|
||||
device_usermacro_map = {"serial": "{$HW_SERIAL}",
|
||||
"role/name": "{$DEV_ROLE}",
|
||||
"url": "{$NB_URL}",
|
||||
"id": "{$NB_ID}"}
|
||||
|
||||
# virtual machine usermacro_map to map NetBox fields to usermacros.
|
||||
vm_usermacro_map = {"memory": "{$TOTAL_MEMORY}",
|
||||
"role/name": "{$DEV_ROLE}",
|
||||
"url": "{$NB_URL}",
|
||||
"id": "{$NB_ID}"}
|
||||
|
||||
# To sync host tags to Zabbix, set to True.
|
||||
tag_sync = False
|
||||
|
||||
# Setting tag_lower to True will lower capital letters ain tag names and values
|
||||
# This is more inline with the Zabbix way of working with tags.
|
||||
#
|
||||
# You can however set this to False to ensure capital letters are synced to Zabbix tags.
|
||||
tag_lower = True
|
||||
|
||||
# We can sync NetBox device/VM tags to Zabbix, but as NetBox tags don't follow the key/value
|
||||
# pattern, we need to specify a tag name to register the NetBox tags in Zabbix.
|
||||
#
|
||||
#
|
||||
#
|
||||
# If tag_name is set to False, we won't sync NetBox device/VM tags to Zabbix.
|
||||
tag_name = 'NetBox'
|
||||
|
||||
# We can choose to use 'name', 'slug' or 'display' NetBox tag properties as a value in Zabbix.
|
||||
# 'name'is used by default.
|
||||
tag_value = "name"
|
||||
|
||||
# device tag_map to map NetBox fields to host tags.
|
||||
device_tag_map = {"site/name": "site",
|
||||
"rack/name": "rack",
|
||||
"platform/name": "target"}
|
||||
|
||||
# Virtual machine tag_map to map NetBox fields to host tags.
|
||||
vm_tag_map = {"site/name": "site",
|
||||
"cluster/name": "cluster",
|
||||
"platform/name": "target"}
|
||||
|
@ -1,16 +1,21 @@
|
||||
#!/usr/bin/env python3
|
||||
# pylint: disable=invalid-name, logging-not-lazy, too-many-locals, logging-fstring-interpolation, too-many-lines
|
||||
# pylint: disable=invalid-name, logging-not-lazy, too-many-locals, logging-fstring-interpolation, too-many-lines, too-many-public-methods
|
||||
"""
|
||||
Device specific handeling for NetBox to Zabbix
|
||||
"""
|
||||
from os import sys
|
||||
from re import search
|
||||
from copy import deepcopy
|
||||
from logging import getLogger
|
||||
from zabbix_utils import APIRequestError
|
||||
from modules.exceptions import (SyncInventoryError, TemplateError, SyncExternalError,
|
||||
InterfaceConfigError, JournalError)
|
||||
from modules.interface import ZabbixInterface
|
||||
from modules.usermacros import ZabbixUsermacros
|
||||
from modules.tags import ZabbixTags
|
||||
from modules.hostgroups import Hostgroup
|
||||
from modules.tools import field_mapper, remove_duplicates
|
||||
|
||||
try:
|
||||
from config import (
|
||||
template_cf, device_cf,
|
||||
@ -18,7 +23,14 @@ try:
|
||||
traverse_regions,
|
||||
inventory_sync,
|
||||
inventory_mode,
|
||||
inventory_map
|
||||
device_inventory_map,
|
||||
usermacro_sync,
|
||||
device_usermacro_map,
|
||||
tag_sync,
|
||||
tag_lower,
|
||||
tag_name,
|
||||
tag_value,
|
||||
device_tag_map
|
||||
)
|
||||
except ModuleNotFoundError:
|
||||
print("Configuration file config.py not found in main directory."
|
||||
@ -53,6 +65,8 @@ class PhysicalDevice():
|
||||
self.nb_journals = nb_journal_class
|
||||
self.inventory_mode = -1
|
||||
self.inventory = {}
|
||||
self.usermacros = {}
|
||||
self.tags = {}
|
||||
self.logger = logger if logger else getLogger(__name__)
|
||||
self._setBasics()
|
||||
|
||||
@ -62,6 +76,18 @@ class PhysicalDevice():
|
||||
def __str__(self):
|
||||
return self.__repr__()
|
||||
|
||||
def _inventory_map(self):
|
||||
""" Use device inventory maps """
|
||||
return device_inventory_map
|
||||
|
||||
def _usermacro_map(self):
|
||||
""" Use device inventory maps """
|
||||
return device_usermacro_map
|
||||
|
||||
def _tag_map(self):
|
||||
""" Use device host tag maps """
|
||||
return device_tag_map
|
||||
|
||||
def _setBasics(self):
|
||||
"""
|
||||
Sets basic information like IP address.
|
||||
@ -180,31 +206,7 @@ class PhysicalDevice():
|
||||
self.inventory = {}
|
||||
if inventory_sync and self.inventory_mode in [0,1]:
|
||||
self.logger.debug(f"Host {self.name}: Starting inventory mapper")
|
||||
# Let's build an inventory dict for each property in the inventory_map
|
||||
for nb_inv_field, zbx_inv_field in inventory_map.items():
|
||||
field_list = nb_inv_field.split("/") # convert str to list based on delimiter
|
||||
# start at the base of the dict...
|
||||
value = nbdevice
|
||||
# ... and step through the dict till we find the needed value
|
||||
for item in field_list:
|
||||
value = value[item] if value else None
|
||||
# Check if the result is usable and expected
|
||||
# We want to apply any int or float 0 values,
|
||||
# even if python thinks those are empty.
|
||||
if ((value and isinstance(value, int | float | str )) or
|
||||
(isinstance(value, int | float) and int(value) ==0)):
|
||||
self.inventory[zbx_inv_field] = str(value)
|
||||
elif not value:
|
||||
# empty value should just be an empty string for API compatibility
|
||||
self.logger.debug(f"Host {self.name}: NetBox inventory lookup for "
|
||||
f"'{nb_inv_field}' returned an empty value")
|
||||
self.inventory[zbx_inv_field] = ""
|
||||
else:
|
||||
# Value is not a string or numeral, probably not what the user expected.
|
||||
self.logger.error(f"Host {self.name}: Inventory lookup for '{nb_inv_field}'"
|
||||
" returned an unexpected type: it will be skipped.")
|
||||
self.logger.debug(f"Host {self.name}: Inventory mapping complete. "
|
||||
f"Mapped {len(list(filter(None, self.inventory.values())))} field(s)")
|
||||
self.inventory = field_mapper(self.name, self._inventory_map(), nbdevice, self.logger)
|
||||
return True
|
||||
|
||||
def isCluster(self):
|
||||
@ -358,6 +360,34 @@ class PhysicalDevice():
|
||||
self.logger.warning(message)
|
||||
raise SyncInventoryError(message) from e
|
||||
|
||||
def set_usermacros(self):
|
||||
"""
|
||||
Generates Usermacros
|
||||
"""
|
||||
macros = ZabbixUsermacros(self.nb, self._usermacro_map(),
|
||||
usermacro_sync, logger=self.logger,
|
||||
host=self.name)
|
||||
if macros.sync is False:
|
||||
self.usermacros = []
|
||||
|
||||
self.usermacros = macros.generate()
|
||||
return True
|
||||
|
||||
|
||||
def set_tags(self):
|
||||
"""
|
||||
Generates Host Tags
|
||||
"""
|
||||
tags = ZabbixTags(self.nb, self._tag_map(),
|
||||
tag_sync, tag_lower, tag_name=tag_name,
|
||||
tag_value=tag_value, logger=self.logger,
|
||||
host=self.name)
|
||||
if tags.sync is False:
|
||||
self.tags = []
|
||||
|
||||
self.tags = tags.generate()
|
||||
return True
|
||||
|
||||
def setProxy(self, proxy_list):
|
||||
"""
|
||||
Sets proxy or proxy group if this
|
||||
@ -427,7 +457,9 @@ class PhysicalDevice():
|
||||
"templates": templateids,
|
||||
"description": description,
|
||||
"inventory_mode": self.inventory_mode,
|
||||
"inventory": self.inventory
|
||||
"inventory": self.inventory,
|
||||
"macros": self.usermacros,
|
||||
"tags": self.tags
|
||||
}
|
||||
# If a Zabbix proxy or Zabbix Proxy group has been defined
|
||||
if self.zbxproxy:
|
||||
@ -542,7 +574,10 @@ class PhysicalDevice():
|
||||
selectGroups=["groupid"],
|
||||
selectHostGroups=["groupid"],
|
||||
selectParentTemplates=["templateid"],
|
||||
selectInventory=list(inventory_map.values()))
|
||||
selectInventory=list(self._inventory_map().values()),
|
||||
selectMacros=["macro","value","type","description"],
|
||||
selectTags=["tag","value"]
|
||||
)
|
||||
if len(host) > 1:
|
||||
e = (f"Got {len(host)} results for Zabbix hosts "
|
||||
f"with ID {self.zabbix_id} - hostname {self.name}.")
|
||||
@ -591,7 +626,6 @@ class PhysicalDevice():
|
||||
if group["groupid"] == self.group_id:
|
||||
self.logger.debug(f"Host {self.name}: hostgroup in-sync.")
|
||||
break
|
||||
else:
|
||||
self.logger.warning(f"Host {self.name}: hostgroup OUT of sync.")
|
||||
self.updateZabbixHost(groups={'groupid': self.group_id})
|
||||
|
||||
@ -664,6 +698,31 @@ class PhysicalDevice():
|
||||
self.logger.warning(f"Host {self.name}: inventory OUT of sync.")
|
||||
self.updateZabbixHost(inventory=self.inventory)
|
||||
|
||||
# Check host usermacros
|
||||
if usermacro_sync:
|
||||
macros_filtered = []
|
||||
# Do not re-sync secret usermacros unless sync is set to 'full'
|
||||
if str(usermacro_sync).lower() != "full":
|
||||
for m in deepcopy(self.usermacros):
|
||||
if m['type'] == str(1):
|
||||
# Remove the value as the api doesn't return it
|
||||
# this will allow us to only update usermacros that don't exist
|
||||
m.pop('value')
|
||||
macros_filtered.append(m)
|
||||
if host['macros'] == self.usermacros or host['macros'] == macros_filtered:
|
||||
self.logger.debug(f"Host {self.name}: usermacros in-sync.")
|
||||
else:
|
||||
self.logger.warning(f"Host {self.name}: usermacros OUT of sync.")
|
||||
self.updateZabbixHost(macros=self.usermacros)
|
||||
|
||||
# Check host usermacros
|
||||
if tag_sync:
|
||||
if remove_duplicates(host['tags'],sortkey='tag') == self.tags:
|
||||
self.logger.debug(f"Host {self.name}: tags in-sync.")
|
||||
else:
|
||||
self.logger.warning(f"Host {self.name}: tags OUT of sync.")
|
||||
self.updateZabbixHost(tags=self.tags)
|
||||
|
||||
# If only 1 interface has been found
|
||||
# pylint: disable=too-many-nested-blocks
|
||||
if len(host['interfaces']) == 1:
|
||||
|
@ -31,3 +31,6 @@ class HostgroupError(SyncError):
|
||||
|
||||
class TemplateError(SyncError):
|
||||
""" Class TemplateError """
|
||||
|
||||
class UsermacroError(SyncError):
|
||||
""" Class UsermacroError """
|
||||
|
117
modules/tags.py
Normal file
117
modules/tags.py
Normal file
@ -0,0 +1,117 @@
|
||||
#!/usr/bin/env python3
|
||||
# pylint: disable=too-many-instance-attributes, too-many-arguments, too-many-positional-arguments, logging-fstring-interpolation
|
||||
"""
|
||||
All of the Zabbix Usermacro related configuration
|
||||
"""
|
||||
from logging import getLogger
|
||||
from modules.tools import field_mapper, remove_duplicates
|
||||
|
||||
class ZabbixTags():
|
||||
"""Class that represents a Zabbix interface."""
|
||||
|
||||
def __init__(self, nb, tag_map, tag_sync, tag_lower=True,
|
||||
tag_name=None, tag_value=None, logger=None, host=None):
|
||||
self.nb = nb
|
||||
self.name = host if host else nb.name
|
||||
self.tag_map = tag_map
|
||||
self.logger = logger if logger else getLogger(__name__)
|
||||
self.tags = {}
|
||||
self.lower = tag_lower
|
||||
self.tag_name = tag_name
|
||||
self.tag_value = tag_value
|
||||
self.tag_sync = tag_sync
|
||||
self.sync = False
|
||||
self._set_config()
|
||||
|
||||
def __repr__(self):
|
||||
return self.name
|
||||
|
||||
def __str__(self):
|
||||
return self.__repr__()
|
||||
|
||||
def _set_config(self):
|
||||
"""
|
||||
Setup class
|
||||
"""
|
||||
if self.tag_sync:
|
||||
self.sync = True
|
||||
|
||||
return True
|
||||
|
||||
def validate_tag(self, tag_name):
|
||||
"""
|
||||
Validates tag name
|
||||
"""
|
||||
if tag_name and isinstance(tag_name, str) and len(tag_name)<=256:
|
||||
return True
|
||||
return False
|
||||
|
||||
def validate_value(self, tag_value):
|
||||
"""
|
||||
Validates tag value
|
||||
"""
|
||||
if tag_value and isinstance(tag_value, str) and len(tag_value)<=256:
|
||||
return True
|
||||
return False
|
||||
|
||||
def render_tag(self, tag_name, tag_value):
|
||||
"""
|
||||
Renders a tag
|
||||
"""
|
||||
tag={}
|
||||
if self.validate_tag(tag_name):
|
||||
if self.lower:
|
||||
tag['tag'] = tag_name.lower()
|
||||
else:
|
||||
tag['tag'] = tag_name
|
||||
else:
|
||||
self.logger.error(f'Tag {tag_name} is not a valid tag name, skipping.')
|
||||
return False
|
||||
|
||||
if self.validate_value(tag_value):
|
||||
if self.lower:
|
||||
tag['value'] = tag_value.lower()
|
||||
else:
|
||||
tag['value'] = tag_value
|
||||
else:
|
||||
self.logger.error(f'Tag {tag_name} has an invalid value: \'{tag_value}\', skipping.')
|
||||
return False
|
||||
return tag
|
||||
|
||||
def generate(self):
|
||||
"""
|
||||
Generate full set of Usermacros
|
||||
"""
|
||||
# pylint: disable=too-many-branches
|
||||
tags=[]
|
||||
# Parse the field mapper for tags
|
||||
if self.tag_map:
|
||||
self.logger.debug(f"Host {self.nb.name}: Starting tag mapper")
|
||||
field_tags = field_mapper(self.nb.name, self.tag_map, self.nb, self.logger)
|
||||
for tag, value in field_tags.items():
|
||||
t = self.render_tag(tag, value)
|
||||
if t:
|
||||
tags.append(t)
|
||||
|
||||
# Parse NetBox config context for tags
|
||||
if ("zabbix" in self.nb.config_context and "tags" in self.nb.config_context['zabbix']
|
||||
and isinstance(self.nb.config_context['zabbix']['tags'], list)):
|
||||
for tag in self.nb.config_context['zabbix']['tags']:
|
||||
if isinstance(tag, dict):
|
||||
for tagname, value in tag.items():
|
||||
t = self.render_tag(tagname, value)
|
||||
if t:
|
||||
tags.append(t)
|
||||
|
||||
# Pull in NetBox device tags if tag_name is set
|
||||
if self.tag_name and isinstance(self.tag_name, str):
|
||||
for tag in self.nb.tags:
|
||||
if self.tag_value.lower() in ['display', 'name', 'slug']:
|
||||
value = tag[self.tag_value]
|
||||
else:
|
||||
value = tag['name']
|
||||
t = self.render_tag(self.tag_name, value)
|
||||
if t:
|
||||
tags.append(t)
|
||||
|
||||
return remove_duplicates(tags, sortkey='tag')
|
@ -1,4 +1,5 @@
|
||||
"""A collection of tools used by several classes"""
|
||||
|
||||
def convert_recordset(recordset):
|
||||
""" Converts netbox RedcordSet to list of dicts. """
|
||||
recordlist = []
|
||||
@ -42,3 +43,47 @@ def proxy_prepper(proxy_list, proxy_group_list):
|
||||
group["monitored_by"] = 2
|
||||
output.append(group)
|
||||
return output
|
||||
|
||||
def field_mapper(host, mapper, nbdevice, logger):
|
||||
"""
|
||||
Maps NetBox field data to Zabbix properties.
|
||||
Used for Inventory, Usermacros and Tag mappings.
|
||||
"""
|
||||
data={}
|
||||
# Let's build an dict for each property in the map
|
||||
for nb_field, zbx_field in mapper.items():
|
||||
field_list = nb_field.split("/") # convert str to list based on delimiter
|
||||
# start at the base of the dict...
|
||||
value = nbdevice
|
||||
# ... and step through the dict till we find the needed value
|
||||
for item in field_list:
|
||||
value = value[item] if value else None
|
||||
# Check if the result is usable and expected
|
||||
# We want to apply any int or float 0 values,
|
||||
# even if python thinks those are empty.
|
||||
if ((value and isinstance(value, int | float | str )) or
|
||||
(isinstance(value, int | float) and int(value) ==0)):
|
||||
data[zbx_field] = str(value)
|
||||
elif not value:
|
||||
# empty value should just be an empty string for API compatibility
|
||||
logger.debug(f"Host {host}: NetBox lookup for "
|
||||
f"'{nb_field}' returned an empty value")
|
||||
data[zbx_field] = ""
|
||||
else:
|
||||
# Value is not a string or numeral, probably not what the user expected.
|
||||
logger.error(f"Host {host}: Lookup for '{nb_field}'"
|
||||
" returned an unexpected type: it will be skipped.")
|
||||
logger.debug(f"Host {host}: Field mapping complete. "
|
||||
f"Mapped {len(list(filter(None, data.values())))} field(s)")
|
||||
return data
|
||||
|
||||
def remove_duplicates(input_list, sortkey=None):
|
||||
"""
|
||||
Removes duplicate entries from a list and sorts the list
|
||||
"""
|
||||
output_list = []
|
||||
if isinstance(input_list, list):
|
||||
output_list = [dict(t) for t in {tuple(d.items()) for d in input_list}]
|
||||
if sortkey and isinstance(sortkey, str):
|
||||
output_list.sort(key=lambda x: x[sortkey])
|
||||
return output_list
|
||||
|
101
modules/usermacros.py
Normal file
101
modules/usermacros.py
Normal file
@ -0,0 +1,101 @@
|
||||
#!/usr/bin/env python3
|
||||
# pylint: disable=too-many-instance-attributes, too-many-arguments, too-many-positional-arguments, logging-fstring-interpolation
|
||||
"""
|
||||
All of the Zabbix Usermacro related configuration
|
||||
"""
|
||||
from re import match
|
||||
from logging import getLogger
|
||||
from modules.tools import field_mapper
|
||||
|
||||
class ZabbixUsermacros():
|
||||
"""Class that represents a Zabbix interface."""
|
||||
|
||||
def __init__(self, nb, usermacro_map, usermacro_sync, logger=None, host=None):
|
||||
self.nb = nb
|
||||
self.name = host if host else nb.name
|
||||
self.usermacro_map = usermacro_map
|
||||
self.logger = logger if logger else getLogger(__name__)
|
||||
self.usermacros = {}
|
||||
self.usermacro_sync = usermacro_sync
|
||||
self.sync = False
|
||||
self.force_sync = False
|
||||
self._set_config()
|
||||
|
||||
def __repr__(self):
|
||||
return self.name
|
||||
|
||||
def __str__(self):
|
||||
return self.__repr__()
|
||||
|
||||
def _set_config(self):
|
||||
"""
|
||||
Setup class
|
||||
"""
|
||||
if str(self.usermacro_sync).lower() == "full":
|
||||
self.sync = True
|
||||
self.force_sync = True
|
||||
elif self.usermacro_sync:
|
||||
self.sync = True
|
||||
return True
|
||||
|
||||
def validate_macro(self, macro_name):
|
||||
"""
|
||||
Validates usermacro name
|
||||
"""
|
||||
pattern = r'\{\$[A-Z0-9\._]*(\:.*)?\}'
|
||||
return match(pattern, macro_name)
|
||||
|
||||
def render_macro(self, macro_name, macro_properties):
|
||||
"""
|
||||
Renders a full usermacro from partial input
|
||||
"""
|
||||
macro={}
|
||||
macrotypes={'text': 0, 'secret': 1, 'vault': 2}
|
||||
if self.validate_macro(macro_name):
|
||||
macro['macro'] = str(macro_name)
|
||||
if isinstance(macro_properties, dict):
|
||||
if not 'value' in macro_properties:
|
||||
self.logger.error(f'Usermacro {macro_name} has no value, skipping.')
|
||||
return False
|
||||
macro['value'] = macro_properties['value']
|
||||
|
||||
if 'type' in macro_properties and macro_properties['type'].lower() in macrotypes:
|
||||
macro['type'] = str(macrotypes[macro_properties['type']])
|
||||
else:
|
||||
macro['type'] = str(0)
|
||||
|
||||
if ('description' in macro_properties and
|
||||
isinstance(macro_properties['description'], str)):
|
||||
macro['description'] = macro_properties['description']
|
||||
else:
|
||||
macro['description'] = ""
|
||||
|
||||
elif isinstance(macro_properties, str):
|
||||
macro['value'] = macro_properties
|
||||
macro['type'] = str(0)
|
||||
macro['description'] = ""
|
||||
else:
|
||||
self.logger.error(f'Usermacro {macro_name} is not a valid usermacro name, skipping.')
|
||||
return False
|
||||
return macro
|
||||
|
||||
def generate(self):
|
||||
"""
|
||||
Generate full set of Usermacros
|
||||
"""
|
||||
macros=[]
|
||||
# Parse the field mapper for usermacros
|
||||
if self.usermacro_map:
|
||||
self.logger.debug(f"Host {self.nb.name}: Starting usermacro mapper")
|
||||
field_macros = field_mapper(self.nb.name, self.usermacro_map, self.nb, self.logger)
|
||||
for macro, value in field_macros.items():
|
||||
m = self.render_macro(macro, value)
|
||||
if m:
|
||||
macros.append(m)
|
||||
# Parse NetBox config context for usermacros
|
||||
if "zabbix" in self.nb.config_context and "usermacros" in self.nb.config_context['zabbix']:
|
||||
for macro, properties in self.nb.config_context['zabbix']['usermacros'].items():
|
||||
m = self.render_macro(macro, properties)
|
||||
if m:
|
||||
macros.append(m)
|
||||
return macros
|
@ -9,6 +9,9 @@ from modules.interface import ZabbixInterface
|
||||
from modules.exceptions import TemplateError, InterfaceConfigError, SyncInventoryError
|
||||
try:
|
||||
from config import (
|
||||
vm_inventory_map,
|
||||
vm_usermacro_map,
|
||||
vm_tag_map,
|
||||
traverse_site_groups,
|
||||
traverse_regions
|
||||
)
|
||||
@ -24,6 +27,18 @@ class VirtualMachine(PhysicalDevice):
|
||||
self.hostgroup = None
|
||||
self.zbx_template_names = None
|
||||
|
||||
def _inventory_map(self):
|
||||
""" use VM inventory maps """
|
||||
return vm_inventory_map
|
||||
|
||||
def _usermacro_map(self):
|
||||
""" use VM usermacro maps """
|
||||
return vm_usermacro_map
|
||||
|
||||
def _tag_map(self):
|
||||
""" use VM tag maps """
|
||||
return vm_tag_map
|
||||
|
||||
def set_hostgroup(self, hg_format, nb_site_groups, nb_regions):
|
||||
"""Set the hostgroup for this device"""
|
||||
# Create new Hostgroup instance
|
||||
|
@ -161,7 +161,7 @@ def main(arguments):
|
||||
try:
|
||||
vm = VirtualMachine(nb_vm, zabbix, netbox_journals, nb_version,
|
||||
create_journal, logger)
|
||||
logger.debug(f"Host {vm.name}: started operations on VM.")
|
||||
logger.debug(f"Host {vm.name}: Started operations on VM.")
|
||||
vm.set_vm_template()
|
||||
# Check if a valid template has been found for this VM.
|
||||
if not vm.zbx_template_names:
|
||||
@ -171,6 +171,9 @@ def main(arguments):
|
||||
# Check if a valid hostgroup has been found for this VM.
|
||||
if not vm.hostgroup:
|
||||
continue
|
||||
vm.set_inventory(nb_vm)
|
||||
vm.set_usermacros()
|
||||
vm.set_tags()
|
||||
# Checks if device is in cleanup state
|
||||
if vm.status in zabbix_device_removal:
|
||||
if vm.zabbix_id:
|
||||
@ -224,6 +227,8 @@ def main(arguments):
|
||||
if not device.hostgroup:
|
||||
continue
|
||||
device.set_inventory(nb_device)
|
||||
device.set_usermacros()
|
||||
device.set_tags()
|
||||
# Checks if device is part of cluster.
|
||||
# Requires clustering variable
|
||||
if device.isCluster() and clustering:
|
||||
|
Loading…
Reference in New Issue
Block a user