mirror of
https://github.com/TheNetworkGuy/netbox-zabbix-sync.git
synced 2025-07-13 15:24:48 -06:00
Merge branch 'develop' into unittesting
This commit is contained in:
commit
45e633b5ed
74
.github/workflows/publish-image.yml
vendored
74
.github/workflows/publish-image.yml
vendored
@ -1,43 +1,57 @@
|
|||||||
name: Publish Docker image to GHCR on a new version
|
name: Build and Push Docker Image
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
|
||||||
env:
|
on:
|
||||||
REGISTRY: ghcr.io
|
release:
|
||||||
IMAGE_NAME: ${{ github.repository }}
|
types: [published]
|
||||||
|
pull_request:
|
||||||
|
types: [opened, synchronize]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test_quality:
|
test_quality:
|
||||||
uses: ./.github/workflows/quality.yml
|
uses: ./.github/workflows/quality.yml
|
||||||
build_and_publish:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout sources
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||||
- 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: 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.
|
||||||
|
4
.gitignore
vendored
4
.gitignore
vendored
@ -1,6 +1,8 @@
|
|||||||
*.log
|
*.log
|
||||||
.venv
|
.venv
|
||||||
/config.py
|
config.py
|
||||||
|
Pipfile
|
||||||
|
Pipfile.lock
|
||||||
# Byte-compiled / optimized / DLL files
|
# Byte-compiled / optimized / DLL files
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
|
@ -1,9 +1,13 @@
|
|||||||
# syntax=docker/dockerfile:1
|
# syntax=docker/dockerfile:1
|
||||||
FROM python:3.12-alpine
|
FROM python:3.12-alpine
|
||||||
RUN mkdir -p /opt/netbox-zabbix
|
RUN mkdir -p /opt/netbox-zabbix && chown -R 1000:1000 /opt/netbox-zabbix
|
||||||
COPY . /opt/netbox-zabbix
|
|
||||||
|
USER 1000:1000
|
||||||
|
COPY --chown=1000:1000 . /opt/netbox-zabbix
|
||||||
WORKDIR /opt/netbox-zabbix
|
WORKDIR /opt/netbox-zabbix
|
||||||
RUN if ! [ -f ./config.py ]; then cp ./config.py.example ./config.py; fi
|
RUN if ! [ -f ./config.py ]; then cp ./config.py.example ./config.py; fi
|
||||||
|
USER root
|
||||||
RUN pip install -r ./requirements.txt
|
RUN pip install -r ./requirements.txt
|
||||||
|
USER 1000:1000
|
||||||
ENTRYPOINT ["python"]
|
ENTRYPOINT ["python"]
|
||||||
CMD ["/opt/netbox-zabbix/netbox_zabbix_sync.py", "-v"]
|
CMD ["/opt/netbox-zabbix/netbox_zabbix_sync.py", "-v"]
|
||||||
|
247
README.md
247
README.md
@ -8,7 +8,7 @@ Currently compatible with Zabbix 7.0. Zabbix 7.2 is unfortunately not supported
|
|||||||
To pull the latest stable version to your local cache, use the following docker
|
To pull the latest stable version to your local cache, use the following docker
|
||||||
pull command:
|
pull command:
|
||||||
|
|
||||||
```sh
|
```bash
|
||||||
docker pull ghcr.io/thenetworkguy/netbox-zabbix-sync:main
|
docker pull ghcr.io/thenetworkguy/netbox-zabbix-sync:main
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -16,7 +16,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
|
(see [here](#set-environment-variables)) on the command line or use an
|
||||||
[env file](https://docs.docker.com/reference/cli/docker/container/run/#env).
|
[env file](https://docs.docker.com/reference/cli/docker/container/run/#env).
|
||||||
|
|
||||||
```sh
|
```bash
|
||||||
docker run -d -t -i -e ZABBIX_HOST='https://zabbix.local' \
|
docker run -d -t -i -e ZABBIX_HOST='https://zabbix.local' \
|
||||||
-e ZABBIX_TOKEN='othersecrettoken' \
|
-e ZABBIX_TOKEN='othersecrettoken' \
|
||||||
-e NETBOX_HOST='https://netbox.local' \
|
-e NETBOX_HOST='https://netbox.local' \
|
||||||
@ -31,7 +31,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
|
volume mount in the docker run command to override with your own config file if
|
||||||
needed (see [config file](#config-file)):
|
needed (see [config file](#config-file)):
|
||||||
|
|
||||||
```sh
|
```bash
|
||||||
docker run -d -t -i -v $(pwd)/config.py:/opt/netbox-zabbix/config.py ...
|
docker run -d -t -i -v $(pwd)/config.py:/opt/netbox-zabbix/config.py ...
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -39,7 +39,7 @@ docker run -d -t -i -v $(pwd)/config.py:/opt/netbox-zabbix/config.py ...
|
|||||||
|
|
||||||
### Cloning the repository
|
### Cloning the repository
|
||||||
|
|
||||||
```sh
|
```bash
|
||||||
git clone https://github.com/TheNetworkGuy/netbox-zabbix-sync.git
|
git clone https://github.com/TheNetworkGuy/netbox-zabbix-sync.git
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -73,19 +73,19 @@ cp config.py.example config.py
|
|||||||
|
|
||||||
Set the following environment variables:
|
Set the following environment variables:
|
||||||
|
|
||||||
```sh
|
```bash
|
||||||
export ZABBIX_HOST="https://zabbix.local"
|
ZABBIX_HOST="https://zabbix.local"
|
||||||
export ZABBIX_USER="username"
|
ZABBIX_USER="username"
|
||||||
export ZABBIX_PASS="Password"
|
ZABBIX_PASS="Password"
|
||||||
export NETBOX_HOST="https://netbox.local"
|
NETBOX_HOST="https://netbox.local"
|
||||||
export NETBOX_TOKEN="secrettoken"
|
NETBOX_TOKEN="secrettoken"
|
||||||
```
|
```
|
||||||
|
|
||||||
Or, you can use a Zabbix API token to login instead of using a username and
|
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.
|
password. In that case `ZABBIX_USER` and `ZABBIX_PASS` will be ignored.
|
||||||
|
|
||||||
```sh
|
```bash
|
||||||
export ZABBIX_TOKEN=othersecrettoken
|
ZABBIX_TOKEN=othersecrettoken
|
||||||
```
|
```
|
||||||
|
|
||||||
If you are using custom SSL certificates for NetBox and/or Zabbix, you can set
|
If you are using custom SSL certificates for NetBox and/or Zabbix, you can set
|
||||||
@ -190,9 +190,9 @@ used:
|
|||||||
| cluster | VM cluster name |
|
| cluster | VM cluster name |
|
||||||
| cluster_type | VM cluster type |
|
| 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"
|
hostgroup_format = "tenant/site/dev_location/role"
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -239,7 +239,7 @@ have a relationship with a tenant.
|
|||||||
- Device_role: PDU
|
- Device_role: PDU
|
||||||
- Site: HQ-AMS
|
- Site: HQ-AMS
|
||||||
|
|
||||||
```
|
```python
|
||||||
hostgroup_format = "site/tenant/device_role"
|
hostgroup_format = "site/tenant/device_role"
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -252,7 +252,7 @@ generated for both hosts:
|
|||||||
|
|
||||||
The same logic applies to custom fields being used in the HG format:
|
The same logic applies to custom fields being used in the HG format:
|
||||||
|
|
||||||
```
|
```python
|
||||||
hostgroup_format = "site/mycustomfieldname"
|
hostgroup_format = "site/mycustomfieldname"
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -299,15 +299,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)
|
[Zabbix Manual](https://www.zabbix.com/documentation/current/en/manual/config/hosts/inventory#building-inventory)
|
||||||
for more information about the modes.
|
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 '/'
|
which Zabbix Inventory fields. For nested properties, you can use the '/'
|
||||||
seperator. For example, the following map will assign the custom field
|
seperator. For example, the following map will assign the custom field
|
||||||
'mycustomfield' to the 'alias' Zabbix inventory field:
|
'mycustomfield' to the 'alias' Zabbix inventory field:
|
||||||
|
|
||||||
```
|
For Virtual Machines, use `vm_inventory_map`.
|
||||||
|
|
||||||
|
```python
|
||||||
inventory_sync = True
|
inventory_sync = True
|
||||||
inventory_mode = "manual"
|
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
|
See `config.py.example` for an extensive example map. Any Zabix Inventory fields
|
||||||
@ -328,14 +331,14 @@ sticking to the custom field.
|
|||||||
You can change the behaviour in the config file. By default this setting is
|
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:
|
false but you can set it to true to use config context:
|
||||||
|
|
||||||
```
|
```python
|
||||||
templates_config_context = True
|
templates_config_context = True
|
||||||
```
|
```
|
||||||
|
|
||||||
After that make sure that for each host there is at least one template defined
|
After that make sure that for each host there is at least one template defined
|
||||||
in the config context in this format:
|
in the config context in this format:
|
||||||
|
|
||||||
```
|
```json
|
||||||
{
|
{
|
||||||
"zabbix": {
|
"zabbix": {
|
||||||
"templates": [
|
"templates": [
|
||||||
@ -353,10 +356,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
|
device specific context defined. In this case the device specific context
|
||||||
template(s) will take priority over the device type custom field template.
|
template(s) will take priority over the device type custom field template.
|
||||||
|
|
||||||
```
|
```python
|
||||||
templates_config_context_overrule = True
|
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
|
## Permissions
|
||||||
|
|
||||||
### NetBox
|
### NetBox
|
||||||
@ -393,9 +582,11 @@ python3 netbox_zabbix_sync.py
|
|||||||
|
|
||||||
### Flags
|
### Flags
|
||||||
|
|
||||||
| Flag | Option | Description |
|
| Flag | Option | Description |
|
||||||
| ---- | ------- | ---------------------- |
|
| ---- | --------- | ------------------------------------- |
|
||||||
| -v | verbose | Log with debugging on. |
|
| -v | verbose | Log with info on. |
|
||||||
|
| -vv | debug | Log with debugging on. |
|
||||||
|
| -vvv | debug-all | Log with debugging on for all modules |
|
||||||
|
|
||||||
## Config context
|
## Config context
|
||||||
|
|
||||||
@ -525,9 +716,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.
|
since the data in NetBox is plain-text.
|
||||||
|
|
||||||
> **_NOTE:_** Not all SNMP data is required for a working configuration.
|
> **_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
|
> [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.
|
> are not all required, depending on your environment.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -80,19 +80,74 @@ inventory_sync = False
|
|||||||
# For nested properties, you can use the '/' seperator.
|
# 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 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:
|
# The following maps should provide some nice defaults:
|
||||||
inventory_map = { "asset_tag": "asset_tag",
|
device_inventory_map = { "asset_tag": "asset_tag",
|
||||||
"virtual_chassis/name": "chassis",
|
"virtual_chassis/name": "chassis",
|
||||||
"status/label": "deployment_status",
|
"status/label": "deployment_status",
|
||||||
"location/name": "location",
|
"location/name": "location",
|
||||||
"latitude": "location_lat",
|
"latitude": "location_lat",
|
||||||
"longitude": "location_lon",
|
"longitude": "location_lon",
|
||||||
"comments": "notes",
|
"comments": "notes",
|
||||||
"name": "name",
|
"name": "name",
|
||||||
"rack/name": "site_rack",
|
"rack/name": "site_rack",
|
||||||
"serial": "serialno_a",
|
"serial": "serialno_a",
|
||||||
"device_type/model": "type",
|
"device_type/model": "type",
|
||||||
"device_type/manufacturer/name": "vendor",
|
"device_type/manufacturer/name": "vendor",
|
||||||
"oob_ip/address": "oob_ip" }
|
"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,28 +1,40 @@
|
|||||||
#!/usr/bin/env python3
|
# pylint: disable=invalid-name, logging-not-lazy, too-many-locals, logging-fstring-interpolation, too-many-lines, too-many-public-methods, duplicate-code
|
||||||
# pylint: disable=invalid-name, logging-not-lazy, too-many-locals, logging-fstring-interpolation, too-many-lines
|
|
||||||
"""
|
"""
|
||||||
Device specific handeling for NetBox to Zabbix
|
Device specific handeling for NetBox to Zabbix
|
||||||
"""
|
"""
|
||||||
from re import search
|
|
||||||
|
from copy import deepcopy
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
|
from re import search
|
||||||
|
|
||||||
from zabbix_utils import APIRequestError
|
from zabbix_utils import APIRequestError
|
||||||
from modules.exceptions import (SyncInventoryError, TemplateError, SyncExternalError,
|
|
||||||
InterfaceConfigError, JournalError)
|
from modules.exceptions import (
|
||||||
from modules.interface import ZabbixInterface
|
InterfaceConfigError,
|
||||||
|
JournalError,
|
||||||
|
SyncExternalError,
|
||||||
|
SyncInventoryError,
|
||||||
|
TemplateError,
|
||||||
|
)
|
||||||
from modules.hostgroups import Hostgroup
|
from modules.hostgroups import Hostgroup
|
||||||
|
from modules.interface import ZabbixInterface
|
||||||
|
from modules.tags import ZabbixTags
|
||||||
|
from modules.tools import field_mapper, remove_duplicates
|
||||||
|
from modules.usermacros import ZabbixUsermacros
|
||||||
from modules.config import load_config
|
from modules.config import load_config
|
||||||
|
|
||||||
config = load_config()
|
config = load_config()
|
||||||
|
|
||||||
|
class PhysicalDevice:
|
||||||
class PhysicalDevice():
|
|
||||||
# pylint: disable=too-many-instance-attributes, too-many-arguments, too-many-positional-arguments
|
# pylint: disable=too-many-instance-attributes, too-many-arguments, too-many-positional-arguments
|
||||||
"""
|
"""
|
||||||
Represents Network device.
|
Represents Network device.
|
||||||
INPUT: (NetBox device class, ZabbixAPI class, journal flag, NB journal class)
|
INPUT: (NetBox device class, ZabbixAPI class, journal flag, NB journal class)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, nb, zabbix, nb_journal_class, nb_version, journal=None, logger=None):
|
def __init__(
|
||||||
|
self, nb, zabbix, nb_journal_class, nb_version, journal=None, logger=None
|
||||||
|
):
|
||||||
self.nb = nb
|
self.nb = nb
|
||||||
self.id = nb.id
|
self.id = nb.id
|
||||||
self.name = nb.name
|
self.name = nb.name
|
||||||
@ -43,6 +55,8 @@ class PhysicalDevice():
|
|||||||
self.nb_journals = nb_journal_class
|
self.nb_journals = nb_journal_class
|
||||||
self.inventory_mode = -1
|
self.inventory_mode = -1
|
||||||
self.inventory = {}
|
self.inventory = {}
|
||||||
|
self.usermacros = {}
|
||||||
|
self.tags = {}
|
||||||
self.logger = logger if logger else getLogger(__name__)
|
self.logger = logger if logger else getLogger(__name__)
|
||||||
self._setBasics()
|
self._setBasics()
|
||||||
|
|
||||||
@ -52,6 +66,18 @@ class PhysicalDevice():
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.__repr__()
|
return self.__repr__()
|
||||||
|
|
||||||
|
def _inventory_map(self):
|
||||||
|
"""Use device inventory maps"""
|
||||||
|
return config["device_inventory_map"]
|
||||||
|
|
||||||
|
def _usermacro_map(self):
|
||||||
|
"""Use device inventory maps"""
|
||||||
|
return config["device_usermacro_map"]
|
||||||
|
|
||||||
|
def _tag_map(self):
|
||||||
|
"""Use device host tag maps"""
|
||||||
|
return config["device_tag_map"]
|
||||||
|
|
||||||
def _setBasics(self):
|
def _setBasics(self):
|
||||||
"""
|
"""
|
||||||
Sets basic information like IP address.
|
Sets basic information like IP address.
|
||||||
@ -62,7 +88,7 @@ class PhysicalDevice():
|
|||||||
self.ip = self.cidr.split("/")[0]
|
self.ip = self.cidr.split("/")[0]
|
||||||
else:
|
else:
|
||||||
e = f"Host {self.name}: no primary IP."
|
e = f"Host {self.name}: no primary IP."
|
||||||
self.logger.info(e)
|
self.logger.warning(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
|
||||||
@ -76,30 +102,38 @@ class PhysicalDevice():
|
|||||||
# Validate hostname format.
|
# Validate hostname format.
|
||||||
odd_character_list = ["ä", "ö", "ü", "Ä", "Ö", "Ü", "ß"]
|
odd_character_list = ["ä", "ö", "ü", "Ä", "Ö", "Ü", "ß"]
|
||||||
self.use_visible_name = False
|
self.use_visible_name = False
|
||||||
if (any(letter in self.name for letter in odd_character_list) or
|
if any(letter in self.name for letter in odd_character_list) or bool(
|
||||||
bool(search('[\u0400-\u04FF]', self.name))):
|
search("[\u0400-\u04ff]", self.name)
|
||||||
|
):
|
||||||
self.name = f"NETBOX_ID{self.id}"
|
self.name = f"NETBOX_ID{self.id}"
|
||||||
self.visible_name = self.nb.name
|
self.visible_name = self.nb.name
|
||||||
self.use_visible_name = True
|
self.use_visible_name = True
|
||||||
self.logger.info(f"Host {self.visible_name} contains special characters. "
|
self.logger.info(
|
||||||
f"Using {self.name} as name for the NetBox object "
|
f"Host {self.visible_name} contains special characters. "
|
||||||
f"and using {self.visible_name} as visible name in Zabbix.")
|
f"Using {self.name} as name for the NetBox object "
|
||||||
|
f"and using {self.visible_name} as visible name in Zabbix."
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def set_hostgroup(self, hg_format, nb_site_groups, nb_regions):
|
def set_hostgroup(self, hg_format, nb_site_groups, nb_regions):
|
||||||
"""Set the hostgroup for this device"""
|
"""Set the hostgroup for this device"""
|
||||||
# Create new Hostgroup instance
|
# Create new Hostgroup instance
|
||||||
hg = Hostgroup("dev", self.nb, self.nb_api_version, logger=self.logger,
|
hg = Hostgroup(
|
||||||
nested_sitegroup_flag=config["traverse_site_groups"],
|
"dev",
|
||||||
nested_region_flag=config["traverse_regions"],
|
self.nb,
|
||||||
nb_groups=nb_site_groups,
|
self.nb_api_version,
|
||||||
nb_regions=nb_regions)
|
logger=self.logger,
|
||||||
|
nested_sitegroup_flag=traverse_site_groups,
|
||||||
|
nested_region_flag=traverse_regions,
|
||||||
|
nb_groups=nb_site_groups,
|
||||||
|
nb_regions=nb_regions,
|
||||||
|
)
|
||||||
# Generate hostgroup based on hostgroup format
|
# Generate hostgroup based on hostgroup format
|
||||||
self.hostgroup = hg.generate(hg_format)
|
self.hostgroup = hg.generate(hg_format)
|
||||||
|
|
||||||
def set_template(self, prefer_config_context, overrule_custom):
|
def set_template(self, prefer_config_context, overrule_custom):
|
||||||
""" Set Template """
|
"""Set Template"""
|
||||||
self.zbx_template_names = None
|
self.zbx_template_names = None
|
||||||
# Gather templates ONLY from the device specific context
|
# Gather templates ONLY from the device specific context
|
||||||
if prefer_config_context:
|
if prefer_config_context:
|
||||||
@ -123,7 +157,7 @@ class PhysicalDevice():
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def get_templates_cf(self):
|
def get_templates_cf(self):
|
||||||
""" Get template from custom field """
|
"""Get template from custom field"""
|
||||||
# Get Zabbix templates from the device type
|
# Get Zabbix templates from the device type
|
||||||
device_type_cfs = self.nb.device_type.custom_fields
|
device_type_cfs = self.nb.device_type.custom_fields
|
||||||
# Check if the ZBX Template CF is present
|
# Check if the ZBX Template CF is present
|
||||||
@ -131,22 +165,29 @@ class PhysicalDevice():
|
|||||||
# Set value to template
|
# Set value to template
|
||||||
return [device_type_cfs[config["template_cf"]]]
|
return [device_type_cfs[config["template_cf"]]]
|
||||||
# Custom field not found, return error
|
# Custom field not found, return error
|
||||||
e = (f'Custom field {config["template_cf"]} not '
|
e = (
|
||||||
f"found for {self.nb.device_type.manufacturer.name}"
|
f"Custom field {template_cf} not "
|
||||||
f" - {self.nb.device_type.display}.")
|
f"found for {self.nb.device_type.manufacturer.name}"
|
||||||
|
f" - {self.nb.device_type.display}."
|
||||||
|
)
|
||||||
|
self.logger.warning(e)
|
||||||
raise TemplateError(e)
|
raise TemplateError(e)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def get_templates_context(self):
|
def get_templates_context(self):
|
||||||
""" Get Zabbix templates from the device context """
|
"""Get Zabbix templates from the device context"""
|
||||||
if "zabbix" not in self.config_context:
|
if "zabbix" not in self.config_context:
|
||||||
e = (f"Host {self.name}: Key 'zabbix' not found in config "
|
e = (
|
||||||
"context for template lookup")
|
f"Host {self.name}: Key 'zabbix' not found in config "
|
||||||
|
"context for template lookup"
|
||||||
|
)
|
||||||
raise TemplateError(e)
|
raise TemplateError(e)
|
||||||
if "templates" not in self.config_context["zabbix"]:
|
if "templates" not in self.config_context["zabbix"]:
|
||||||
e = (f"Host {self.name}: Key 'templates' not found in config "
|
e = (
|
||||||
"context 'zabbix' for template lookup")
|
f"Host {self.name}: Key 'templates' not found in config "
|
||||||
|
"context 'zabbix' for template lookup"
|
||||||
|
)
|
||||||
raise TemplateError(e)
|
raise TemplateError(e)
|
||||||
# Check if format is list or string.
|
# Check if format is list or string.
|
||||||
if isinstance(self.config_context["zabbix"]["templates"], str):
|
if isinstance(self.config_context["zabbix"]["templates"], str):
|
||||||
@ -154,7 +195,7 @@ class PhysicalDevice():
|
|||||||
return self.config_context["zabbix"]["templates"]
|
return self.config_context["zabbix"]["templates"]
|
||||||
|
|
||||||
def set_inventory(self, nbdevice):
|
def set_inventory(self, nbdevice):
|
||||||
""" Set host inventory """
|
"""Set host inventory"""
|
||||||
# Set inventory mode. Default is disabled (see class init function).
|
# Set inventory mode. Default is disabled (see class init function).
|
||||||
if config["inventory_mode"] == "disabled":
|
if config["inventory_mode"] == "disabled":
|
||||||
if config["inventory_sync"]:
|
if config["inventory_sync"]:
|
||||||
@ -167,37 +208,17 @@ class PhysicalDevice():
|
|||||||
elif config["inventory_mode"] == "automatic":
|
elif config["inventory_mode"] == "automatic":
|
||||||
self.inventory_mode = 1
|
self.inventory_mode = 1
|
||||||
else:
|
else:
|
||||||
self.logger.error(f"Host {self.name}: Specified value for inventory mode in "
|
self.logger.error(
|
||||||
f'config is not valid. Got value {config["inventory_mode"]}')
|
f"Host {self.name}: Specified value for inventory mode in"
|
||||||
|
f" config is not valid. Got value {config['inventory_mode']}"
|
||||||
|
)
|
||||||
return False
|
return False
|
||||||
self.inventory = {}
|
self.inventory = {}
|
||||||
if config["inventory_sync"] and self.inventory_mode in [0, 1]:
|
if config["inventory_sync"] and self.inventory_mode in [0, 1]:
|
||||||
self.logger.debug(f"Host {self.name}: Starting inventory mapper")
|
self.logger.debug(f"Host {self.name}: Starting inventory mapper")
|
||||||
# Let's build an inventory dict for each property in the inventory_map
|
self.inventory = field_mapper(
|
||||||
for nb_inv_field, zbx_inv_field in config["inventory_map"].items():
|
self.name, self._inventory_map(), nbdevice, self.logger
|
||||||
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)")
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def isCluster(self):
|
def isCluster(self):
|
||||||
@ -211,13 +232,17 @@ class PhysicalDevice():
|
|||||||
Returns chassis master ID.
|
Returns chassis master ID.
|
||||||
"""
|
"""
|
||||||
if not self.isCluster():
|
if not self.isCluster():
|
||||||
e = (f"Unable to proces {self.name} for cluster calculation: "
|
e = (
|
||||||
f"not part of a cluster.")
|
f"Unable to proces {self.name} for cluster calculation: "
|
||||||
|
f"not part of a cluster."
|
||||||
|
)
|
||||||
self.logger.warning(e)
|
self.logger.warning(e)
|
||||||
raise SyncInventoryError(e)
|
raise SyncInventoryError(e)
|
||||||
if not self.nb.virtual_chassis.master:
|
if not self.nb.virtual_chassis.master:
|
||||||
e = (f"{self.name} is part of a NetBox virtual chassis which does "
|
e = (
|
||||||
"not have a master configured. Skipping for this reason.")
|
f"{self.name} is part of a NetBox virtual chassis which does "
|
||||||
|
"not have a master configured. Skipping for this reason."
|
||||||
|
)
|
||||||
self.logger.error(e)
|
self.logger.error(e)
|
||||||
raise SyncInventoryError(e)
|
raise SyncInventoryError(e)
|
||||||
return self.nb.virtual_chassis.master.id
|
return self.nb.virtual_chassis.master.id
|
||||||
@ -230,9 +255,11 @@ class PhysicalDevice():
|
|||||||
"""
|
"""
|
||||||
masterid = self.getClusterMaster()
|
masterid = self.getClusterMaster()
|
||||||
if masterid == self.id:
|
if masterid == self.id:
|
||||||
self.logger.debug(f"Host {self.name} is primary cluster member. "
|
self.logger.debug(
|
||||||
f"Modifying hostname from {self.name} to " +
|
f"Host {self.name} is primary cluster member. "
|
||||||
f"{self.nb.virtual_chassis.name}.")
|
f"Modifying hostname from {self.name} to "
|
||||||
|
+ f"{self.nb.virtual_chassis.name}."
|
||||||
|
)
|
||||||
self.name = self.nb.virtual_chassis.name
|
self.name = self.nb.virtual_chassis.name
|
||||||
return True
|
return True
|
||||||
self.logger.debug(f"Host {self.name} is non-primary cluster member.")
|
self.logger.debug(f"Host {self.name} is non-primary cluster member.")
|
||||||
@ -257,18 +284,24 @@ class PhysicalDevice():
|
|||||||
# Go through all templates found in Zabbix
|
# Go through all templates found in Zabbix
|
||||||
for zbx_template in templates:
|
for zbx_template in templates:
|
||||||
# If the template names match
|
# If the template names match
|
||||||
if zbx_template['name'] == nb_template:
|
if zbx_template["name"] == nb_template:
|
||||||
# Set match variable to true, add template details
|
# Set match variable to true, add template details
|
||||||
# to class variable and return debug log
|
# to class variable and return debug log
|
||||||
template_match = True
|
template_match = True
|
||||||
self.zbx_templates.append({"templateid": zbx_template['templateid'],
|
self.zbx_templates.append(
|
||||||
"name": zbx_template['name']})
|
{
|
||||||
|
"templateid": zbx_template["templateid"],
|
||||||
|
"name": zbx_template["name"],
|
||||||
|
}
|
||||||
|
)
|
||||||
e = f"Host {self.name}: found template {zbx_template['name']}"
|
e = f"Host {self.name}: found template {zbx_template['name']}"
|
||||||
self.logger.debug(e)
|
self.logger.debug(e)
|
||||||
# Return error should the template not be found in Zabbix
|
# Return error should the template not be found in Zabbix
|
||||||
if not template_match:
|
if not template_match:
|
||||||
e = (f"Unable to find template {nb_template} "
|
e = (
|
||||||
f"for host {self.name} in Zabbix. Skipping host...")
|
f"Unable to find template {nb_template} "
|
||||||
|
f"for host {self.name} in Zabbix. Skipping host..."
|
||||||
|
)
|
||||||
self.logger.warning(e)
|
self.logger.warning(e)
|
||||||
raise SyncInventoryError(e)
|
raise SyncInventoryError(e)
|
||||||
|
|
||||||
@ -280,8 +313,8 @@ class PhysicalDevice():
|
|||||||
"""
|
"""
|
||||||
# Go through all groups
|
# Go through all groups
|
||||||
for group in groups:
|
for group in groups:
|
||||||
if group['name'] == self.hostgroup:
|
if group["name"] == self.hostgroup:
|
||||||
self.group_id = group['groupid']
|
self.group_id = group["groupid"]
|
||||||
e = f"Host {self.name}: matched group {group['name']}"
|
e = f"Host {self.name}: matched group {group['name']}"
|
||||||
self.logger.debug(e)
|
self.logger.debug(e)
|
||||||
return True
|
return True
|
||||||
@ -295,10 +328,13 @@ class PhysicalDevice():
|
|||||||
if self.zabbix_id:
|
if self.zabbix_id:
|
||||||
try:
|
try:
|
||||||
# Check if the Zabbix host exists in Zabbix
|
# Check if the Zabbix host exists in Zabbix
|
||||||
zbx_host = bool(self.zabbix.host.get(filter={'hostid': self.zabbix_id},
|
zbx_host = bool(
|
||||||
output=[]))
|
self.zabbix.host.get(filter={"hostid": self.zabbix_id}, output=[])
|
||||||
e = (f"Host {self.name}: was already deleted from Zabbix."
|
)
|
||||||
" Removed link in NetBox.")
|
e = (
|
||||||
|
f"Host {self.name}: was already deleted from Zabbix."
|
||||||
|
" Removed link in NetBox."
|
||||||
|
)
|
||||||
if zbx_host:
|
if zbx_host:
|
||||||
# Delete host should it exists
|
# Delete host should it exists
|
||||||
self.zabbix.host.delete(self.zabbix_id)
|
self.zabbix.host.delete(self.zabbix_id)
|
||||||
@ -323,9 +359,9 @@ class PhysicalDevice():
|
|||||||
"""
|
"""
|
||||||
# Validate the hostname or visible name field
|
# Validate the hostname or visible name field
|
||||||
if not self.use_visible_name:
|
if not self.use_visible_name:
|
||||||
zbx_filter = {'host': self.name}
|
zbx_filter = {"host": self.name}
|
||||||
else:
|
else:
|
||||||
zbx_filter = {'name': self.visible_name}
|
zbx_filter = {"name": self.visible_name}
|
||||||
host = self.zabbix.host.get(filter=zbx_filter, output=[])
|
host = self.zabbix.host.get(filter=zbx_filter, output=[])
|
||||||
return bool(host)
|
return bool(host)
|
||||||
|
|
||||||
@ -351,6 +387,43 @@ class PhysicalDevice():
|
|||||||
self.logger.warning(message)
|
self.logger.warning(message)
|
||||||
raise SyncInventoryError(message) from e
|
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):
|
def setProxy(self, proxy_list):
|
||||||
"""
|
"""
|
||||||
Sets proxy or proxy group if this
|
Sets proxy or proxy group if this
|
||||||
@ -361,14 +434,16 @@ class PhysicalDevice():
|
|||||||
# check if the key Zabbix is defined in the config context
|
# check if the key Zabbix is defined in the config context
|
||||||
if "zabbix" not in self.nb.config_context:
|
if "zabbix" not in self.nb.config_context:
|
||||||
return False
|
return False
|
||||||
if ("proxy" in self.nb.config_context["zabbix"] and
|
if (
|
||||||
not self.nb.config_context["zabbix"]["proxy"]):
|
"proxy" in self.nb.config_context["zabbix"]
|
||||||
|
and not self.nb.config_context["zabbix"]["proxy"]
|
||||||
|
):
|
||||||
return False
|
return False
|
||||||
# Proxy group takes priority over a proxy due
|
# Proxy group takes priority over a proxy due
|
||||||
# to it being HA and therefore being more reliable
|
# to it being HA and therefore being more reliable
|
||||||
# Includes proxy group fix since Zabbix <= 6 should ignore this
|
# Includes proxy group fix since Zabbix <= 6 should ignore this
|
||||||
proxy_types = ["proxy"]
|
proxy_types = ["proxy"]
|
||||||
if str(self.zabbix.version).startswith('7'):
|
if str(self.zabbix.version).startswith("7"):
|
||||||
# Only insert groups in front of list for Zabbix7
|
# Only insert groups in front of list for Zabbix7
|
||||||
proxy_types.insert(0, "proxy_group")
|
proxy_types.insert(0, "proxy_group")
|
||||||
for proxy_type in proxy_types:
|
for proxy_type in proxy_types:
|
||||||
@ -382,15 +457,23 @@ class PhysicalDevice():
|
|||||||
continue
|
continue
|
||||||
# If the proxy name matches
|
# If the proxy name matches
|
||||||
if proxy["name"] == proxy_name:
|
if proxy["name"] == proxy_name:
|
||||||
self.logger.debug(f"Host {self.name}: using {proxy['type']}"
|
self.logger.debug(
|
||||||
f" {proxy_name}")
|
f"Host {self.name}: using {proxy['type']}" f" {proxy_name}"
|
||||||
|
)
|
||||||
self.zbxproxy = proxy
|
self.zbxproxy = proxy
|
||||||
return True
|
return True
|
||||||
self.logger.warning(f"Host {self.name}: unable to find proxy {proxy_name}")
|
self.logger.warning(
|
||||||
|
f"Host {self.name}: unable to find proxy {proxy_name}"
|
||||||
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def createInZabbix(self, groups, templates, proxies,
|
def createInZabbix(
|
||||||
description="Host added by NetBox sync script."):
|
self,
|
||||||
|
groups,
|
||||||
|
templates,
|
||||||
|
proxies,
|
||||||
|
description="Host added by NetBox sync script.",
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Creates Zabbix host object with parameters from NetBox object.
|
Creates Zabbix host object with parameters from NetBox object.
|
||||||
"""
|
"""
|
||||||
@ -398,14 +481,16 @@ class PhysicalDevice():
|
|||||||
if not self._zabbixHostnameExists():
|
if not self._zabbixHostnameExists():
|
||||||
# Set group and template ID's for host
|
# Set group and template ID's for host
|
||||||
if not self.setZabbixGroupID(groups):
|
if not self.setZabbixGroupID(groups):
|
||||||
e = (f"Unable to find group '{self.hostgroup}' "
|
e = (
|
||||||
f"for host {self.name} in Zabbix.")
|
f"Unable to find group '{self.hostgroup}' "
|
||||||
|
f"for host {self.name} in Zabbix."
|
||||||
|
)
|
||||||
self.logger.warning(e)
|
self.logger.warning(e)
|
||||||
raise SyncInventoryError(e)
|
raise SyncInventoryError(e)
|
||||||
self.zbxTemplatePrepper(templates)
|
self.zbxTemplatePrepper(templates)
|
||||||
templateids = []
|
templateids = []
|
||||||
for template in self.zbx_templates:
|
for template in self.zbx_templates:
|
||||||
templateids.append({'templateid': template['templateid']})
|
templateids.append({"templateid": template["templateid"]})
|
||||||
# Set interface, group and template configuration
|
# Set interface, group and template configuration
|
||||||
interfaces = self.setInterfaceDetails()
|
interfaces = self.setInterfaceDetails()
|
||||||
groups = [{"groupid": self.group_id}]
|
groups = [{"groupid": self.group_id}]
|
||||||
@ -421,13 +506,15 @@ class PhysicalDevice():
|
|||||||
"templates": templateids,
|
"templates": templateids,
|
||||||
"description": description,
|
"description": description,
|
||||||
"inventory_mode": self.inventory_mode,
|
"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 a Zabbix proxy or Zabbix Proxy group has been defined
|
||||||
if self.zbxproxy:
|
if self.zbxproxy:
|
||||||
# If a lower version than 7 is used, we can assume that
|
# If a lower version than 7 is used, we can assume that
|
||||||
# the proxy is a normal proxy and not a proxy group
|
# the proxy is a normal proxy and not a proxy group
|
||||||
if not str(self.zabbix.version).startswith('7'):
|
if not str(self.zabbix.version).startswith("7"):
|
||||||
create_data["proxy_hostid"] = self.zbxproxy["id"]
|
create_data["proxy_hostid"] = self.zbxproxy["id"]
|
||||||
else:
|
else:
|
||||||
# Configure either a proxy or proxy group
|
# Configure either a proxy or proxy group
|
||||||
@ -438,9 +525,9 @@ class PhysicalDevice():
|
|||||||
host = self.zabbix.host.create(**create_data)
|
host = self.zabbix.host.create(**create_data)
|
||||||
self.zabbix_id = host["hostids"][0]
|
self.zabbix_id = host["hostids"][0]
|
||||||
except APIRequestError as e:
|
except APIRequestError as e:
|
||||||
e = f"Host {self.name}: Couldn't create. Zabbix returned {str(e)}."
|
msg = f"Host {self.name}: Couldn't create. Zabbix returned {str(e)}."
|
||||||
self.logger.error(e)
|
self.logger.error(msg)
|
||||||
raise SyncExternalError(e) from None
|
raise SyncExternalError(msg) from e
|
||||||
# Set NetBox custom field to hostID value.
|
# Set NetBox custom field to hostID value.
|
||||||
self.nb.custom_fields[config["device_cf"]] = int(self.zabbix_id)
|
self.nb.custom_fields[config["device_cf"]] = int(self.zabbix_id)
|
||||||
self.nb.save()
|
self.nb.save()
|
||||||
@ -448,8 +535,9 @@ class PhysicalDevice():
|
|||||||
self.logger.info(msg)
|
self.logger.info(msg)
|
||||||
self.create_journal_entry("success", msg)
|
self.create_journal_entry("success", msg)
|
||||||
else:
|
else:
|
||||||
e = f"Host {self.name}: Unable to add to Zabbix. Host already present."
|
self.logger.error(
|
||||||
self.logger.warning(e)
|
f"Host {self.name}: Unable to add to Zabbix. Host already present."
|
||||||
|
)
|
||||||
|
|
||||||
def createZabbixHostgroup(self, hostgroups):
|
def createZabbixHostgroup(self, hostgroups):
|
||||||
"""
|
"""
|
||||||
@ -458,8 +546,8 @@ class PhysicalDevice():
|
|||||||
"""
|
"""
|
||||||
final_data = []
|
final_data = []
|
||||||
# Check if the hostgroup is in a nested format and check each parent
|
# Check if the hostgroup is in a nested format and check each parent
|
||||||
for pos in range(len(self.hostgroup.split('/'))):
|
for pos in range(len(self.hostgroup.split("/"))):
|
||||||
zabbix_hg = self.hostgroup.rsplit('/', pos)[0]
|
zabbix_hg = self.hostgroup.rsplit("/", pos)[0]
|
||||||
if self.lookupZabbixHostgroup(hostgroups, zabbix_hg):
|
if self.lookupZabbixHostgroup(hostgroups, zabbix_hg):
|
||||||
# Hostgroup already exists
|
# Hostgroup already exists
|
||||||
continue
|
continue
|
||||||
@ -470,7 +558,9 @@ class PhysicalDevice():
|
|||||||
e = f"Hostgroup '{zabbix_hg}': created in Zabbix."
|
e = f"Hostgroup '{zabbix_hg}': created in Zabbix."
|
||||||
self.logger.info(e)
|
self.logger.info(e)
|
||||||
# Add group to final data
|
# Add group to final data
|
||||||
final_data.append({'groupid': groupid["groupids"][0], 'name': zabbix_hg})
|
final_data.append(
|
||||||
|
{"groupid": groupid["groupids"][0], "name": zabbix_hg}
|
||||||
|
)
|
||||||
except APIRequestError as e:
|
except APIRequestError as e:
|
||||||
msg = f"Hostgroup '{zabbix_hg}': unable to create. Zabbix returned {str(e)}."
|
msg = f"Hostgroup '{zabbix_hg}': unable to create. Zabbix returned {str(e)}."
|
||||||
self.logger.error(msg)
|
self.logger.error(msg)
|
||||||
@ -497,20 +587,24 @@ class PhysicalDevice():
|
|||||||
try:
|
try:
|
||||||
self.zabbix.host.update(hostid=self.zabbix_id, **kwargs)
|
self.zabbix.host.update(hostid=self.zabbix_id, **kwargs)
|
||||||
except APIRequestError as e:
|
except APIRequestError as e:
|
||||||
e = (f"Host {self.name}: Unable to update. "
|
e = (
|
||||||
f"Zabbix returned the following error: {str(e)}.")
|
f"Host {self.name}: Unable to update. "
|
||||||
|
f"Zabbix returned the following error: {str(e)}."
|
||||||
|
)
|
||||||
self.logger.error(e)
|
self.logger.error(e)
|
||||||
raise SyncExternalError(e) from None
|
raise SyncExternalError(e) from None
|
||||||
self.logger.info(f"Updated host {self.name} with data {kwargs}.")
|
self.logger.info(f"Updated host {self.name} with data {kwargs}.")
|
||||||
self.create_journal_entry("info", "Updated host in Zabbix with latest NB data.")
|
self.create_journal_entry("info", "Updated host in Zabbix with latest NB data.")
|
||||||
|
|
||||||
def ConsistencyCheck(self, groups, templates, proxies, proxy_power, create_hostgroups):
|
def ConsistencyCheck(
|
||||||
|
self, groups, templates, proxies, proxy_power, create_hostgroups
|
||||||
|
):
|
||||||
# pylint: disable=too-many-branches, too-many-statements
|
# pylint: disable=too-many-branches, too-many-statements
|
||||||
"""
|
"""
|
||||||
Checks if Zabbix object is still valid with NetBox parameters.
|
Checks if Zabbix object is still valid with NetBox parameters.
|
||||||
"""
|
"""
|
||||||
# If group is found or if the hostgroup is nested
|
# If group is found or if the hostgroup is nested
|
||||||
if not self.setZabbixGroupID(groups) or len(self.hostgroup.split('/')) > 1:
|
if not self.setZabbixGroupID(groups) or len(self.hostgroup.split("/")) > 1:
|
||||||
if create_hostgroups:
|
if create_hostgroups:
|
||||||
# Script is allowed to create a new hostgroup
|
# Script is allowed to create a new hostgroup
|
||||||
new_groups = self.createZabbixHostgroup(groups)
|
new_groups = self.createZabbixHostgroup(groups)
|
||||||
@ -521,47 +615,59 @@ class PhysicalDevice():
|
|||||||
if not self.group_id:
|
if not self.group_id:
|
||||||
# Function returns true / false but also sets GroupID
|
# Function returns true / false but also sets GroupID
|
||||||
if not self.setZabbixGroupID(groups) and not create_hostgroups:
|
if not self.setZabbixGroupID(groups) and not create_hostgroups:
|
||||||
e = (f"Host {self.name}: different hostgroup is required but "
|
e = (
|
||||||
"unable to create hostgroup without generation permission.")
|
f"Host {self.name}: different hostgroup is required but "
|
||||||
|
"unable to create hostgroup without generation permission."
|
||||||
|
)
|
||||||
self.logger.warning(e)
|
self.logger.warning(e)
|
||||||
raise SyncInventoryError(e)
|
raise SyncInventoryError(e)
|
||||||
# Prepare templates and proxy config
|
# Prepare templates and proxy config
|
||||||
self.zbxTemplatePrepper(templates)
|
self.zbxTemplatePrepper(templates)
|
||||||
self.setProxy(proxies)
|
self.setProxy(proxies)
|
||||||
# Get host object from Zabbix
|
# Get host object from Zabbix
|
||||||
host = self.zabbix.host.get(filter={'hostid': self.zabbix_id},
|
host = self.zabbix.host.get(
|
||||||
selectInterfaces=['type', 'ip',
|
filter={"hostid": self.zabbix_id},
|
||||||
'port', 'details',
|
selectInterfaces=["type", "ip", "port", "details", "interfaceid"],
|
||||||
'interfaceid'],
|
selectGroups=["groupid"],
|
||||||
selectGroups=["groupid"],
|
selectHostGroups=["groupid"],
|
||||||
selectHostGroups=["groupid"],
|
selectParentTemplates=["templateid"],
|
||||||
selectParentTemplates=["templateid"],
|
selectInventory=list(self._inventory_map().values()),
|
||||||
selectInventory=list(config["inventory_map"].values()))
|
selectMacros=["macro", "value", "type", "description"],
|
||||||
|
selectTags=["tag", "value"],
|
||||||
|
)
|
||||||
if len(host) > 1:
|
if len(host) > 1:
|
||||||
e = (f"Got {len(host)} results for Zabbix hosts "
|
e = (
|
||||||
f"with ID {self.zabbix_id} - hostname {self.name}.")
|
f"Got {len(host)} results for Zabbix hosts "
|
||||||
|
f"with ID {self.zabbix_id} - hostname {self.name}."
|
||||||
|
)
|
||||||
self.logger.error(e)
|
self.logger.error(e)
|
||||||
raise SyncInventoryError(e)
|
raise SyncInventoryError(e)
|
||||||
if len(host) == 0:
|
if len(host) == 0:
|
||||||
e = (f"Host {self.name}: No Zabbix host found. "
|
e = (
|
||||||
f"This is likely the result of a deleted Zabbix host "
|
f"Host {self.name}: No Zabbix host found. "
|
||||||
f"without zeroing the ID field in NetBox.")
|
f"This is likely the result of a deleted Zabbix host "
|
||||||
|
f"without zeroing the ID field in NetBox."
|
||||||
|
)
|
||||||
self.logger.error(e)
|
self.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:
|
||||||
self.logger.debug(f"Host {self.name}: hostname in-sync.")
|
self.logger.debug(f"Host {self.name}: hostname in-sync.")
|
||||||
else:
|
else:
|
||||||
self.logger.warning(f"Host {self.name}: hostname OUT of sync. "
|
self.logger.warning(
|
||||||
f"Received value: {host['host']}")
|
f"Host {self.name}: hostname OUT of sync. "
|
||||||
|
f"Received value: {host['host']}"
|
||||||
|
)
|
||||||
self.updateZabbixHost(host=self.name)
|
self.updateZabbixHost(host=self.name)
|
||||||
# Execute check depending on wether the name is special or not
|
# Execute check depending on wether the name is special or not
|
||||||
if self.use_visible_name:
|
if self.use_visible_name:
|
||||||
if host["name"] == self.visible_name:
|
if host["name"] == self.visible_name:
|
||||||
self.logger.debug(f"Host {self.name}: visible name in-sync.")
|
self.logger.debug(f"Host {self.name}: visible name in-sync.")
|
||||||
else:
|
else:
|
||||||
self.logger.warning(f"Host {self.name}: visible name OUT of sync."
|
self.logger.warning(
|
||||||
f" Received value: {host['name']}")
|
f"Host {self.name}: visible name OUT of sync."
|
||||||
|
f" Received value: {host['name']}"
|
||||||
|
)
|
||||||
self.updateZabbixHost(name=self.visible_name)
|
self.updateZabbixHost(name=self.visible_name)
|
||||||
|
|
||||||
# Check if the templates are in-sync
|
# Check if the templates are in-sync
|
||||||
@ -570,24 +676,24 @@ class PhysicalDevice():
|
|||||||
# Prepare Templates for API parsing
|
# Prepare Templates for API parsing
|
||||||
templateids = []
|
templateids = []
|
||||||
for template in self.zbx_templates:
|
for template in self.zbx_templates:
|
||||||
templateids.append({'templateid': template['templateid']})
|
templateids.append({"templateid": template["templateid"]})
|
||||||
# Update Zabbix with NB templates and clear any old / lost templates
|
# Update Zabbix with NB templates and clear any old / lost templates
|
||||||
self.updateZabbixHost(templates_clear=host["parentTemplates"],
|
self.updateZabbixHost(
|
||||||
templates=templateids)
|
templates_clear=host["parentTemplates"], templates=templateids
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
self.logger.debug(f"Host {self.name}: template(s) in-sync.")
|
self.logger.debug(f"Host {self.name}: template(s) in-sync.")
|
||||||
|
|
||||||
# Check if Zabbix version is 6 or higher. Issue #93
|
# Check if Zabbix version is 6 or higher. Issue #93
|
||||||
group_dictname = "hostgroups"
|
group_dictname = "hostgroups"
|
||||||
if str(self.zabbix.version).startswith(('6', '5')):
|
if str(self.zabbix.version).startswith(("6", "5")):
|
||||||
group_dictname = "groups"
|
group_dictname = "groups"
|
||||||
for group in host[group_dictname]:
|
for group in host[group_dictname]:
|
||||||
if group["groupid"] == self.group_id:
|
if group["groupid"] == self.group_id:
|
||||||
self.logger.debug(f"Host {self.name}: hostgroup in-sync.")
|
self.logger.debug(f"Host {self.name}: hostgroup in-sync.")
|
||||||
break
|
break
|
||||||
else:
|
|
||||||
self.logger.warning(f"Host {self.name}: hostgroup OUT of sync.")
|
self.logger.warning(f"Host {self.name}: hostgroup OUT of sync.")
|
||||||
self.updateZabbixHost(groups={'groupid': self.group_id})
|
self.updateZabbixHost(groups={"groupid": self.group_id})
|
||||||
|
|
||||||
if int(host["status"]) == self.zabbix_state:
|
if int(host["status"]) == self.zabbix_state:
|
||||||
self.logger.debug(f"Host {self.name}: status in-sync.")
|
self.logger.debug(f"Host {self.name}: status in-sync.")
|
||||||
@ -607,13 +713,15 @@ class PhysicalDevice():
|
|||||||
else:
|
else:
|
||||||
self.logger.warning(f"Host {self.name}: proxy OUT of sync.")
|
self.logger.warning(f"Host {self.name}: proxy OUT of sync.")
|
||||||
# Zabbix <= 6 patch
|
# Zabbix <= 6 patch
|
||||||
if not str(self.zabbix.version).startswith('7'):
|
if not str(self.zabbix.version).startswith("7"):
|
||||||
self.updateZabbixHost(proxy_hostid=self.zbxproxy['id'])
|
self.updateZabbixHost(proxy_hostid=self.zbxproxy["id"])
|
||||||
# Zabbix 7+
|
# Zabbix 7+
|
||||||
else:
|
else:
|
||||||
# Prepare data structure for updating either proxy or group
|
# Prepare data structure for updating either proxy or group
|
||||||
update_data = {self.zbxproxy["idtype"]: self.zbxproxy["id"],
|
update_data = {
|
||||||
"monitored_by": self.zbxproxy['monitored_by']}
|
self.zbxproxy["idtype"]: self.zbxproxy["id"],
|
||||||
|
"monitored_by": self.zbxproxy["monitored_by"],
|
||||||
|
}
|
||||||
self.updateZabbixHost(**update_data)
|
self.updateZabbixHost(**update_data)
|
||||||
else:
|
else:
|
||||||
# No proxy is defined in NetBox
|
# No proxy is defined in NetBox
|
||||||
@ -625,8 +733,10 @@ class PhysicalDevice():
|
|||||||
proxy_set = True
|
proxy_set = True
|
||||||
if proxy_power and proxy_set:
|
if proxy_power and proxy_set:
|
||||||
# Zabbix <= 6 fix
|
# Zabbix <= 6 fix
|
||||||
self.logger.warning(f"Host {self.name}: no proxy is configured in NetBox "
|
self.logger.warning(
|
||||||
"but is configured in Zabbix. Removing proxy config in Zabbix")
|
f"Host {self.name}: no proxy is configured in NetBox "
|
||||||
|
"but is configured in Zabbix. Removing proxy config in Zabbix"
|
||||||
|
)
|
||||||
if "proxy_hostid" in host and bool(host["proxy_hostid"]):
|
if "proxy_hostid" in host and bool(host["proxy_hostid"]):
|
||||||
self.updateZabbixHost(proxy_hostid=0)
|
self.updateZabbixHost(proxy_hostid=0)
|
||||||
# Zabbix 7 proxy
|
# Zabbix 7 proxy
|
||||||
@ -638,29 +748,60 @@ class PhysicalDevice():
|
|||||||
# Checks if a proxy has been defined in Zabbix and if proxy_power config has been set
|
# Checks if a proxy has been defined in Zabbix and if proxy_power config has been set
|
||||||
if proxy_set and not proxy_power:
|
if proxy_set and not proxy_power:
|
||||||
# Display error message
|
# Display error message
|
||||||
self.logger.error(f"Host {self.name} is configured "
|
self.logger.error(
|
||||||
f"with proxy in Zabbix but not in NetBox. The"
|
f"Host {self.name} is configured "
|
||||||
" -p flag was ommited: no "
|
f"with proxy in Zabbix but not in NetBox. The"
|
||||||
"changes have been made.")
|
" -p flag was ommited: no "
|
||||||
|
"changes have been made."
|
||||||
|
)
|
||||||
if not proxy_set:
|
if not proxy_set:
|
||||||
self.logger.debug(f"Host {self.name}: proxy in-sync.")
|
self.logger.debug(f"Host {self.name}: proxy in-sync.")
|
||||||
# Check host inventory mode
|
# Check host inventory mode
|
||||||
if str(host['inventory_mode']) == str(self.inventory_mode):
|
if str(host["inventory_mode"]) == str(self.inventory_mode):
|
||||||
self.logger.debug(f"Host {self.name}: inventory_mode in-sync.")
|
self.logger.debug(f"Host {self.name}: inventory_mode in-sync.")
|
||||||
else:
|
else:
|
||||||
self.logger.warning(f"Host {self.name}: inventory_mode OUT of sync.")
|
self.logger.warning(f"Host {self.name}: inventory_mode OUT of sync.")
|
||||||
self.updateZabbixHost(inventory_mode=str(self.inventory_mode))
|
self.updateZabbixHost(inventory_mode=str(self.inventory_mode))
|
||||||
if config["inventory_sync"] and self.inventory_mode in [0, 1]:
|
if config["inventory_sync"] and self.inventory_mode in [0, 1]:
|
||||||
# Check host inventory mapping
|
# Check host inventory mapping
|
||||||
if host['inventory'] == self.inventory:
|
if host["inventory"] == self.inventory:
|
||||||
self.logger.debug(f"Host {self.name}: inventory in-sync.")
|
self.logger.debug(f"Host {self.name}: inventory in-sync.")
|
||||||
else:
|
else:
|
||||||
self.logger.warning(f"Host {self.name}: inventory OUT of sync.")
|
self.logger.warning(f"Host {self.name}: inventory OUT of sync.")
|
||||||
self.updateZabbixHost(inventory=self.inventory)
|
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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
(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
|
# 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:
|
||||||
updates = {}
|
updates = {}
|
||||||
# Go through each key / item and check if it matches Zabbix
|
# Go through each key / item and check if it matches Zabbix
|
||||||
for key, item in self.setInterfaceDetails()[0].items():
|
for key, item in self.setInterfaceDetails()[0].items():
|
||||||
@ -696,12 +837,14 @@ class PhysicalDevice():
|
|||||||
self.logger.warning(f"Host {self.name}: Interface OUT of sync.")
|
self.logger.warning(f"Host {self.name}: Interface OUT of sync.")
|
||||||
if "type" in updates:
|
if "type" in updates:
|
||||||
# Changing interface type not supported. Raise exception.
|
# Changing interface type not supported. Raise exception.
|
||||||
e = (f"Host {self.name}: changing interface type to "
|
e = (
|
||||||
f"{str(updates['type'])} is not supported.")
|
f"Host {self.name}: changing interface type to "
|
||||||
|
f"{str(updates['type'])} is not supported."
|
||||||
|
)
|
||||||
self.logger.error(e)
|
self.logger.error(e)
|
||||||
raise InterfaceConfigError(e)
|
raise InterfaceConfigError(e)
|
||||||
# Set interfaceID for Zabbix config
|
# Set interfaceID for Zabbix config
|
||||||
updates["interfaceid"] = host["interfaces"][0]['interfaceid']
|
updates["interfaceid"] = host["interfaces"][0]["interfaceid"]
|
||||||
try:
|
try:
|
||||||
# API call to Zabbix
|
# API call to Zabbix
|
||||||
self.zabbix.hostinterface.update(updates)
|
self.zabbix.hostinterface.update(updates)
|
||||||
@ -717,9 +860,11 @@ class PhysicalDevice():
|
|||||||
e = f"Host {self.name}: interface in-sync."
|
e = f"Host {self.name}: interface in-sync."
|
||||||
self.logger.debug(e)
|
self.logger.debug(e)
|
||||||
else:
|
else:
|
||||||
e = (f"Host {self.name} has unsupported interface configuration."
|
e = (
|
||||||
f" Host has total of {len(host['interfaces'])} interfaces. "
|
f"Host {self.name} has unsupported interface configuration."
|
||||||
"Manual interfention required.")
|
f" Host has total of {len(host['interfaces'])} interfaces. "
|
||||||
|
"Manual interfention required."
|
||||||
|
)
|
||||||
self.logger.error(e)
|
self.logger.error(e)
|
||||||
raise SyncInventoryError(e)
|
raise SyncInventoryError(e)
|
||||||
|
|
||||||
@ -731,20 +876,25 @@ class PhysicalDevice():
|
|||||||
if self.journal:
|
if self.journal:
|
||||||
# Check if the severity is valid
|
# Check if the severity is valid
|
||||||
if severity not in ["info", "success", "warning", "danger"]:
|
if severity not in ["info", "success", "warning", "danger"]:
|
||||||
self.logger.warning(f"Value {severity} not valid for NB journal entries.")
|
self.logger.warning(
|
||||||
|
f"Value {severity} not valid for NB journal entries."
|
||||||
|
)
|
||||||
return False
|
return False
|
||||||
journal = {"assigned_object_type": "dcim.device",
|
journal = {
|
||||||
"assigned_object_id": self.id,
|
"assigned_object_type": "dcim.device",
|
||||||
"kind": severity,
|
"assigned_object_id": self.id,
|
||||||
"comments": message
|
"kind": severity,
|
||||||
}
|
"comments": message,
|
||||||
|
}
|
||||||
try:
|
try:
|
||||||
self.nb_journals.create(journal)
|
self.nb_journals.create(journal)
|
||||||
self.logger.debug(f"Host {self.name}: Created journal entry in NetBox")
|
self.logger.debug(f"Host {self.name}: Created journal entry in NetBox")
|
||||||
return True
|
return True
|
||||||
except JournalError(e) as e:
|
except JournalError(e) as e:
|
||||||
self.logger.warning("Unable to create journal entry for "
|
self.logger.warning(
|
||||||
f"{self.name}: NB returned {e}")
|
"Unable to create journal entry for "
|
||||||
|
f"{self.name}: NB returned {e}"
|
||||||
|
)
|
||||||
return False
|
return False
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -767,10 +917,15 @@ class PhysicalDevice():
|
|||||||
# and add this NB template to the list of successfull templates
|
# and add this NB template to the list of successfull templates
|
||||||
tmpls_from_zabbix.pop(pos)
|
tmpls_from_zabbix.pop(pos)
|
||||||
succesfull_templates.append(nb_tmpl)
|
succesfull_templates.append(nb_tmpl)
|
||||||
self.logger.debug(f"Host {self.name}: template "
|
self.logger.debug(
|
||||||
f"{nb_tmpl['name']} is present in Zabbix.")
|
f"Host {self.name}: template "
|
||||||
|
f"{nb_tmpl['name']} is present in Zabbix."
|
||||||
|
)
|
||||||
break
|
break
|
||||||
if len(succesfull_templates) == len(self.zbx_templates) and len(tmpls_from_zabbix) == 0:
|
if (
|
||||||
|
len(succesfull_templates) == len(self.zbx_templates)
|
||||||
|
and len(tmpls_from_zabbix) == 0
|
||||||
|
):
|
||||||
# All of the NetBox templates have been confirmed as successfull
|
# All of the NetBox templates have been confirmed as successfull
|
||||||
# and the ZBX template list is empty. This means that
|
# and the ZBX template list is empty. This means that
|
||||||
# all of the templates match.
|
# all of the templates match.
|
||||||
|
@ -2,32 +2,47 @@
|
|||||||
"""
|
"""
|
||||||
All custom exceptions used for Exception generation
|
All custom exceptions used for Exception generation
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
class SyncError(Exception):
|
class SyncError(Exception):
|
||||||
""" Class SyncError """
|
"""Class SyncError"""
|
||||||
|
|
||||||
|
|
||||||
class JournalError(Exception):
|
class JournalError(Exception):
|
||||||
""" Class SyncError """
|
"""Class SyncError"""
|
||||||
|
|
||||||
|
|
||||||
class SyncExternalError(SyncError):
|
class SyncExternalError(SyncError):
|
||||||
""" Class SyncExternalError """
|
"""Class SyncExternalError"""
|
||||||
|
|
||||||
|
|
||||||
class SyncInventoryError(SyncError):
|
class SyncInventoryError(SyncError):
|
||||||
""" Class SyncInventoryError """
|
"""Class SyncInventoryError"""
|
||||||
|
|
||||||
|
|
||||||
class SyncDuplicateError(SyncError):
|
class SyncDuplicateError(SyncError):
|
||||||
""" Class SyncDuplicateError """
|
"""Class SyncDuplicateError"""
|
||||||
|
|
||||||
|
|
||||||
class EnvironmentVarError(SyncError):
|
class EnvironmentVarError(SyncError):
|
||||||
""" Class EnvironmentVarError """
|
"""Class EnvironmentVarError"""
|
||||||
|
|
||||||
|
|
||||||
class InterfaceConfigError(SyncError):
|
class InterfaceConfigError(SyncError):
|
||||||
""" Class InterfaceConfigError """
|
"""Class InterfaceConfigError"""
|
||||||
|
|
||||||
|
|
||||||
class ProxyConfigError(SyncError):
|
class ProxyConfigError(SyncError):
|
||||||
""" Class ProxyConfigError """
|
"""Class ProxyConfigError"""
|
||||||
|
|
||||||
|
|
||||||
class HostgroupError(SyncError):
|
class HostgroupError(SyncError):
|
||||||
""" Class HostgroupError """
|
"""Class HostgroupError"""
|
||||||
|
|
||||||
|
|
||||||
class TemplateError(SyncError):
|
class TemplateError(SyncError):
|
||||||
""" Class TemplateError """
|
"""Class TemplateError"""
|
||||||
|
|
||||||
|
|
||||||
|
class UsermacroError(SyncError):
|
||||||
|
"""Class UsermacroError"""
|
||||||
|
@ -1,14 +1,27 @@
|
|||||||
"""Module for all hostgroup related code"""
|
"""Module for all hostgroup related code"""
|
||||||
|
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
|
|
||||||
from modules.exceptions import HostgroupError
|
from modules.exceptions import HostgroupError
|
||||||
from modules.tools import build_path
|
from modules.tools import build_path
|
||||||
|
|
||||||
class Hostgroup():
|
|
||||||
|
class Hostgroup:
|
||||||
"""Hostgroup class for devices and VM's
|
"""Hostgroup class for devices and VM's
|
||||||
Takes type (vm or dev) and NB object"""
|
Takes type (vm or dev) and NB object"""
|
||||||
def __init__(self, obj_type, nb_obj, version, logger=None, #pylint: disable=too-many-arguments, too-many-positional-arguments
|
|
||||||
nested_sitegroup_flag=False, nested_region_flag=False,
|
# pylint: disable=too-many-arguments, disable=too-many-positional-arguments
|
||||||
nb_regions=None, nb_groups=None):
|
def __init__(
|
||||||
|
self,
|
||||||
|
obj_type,
|
||||||
|
nb_obj,
|
||||||
|
version,
|
||||||
|
logger=None,
|
||||||
|
nested_sitegroup_flag=False,
|
||||||
|
nested_region_flag=False,
|
||||||
|
nb_regions=None,
|
||||||
|
nb_groups=None,
|
||||||
|
):
|
||||||
self.logger = logger if logger else getLogger(__name__)
|
self.logger = logger if logger else getLogger(__name__)
|
||||||
if obj_type not in ("vm", "dev"):
|
if obj_type not in ("vm", "dev"):
|
||||||
msg = f"Unable to create hostgroup with type {type}"
|
msg = f"Unable to create hostgroup with type {type}"
|
||||||
@ -19,8 +32,9 @@ class Hostgroup():
|
|||||||
self.name = self.nb.name
|
self.name = self.nb.name
|
||||||
self.nb_version = version
|
self.nb_version = version
|
||||||
# Used for nested data objects
|
# Used for nested data objects
|
||||||
self.set_nesting(nested_sitegroup_flag, nested_region_flag,
|
self.set_nesting(
|
||||||
nb_groups, nb_regions)
|
nested_sitegroup_flag, nested_region_flag, nb_groups, nb_regions
|
||||||
|
)
|
||||||
self._set_format_options()
|
self._set_format_options()
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
@ -49,20 +63,28 @@ class Hostgroup():
|
|||||||
format_options["site_group"] = None
|
format_options["site_group"] = None
|
||||||
if self.nb.site:
|
if self.nb.site:
|
||||||
if self.nb.site.region:
|
if self.nb.site.region:
|
||||||
format_options["region"] = self.generate_parents("region",
|
format_options["region"] = self.generate_parents(
|
||||||
str(self.nb.site.region))
|
"region", str(self.nb.site.region)
|
||||||
|
)
|
||||||
if self.nb.site.group:
|
if self.nb.site.group:
|
||||||
format_options["site_group"] = self.generate_parents("site_group",
|
format_options["site_group"] = self.generate_parents(
|
||||||
str(self.nb.site.group))
|
"site_group", str(self.nb.site.group)
|
||||||
|
)
|
||||||
format_options["role"] = role
|
format_options["role"] = role
|
||||||
format_options["site"] = self.nb.site.name if self.nb.site else None
|
format_options["site"] = self.nb.site.name if self.nb.site else None
|
||||||
format_options["tenant"] = str(self.nb.tenant) if self.nb.tenant else None
|
format_options["tenant"] = str(self.nb.tenant) if self.nb.tenant else None
|
||||||
format_options["tenant_group"] = str(self.nb.tenant.group) if self.nb.tenant else None
|
format_options["tenant_group"] = (
|
||||||
format_options["platform"] = self.nb.platform.name if self.nb.platform else None
|
str(self.nb.tenant.group) if self.nb.tenant else None
|
||||||
|
)
|
||||||
|
format_options["platform"] = (
|
||||||
|
self.nb.platform.name if self.nb.platform else None
|
||||||
|
)
|
||||||
# Variables only applicable for devices
|
# Variables only applicable for devices
|
||||||
if self.type == "dev":
|
if self.type == "dev":
|
||||||
format_options["manufacturer"] = self.nb.device_type.manufacturer.name
|
format_options["manufacturer"] = self.nb.device_type.manufacturer.name
|
||||||
format_options["location"] = str(self.nb.location) if self.nb.location else None
|
format_options["location"] = (
|
||||||
|
str(self.nb.location) if self.nb.location else None
|
||||||
|
)
|
||||||
# Variables only applicable for VM's
|
# Variables only applicable for VM's
|
||||||
if self.type == "vm":
|
if self.type == "vm":
|
||||||
# Check if a cluster is configured. Could also be configured in a site.
|
# Check if a cluster is configured. Could also be configured in a site.
|
||||||
@ -72,17 +94,22 @@ class Hostgroup():
|
|||||||
|
|
||||||
self.format_options = format_options
|
self.format_options = format_options
|
||||||
|
|
||||||
def set_nesting(self, nested_sitegroup_flag, nested_region_flag,
|
def set_nesting(
|
||||||
nb_groups, nb_regions):
|
self, nested_sitegroup_flag, nested_region_flag, nb_groups, nb_regions
|
||||||
|
):
|
||||||
"""Set nesting options for this Hostgroup"""
|
"""Set nesting options for this Hostgroup"""
|
||||||
self.nested_objects = {"site_group": {"flag": nested_sitegroup_flag, "data": nb_groups},
|
self.nested_objects = {
|
||||||
"region": {"flag": nested_region_flag, "data": nb_regions}}
|
"site_group": {"flag": nested_sitegroup_flag, "data": nb_groups},
|
||||||
|
"region": {"flag": nested_region_flag, "data": nb_regions},
|
||||||
|
}
|
||||||
|
|
||||||
def generate(self, hg_format=None):
|
def generate(self, hg_format=None):
|
||||||
"""Generate hostgroup based on a provided format"""
|
"""Generate hostgroup based on a provided format"""
|
||||||
# Set format to default in case its not specified
|
# Set format to default in case its not specified
|
||||||
if not hg_format:
|
if not hg_format:
|
||||||
hg_format = "site/manufacturer/role" if self.type == "dev" else "cluster/role"
|
hg_format = (
|
||||||
|
"site/manufacturer/role" if self.type == "dev" else "cluster/role"
|
||||||
|
)
|
||||||
# Split all given names
|
# Split all given names
|
||||||
hg_output = []
|
hg_output = []
|
||||||
hg_items = hg_format.split("/")
|
hg_items = hg_format.split("/")
|
||||||
@ -93,8 +120,10 @@ class Hostgroup():
|
|||||||
cf_data = self.custom_field_lookup(hg_item)
|
cf_data = self.custom_field_lookup(hg_item)
|
||||||
# CF does not exist
|
# CF does not exist
|
||||||
if not cf_data["result"]:
|
if not cf_data["result"]:
|
||||||
msg = (f"Unable to generate hostgroup for host {self.name}. "
|
msg = (
|
||||||
f"Item type {hg_item} not supported.")
|
f"Unable to generate hostgroup for host {self.name}. "
|
||||||
|
f"Item type {hg_item} not supported."
|
||||||
|
)
|
||||||
self.logger.error(msg)
|
self.logger.error(msg)
|
||||||
raise HostgroupError(msg)
|
raise HostgroupError(msg)
|
||||||
# CF data is populated
|
# CF data is populated
|
||||||
@ -109,10 +138,12 @@ class Hostgroup():
|
|||||||
# Check if the hostgroup is populated with at least one item.
|
# Check if the hostgroup is populated with at least one item.
|
||||||
if bool(hg_output):
|
if bool(hg_output):
|
||||||
return "/".join(hg_output)
|
return "/".join(hg_output)
|
||||||
msg = (f"Unable to generate hostgroup for host {self.name}."
|
msg = (
|
||||||
" Not enough valid items. This is most likely"
|
f"Unable to generate hostgroup for host {self.name}."
|
||||||
" due to the use of custom fields that are empty"
|
" Not enough valid items. This is most likely"
|
||||||
" or an invalid hostgroup format.")
|
" due to the use of custom fields that are empty"
|
||||||
|
" or an invalid hostgroup format."
|
||||||
|
)
|
||||||
self.logger.error(msg)
|
self.logger.error(msg)
|
||||||
raise HostgroupError(msg)
|
raise HostgroupError(msg)
|
||||||
|
|
||||||
@ -157,7 +188,9 @@ class Hostgroup():
|
|||||||
return child_object
|
return child_object
|
||||||
# If the nested flag is True, perform parent calculation
|
# If the nested flag is True, perform parent calculation
|
||||||
if self.nested_objects[nest_type]["flag"]:
|
if self.nested_objects[nest_type]["flag"]:
|
||||||
final_nested_object = build_path(child_object, self.nested_objects[nest_type]["data"])
|
final_nested_object = build_path(
|
||||||
|
child_object, self.nested_objects[nest_type]["data"]
|
||||||
|
)
|
||||||
return "/".join(final_nested_object)
|
return "/".join(final_nested_object)
|
||||||
# Nesting is not allowed for this object. Return child_object
|
# Nesting is not allowed for this object. Return child_object
|
||||||
return child_object
|
return child_object
|
||||||
|
@ -4,7 +4,8 @@ All of the Zabbix interface related configuration
|
|||||||
"""
|
"""
|
||||||
from modules.exceptions import InterfaceConfigError
|
from modules.exceptions import InterfaceConfigError
|
||||||
|
|
||||||
class ZabbixInterface():
|
|
||||||
|
class ZabbixInterface:
|
||||||
"""Class that represents a Zabbix interface."""
|
"""Class that represents a Zabbix interface."""
|
||||||
|
|
||||||
def __init__(self, context, ip):
|
def __init__(self, context, ip):
|
||||||
@ -15,21 +16,16 @@ class ZabbixInterface():
|
|||||||
|
|
||||||
def _set_default_port(self):
|
def _set_default_port(self):
|
||||||
"""Sets default TCP / UDP port for different interface types"""
|
"""Sets default TCP / UDP port for different interface types"""
|
||||||
interface_mapping = {
|
interface_mapping = {1: 10050, 2: 161, 3: 623, 4: 12345}
|
||||||
1: 10050,
|
|
||||||
2: 161,
|
|
||||||
3: 623,
|
|
||||||
4: 12345
|
|
||||||
}
|
|
||||||
# Check if interface type is listed in mapper.
|
# Check if interface type is listed in mapper.
|
||||||
if self.interface['type'] not in interface_mapping:
|
if self.interface["type"] not in interface_mapping:
|
||||||
return False
|
return False
|
||||||
# Set default port to interface
|
# Set default port to interface
|
||||||
self.interface["port"] = str(interface_mapping[self.interface['type']])
|
self.interface["port"] = str(interface_mapping[self.interface["type"]])
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def get_context(self):
|
def get_context(self):
|
||||||
""" check if NetBox custom context has been defined. """
|
"""check if NetBox custom context has been defined."""
|
||||||
if "zabbix" in self.context:
|
if "zabbix" in self.context:
|
||||||
zabbix = self.context["zabbix"]
|
zabbix = self.context["zabbix"]
|
||||||
if "interface_type" in zabbix:
|
if "interface_type" in zabbix:
|
||||||
@ -43,7 +39,7 @@ class ZabbixInterface():
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def set_snmp(self):
|
def set_snmp(self):
|
||||||
""" Check if interface is type SNMP """
|
"""Check if interface is type SNMP"""
|
||||||
# pylint: disable=too-many-branches
|
# pylint: disable=too-many-branches
|
||||||
if self.interface["type"] == 2:
|
if self.interface["type"] == 2:
|
||||||
# Checks if SNMP settings are defined in NetBox
|
# Checks if SNMP settings are defined in NetBox
|
||||||
@ -63,7 +59,7 @@ class ZabbixInterface():
|
|||||||
e = "SNMP version option is not defined."
|
e = "SNMP version option is not defined."
|
||||||
raise InterfaceConfigError(e)
|
raise InterfaceConfigError(e)
|
||||||
# If version 1 or 2 is used, get community string
|
# If version 1 or 2 is used, get community string
|
||||||
if self.interface["details"]["version"] in ['1','2']:
|
if self.interface["details"]["version"] in ["1", "2"]:
|
||||||
if "community" in snmp:
|
if "community" in snmp:
|
||||||
# Set SNMP community to confix context value
|
# Set SNMP community to confix context value
|
||||||
community = snmp["community"]
|
community = snmp["community"]
|
||||||
@ -73,10 +69,16 @@ class ZabbixInterface():
|
|||||||
self.interface["details"]["community"] = str(community)
|
self.interface["details"]["community"] = str(community)
|
||||||
# If version 3 has been used, get all
|
# If version 3 has been used, get all
|
||||||
# SNMPv3 NetBox related configs
|
# SNMPv3 NetBox related configs
|
||||||
elif self.interface["details"]["version"] == '3':
|
elif self.interface["details"]["version"] == "3":
|
||||||
items = ["securityname", "securitylevel", "authpassphrase",
|
items = [
|
||||||
"privpassphrase", "authprotocol", "privprotocol",
|
"securityname",
|
||||||
"contextname"]
|
"securitylevel",
|
||||||
|
"authpassphrase",
|
||||||
|
"privpassphrase",
|
||||||
|
"authprotocol",
|
||||||
|
"privprotocol",
|
||||||
|
"contextname",
|
||||||
|
]
|
||||||
for key, item in snmp.items():
|
for key, item in snmp.items():
|
||||||
if key in items:
|
if key in items:
|
||||||
self.interface["details"][key] = str(item)
|
self.interface["details"][key] = str(item)
|
||||||
@ -91,13 +93,15 @@ class ZabbixInterface():
|
|||||||
raise InterfaceConfigError(e)
|
raise InterfaceConfigError(e)
|
||||||
|
|
||||||
def set_default_snmp(self):
|
def set_default_snmp(self):
|
||||||
""" Set default config to SNMPv2, port 161 and community macro. """
|
"""Set default config to SNMPv2, port 161 and community macro."""
|
||||||
self.interface = self.skelet
|
self.interface = self.skelet
|
||||||
self.interface["type"] = "2"
|
self.interface["type"] = "2"
|
||||||
self.interface["port"] = "161"
|
self.interface["port"] = "161"
|
||||||
self.interface["details"] = {"version": "2",
|
self.interface["details"] = {
|
||||||
"community": "{$SNMP_COMMUNITY}",
|
"version": "2",
|
||||||
"bulk": "1"}
|
"community": "{$SNMP_COMMUNITY}",
|
||||||
|
"bulk": "1",
|
||||||
|
}
|
||||||
|
|
||||||
def set_default_agent(self):
|
def set_default_agent(self):
|
||||||
"""Sets interface to Zabbix agent defaults"""
|
"""Sets interface to Zabbix agent defaults"""
|
||||||
|
40
modules/logging.py
Normal file
40
modules/logging.py
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
"""
|
||||||
|
Logging module for Netbox-Zabbix-sync
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from os import path
|
||||||
|
|
||||||
|
logger = logging.getLogger("NetBox-Zabbix-sync")
|
||||||
|
|
||||||
|
|
||||||
|
def get_logger():
|
||||||
|
"""
|
||||||
|
Return the logger for Netbox Zabbix Sync
|
||||||
|
"""
|
||||||
|
return logger
|
||||||
|
|
||||||
|
|
||||||
|
def setup_logger():
|
||||||
|
"""
|
||||||
|
Prepare a logger with stream and file handlers
|
||||||
|
"""
|
||||||
|
# Set logging
|
||||||
|
lgout = logging.StreamHandler()
|
||||||
|
lgfile = logging.FileHandler(
|
||||||
|
path.join(path.dirname(path.realpath(__file__)), "sync.log")
|
||||||
|
)
|
||||||
|
|
||||||
|
logging.basicConfig(
|
||||||
|
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
||||||
|
level=logging.WARNING,
|
||||||
|
handlers=[lgout, lgfile],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def set_log_levels(root_level, own_level):
|
||||||
|
"""
|
||||||
|
Configure log levels for root and Netbox-Zabbix-sync logger
|
||||||
|
"""
|
||||||
|
logging.getLogger().setLevel(root_level)
|
||||||
|
logger.setLevel(own_level)
|
133
modules/tags.py
Normal file
133
modules/tags.py
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
#!/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.warning(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.warning(
|
||||||
|
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,11 +1,14 @@
|
|||||||
"""A collection of tools used by several classes"""
|
"""A collection of tools used by several classes"""
|
||||||
|
|
||||||
|
|
||||||
def convert_recordset(recordset):
|
def convert_recordset(recordset):
|
||||||
""" Converts netbox RedcordSet to list of dicts. """
|
"""Converts netbox RedcordSet to list of dicts."""
|
||||||
recordlist = []
|
recordlist = []
|
||||||
for record in recordset:
|
for record in recordset:
|
||||||
recordlist.append(record.__dict__)
|
recordlist.append(record.__dict__)
|
||||||
return recordlist
|
return recordlist
|
||||||
|
|
||||||
|
|
||||||
def build_path(endpoint, list_of_dicts):
|
def build_path(endpoint, list_of_dicts):
|
||||||
"""
|
"""
|
||||||
Builds a path list of related parent/child items.
|
Builds a path list of related parent/child items.
|
||||||
@ -13,16 +16,17 @@ def build_path(endpoint, list_of_dicts):
|
|||||||
be used in hostgroups.
|
be used in hostgroups.
|
||||||
"""
|
"""
|
||||||
item_path = []
|
item_path = []
|
||||||
itemlist = [i for i in list_of_dicts if i['name'] == endpoint]
|
itemlist = [i for i in list_of_dicts if i["name"] == endpoint]
|
||||||
item = itemlist[0] if len(itemlist) == 1 else None
|
item = itemlist[0] if len(itemlist) == 1 else None
|
||||||
item_path.append(item['name'])
|
item_path.append(item["name"])
|
||||||
while item['_depth'] > 0:
|
while item["_depth"] > 0:
|
||||||
itemlist = [i for i in list_of_dicts if i['name'] == str(item['parent'])]
|
itemlist = [i for i in list_of_dicts if i["name"] == str(item["parent"])]
|
||||||
item = itemlist[0] if len(itemlist) == 1 else None
|
item = itemlist[0] if len(itemlist) == 1 else None
|
||||||
item_path.append(item['name'])
|
item_path.append(item["name"])
|
||||||
item_path.reverse()
|
item_path.reverse()
|
||||||
return item_path
|
return item_path
|
||||||
|
|
||||||
|
|
||||||
def proxy_prepper(proxy_list, proxy_group_list):
|
def proxy_prepper(proxy_list, proxy_group_list):
|
||||||
"""
|
"""
|
||||||
Function that takes 2 lists and converts them using a
|
Function that takes 2 lists and converts them using a
|
||||||
@ -42,3 +46,56 @@ def proxy_prepper(proxy_list, proxy_group_list):
|
|||||||
group["monitored_by"] = 2
|
group["monitored_by"] = 2
|
||||||
output.append(group)
|
output.append(group)
|
||||||
return output
|
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
|
||||||
|
120
modules/usermacros.py
Normal file
120
modules/usermacros.py
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
#!/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 re import match
|
||||||
|
|
||||||
|
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.warning(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) and macro_properties:
|
||||||
|
macro["value"] = macro_properties
|
||||||
|
macro["type"] = str(0)
|
||||||
|
macro["description"] = ""
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.logger.warning(f"Usermacro {macro_name} has no value, skipping.")
|
||||||
|
return False
|
||||||
|
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
|
@ -1,9 +1,10 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# pylint: disable=duplicate-code
|
# pylint: disable=duplicate-code
|
||||||
"""Module that hosts all functions for virtual machine processing"""
|
"""Module that hosts all functions for virtual machine processing"""
|
||||||
from modules.device import PhysicalDevice
|
from modules.device import PhysicalDevice
|
||||||
|
from modules.exceptions import InterfaceConfigError, SyncInventoryError, TemplateError
|
||||||
from modules.hostgroups import Hostgroup
|
from modules.hostgroups import Hostgroup
|
||||||
from modules.interface import ZabbixInterface
|
from modules.interface import ZabbixInterface
|
||||||
|
|
||||||
from modules.exceptions import (TemplateError, InterfaceConfigError,
|
from modules.exceptions import (TemplateError, InterfaceConfigError,
|
||||||
SyncInventoryError)
|
SyncInventoryError)
|
||||||
from modules.config import load_config
|
from modules.config import load_config
|
||||||
@ -13,24 +14,42 @@ config = load_config()
|
|||||||
|
|
||||||
class VirtualMachine(PhysicalDevice):
|
class VirtualMachine(PhysicalDevice):
|
||||||
"""Model for virtual machines"""
|
"""Model for virtual machines"""
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.hostgroup = None
|
self.hostgroup = None
|
||||||
self.zbx_template_names = 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):
|
def set_hostgroup(self, hg_format, nb_site_groups, nb_regions):
|
||||||
"""Set the hostgroup for this device"""
|
"""Set the hostgroup for this device"""
|
||||||
# Create new Hostgroup instance
|
# Create new Hostgroup instance
|
||||||
hg = Hostgroup("vm", self.nb, self.nb_api_version, logger=self.logger,
|
hg = Hostgroup(
|
||||||
nested_sitegroup_flag=config["traverse_site_groups"],
|
"vm",
|
||||||
nested_region_flag=config["traverse_regions"],
|
self.nb,
|
||||||
nb_groups=nb_site_groups,
|
self.nb_api_version,
|
||||||
nb_regions=nb_regions)
|
logger=self.logger,
|
||||||
|
nested_sitegroup_flag=traverse_site_groups,
|
||||||
|
nested_region_flag=traverse_regions,
|
||||||
|
nb_groups=nb_site_groups,
|
||||||
|
nb_regions=nb_regions,
|
||||||
|
)
|
||||||
# Generate hostgroup based on hostgroup format
|
# Generate hostgroup based on hostgroup format
|
||||||
self.hostgroup = hg.generate(hg_format)
|
self.hostgroup = hg.generate(hg_format)
|
||||||
|
|
||||||
def set_vm_template(self):
|
def set_vm_template(self):
|
||||||
""" Set Template for VMs. Overwrites default class
|
"""Set Template for VMs. Overwrites default class
|
||||||
to skip a lookup of custom fields."""
|
to skip a lookup of custom fields."""
|
||||||
# Gather templates ONLY from the device specific context
|
# Gather templates ONLY from the device specific context
|
||||||
try:
|
try:
|
||||||
|
@ -2,38 +2,27 @@
|
|||||||
# 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 logging
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import logging
|
||||||
import ssl
|
import ssl
|
||||||
from os import environ, path, 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 ZabbixAPI, APIRequestError, ProcessingError
|
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.virtual_machine import VirtualMachine
|
|
||||||
from modules.tools import convert_recordset, proxy_prepper
|
|
||||||
from modules.exceptions import EnvironmentVarError, HostgroupError, SyncError
|
from modules.exceptions import EnvironmentVarError, HostgroupError, SyncError
|
||||||
|
from modules.logging import get_logger, set_log_levels, setup_logger
|
||||||
|
from modules.tools import convert_recordset, proxy_prepper
|
||||||
|
from modules.virtual_machine import VirtualMachine
|
||||||
|
|
||||||
config = load_config()
|
config = load_config()
|
||||||
|
|
||||||
# Set logging
|
|
||||||
log_format = logging.Formatter('%(asctime)s - %(name)s - '
|
|
||||||
'%(levelname)s - %(message)s')
|
|
||||||
lgout = logging.StreamHandler()
|
|
||||||
lgout.setFormatter(log_format)
|
|
||||||
lgout.setLevel(logging.DEBUG)
|
|
||||||
|
|
||||||
lgfile = logging.FileHandler(path.join(path.dirname(
|
setup_logger()
|
||||||
path.realpath(__file__)), "sync.log"))
|
logger = get_logger()
|
||||||
lgfile.setFormatter(log_format)
|
|
||||||
lgfile.setLevel(logging.DEBUG)
|
|
||||||
|
|
||||||
logger = logging.getLogger("NetBox-Zabbix-sync")
|
|
||||||
logger.addHandler(lgout)
|
|
||||||
logger.addHandler(lgfile)
|
|
||||||
logger.setLevel(logging.WARNING)
|
|
||||||
|
|
||||||
|
|
||||||
def main(arguments):
|
def main(arguments):
|
||||||
@ -41,7 +30,14 @@ def main(arguments):
|
|||||||
# 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:
|
||||||
logger.setLevel(logging.DEBUG)
|
set_log_levels(logging.WARNING, logging.INFO)
|
||||||
|
if arguments.debug:
|
||||||
|
set_log_levels(logging.WARNING, logging.DEBUG)
|
||||||
|
if arguments.debug_all:
|
||||||
|
set_log_levels(logging.DEBUG, logging.DEBUG)
|
||||||
|
if arguments.quiet:
|
||||||
|
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")
|
||||||
@ -69,15 +65,26 @@ def main(arguments):
|
|||||||
netbox = api(netbox_host, token=netbox_token, threading=True)
|
netbox = api(netbox_host, token=netbox_token, threading=True)
|
||||||
# Check if the provided Hostgroup layout is valid
|
# Check if the provided Hostgroup layout is valid
|
||||||
hg_objects = config["hostgroup_format"].split("/")
|
hg_objects = config["hostgroup_format"].split("/")
|
||||||
allowed_objects = ["location", "role", "manufacturer", "region",
|
allowed_objects = [
|
||||||
"site", "site_group", "tenant", "tenant_group"]
|
"location",
|
||||||
|
"role",
|
||||||
|
"manufacturer",
|
||||||
|
"region",
|
||||||
|
"site",
|
||||||
|
"site_group",
|
||||||
|
"tenant",
|
||||||
|
"tenant_group",
|
||||||
|
]
|
||||||
# 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:
|
||||||
device_cfs = list(netbox.extras.custom_fields.filter(
|
device_cfs = list(
|
||||||
type="text", content_type_id=23))
|
netbox.extras.custom_fields.filter(type="text", content_type_id=23)
|
||||||
|
)
|
||||||
except RequestsConnectionError:
|
except RequestsConnectionError:
|
||||||
logger.error(f"Unable to connect to NetBox with URL {netbox_host}."
|
logger.error(
|
||||||
" Please check the URL and status of NetBox.")
|
f"Unable to connect to NetBox with URL {netbox_host}."
|
||||||
|
" Please check the URL and status of NetBox."
|
||||||
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
except NBRequestError as e:
|
except NBRequestError as e:
|
||||||
logger.error(f"NetBox error: {e}")
|
logger.error(f"NetBox error: {e}")
|
||||||
@ -86,8 +93,10 @@ def main(arguments):
|
|||||||
allowed_objects.append(cf.name)
|
allowed_objects.append(cf.name)
|
||||||
for hg_object in hg_objects:
|
for hg_object in hg_objects:
|
||||||
if hg_object not in allowed_objects:
|
if hg_object not in allowed_objects:
|
||||||
e = (f"Hostgroup item {hg_object} is not valid. Make sure you"
|
e = (
|
||||||
" use valid items and seperate them with '/'.")
|
f"Hostgroup item {hg_object} is not valid. Make sure you"
|
||||||
|
" use valid items and seperate them with '/'."
|
||||||
|
)
|
||||||
logger.error(e)
|
logger.error(e)
|
||||||
raise HostgroupError(e)
|
raise HostgroupError(e)
|
||||||
# Set Zabbix API
|
# Set Zabbix API
|
||||||
@ -99,18 +108,18 @@ def main(arguments):
|
|||||||
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_host, user=zabbix_user,
|
|
||||||
password=zabbix_pass, ssl_context=ssl_ctx)
|
|
||||||
else:
|
|
||||||
zabbix = ZabbixAPI(
|
zabbix = ZabbixAPI(
|
||||||
zabbix_host, token=zabbix_token, ssl_context=ssl_ctx)
|
zabbix_host, user=zabbix_user, password=zabbix_pass, ssl_context=ssl_ctx
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
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"
|
||||||
@ -123,18 +132,17 @@ def main(arguments):
|
|||||||
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(
|
zabbix_proxygroups = zabbix.proxygroup.get(output=["proxy_groupid", "name"])
|
||||||
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)
|
||||||
|
|
||||||
@ -156,27 +164,36 @@ def main(arguments):
|
|||||||
# 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.hostgroup:
|
if not vm.hostgroup:
|
||||||
continue
|
continue
|
||||||
|
vm.set_inventory(nb_vm)
|
||||||
|
vm.set_usermacros()
|
||||||
|
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(f"VM {vm.name}: cleanup complete")
|
logger.debug(f"VM {vm.name}: cleanup complete")
|
||||||
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(f"VM {vm.name}: skipping since this VM is "
|
logger.info(
|
||||||
f"not in the active state.")
|
f"VM {vm.name}: skipping since this VM is "
|
||||||
|
f"not in the active state."
|
||||||
|
)
|
||||||
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
|
||||||
# Check if VM is already in Zabbix
|
# Check if VM is already in Zabbix
|
||||||
if vm.zabbix_id:
|
if vm.zabbix_id:
|
||||||
vm.ConsistencyCheck(zabbix_groups, zabbix_templates,
|
vm.ConsistencyCheck(
|
||||||
zabbix_proxy_list, config["full_proxy_sync"],
|
zabbix_groups,
|
||||||
config["create_hostgroups"])
|
zabbix_templates,
|
||||||
|
zabbix_proxy_list,
|
||||||
|
config["full_proxy_sync"],
|
||||||
|
config["create_hostgroups"],
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
# Add hostgroup is config is set
|
# Add hostgroup is config is set
|
||||||
if config["create_hostgroups"]:
|
if config["create_hostgroups"]:
|
||||||
@ -187,8 +204,7 @@ def main(arguments):
|
|||||||
# Add new hostgroups to zabbix group list
|
# Add new hostgroups to zabbix group list
|
||||||
zabbix_groups.append(group)
|
zabbix_groups.append(group)
|
||||||
# Add VM to Zabbix
|
# Add VM to Zabbix
|
||||||
vm.createInZabbix(zabbix_groups, zabbix_templates,
|
vm.createInZabbix(zabbix_groups, zabbix_templates, zabbix_proxy_list)
|
||||||
zabbix_proxy_list)
|
|
||||||
except SyncError:
|
except SyncError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -209,19 +225,22 @@ def main(arguments):
|
|||||||
if not device.hostgroup:
|
if not device.hostgroup:
|
||||||
continue
|
continue
|
||||||
device.set_inventory(nb_device)
|
device.set_inventory(nb_device)
|
||||||
|
device.set_usermacros()
|
||||||
|
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():
|
||||||
e = (f"Device {device.name}: is "
|
e = f"Device {device.name}: is " f"part of cluster and primary."
|
||||||
f"part of cluster and primary.")
|
|
||||||
logger.info(e)
|
logger.info(e)
|
||||||
else:
|
else:
|
||||||
# Device is secondary in cluster.
|
# Device is secondary in cluster.
|
||||||
# Don't continue with this device.
|
# Don't continue with this device.
|
||||||
e = (f"Device {device.name}: is part of cluster "
|
e = (
|
||||||
f"but not primary. Skipping this host...")
|
f"Device {device.name}: is part of cluster "
|
||||||
|
f"but not primary. Skipping this host..."
|
||||||
|
)
|
||||||
logger.info(e)
|
logger.info(e)
|
||||||
continue
|
continue
|
||||||
# Checks if device is in cleanup state
|
# Checks if device is in cleanup state
|
||||||
@ -234,17 +253,23 @@ def main(arguments):
|
|||||||
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(f"Device {device.name}: skipping since this device is "
|
logger.info(
|
||||||
f"not in the active state.")
|
f"Device {device.name}: skipping since this device is "
|
||||||
|
f"not in the active state."
|
||||||
|
)
|
||||||
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
|
||||||
# Check if device is already in Zabbix
|
# Check if device is already in Zabbix
|
||||||
if device.zabbix_id:
|
if device.zabbix_id:
|
||||||
device.ConsistencyCheck(zabbix_groups, zabbix_templates,
|
device.ConsistencyCheck(
|
||||||
zabbix_proxy_list, config["full_proxy_sync"],
|
zabbix_groups,
|
||||||
config["create_hostgroups"])
|
zabbix_templates,
|
||||||
|
zabbix_proxy_list,
|
||||||
|
config["full_proxy_sync"],
|
||||||
|
config["create_hostgroups"],
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
# Add hostgroup is config is set
|
# Add hostgroup is config is set
|
||||||
if config["create_hostgroups"]:
|
if config["create_hostgroups"]:
|
||||||
@ -255,17 +280,27 @@ def main(arguments):
|
|||||||
# Add new hostgroups to zabbix group list
|
# Add new hostgroups to zabbix group list
|
||||||
zabbix_groups.append(group)
|
zabbix_groups.append(group)
|
||||||
# Add device to Zabbix
|
# Add device to Zabbix
|
||||||
device.createInZabbix(zabbix_groups, zabbix_templates,
|
device.createInZabbix(zabbix_groups, zabbix_templates, zabbix_proxy_list)
|
||||||
zabbix_proxy_list)
|
|
||||||
except SyncError:
|
except SyncError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
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("-v", "--verbose", help="Turn on debugging.",
|
parser.add_argument(
|
||||||
action="store_true")
|
"-v", "--verbose", help="Turn on debugging.", action="store_true"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-vv", "--debug", help="Turn on debugging.", action="store_true"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-vvv",
|
||||||
|
"--debug-all",
|
||||||
|
help="Turn on debugging for all modules.",
|
||||||
|
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)
|
||||||
|
Loading…
Reference in New Issue
Block a user