From aee3ebece23117633da85ff00e5dc860c7ca8e36 Mon Sep 17 00:00:00 2001
From: ctfrookie <112057820+ctfrookie@users.noreply.github.com>
Date: Wed, 22 Apr 2026 22:52:27 +0800
Subject: [PATCH 01/11] Fix (template): Internationalization processing of
field set names correction
---
nbxsync/templates/nbxsync/forms/zabbixhostinterface.html | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/nbxsync/templates/nbxsync/forms/zabbixhostinterface.html b/nbxsync/templates/nbxsync/forms/zabbixhostinterface.html
index 2a5d916..dbfa9d9 100644
--- a/nbxsync/templates/nbxsync/forms/zabbixhostinterface.html
+++ b/nbxsync/templates/nbxsync/forms/zabbixhostinterface.html
@@ -123,7 +123,7 @@
SNMPv3 Configuration
{% for fieldset in form.fieldsets %}
- {% if fieldset.name == "Assignment" %}
+ {% if fieldset.name == _('Assignment') %}
{% render_fieldset form fieldset %}
{% endif %}
{% endfor %}
From b32690fbb5b15763ad970c0562b8fcdc8a516909 Mon Sep 17 00:00:00 2001
From: bvbaekel
Date: Sun, 3 May 2026 10:05:27 +0200
Subject: [PATCH 02/11] Fix typo in last_sync_message
---
nbxsync/jobs/synctemplates.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/nbxsync/jobs/synctemplates.py b/nbxsync/jobs/synctemplates.py
index e64fee1..71d6b93 100644
--- a/nbxsync/jobs/synctemplates.py
+++ b/nbxsync/jobs/synctemplates.py
@@ -91,7 +91,7 @@ def run(self):
self.instance.last_sync_state = True
self.instance.last_sync = make_aware(datetime.datetime.now())
- self.instance.last_sync_message = 'Succes'
+ self.instance.last_sync_message = 'Success'
self.instance.save()
except ConnectionError as e:
From 7aa2181c4030c2ae2c1be321058253a1dbd2ef62 Mon Sep 17 00:00:00 2001
From: bvbaekel
Date: Sun, 3 May 2026 10:06:08 +0200
Subject: [PATCH 03/11] Fix internationalization of fieldset name
---
nbxsync/templates/nbxsync/forms/zabbixmacroassignment.html | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/nbxsync/templates/nbxsync/forms/zabbixmacroassignment.html b/nbxsync/templates/nbxsync/forms/zabbixmacroassignment.html
index 27f04dc..dad8881 100644
--- a/nbxsync/templates/nbxsync/forms/zabbixmacroassignment.html
+++ b/nbxsync/templates/nbxsync/forms/zabbixmacroassignment.html
@@ -23,7 +23,7 @@ Generic
{% for fieldset in form.fieldsets %}
- {% if fieldset.name == "Assignment" %}
+ {% if fieldset.name == _("Assignment") %}
{% render_fieldset form fieldset %}
{% endif %}
{% endfor %}
From 3764b84d93392c448f9bb65c2c51284d83a16f7e Mon Sep 17 00:00:00 2001
From: bvbaekel
Date: Sun, 3 May 2026 12:13:25 +0200
Subject: [PATCH 04/11] Streamline user experience by not allowing the user to
select already assigned templates/tags
---
nbxsync/forms/zabbixtagassignment.py | 25 +++++++++++++++++++----
nbxsync/forms/zabbixtemplateassignment.py | 22 +++++++++++++++++---
2 files changed, 40 insertions(+), 7 deletions(-)
diff --git a/nbxsync/forms/zabbixtagassignment.py b/nbxsync/forms/zabbixtagassignment.py
index 43191a1..9f7897e 100644
--- a/nbxsync/forms/zabbixtagassignment.py
+++ b/nbxsync/forms/zabbixtagassignment.py
@@ -11,6 +11,7 @@
from nbxsync.constants import ASSIGNMENT_TYPE_TO_FIELD
from nbxsync.models import ZabbixTag, ZabbixTagAssignment, ZabbixConfigurationGroup
+from nbxsync.utils import get_assigned_zabbixobjects
__all__ = ('ZabbixTagAssignmentForm', 'ZabbixTagAssignmentFilterForm', 'ZabbixTagAssignmentBulkEditForm')
logger = logging.getLogger(__name__)
@@ -71,21 +72,23 @@ def assignable_fields(self):
def __init__(self, *args, **kwargs):
instance = kwargs.get('instance')
initial = kwargs.get('initial', {}).copy()
+ target = None
if instance and instance.assigned_object:
+ target = instance.assigned_object
for model_class, field in ASSIGNMENT_TYPE_TO_FIELD.items():
if isinstance(instance.assigned_object, model_class):
- initial[field] = instance.assigned_object
+ initial[field] = target
break
elif 'assigned_object_type' in initial and 'assigned_object_id' in initial:
try:
content_type = ContentType.objects.get(pk=initial['assigned_object_type'])
- obj = content_type.get_object_for_this_type(pk=initial['assigned_object_id'])
+ target = content_type.get_object_for_this_type(pk=initial['assigned_object_id'])
for model_class, field in ASSIGNMENT_TYPE_TO_FIELD.items():
- if isinstance(obj, model_class):
- initial[field] = obj.pk
+ if isinstance(target, model_class):
+ initial[field] = target.pk
break
except Exception as e:
@@ -95,6 +98,20 @@ def __init__(self, *args, **kwargs):
kwargs['initial'] = initial
super().__init__(*args, **kwargs)
+ if target is not None:
+ assigned = get_assigned_zabbixobjects(target)
+ excluded_ids = set()
+ for assigned_tag in assigned['tags']:
+ excluded_ids.add(assigned_tag.zabbixtag_id)
+
+ if instance is not None and instance.pk and instance.zabbixtag_id:
+ excluded_ids.discard(instance.zabbixtag_id)
+
+ if excluded_ids:
+ self.fields['zabbixtag'].queryset = ZabbixTag.objects.exclude(pk__in=excluded_ids)
+ self.fields['zabbixtag'].widget.add_query_params({'id__n': list(excluded_ids)})
+
+
def clean(self):
super().clean()
diff --git a/nbxsync/forms/zabbixtemplateassignment.py b/nbxsync/forms/zabbixtemplateassignment.py
index c302f4f..78f5cd3 100644
--- a/nbxsync/forms/zabbixtemplateassignment.py
+++ b/nbxsync/forms/zabbixtemplateassignment.py
@@ -11,6 +11,7 @@
from nbxsync.constants import ASSIGNMENT_TYPE_TO_FIELD, ASSIGNMENT_TYPE_TO_FIELD_NBOBJS
from nbxsync.models import ZabbixTemplate, ZabbixTemplateAssignment, ZabbixConfigurationGroup
+from nbxsync.utils import get_assigned_zabbixobjects
__all__ = ('ZabbixTemplateAssignmentForm', 'ZabbixTemplateAssignmentFilterForm')
logger = logging.getLogger(__name__)
@@ -71,8 +72,10 @@ def assignable_fields(self):
def __init__(self, *args, **kwargs):
instance = kwargs.get('instance')
initial = kwargs.get('initial', {}).copy()
+ target = None
if instance and instance.assigned_object:
+ target = instance.assigned_object
for model_class, field in ASSIGNMENT_TYPE_TO_FIELD.items():
if isinstance(instance.assigned_object, model_class):
initial[field] = instance.assigned_object
@@ -81,11 +84,11 @@ def __init__(self, *args, **kwargs):
elif 'assigned_object_type' in initial and 'assigned_object_id' in initial:
try:
content_type = ContentType.objects.get(pk=initial['assigned_object_type'])
- obj = content_type.get_object_for_this_type(pk=initial['assigned_object_id'])
+ target = content_type.get_object_for_this_type(pk=initial['assigned_object_id'])
for model_class, field in ASSIGNMENT_TYPE_TO_FIELD.items():
- if isinstance(obj, model_class):
- initial[field] = obj.pk
+ if isinstance(target, model_class):
+ initial[field] = target.pk
break
except Exception as e:
@@ -95,6 +98,19 @@ def __init__(self, *args, **kwargs):
kwargs['initial'] = initial
super().__init__(*args, **kwargs)
+ if target is not None:
+ assigned = get_assigned_zabbixobjects(target)
+ excluded_ids = set()
+ for assigned_template in assigned['templates']:
+ excluded_ids.add(assigned_template.zabbixtemplate_id)
+
+ if instance is not None and instance.pk and instance.zabbixtemplate_id:
+ excluded_ids.discard(instance.zabbixtemplate_id)
+
+ if excluded_ids:
+ self.fields['zabbixtemplate'].queryset = ZabbixTemplate.objects.exclude(pk__in=excluded_ids)
+ self.fields['zabbixtemplate'].widget.add_query_params({'id__n': list(excluded_ids)})
+
def clean(self):
super().clean()
From ef5afc9fd3a4459616ee624c09657129853f2129 Mon Sep 17 00:00:00 2001
From: bvbaekel
Date: Sun, 3 May 2026 14:04:25 +0200
Subject: [PATCH 05/11] Fix schema generation
---
nbxsync/api/views/zabbixsync.py | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/nbxsync/api/views/zabbixsync.py b/nbxsync/api/views/zabbixsync.py
index 4a0b4f5..2a326aa 100644
--- a/nbxsync/api/views/zabbixsync.py
+++ b/nbxsync/api/views/zabbixsync.py
@@ -5,16 +5,15 @@
from rest_framework.viewsets import ViewSet
from django.apps import apps
from django.shortcuts import get_object_or_404
+from drf_spectacular.utils import extend_schema
-from nbxsync.api.serializers import ZabbixHostInterfaceSerializer
-from nbxsync.models import ZabbixHostInterface
from nbxsync.constants import OBJECT_TYPE_MODEL_MAP
class ZabbixSyncViewSet(ViewSet):
permission_classes = [IsAuthenticated]
- serializer_class = ZabbixHostInterfaceSerializer
+ @extend_schema(exclude=True)
def create(self, request, **kwargs):
obj_type = (request.data.get('obj_type') or '').strip().lower()
obj_id = request.data.get('obj_id')
From c462920e98724aa37f5aaaed1c885b5a30d83c7c Mon Sep 17 00:00:00 2001
From: bvbaekel
Date: Sun, 3 May 2026 14:05:08 +0200
Subject: [PATCH 06/11] Manual device sync fails for templated Zabbix
hostgroups when the rendered local hostgroup does not already exis
---
nbxsync/tests/utils/test_hostgroupsync.py | 12 ++++++++++++
nbxsync/utils/sync/hostgroupsync.py | 14 ++++++++++----
2 files changed, 22 insertions(+), 4 deletions(-)
diff --git a/nbxsync/tests/utils/test_hostgroupsync.py b/nbxsync/tests/utils/test_hostgroupsync.py
index ae89744..f8df027 100644
--- a/nbxsync/tests/utils/test_hostgroupsync.py
+++ b/nbxsync/tests/utils/test_hostgroupsync.py
@@ -77,3 +77,15 @@ def test_set_id_dynamic_updates_existing_hostgroup(self):
self.assertEqual(updated_hg.groupid, 999)
self.assertEqual(updated_hg.name, 'ExistingGroup')
self.assertEqual(updated_hg.zabbixserver, self.zabbixserver)
+
+ def test_set_id_dynamic_creates_rendered_hostgroup_when_missing(self):
+ rendered_name, ok = self.assignment_dynamic.render()
+ self.assertTrue(ok)
+
+ sync = HostGroupSync(api=None, netbox_obj=self.assignment_dynamic)
+ sync.set_id(321)
+
+ created_hg = ZabbixHostgroup.objects.get(zabbixserver=self.zabbixserver, name=rendered_name)
+ self.assertEqual(created_hg.value, rendered_name)
+ self.assertEqual(created_hg.groupid, 321)
+ self.assertEqual(created_hg.description, 'Automatically generated from template')
diff --git a/nbxsync/utils/sync/hostgroupsync.py b/nbxsync/utils/sync/hostgroupsync.py
index 183516d..9bd4541 100644
--- a/nbxsync/utils/sync/hostgroupsync.py
+++ b/nbxsync/utils/sync/hostgroupsync.py
@@ -65,8 +65,14 @@ def set_id(self, value):
name, _state = self.obj.render()
- # Try to get the hostgroup by value (the Zabbix name)
- hostgroup = ZabbixHostgroup.objects.get(zabbixserver=self.obj.zabbixhostgroup.zabbixserver, value=name)
+ zabbixserver = self.obj.zabbixhostgroup.zabbixserver
+ # Try to find an existing local representation for the rendered Zabbix group.
+ # Dynamic groups created from templates are stored with the rendered group
+ # name in both `name` and `value`, so check either field without raising.
+ hostgroup = (
+ ZabbixHostgroup.objects.filter(zabbixserver=zabbixserver, name=name).first()
+ or ZabbixHostgroup.objects.filter(zabbixserver=zabbixserver, value=name).first()
+ )
if hostgroup:
hostgroup.groupid = value
@@ -74,8 +80,8 @@ def set_id(self, value):
return
# Not found by name, so create it
- ZabbixHostgroup(zabbixserver=self.obj.zabbixhostgroup.zabbixserver, name=name, value=name, groupid=value, description='Automatically generated from template').save()
-
+ ZabbixHostgroup(zabbixserver=zabbixserver, name=name, value=name, groupid=value, description='Automatically generated from template').save()
+
def get_id(self):
if self.obj.is_template():
# print('HostGroupSync: Detected template value, skipping groupid usage.')
From ddc0ceac031638bd25cc2423f58bc8b0078562f1 Mon Sep 17 00:00:00 2001
From: bvbaekel
Date: Sun, 3 May 2026 16:18:56 +0200
Subject: [PATCH 07/11] Fix HostInterfaceSync raises RuntimeError "Cannot
switch host for interface."
---
nbxsync/utils/sync/hostinterfacesync.py | 25 +++++++++++++++++++++++++
1 file changed, 25 insertions(+)
diff --git a/nbxsync/utils/sync/hostinterfacesync.py b/nbxsync/utils/sync/hostinterfacesync.py
index 6398e15..93a8cf8 100644
--- a/nbxsync/utils/sync/hostinterfacesync.py
+++ b/nbxsync/utils/sync/hostinterfacesync.py
@@ -77,6 +77,9 @@ def get_create_params(self):
def get_update_params(self, **kwargs):
params = self.get_create_params()
params['interfaceid'] = self.obj.interfaceid
+
+ # Zabbix forbids changing hostid on update
+ params.pop('hostid', None)
return params
def result_key(self):
@@ -126,3 +129,25 @@ def sync_from_zabbix(self, data):
except Exception as err:
self.obj.update_sync_info(success=False, message=str(err))
+
+ def find_by_id(self):
+ if not self.obj.interfaceid:
+ return []
+
+ found = self.api_object().get(interfaceids=self.obj.interfaceid, output=['interfaceid', 'hostid'])
+
+ if not found:
+ # interfaceid no longer exists in Zabbix, clear it so the next sync cycle falls through to find_by_name / try_create
+ self.obj.interfaceid = None
+ self.obj.save(update_fields=['interfaceid'])
+ return []
+
+ expected_hostid = str(self.context.get('hostid') or '')
+ if expected_hostid and str(found[0]['hostid']) != expected_hostid:
+ # interfaceid exists but belongs to a different host: this is a stale reference.
+ # Clear it and let the sync re-establish the correct interface
+ self.obj.interfaceid = None
+ self.obj.save(update_fields=['interfaceid'])
+ return []
+
+ return found
From d16f1ccc7af2b8db1ff7a6b13ef51b5a042c6b25 Mon Sep 17 00:00:00 2001
From: bvbaekel
Date: Sun, 3 May 2026 16:20:38 +0200
Subject: [PATCH 08/11] Fix logic to actually delete hosts from Zabbix
---
nbxsync/utils/sync/hostsync.py | 22 +++++++++++++++-------
1 file changed, 15 insertions(+), 7 deletions(-)
diff --git a/nbxsync/utils/sync/hostsync.py b/nbxsync/utils/sync/hostsync.py
index 1882144..7e1fb9b 100644
--- a/nbxsync/utils/sync/hostsync.py
+++ b/nbxsync/utils/sync/hostsync.py
@@ -406,12 +406,20 @@ def delete(self):
pass
return
+ # The assigned object (Device/VM) may already be gone if this job runs
+ # after a cascade delete. Resolve it once and guard all uses below.
+ assigned_object = self.obj.assigned_object
+ if assigned_object is None:
+ # NetBox object is gone — still delete the Zabbix host, then clean up
+ # what we can without touching the now-invalid assignment row.
+ try:
+ self.api_object().delete([self.obj.hostid])
+ except Exception as e:
+ raise RuntimeError(f'Failed to delete orphaned host {self.obj.hostid} from Zabbix: {e}')
+ return
+
try:
- # Check for maintenances where this host is attached to
- # If found:
- # Check if this host is the only host for this maintenance; if so - delete the maintenance window
- # If not: delete the host from the maintenance window
- object_ct = ContentType.objects.get_for_model(self.obj.assigned_object)
+ object_ct = ContentType.objects.get_for_model(assigned_object)
maintenances = self.api.maintenance.get(hostids=[self.obj.hostid], selectHosts='extend')
for mw in maintenances:
# Check per maintenance window if this host is the only host in the window or not. If it is, we can delete it
@@ -421,7 +429,7 @@ def delete(self):
hosts = [{'hostid': host['hostid']} for host in mw['hosts'] if int(host['hostid']) != self.obj.hostid]
# Update the maintenance window in Zabbix without our hostid in it
self.api.maintenance.update(maintenanceid=mw['maintenanceid'], hosts=hosts)
- for assignment in ZabbixMaintenanceObjectAssignment.objects.filter(maintenanceid=mw['maintenanceid'], assigned_object_type=object_ct, assigned_object_id=self.obj.assigned_object.id):
+ for assignment in ZabbixMaintenanceObjectAssignment.objects.filter(maintenanceid=mw['maintenanceid'], assigned_object_type=object_ct, assigned_object_id=assigned_object.id):
assignment.delete() # Delete the Assignment from Netbox;
# If our host is the only one in the Maintenance Object
@@ -442,7 +450,7 @@ def delete(self):
# Also clear host IDs from related interfaces
try:
- ZabbixHostInterface.objects.filter(assigned_object_type=self.obj.assigned_object_type, assigned_object_id=self.obj.assigned_object.id, zabbixserver=self.obj.zabbixserver).update(interfaceid=None)
+ ZabbixHostInterface.objects.filter(assigned_object_type=self.obj.assigned_object_type, assigned_object_id=assigned_object.id, zabbixserver=self.obj.zabbixserver).update(interfaceid=None)
self.obj.update_sync_info(success=True, message='Host deleted from Zabbix.')
except Exception:
From ef43241ce429282bab9c4e00e07ac102da7af7cf Mon Sep 17 00:00:00 2001
From: bvbaekel
Date: Sun, 3 May 2026 20:39:35 +0200
Subject: [PATCH 09/11] Ruff formatting for hostgroupsync
---
nbxsync/utils/sync/hostgroupsync.py | 7 ++-----
1 file changed, 2 insertions(+), 5 deletions(-)
diff --git a/nbxsync/utils/sync/hostgroupsync.py b/nbxsync/utils/sync/hostgroupsync.py
index 9bd4541..1a54837 100644
--- a/nbxsync/utils/sync/hostgroupsync.py
+++ b/nbxsync/utils/sync/hostgroupsync.py
@@ -69,10 +69,7 @@ def set_id(self, value):
# Try to find an existing local representation for the rendered Zabbix group.
# Dynamic groups created from templates are stored with the rendered group
# name in both `name` and `value`, so check either field without raising.
- hostgroup = (
- ZabbixHostgroup.objects.filter(zabbixserver=zabbixserver, name=name).first()
- or ZabbixHostgroup.objects.filter(zabbixserver=zabbixserver, value=name).first()
- )
+ hostgroup = ZabbixHostgroup.objects.filter(zabbixserver=zabbixserver, name=name).first() or ZabbixHostgroup.objects.filter(zabbixserver=zabbixserver, value=name).first()
if hostgroup:
hostgroup.groupid = value
@@ -81,7 +78,7 @@ def set_id(self, value):
# Not found by name, so create it
ZabbixHostgroup(zabbixserver=zabbixserver, name=name, value=name, groupid=value, description='Automatically generated from template').save()
-
+
def get_id(self):
if self.obj.is_template():
# print('HostGroupSync: Detected template value, skipping groupid usage.')
From d8862ca793a09ca2b030a3a1e0863e0ba99aee6b Mon Sep 17 00:00:00 2001
From: bvbaekel
Date: Sun, 3 May 2026 20:44:54 +0200
Subject: [PATCH 10/11] Implement dynamic/templates values for Zabbix Macro's
---
nbxsync/models/zabbixmacroassignment.py | 46 ++++++++++++++++++-
nbxsync/tables/columns.py | 10 ++++
nbxsync/tables/zabbixmacroassignment.py | 23 +++++++---
.../nbxsync/forms/zabbixmacroassignment.html | 2 +-
.../nbxsync/zabbixmacroassignment.html | 8 +++-
nbxsync/templatetags/__init__.py | 1 +
nbxsync/templatetags/zabbix_macros.py | 9 ++++
nbxsync/tests/utils/test_hostsync.py | 6 +++
nbxsync/utils/inheritance.py | 6 ++-
nbxsync/utils/sync/hostsync.py | 3 +-
10 files changed, 100 insertions(+), 14 deletions(-)
create mode 100644 nbxsync/templatetags/zabbix_macros.py
diff --git a/nbxsync/models/zabbixmacroassignment.py b/nbxsync/models/zabbixmacroassignment.py
index 5e96262..0e10e07 100644
--- a/nbxsync/models/zabbixmacroassignment.py
+++ b/nbxsync/models/zabbixmacroassignment.py
@@ -1,15 +1,21 @@
+import re
+
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.db import models
-from netbox.models import NetBoxModel
+from jinja2 import TemplateError, TemplateSyntaxError, UndefinedError
+from utilities.jinja2 import render_jinja2
+from netbox.models import NetBoxModel
from nbxsync.constants import ASSIGNMENT_MODELS
-from nbxsync.models import SyncInfoModel
+from nbxsync.models import SyncInfoModel, ZabbixConfigurationGroup
__all__ = ('ZabbixMacroAssignment',)
+TEMPLATE_PATTERN = re.compile(r'({{.*?}}|{%-?\s*.*?\s*-?%}|{#.*?#})')
+
class ZabbixMacroAssignment(SyncInfoModel, NetBoxModel):
zabbixmacro = models.ForeignKey('nbxsync.ZabbixMacro', on_delete=models.CASCADE, related_name='zabbixmacroassignment')
@@ -59,6 +65,42 @@ def __str__(self):
return f'{self.zabbixmacro.macro[:-1]}:{self.context}}}'
return self.zabbixmacro.macro
+ def is_template(self):
+ return bool(TEMPLATE_PATTERN.search(self.value))
+
+ def render(self, **context):
+ if isinstance(self.assigned_object, ZabbixConfigurationGroup):
+ return self.value, True
+
+ context = self.get_context(**context)
+
+ try:
+ output = render_jinja2(self.value, context)
+ output = output.replace('\r\n', '\n')
+ return output, True
+
+ except TemplateSyntaxError as err:
+ return self.value, False
+
+ except UndefinedError as err:
+ return self.value, False
+
+ except TemplateError as err:
+ return self.value, False
+
+ except Exception as err:
+ return self.value, False
+
+ def get_context(self, **extra_context):
+ context = {
+ 'object': self.assigned_object,
+ 'macro': self.zabbixmacro.macro,
+ 'value': self.value,
+ 'description': self.zabbixmacro.description,
+ }
+ context.update(extra_context)
+ return context
+
@property
def full_name(self):
return self
diff --git a/nbxsync/tables/columns.py b/nbxsync/tables/columns.py
index 1e8738a..4910f69 100644
--- a/nbxsync/tables/columns.py
+++ b/nbxsync/tables/columns.py
@@ -39,3 +39,13 @@ def render(self, value):
if model is None:
return capfirst(value.model.replace('_', ' '))
return capfirst(model._meta.verbose_name)
+
+
+class JinjaValueColumn(tables.Column):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ def render(self, value, record, table):
+ instance = getattr(table, 'instance', None)
+ output, success = record.render(object=instance)
+ return output
diff --git a/nbxsync/tables/zabbixmacroassignment.py b/nbxsync/tables/zabbixmacroassignment.py
index 34bfcd1..c748e9d 100644
--- a/nbxsync/tables/zabbixmacroassignment.py
+++ b/nbxsync/tables/zabbixmacroassignment.py
@@ -6,7 +6,7 @@
from nbxsync.models import ZabbixMacroAssignment
from nbxsync.tables import ZabbixInheritedAssignmentTable
-from nbxsync.tables.columns import ContentTypeModelNameColumn, InheritanceAwareActionsColumn
+from nbxsync.tables.columns import ContentTypeModelNameColumn, InheritanceAwareActionsColumn, JinjaValueColumn
from nbxsync.choices import ZabbixMacroTypeChoices
__all__ = ('ZabbixMacroAssignmentTable', 'ZabbixMacroAssignmentObjectViewTable')
@@ -18,6 +18,11 @@ class ZabbixMacroAssignmentTable(ZabbixInheritedAssignmentTable, NetBoxTable):
zabbixmacro = tables.Column(accessor='zabbixmacro.macro', verbose_name=_('Zabbix Macro'), linkify={'viewname': 'plugins:nbxsync:zabbixmacro', 'args': [A('zabbixmacro.pk')]})
macro_full_name = tables.Column(accessor='full_name', verbose_name=_('Macro'), order_by='zabbixmacro__macro')
actions = InheritanceAwareActionsColumn()
+ rendered_output = JinjaValueColumn(
+ verbose_name='Value',
+ orderable=False,
+ accessor='value',
+ )
class Meta(NetBoxTable.Meta):
model = ZabbixMacroAssignment
@@ -30,10 +35,10 @@ class Meta(NetBoxTable.Meta):
'inherited_from',
'is_context',
'regex',
- 'value',
+ 'rendered_output',
'created',
'last_updated',
- 'actions,',
+ 'actions',
)
default_columns = (
'pk',
@@ -43,7 +48,7 @@ class Meta(NetBoxTable.Meta):
'macro_full_name',
'is_context',
'regex',
- 'value',
+ 'rendered_output',
'inherited_from',
)
@@ -58,7 +63,11 @@ class ZabbixMacroAssignmentObjectViewTable(ZabbixInheritedAssignmentTable, NetBo
assigned_object = tables.Column(verbose_name=_('Assigned To'), linkify=True, orderable=False)
macro_full_name = tables.Column(accessor='full_name', verbose_name=_('Macro'), order_by='zabbixmacro__macro', linkify={'viewname': 'plugins:nbxsync:zabbixmacro', 'args': [A('zabbixmacro.pk')]})
actions = InheritanceAwareActionsColumn()
- value = tables.Column(verbose_name='Value')
+ rendered_output = JinjaValueColumn(
+ verbose_name='Value',
+ orderable=False,
+ accessor='value',
+ )
class Meta(NetBoxTable.Meta):
model = ZabbixMacroAssignment
@@ -71,7 +80,7 @@ class Meta(NetBoxTable.Meta):
'inherited_from',
'is_regex',
'context',
- 'value',
+ 'rendered_output',
'created',
'last_updated',
'actions,',
@@ -81,7 +90,7 @@ class Meta(NetBoxTable.Meta):
'macro_full_name',
'is_regex',
'context',
- 'value',
+ 'rendered_output',
'inherited_from',
)
diff --git a/nbxsync/templates/nbxsync/forms/zabbixmacroassignment.html b/nbxsync/templates/nbxsync/forms/zabbixmacroassignment.html
index dad8881..df9e831 100644
--- a/nbxsync/templates/nbxsync/forms/zabbixmacroassignment.html
+++ b/nbxsync/templates/nbxsync/forms/zabbixmacroassignment.html
@@ -23,7 +23,7 @@ Generic
{% for fieldset in form.fieldsets %}
- {% if fieldset.name == _("Assignment") %}
+ {% if fieldset.name == _('Assignment') %}
{% render_fieldset form fieldset %}
{% endif %}
{% endfor %}
diff --git a/nbxsync/templates/nbxsync/zabbixmacroassignment.html b/nbxsync/templates/nbxsync/zabbixmacroassignment.html
index 503d648..4972668 100644
--- a/nbxsync/templates/nbxsync/zabbixmacroassignment.html
+++ b/nbxsync/templates/nbxsync/zabbixmacroassignment.html
@@ -6,6 +6,7 @@
{% load static %}
{% load plugins %}
{% load render_table from django_tables2 %}
+{% load zabbix_macros %}
{% block content %}
@@ -43,8 +44,13 @@
{% plugin_left_page object %}
+ {% render_zabbix_macro_assignment object as rendered_output %}
+
+
+
{{ rendered_output|safe }}
+
{% plugin_right_page object %}
{% include 'inc/panels/tags.html' %}
-
{% endblock %}
diff --git a/nbxsync/templatetags/__init__.py b/nbxsync/templatetags/__init__.py
index 2d374cf..054b0a2 100644
--- a/nbxsync/templatetags/__init__.py
+++ b/nbxsync/templatetags/__init__.py
@@ -1,4 +1,5 @@
from .zabbix_tags import *
from .zabbix_hostgroups import *
from .zabbix_hostinterfaces import *
+from .zabbix_macros import *
from .render_field import *
diff --git a/nbxsync/templatetags/zabbix_macros.py b/nbxsync/templatetags/zabbix_macros.py
new file mode 100644
index 0000000..dedd154
--- /dev/null
+++ b/nbxsync/templatetags/zabbix_macros.py
@@ -0,0 +1,9 @@
+from django import template
+
+register = template.Library()
+
+
+@register.simple_tag
+def render_zabbix_macro_assignment(assignment, **context):
+ output, success = assignment.render(**context)
+ return output
diff --git a/nbxsync/tests/utils/test_hostsync.py b/nbxsync/tests/utils/test_hostsync.py
index 8863c23..5f2b193 100644
--- a/nbxsync/tests/utils/test_hostsync.py
+++ b/nbxsync/tests/utils/test_hostsync.py
@@ -239,6 +239,9 @@ def __init__(self):
self.zabbixmacro = DummyZabbixMacro(type_=1, description='Test Macro')
self.value = 'secret-value'
+ def render(self, object):
+ return 'secret-value', True
+
def __str__(self):
return '{$TEST_MACRO}'
@@ -265,6 +268,9 @@ def __init__(self):
self.zabbixmacro = DummyZabbixMacro(type_=1, description='From NetBox')
self.value = 'nb-value'
+ def render(self, object):
+ return 'nb-value', True
+
def __str__(self):
return '{$FROM_NETBOX}'
diff --git a/nbxsync/utils/inheritance.py b/nbxsync/utils/inheritance.py
index f679883..59b50b7 100644
--- a/nbxsync/utils/inheritance.py
+++ b/nbxsync/utils/inheritance.py
@@ -18,16 +18,18 @@ def get_zabbixassignments_for_request(instance, request):
assignments = get_assigned_zabbixobjects(instance)
content_type = ContentType.objects.get_for_model(instance)
- def table_or_none(data, table_cls):
+ def table_or_none(data, table_cls, attach_instance=False):
if data:
table = table_cls(data)
table.configure(request)
+ if attach_instance:
+ table.instance = instance
return table
return None
return {
'zabbix_template_table': table_or_none(assignments['templates'], ZabbixTemplateAssignmentObjectViewTable),
- 'zabbix_macro_table': table_or_none(assignments['macros'], ZabbixMacroAssignmentObjectViewTable),
+ 'zabbix_macro_table': table_or_none(assignments['macros'], ZabbixMacroAssignmentObjectViewTable, attach_instance=True),
'zabbix_tag_table': table_or_none(assignments['tags'], ZabbixTagAssignmentObjectViewTable),
'zabbix_hostgroup_table': table_or_none(assignments['hostgroups'], ZabbixHostgroupAssignmentObjectViewTable),
'object': instance,
diff --git a/nbxsync/utils/sync/hostsync.py b/nbxsync/utils/sync/hostsync.py
index 7e1fb9b..e92c462 100644
--- a/nbxsync/utils/sync/hostsync.py
+++ b/nbxsync/utils/sync/hostsync.py
@@ -96,12 +96,13 @@ def get_proxy_or_proxygroup(self):
def get_defined_macros(self):
result = []
for macro in self.context.get('all_objects', {}).get('macros'):
+ rendered_value, _ = macro.render(object=self.obj.assigned_object)
result.append(
{
'macro': str(macro),
'type': macro.zabbixmacro.type,
'description': macro.zabbixmacro.description,
- 'value': macro.value,
+ 'value': rendered_value,
}
)
From 8fab03f5cbae146fb5c5966b5079711c9ff1e52c Mon Sep 17 00:00:00 2001
From: bvbaekel
Date: Sun, 3 May 2026 21:06:57 +0200
Subject: [PATCH 11/11] Fix ruff formatting
---
nbxsync/forms/zabbixtagassignment.py | 1 -
1 file changed, 1 deletion(-)
diff --git a/nbxsync/forms/zabbixtagassignment.py b/nbxsync/forms/zabbixtagassignment.py
index 9f7897e..d4dc78b 100644
--- a/nbxsync/forms/zabbixtagassignment.py
+++ b/nbxsync/forms/zabbixtagassignment.py
@@ -111,7 +111,6 @@ def __init__(self, *args, **kwargs):
self.fields['zabbixtag'].queryset = ZabbixTag.objects.exclude(pk__in=excluded_ids)
self.fields['zabbixtag'].widget.add_query_params({'id__n': list(excluded_ids)})
-
def clean(self):
super().clean()