Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,13 @@ None
- Ensure that the worker doesn't crash on certain race conditions with regards to the hostinterfacesync job ([#86])
- Fix issue with DeleteHost so it now actually removed the device/object from Zabbix when its deleted from NetBox ([#88])


## [Unreleased]

### New features

- Added Site, SiteGroup, and Region as assignment targets with inheritance. Assignments (proxy, templates, tags, hostgroups, interfaces, inventory) made at the Site/SiteGroup/Region level are now inherited by all devices and VMs at that site or below.
- Added `ZabbixTemplateRule` model for regex-based template, hostgroup, and tag assignment by platform name. Includes a 2-second timeout to prevent catastrophic backtracking (ReDoS).
## [1.0.5] - Minor update

### New features
Expand Down
51 changes: 51 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,15 @@ The plugin is configuration to do exactly what you want, by means of the plugin
'snmp_privpass': '{$SNMP_PRIVPASS}',
},
'inheritance_chain': [
['device', 'site'],
['site'],
['site', 'group'],
['group', 'parent'],
['site', 'group', 'parent'],
['site', 'region'],
['region'],
['region', 'parent'],
['cluster', 'site'],
['device'],
['role'],
['device', 'role'],
Expand Down Expand Up @@ -84,6 +93,48 @@ The plugin is configuration to do exactly what you want, by means of the plugin
}
```

## Inheritance Chain

The `inheritance_chain` setting defines which NetBox objects are traversed when resolving Zabbix assignments. Assignments (templates, tags, hostgroups, macros, proxy/server, interfaces, inventory) made on any object in the chain are inherited by the device or VM being synced, with direct assignments taking priority.

### Site, SiteGroup, and Region Inheritance

The chain includes paths for site-level inheritance, allowing assignments made at the `Site`, `SiteGroup`, or `Region` level to be inherited by all devices and VMs at that site or below:

| Path | Description |
|------|-------------|
| `['device', 'site']` | The device's site |
| `['site']` | Site (direct) |
| `['site', 'group']` | The site's SiteGroup |
| `['group', 'parent']` | Parent SiteGroup (traverses the full hierarchy) |
| `['site', 'group', 'parent']` | Full path: device → site → group → parent group |
| `['site', 'region']` | The site's region |
| `['region']` | Region (direct) |
| `['region', 'parent']` | Parent region (traverses the full hierarchy) |
| `['cluster', 'site']` | The cluster's site (for VMs) |

For example, assigning a `ZabbixServerAssignment` (proxy) to a `SiteGroup` means every device at every site in that SiteGroup inherits the proxy — no per-device assignment needed.

## Zabbix Template Rules

`ZabbixTemplateRule` allows automatic template assignment based on the device's or VM's platform name. Each rule has a regex pattern that is matched (case-insensitive) against the platform name. When a rule matches, the configured Zabbix template is assigned to the host.

Rules are resolved after all direct and inherited assignments, so explicit `ZabbixTemplateAssignment` objects always take priority.

Each rule can optionally also assign a hostgroup and a tag when the pattern matches — useful for OS-family grouping (e.g. a `Windows` rule that assigns the `Windows by Zabbix agent` template, a `Windows` hostgroup, and an `os_family=Windows` tag in one rule).

| Field | Description |
|-------|-------------|
| `name` | Human-readable name |
| `pattern` | Regex pattern matched against platform name (case-insensitive) |
| `zabbixtemplate` | Template assigned when the rule matches |
| `zabbixhostgroup` | Optional hostgroup assigned on match (nullable) |
| `zabbixtag` | Optional tag assigned on match (nullable) |
| `enabled` | Enable/disable rule without deleting it |
| `priority` | Lower value = higher priority (rules evaluated in order) |

Patterns are validated at save time (`re.compile`). A timeout (2 seconds) protects against catastrophic backtracking during matching.

## Configuration values

### Source of Truth
Expand Down
7 changes: 7 additions & 0 deletions nbxsync/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,13 @@ class nbxSync(PluginConfig):
'snmp_privpass': '{$SNMP_PRIVPASS}',
},
'inheritance_chain': [
['device', 'site'],
['site'],
['site', 'group'],
['site', 'region'],
['region'],
['region', 'parent'],
['cluster', 'site'],
['device'],
['role'],
['device', 'role'],
Expand Down
2 changes: 2 additions & 0 deletions nbxsync/constants/assignment_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
ASSIGNMENT_MODELS = Q(
Q(app_label='dcim', model='device')
| Q(app_label='dcim', model='virtualdevicecontext')
| Q(app_label='dcim', model='site')
| Q(app_label='dcim', model='sitegroup')
| Q(app_label='dcim', model='manufacturer')
| Q(app_label='dcim', model='devicerole')
| Q(app_label='dcim', model='devicetype')
Expand Down
8 changes: 7 additions & 1 deletion nbxsync/constants/assignment_type_to_field.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from dcim.models import Device, VirtualDeviceContext, DeviceRole, DeviceType, Manufacturer, Platform
from dcim.models import Device, VirtualDeviceContext, DeviceRole, DeviceType, Manufacturer, Platform, Site, SiteGroup, Region
from virtualization.models import Cluster, ClusterType, VirtualMachine

from nbxsync.models import ZabbixHostgroup, ZabbixServer, ZabbixTag, ZabbixTemplate, ZabbixConfigurationGroup
Expand All @@ -14,6 +14,9 @@
Cluster: 'cluster',
ClusterType: 'clustertype',
VirtualDeviceContext: 'virtualdevicecontext',
Site: 'site',
SiteGroup: 'sitegroup',
Region: 'region',
ZabbixConfigurationGroup: 'zabbixconfigurationgroup',
}

Expand All @@ -28,6 +31,9 @@
Device: 'device',
VirtualMachine: 'virtualmachine',
VirtualDeviceContext: 'virtualdevicecontext',
Site: 'site',
SiteGroup: 'sitegroup',
Region: 'region',
ZabbixHostgroup: 'zabbixhostgroup',
}

Expand Down
7 changes: 7 additions & 0 deletions nbxsync/constants/path_labels.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
PATH_LABELS = {
('device', 'site'): 'Site',
('site',): 'Site',
('site', 'group'): 'Site Group',
('site', 'region'): 'Region',
('region',): 'Region',
('region', 'parent'): 'Region',
('cluster', 'site'): 'Site',
('device_type',): 'Device Type',
('device_type', 'manufacturer'): 'Manufacturer',
('zabbixconfigurationgroup'): 'Zabbix Configuration Group',
Expand Down
2 changes: 2 additions & 0 deletions nbxsync/filtersets/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,5 @@
from .zabbixmaintenancetagassignment import *
from .zabbixconfigurationgroup import *
from .zabbixconfigurationgroupassignment import *

from .zabbixtemplaterule import *
24 changes: 24 additions & 0 deletions nbxsync/filtersets/zabbixtemplaterule.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from django.db.models import Q
from django_filters import CharFilter

from netbox.filtersets import NetBoxModelFilterSet

from nbxsync.models import ZabbixTemplateRule

__all__ = ('ZabbixTemplateRuleFilterSet',)


class ZabbixTemplateRuleFilterSet(NetBoxModelFilterSet):
q = CharFilter(method='search', label='Search')
name = CharFilter(lookup_expr='icontains')
description = CharFilter(lookup_expr='icontains')
pattern = CharFilter(lookup_expr='icontains')

class Meta:
model = ZabbixTemplateRule
fields = ('id', 'name', 'description', 'pattern', 'enabled', 'priority')

def search(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(Q(name__icontains=value) | Q(description__icontains=value) | Q(pattern__icontains=value)).distinct()
2 changes: 2 additions & 0 deletions nbxsync/forms/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,5 @@
from .zabbixmaintenancetagassignment import *
from .zabbixconfigurationgroup import *
from .zabbixconfigurationgroupassignment import *

from .zabbixtemplaterule import *
12 changes: 11 additions & 1 deletion nbxsync/forms/zabbixhostgroupassignment.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from netbox.forms import NetBoxModelBulkEditForm, NetBoxModelFilterSetForm, NetBoxModelForm
from utilities.forms.fields import DynamicModelChoiceField, TagFilterField
from utilities.forms.rendering import FieldSet, TabbedGroups
from dcim.models import Device, VirtualDeviceContext, DeviceRole, DeviceType, Manufacturer, Platform
from dcim.models import Device, VirtualDeviceContext, DeviceRole, DeviceType, Manufacturer, Platform, Site, SiteGroup, Region
from virtualization.models import Cluster, ClusterType, VirtualMachine

from nbxsync.constants import ASSIGNMENT_TYPE_TO_FIELD, ASSIGNMENT_TYPE_TO_FIELD_NBOBJS
Expand All @@ -25,7 +25,10 @@ class ZabbixHostgroupAssignmentForm(NetBoxModelForm):
manufacturer = DynamicModelChoiceField(queryset=Manufacturer.objects.all(), required=False, selector=True, label=_('Manufacturer'))
platform = DynamicModelChoiceField(queryset=Platform.objects.all(), required=False, selector=True, label=_('Platform'))
virtualmachine = DynamicModelChoiceField(queryset=VirtualMachine.objects.all(), required=False, selector=True, label=_('Virtual Machine'))
site = DynamicModelChoiceField(queryset=Site.objects.all(), required=False, selector=True, label=_('Site'))
sitegroup = DynamicModelChoiceField(queryset=SiteGroup.objects.all(), required=False, selector=True, label=_('Site Group'))
cluster = DynamicModelChoiceField(queryset=Cluster.objects.all(), required=False, selector=True, label=_('Cluster'))
region = DynamicModelChoiceField(queryset=Region.objects.all(), required=False, label=_('Region'))
clustertype = DynamicModelChoiceField(queryset=ClusterType.objects.all(), required=False, selector=True, label=_('Cluster Type'))
zabbixconfigurationgroup = DynamicModelChoiceField(queryset=ZabbixConfigurationGroup.objects.all(), required=False, selector=True, label=_('Zabbix Configuration Group'))

Expand All @@ -42,6 +45,9 @@ class ZabbixHostgroupAssignmentForm(NetBoxModelForm):
FieldSet('virtualmachine', name=_('Virtual Machine')),
FieldSet('cluster', name=_('Cluster')),
FieldSet('clustertype', name=_('Cluster Type')),
FieldSet('site', name=_('Site')),
FieldSet('sitegroup', name=_('Site Group')),
FieldSet('region', name=_('Region')),
FieldSet('zabbixconfigurationgroup', name=_('Zabbix Configuration Group')),
),
name=_('Assignment'),
Expand All @@ -53,6 +59,7 @@ class Meta:
fields = (
'zabbixhostgroup',
'device',
'virtualdevicecontext',
'virtualmachine',
'cluster',
'clustertype',
Expand All @@ -61,6 +68,9 @@ class Meta:
'manufacturer',
'platform',
'zabbixconfigurationgroup',
'site',
'sitegroup',
'region',
)

@property
Expand Down
38 changes: 35 additions & 3 deletions nbxsync/forms/zabbixhostinventory.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
from netbox.forms import NetBoxModelBulkEditForm, NetBoxModelFilterSetForm, NetBoxModelForm
from utilities.forms.fields import DynamicModelChoiceField, TagFilterField
from utilities.forms.rendering import FieldSet, TabbedGroups
from dcim.models import Device, VirtualDeviceContext
from virtualization.models import VirtualMachine
from dcim.models import Device, VirtualDeviceContext, DeviceRole, DeviceType, Manufacturer, Platform, Site, SiteGroup, Region
from virtualization.models import VirtualMachine, Cluster, ClusterType
from nbxsync.models import ZabbixConfigurationGroup

from nbxsync.choices import ZabbixHostInventoryModeChoices
from nbxsync.constants import ASSIGNMENT_TYPE_TO_FIELD_NBOBJS
Expand Down Expand Up @@ -75,6 +76,16 @@ class ZabbixHostInventoryForm(NetBoxModelForm):
device = DynamicModelChoiceField(queryset=Device.objects.all(), required=False, selector=True, label=_('Device'))
virtualdevicecontext = DynamicModelChoiceField(queryset=VirtualDeviceContext.objects.all(), required=False, selector=True, label=_('Virtual Device Context'))
virtualmachine = DynamicModelChoiceField(queryset=VirtualMachine.objects.all(), required=False, selector=True, label=_('Virtual Machine'))
site = DynamicModelChoiceField(queryset=Site.objects.all(), required=False, selector=True, label=_('Site'))
sitegroup = DynamicModelChoiceField(queryset=SiteGroup.objects.all(), required=False, selector=True, label=_('Site Group'))
region = DynamicModelChoiceField(queryset=Region.objects.all(), required=False, label=_('Region'))
platform = DynamicModelChoiceField(queryset=Platform.objects.all(), required=False, selector=True, label=_('Platform'))
devicerole = DynamicModelChoiceField(queryset=DeviceRole.objects.all(), required=False, selector=True, label=_('Device Role'))
devicetype = DynamicModelChoiceField(queryset=DeviceType.objects.all(), required=False, selector=True, label=_('Device Type'))
manufacturer = DynamicModelChoiceField(queryset=Manufacturer.objects.all(), required=False, selector=True, label=_('Manufacturer'))
cluster = DynamicModelChoiceField(queryset=Cluster.objects.all(), required=False, selector=True, label=_('Cluster'))
clustertype = DynamicModelChoiceField(queryset=ClusterType.objects.all(), required=False, selector=True, label=_('Cluster Type'))
zabbixconfigurationgroup = DynamicModelChoiceField(queryset=ZabbixConfigurationGroup.objects.all(), required=False, selector=True, label=_('Zabbix Configuration Group'))

fieldsets = (
FieldSet(
Expand Down Expand Up @@ -178,8 +189,18 @@ class ZabbixHostInventoryForm(NetBoxModelForm):
FieldSet(
TabbedGroups(
FieldSet('device', name=_('Device')),
FieldSet('virtualdevicecotext', name=_('Virtual Device Context')),
FieldSet('virtualdevicecontext', name=_('Virtual Device Context')),
FieldSet('virtualmachine', name=_('Virtual Machine')),
FieldSet('site', name=_('Site')),
FieldSet('sitegroup', name=_('Site Group')),
FieldSet('region', name=_('Region')),
FieldSet('platform', name=_('Platform')),
FieldSet('devicerole', name=_('Device Role')),
FieldSet('devicetype', name=_('Device Type')),
FieldSet('manufacturer', name=_('Manufacturer')),
FieldSet('cluster', name=_('Cluster')),
FieldSet('clustertype', name=_('Cluster Type')),
FieldSet('zabbixconfigurationgroup', name=_('Config Group')),
),
name=_('Assignment'),
),
Expand Down Expand Up @@ -260,7 +281,18 @@ class Meta:
'name',
'notes',
'device',
'virtualdevicecontext',
'virtualmachine',
'site',
'sitegroup',
'region',
'platform',
'devicerole',
'devicetype',
'manufacturer',
'cluster',
'clustertype',
'zabbixconfigurationgroup',
)

@property
Expand Down
11 changes: 10 additions & 1 deletion nbxsync/forms/zabbixmacroassignment.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from netbox.forms import NetBoxModelBulkEditForm, NetBoxModelFilterSetForm, NetBoxModelForm
from utilities.forms.fields import DynamicModelChoiceField, TagFilterField
from utilities.forms.rendering import FieldSet, TabbedGroups
from dcim.models import Device, VirtualDeviceContext, DeviceRole, DeviceType, Manufacturer, Platform
from dcim.models import Device, VirtualDeviceContext, DeviceRole, DeviceType, Manufacturer, Platform, Site, SiteGroup, Region
from virtualization.models import Cluster, ClusterType, VirtualMachine

from nbxsync.constants import ASSIGNMENT_TYPE_TO_FIELD
Expand All @@ -26,6 +26,9 @@ class ZabbixMacroAssignmentForm(NetBoxModelForm):
manufacturer = DynamicModelChoiceField(queryset=Manufacturer.objects.all(), required=False, selector=True, label=_('Manufacturer'))
platform = DynamicModelChoiceField(queryset=Platform.objects.all(), required=False, selector=True, label=_('Platform'))
virtualmachine = DynamicModelChoiceField(queryset=VirtualMachine.objects.all(), required=False, selector=True, label=_('Virtual Machine'))
site = DynamicModelChoiceField(queryset=Site.objects.all(), required=False, selector=True, label=_('Site'))
sitegroup = DynamicModelChoiceField(queryset=SiteGroup.objects.all(), required=False, selector=True, label=_('Site Group'))
region = DynamicModelChoiceField(queryset=Region.objects.all(), required=False, label=_('Region'))
cluster = DynamicModelChoiceField(queryset=Cluster.objects.all(), required=False, selector=True, label=_('Cluster'))
clustertype = DynamicModelChoiceField(queryset=ClusterType.objects.all(), required=False, selector=True, label=_('Cluster Type'))
zabbixconfigurationgroup = DynamicModelChoiceField(queryset=ZabbixConfigurationGroup.objects.all(), required=False, selector=True, label=_('Zabbix Configuration Group'))
Expand All @@ -43,6 +46,9 @@ class ZabbixMacroAssignmentForm(NetBoxModelForm):
FieldSet('virtualmachine', name=_('Virtual Machine')),
FieldSet('cluster', name=_('Cluster')),
FieldSet('clustertype', name=_('Cluster Type')),
FieldSet('site', name=_('Site')),
FieldSet('sitegroup', name=_('Site Group')),
FieldSet('region', name=_('Region')),
FieldSet('zabbixconfigurationgroup', name=_('Zabbix Configuration Group')),
),
name=_('Assignment'),
Expand All @@ -66,6 +72,9 @@ class Meta:
'manufacturer',
'platform',
'zabbixconfigurationgroup',
'site',
'sitegroup',
'region',
)

@property
Expand Down
11 changes: 10 additions & 1 deletion nbxsync/forms/zabbixserverassignment.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from netbox.forms import NetBoxModelImportForm, NetBoxModelBulkEditForm, NetBoxModelFilterSetForm, NetBoxModelForm
from utilities.forms.fields import DynamicModelChoiceField, TagFilterField, CSVModelChoiceField
from utilities.forms.rendering import FieldSet, TabbedGroups
from dcim.models import Device, VirtualDeviceContext
from dcim.models import Device, VirtualDeviceContext, Site, SiteGroup, Region
from virtualization.models import VirtualMachine

from nbxsync.constants import ASSIGNMENT_TYPE_TO_FIELD, ASSIGNMENT_TYPE_TO_FIELD_NBOBJS
Expand All @@ -27,6 +27,9 @@ class ZabbixServerAssignmentForm(NetBoxModelForm):
device = DynamicModelChoiceField(queryset=Device.objects.all(), required=False, selector=True, label=_('Device'))
virtualdevicecontext = DynamicModelChoiceField(queryset=VirtualDeviceContext.objects.all(), required=False, selector=True, label=_('Virtual Device Context'))
virtualmachine = DynamicModelChoiceField(queryset=VirtualMachine.objects.all(), required=False, selector=True, label=_('Virtual Machine'))
site = DynamicModelChoiceField(queryset=Site.objects.all(), required=False, selector=True, label=_('Site'))
sitegroup = DynamicModelChoiceField(queryset=SiteGroup.objects.all(), required=False, selector=True, label=_('Site Group'))
region = DynamicModelChoiceField(queryset=Region.objects.all(), required=False, label=_('Region'))

fieldsets = (
FieldSet('zabbixserver', 'sync_enabled', name=_('Generic')),
Expand All @@ -42,6 +45,9 @@ class ZabbixServerAssignmentForm(NetBoxModelForm):
FieldSet('device', name=_('Device')),
FieldSet('virtualdevicecontext', name=_('Virtual Device Context')),
FieldSet('virtualmachine', name=_('Virtual Machine')),
FieldSet('site', name=_('Site')),
FieldSet('sitegroup', name=_('Site Group')),
FieldSet('region', name=_('Region')),
FieldSet('zabbixconfigurationgroup', name=_('Zabbix Configuration Group')),
),
name=_('Device Assignment'),
Expand All @@ -58,6 +64,9 @@ class Meta:
'device',
'virtualdevicecontext',
'virtualmachine',
'site',
'sitegroup',
'region',
'zabbixconfigurationgroup',
)

Expand Down
Loading