Skip to content

fix(inheritance): recursive SiteGroup/Region/Role ancestor traversal#124

Open
berlikm wants to merge 10 commits into
OpensourceICTSolutions:developmentfrom
berlikm:fix/recursive-sitegroup-ancestry
Open

fix(inheritance): recursive SiteGroup/Region/Role ancestor traversal#124
berlikm wants to merge 10 commits into
OpensourceICTSolutions:developmentfrom
berlikm:fix/recursive-sitegroup-ancestry

Conversation

@berlikm

@berlikm berlikm commented Jul 14, 2026

Copy link
Copy Markdown

Summary

Replaces the fixed one-hop ('site', 'group', 'parent') inheritance path with recursive ancestor traversal via _walk_ancestors().

Problem

The resolve_path() function followed configured path tuples sequentially. For ('site', 'group', 'parent'), it resolved site.group.parent — exactly one level. For deep hierarchies (e.g. HU → HU-DEB → HU-DEB-NAG → site), a device at site HU-DEB-NAG-B resolved its grandparent SiteGroup HU-DEB, but never reached the root HU where the country-level assignment lives.

Fix

  • _walk_ancestors(obj, parent_attr='parent') — yields obj, then each ancestor via parent attr, until None. Cycle-safe via seen-set.
  • resolve_inherited_zabbix_assignments() — when the resolved object is a SiteGroup, Region, or DeviceRole, walks all ancestors instead of checking just one.
  • Nearest-ancestor-wins ordering preserved (nearest yielded first, seen_* sets prevent duplicates).

Dependencies

Stacks on PR #115 (modifies inheritance.py which PR #115 adds). Merge after #115.

Testing

9 new tests covering: 3-level SiteGroup hierarchy, nearest-ancestor-wins, Region parents, DeviceRole parents, cycle detection, None input.

Part of #121.

berlikm added 4 commits July 14, 2026 14:15
Adds Site, SiteGroup, and Region as valid assignment targets for all
ZabbixServerAssignment, ZabbixHostgroupAssignment, ZabbixTemplateAssignment,
ZabbixTagAssignment, ZabbixMacroAssignment, and ZabbixHostInventory objects.

This allows assignments made at the Site/SiteGroup/Region level to be
inherited by all devices and VMs at that site or below, via the existing
inheritance_chain mechanism.

Changes:
- Constants: Site/SiteGroup/Region added to ASSIGNMENT_MODELS, ASSIGNMENT_TYPE_TO_FIELD, PATH_LABELS
- Forms: All 7 assignment forms get Site/SiteGroup/Region DynamicModelChoiceField + FieldSet
- Settings: inheritance_chain extended with Site/SiteGroup/Region resolution paths
- Jobs: SyncHostJob._prepare_assignment() creates detached copies for inherited assignments
- HostSync: _get_sync_target() resolves actual Device/VM for Jinja2 rendering
- HostSync: get_tag_attributes() deduplicates tags by (tag, value) to prevent
  Zabbix 7 rejecting duplicate pairs inherited from multiple sources
- HostInterfaceSync: get_create_params() falls back to the device's primary IP
  when the interface has no IP assigned (e.g. when inherited from SiteGroup)
- HostInterfaceSync: errors are caught per-interface so one failing interface
  does not prevent the remaining interfaces and templates from being synced
- Inheritance: ConfigGroup interface expansion removed; interfaces are now
  resolved naturally via the inheritance chain with primary IP fallback
- SyncBase: _is_inherited_copy guard prevents save()/update_sync_info() on inherited copies
- Views: ZabbixSiteTabView, ZabbixSiteGroupTabView, ZabbixRegionTabView added
- Tests: 9 new tests for inheritance resolution

Tested: all 1076 existing tests pass. Verified end-to-end on dev NetBox.
- Filter inherited/direct ZabbixServerAssignment and ZabbixHostInterface by zabbixserver
- Resolve ConfigGroup interface IP from primary_ip4/primary_ip6
- Add seen-set cycle guard to resolve_path
- Fixes silent dropping of Site/SiteGroup assignments
Fixes inherited Site/SiteGroup/Region assignments reading custom fields from
the intermediate object instead of the actual Device/VM.
@berlikm
berlikm force-pushed the fix/recursive-sitegroup-ancestry branch 3 times, most recently from e46be7a to fcef724 Compare July 14, 2026 22:44
berlikm added 4 commits July 14, 2026 23:48
- Add _walk_ancestors() helper that yields an object and each parent
  until None, cycle-safe.
- In resolve_inherited_zabbix_assignments(), when a path ends with
  'parent', check the resolved object plus all ancestors; nearest
  ancestor wins via existing seen_* sets.
- Add tests for 3-level SiteGroup hierarchy.
Rewrites the resolver into collect/batch/distribute phases: walk all
inheritance-chain paths once gathering (content_type, pk, label) triples,
issue one query per assignment model (7) plus one for hostinventory across
all triples, then distribute results into the resolved OrderedDicts in
ancestor (leaf-first) order so first-seen-wins dedup is preserved.

Query count drops from 7×N to a constant (~8) regardless of ancestor depth.

Adds two contract tests: query-count-stays-bounded-as-depth-grows, and
leaf-assignment-wins-over-root (first-seen-wins ordering).
@berlikm
berlikm force-pushed the fix/recursive-sitegroup-ancestry branch from 60c1d5e to 719f21d Compare July 16, 2026 07:34
@berlikm

berlikm commented Jul 16, 2026

Copy link
Copy Markdown
Author

Conflict resolution note for rebase

When rebasing this PR after #117 lands (which adds ZabbixTemplateRule to the imports), there will be a conflict in nbxsync/utils/inheritance.py on the import block:

Correct resolution: Combine both import sets:

from django.db.models import Q, QuerySet
from django.db.models.manager import BaseManager
from dcim.models import DeviceRole, Region, SiteGroup
from nbxsync.constants import PATH_LABELS
from nbxsync.models import ZabbixConfigurationGroupAssignment, ..., ZabbixTemplateAssignment, ZabbixTemplateRule

Note: PR #112 (SiteGroup parent traversal) is closed in favor of this PR — _walk_ancestors fully replaces the one-hop ('site', 'group', 'parent') path.

Verified in the integration-test branch: 1135 tests pass, E2E confirmed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant