Skip to content

Commit c0b7710

Browse files
authored
fix: add additional 4.3 logical matchers (#117)
* fix: add versioned NetBox 4.3 logical matcher for service model * fix: adds additional logical matchers for types with constraint changes in 4.3
1 parent 2c3c80a commit c0b7710

File tree

2 files changed

+72
-14
lines changed

2 files changed

+72
-14
lines changed

netbox_diode_plugin/api/compat.py

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -28,19 +28,11 @@ def apply_entity_migrations(data: dict, object_type: str):
2828

2929
def _register_migration(func, min_version, max_version, object_type):
3030
"""Registers a migration function."""
31-
min_version = version.parse(min_version)
32-
max_version = version.parse(max_version) if max_version else None
33-
current_version = _current_netbox_version()
34-
35-
if current_version < min_version:
36-
logger.debug(f"Skipping migration {func.__name__} for {object_type}: min version {min_version}")
37-
return
38-
if max_version and current_version > max_version:
39-
logger.debug(f"Skipping migration {func.__name__} for {object_type}: max version {max_version}")
40-
return
41-
42-
logger.debug(f"Registering migration {func.__name__} for {object_type}.")
43-
_MIGRATIONS_BY_OBJECT_TYPE[object_type].append(func)
31+
if in_version_range(min_version, max_version):
32+
logger.debug(f"Registering migration {func.__name__} for {object_type}.")
33+
_MIGRATIONS_BY_OBJECT_TYPE[object_type].append(func)
34+
else:
35+
logger.debug(f"Skipping migration {func.__name__} for {object_type}: {min_version} to {max_version}.")
4436

4537
@cache
4638
def _current_netbox_version():
@@ -51,6 +43,17 @@ def _current_netbox_version():
5143
logger.exception("Failed to determine current version of NetBox.")
5244
return (0, 0, 0)
5345

46+
def in_version_range(min_version: str | None, max_version: str | None):
47+
"""Returns True if the current version of NetBox is within the given version range."""
48+
min_version = version.parse(min_version) if min_version else None
49+
max_version = version.parse(max_version) if max_version else None
50+
current_version = _current_netbox_version()
51+
if min_version and current_version < min_version:
52+
return False
53+
if max_version and current_version > max_version:
54+
return False
55+
return True
56+
5457
def diode_migration(min_version: str, max_version: str | None, object_type: str):
5558
"""Decorator to mark a function as a diode migration."""
5659
def decorator(func):

netbox_diode_plugin/api/matcher.py

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from extras.models.customfields import CustomField
1818

1919
from .common import UnresolvedReference
20+
from .compat import in_version_range
2021
from .plugin_utils import content_type_id, get_object_type, get_object_type_model
2122

2223
logger = logging.getLogger(__name__)
@@ -162,18 +163,28 @@
162163
name="logical_service_name_no_device_or_vm",
163164
model_class=get_object_type_model("ipam.service"),
164165
condition=Q(device__isnull=True, virtual_machine__isnull=True),
166+
max_version="4.2.99",
165167
),
166168
ObjectMatchCriteria(
167169
fields=("name", "device"),
168170
name="logical_service_name_on_device",
169171
model_class=get_object_type_model("ipam.service"),
170172
condition=Q(device__isnull=False),
173+
max_version="4.2.99",
171174
),
172175
ObjectMatchCriteria(
173176
fields=("name", "virtual_machine"),
174177
name="logical_service_name_on_vm",
175178
model_class=get_object_type_model("ipam.service"),
176179
condition=Q(virtual_machine__isnull=False),
180+
max_version="4.2.99",
181+
),
182+
ObjectMatchCriteria(
183+
fields=("name", "parent_object_type", "parent_object_id"),
184+
name="logical_service_name_on_parent",
185+
model_class=get_object_type_model("ipam.service"),
186+
condition=Q(parent_object_type__isnull=False),
187+
min_version="4.3.0"
177188
),
178189
],
179190
"dcim.modulebay": lambda: [
@@ -201,6 +212,32 @@
201212
model_class=get_object_type_model("ipam.fhrpgroup"),
202213
)
203214
],
215+
"tenancy.contact": lambda: [
216+
ObjectMatchCriteria(
217+
# contacts are unconstrained in 4.3.0
218+
# in 4.2 they are constrained by unique name per group
219+
fields=("name", ),
220+
name="logical_contact_name",
221+
model_class=get_object_type_model("tenancy.contact"),
222+
min_version="4.3.0",
223+
)
224+
],
225+
"dcim.devicerole": lambda: [
226+
ObjectMatchCriteria(
227+
fields=("name",),
228+
name="logical_device_role_name_no_parent",
229+
model_class=get_object_type_model("dcim.devicerole"),
230+
condition=Q(parent__isnull=True),
231+
min_version="4.3.0",
232+
),
233+
ObjectMatchCriteria(
234+
fields=("slug",),
235+
name="logical_device_role_slug_no_parent",
236+
model_class=get_object_type_model("dcim.devicerole"),
237+
condition=Q(parent__isnull=True),
238+
min_version="4.3.0",
239+
)
240+
],
204241
}
205242

206243
@dataclass
@@ -223,6 +260,9 @@ class ObjectMatchCriteria:
223260
model_class: type[models.Model] | None = None
224261
name: str | None = None
225262

263+
min_version: str | None = None
264+
max_version: str | None = None
265+
226266
def __hash__(self):
227267
"""Hash the object match criteria."""
228268
return hash((self.fields, self.expressions, self.condition, self.model_class.__name__, self.name))
@@ -414,6 +454,9 @@ class CustomFieldMatcher:
414454
custom_field: str
415455
model_class: type[models.Model]
416456

457+
min_version: str | None = None
458+
max_version: str | None = None
459+
417460
def fingerprint(self, data: dict) -> str|None:
418461
"""Fingerprint the custom field value."""
419462
if not self.has_required_fields(data):
@@ -450,6 +493,9 @@ class GlobalIPNetworkIPMatcher:
450493
model_class: type[models.Model]
451494
name: str
452495

496+
min_version: str | None = None
497+
max_version: str | None = None
498+
453499
def _check_condition(self, data: dict) -> bool:
454500
"""Check the condition for the custom field."""
455501
return data.get(self.vrf_field, None) is None
@@ -510,6 +556,9 @@ class VRFIPNetworkIPMatcher:
510556
model_class: type[models.Model]
511557
name: str
512558

559+
min_version: str | None = None
560+
max_version: str | None = None
561+
513562
def _check_condition(self, data: dict) -> bool:
514563
"""Check the condition for the custom field."""
515564
return data.get(self.vrf_field, None) is not None
@@ -584,6 +633,9 @@ class AutoSlugMatcher:
584633
slug_field: str
585634
model_class: type[models.Model]
586635

636+
min_version: str | None = None
637+
max_version: str | None = None
638+
587639
def fingerprint(self, data: dict) -> str|None:
588640
"""Fingerprint the custom field value."""
589641
if not self.has_required_fields(data):
@@ -649,7 +701,10 @@ def _get_autoslug_matchers(model_class) -> list:
649701
@lru_cache(maxsize=256)
650702
def _get_model_matchers(model_class) -> list[ObjectMatchCriteria]:
651703
object_type = get_object_type(model_class)
652-
matchers = _LOGICAL_MATCHERS.get(object_type, lambda: [])()
704+
matchers = [
705+
x for x in _LOGICAL_MATCHERS.get(object_type, lambda: [])()
706+
if in_version_range(x.min_version, x.max_version)
707+
]
653708

654709
# collect single fields that are unique
655710
for field in model_class._meta.fields:

0 commit comments

Comments
 (0)