ipfabric_netbox 4.3.2b9__py3-none-any.whl → 4.3.2b11__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of ipfabric_netbox might be problematic. Click here for more details.

Files changed (50) hide show
  1. ipfabric_netbox/__init__.py +1 -1
  2. ipfabric_netbox/api/serializers.py +112 -7
  3. ipfabric_netbox/api/urls.py +6 -0
  4. ipfabric_netbox/api/views.py +23 -0
  5. ipfabric_netbox/choices.py +74 -40
  6. ipfabric_netbox/data/endpoint.json +52 -0
  7. ipfabric_netbox/data/filters.json +51 -0
  8. ipfabric_netbox/data/transform_map.json +190 -176
  9. ipfabric_netbox/exceptions.py +7 -5
  10. ipfabric_netbox/filtersets.py +310 -41
  11. ipfabric_netbox/forms.py +330 -80
  12. ipfabric_netbox/graphql/__init__.py +6 -0
  13. ipfabric_netbox/graphql/enums.py +5 -5
  14. ipfabric_netbox/graphql/filters.py +56 -4
  15. ipfabric_netbox/graphql/schema.py +28 -0
  16. ipfabric_netbox/graphql/types.py +61 -1
  17. ipfabric_netbox/jobs.py +12 -1
  18. ipfabric_netbox/migrations/0022_prepare_for_filters.py +182 -0
  19. ipfabric_netbox/migrations/0023_populate_filters_data.py +303 -0
  20. ipfabric_netbox/migrations/0024_finish_filters.py +29 -0
  21. ipfabric_netbox/migrations/0025_add_vss_chassis_endpoint.py +166 -0
  22. ipfabric_netbox/models.py +432 -17
  23. ipfabric_netbox/navigation.py +98 -24
  24. ipfabric_netbox/tables.py +194 -9
  25. ipfabric_netbox/templates/ipfabric_netbox/htmx_list.html +5 -0
  26. ipfabric_netbox/templates/ipfabric_netbox/inc/combined_expressions.html +59 -0
  27. ipfabric_netbox/templates/ipfabric_netbox/inc/combined_expressions_content.html +39 -0
  28. ipfabric_netbox/templates/ipfabric_netbox/inc/endpoint_filters_with_selector.html +54 -0
  29. ipfabric_netbox/templates/ipfabric_netbox/ipfabricendpoint.html +39 -0
  30. ipfabric_netbox/templates/ipfabric_netbox/ipfabricfilter.html +51 -0
  31. ipfabric_netbox/templates/ipfabric_netbox/ipfabricfilterexpression.html +39 -0
  32. ipfabric_netbox/templates/ipfabric_netbox/ipfabricfilterexpression_edit.html +150 -0
  33. ipfabric_netbox/templates/ipfabric_netbox/ipfabricsync.html +1 -1
  34. ipfabric_netbox/templates/ipfabric_netbox/ipfabrictransformmap.html +16 -2
  35. ipfabric_netbox/templatetags/ipfabric_netbox_helpers.py +68 -0
  36. ipfabric_netbox/tests/api/test_api.py +333 -13
  37. ipfabric_netbox/tests/test_filtersets.py +2592 -0
  38. ipfabric_netbox/tests/test_forms.py +1349 -74
  39. ipfabric_netbox/tests/test_models.py +242 -34
  40. ipfabric_netbox/tests/test_views.py +2031 -26
  41. ipfabric_netbox/urls.py +35 -0
  42. ipfabric_netbox/utilities/endpoint.py +83 -0
  43. ipfabric_netbox/utilities/filters.py +88 -0
  44. ipfabric_netbox/utilities/ipfutils.py +393 -377
  45. ipfabric_netbox/utilities/logging.py +7 -7
  46. ipfabric_netbox/utilities/transform_map.py +144 -5
  47. ipfabric_netbox/views.py +719 -5
  48. {ipfabric_netbox-4.3.2b9.dist-info → ipfabric_netbox-4.3.2b11.dist-info}/METADATA +2 -2
  49. {ipfabric_netbox-4.3.2b9.dist-info → ipfabric_netbox-4.3.2b11.dist-info}/RECORD +50 -33
  50. {ipfabric_netbox-4.3.2b9.dist-info → ipfabric_netbox-4.3.2b11.dist-info}/WHEEL +1 -1
@@ -66,22 +66,22 @@ class SyncLogging:
66
66
  self._log(obj, message, level=LogLevelChoices.LOG_FAILURE)
67
67
  self.logger.info(f"Failure | {obj}: {message}")
68
68
 
69
- def init_statistics(self, model: str, total: int) -> dict[str, int]:
69
+ def init_statistics(self, model_string: str, total: int) -> dict[str, int]:
70
70
  statistics = self.log_data.get("statistics")
71
- if not statistics.get(model):
72
- stats = statistics[model] = {"current": 0, "total": total}
71
+ if not statistics.get(model_string):
72
+ stats = statistics[model_string] = {"current": 0, "total": total}
73
73
  else:
74
- stats = statistics.get(model)
74
+ stats = statistics.get(model_string)
75
75
  return stats
76
76
 
77
- def increment_statistics(self, model: str, total: int = None) -> None:
78
- stats = self.init_statistics(model, total)
77
+ def increment_statistics(self, model_string: str, total: int = None) -> None:
78
+ stats = self.init_statistics(model_string, total)
79
79
  if total:
80
80
  stats["total"] = total
81
81
  stats["current"] += 1
82
82
  cache.set(self.cache_key, self.log_data, self.cache_timeout)
83
83
  self.logger.info(
84
- f"{model} - {stats['current']} out of {stats['total']} processed"
84
+ f"{model_string} - {stats['current']} out of {stats['total']} processed"
85
85
  )
86
86
 
87
87
  def clear_log(self):
@@ -1,8 +1,14 @@
1
1
  import importlib.resources
2
2
  import json
3
+ import logging
4
+ from typing import Any
5
+ from typing import Callable
3
6
 
4
7
  from django.apps import apps as django_apps
5
8
 
9
+
10
+ logger = logging.getLogger("ipfabric_netbox.utilities.transform_map")
11
+
6
12
  # region Transform Map Creation
7
13
 
8
14
  # These functions are used in the migration file to prepare the transform maps
@@ -33,14 +39,81 @@ def build_fields(data, apps, db_alias):
33
39
 
34
40
  def build_transform_maps(data, apps: django_apps = None, db_alias: str = "default"):
35
41
  apps = apps or django_apps
42
+ ContentType = apps.get_model("contenttypes", "ContentType")
36
43
  IPFabricTransformMap = apps.get_model("ipfabric_netbox", "IPFabricTransformMap")
37
44
  IPFabricTransformField = apps.get_model("ipfabric_netbox", "IPFabricTransformField")
38
45
  IPFabricRelationshipField = apps.get_model(
39
46
  "ipfabric_netbox", "IPFabricRelationshipField"
40
47
  )
48
+
49
+ # Mapping for backward compatibility (endpoint -> source_model)
50
+ # Used only when running against old model that still has source_model field
51
+ # TODO: Remove once migrations are squashed and old versions are no longer supported
52
+ endpoint_to_source_model = {
53
+ "/technology/addressing/managed-ip/ipv4": "ipaddress",
54
+ "/inventory/devices": "device",
55
+ "/inventory/sites/overview": "site",
56
+ "/inventory/interfaces": "interface",
57
+ "/inventory/part-numbers": "part_number",
58
+ "/technology/vlans/site-summary": "vlan",
59
+ "/technology/routing/vrf/detail": "vrf",
60
+ "/technology/networks/managed-networks": "prefix",
61
+ "/technology/platforms/stack1/members": "virtualchassis",
62
+ }
63
+
64
+ model_fields = {f.name for f in IPFabricTransformMap._meta.get_fields()}
41
65
  for tm in data:
42
66
  field_data = build_fields(tm["data"], apps, db_alias)
67
+
68
+ endpoint_value = field_data.pop("source_endpoint")
69
+
70
+ if "source_endpoint" in model_fields:
71
+ # New model: use source_endpoint foreign key to IPFabricEndpoint
72
+ # This models does not exist when TMs are populated, so need to get it here
73
+ IPFabricEndpoint = apps.get_model("ipfabric_netbox", "IPFabricEndpoint")
74
+ try:
75
+ endpoint = IPFabricEndpoint.objects.using(db_alias).get(
76
+ endpoint=endpoint_value
77
+ )
78
+ field_data["source_endpoint"] = endpoint
79
+ except IPFabricEndpoint.DoesNotExist:
80
+ # Use first endpoint as fallback
81
+ endpoint = IPFabricEndpoint.objects.using(db_alias).first()
82
+ field_data["source_endpoint"] = endpoint
83
+ field_data[
84
+ "name"
85
+ ] = f"[NEEDS CORRECTION - Expected endpoint '{endpoint_value}' not found] {field_data['name']}"
86
+
87
+ else:
88
+ # Old model: convert source_endpoint value to source_model string
89
+ source_model_value = endpoint_to_source_model.get(endpoint_value, "device")
90
+ field_data["source_model"] = source_model_value
91
+
92
+ # This field was not present in the old model, so remove it if exists
93
+ tm_parents = []
94
+ if parents := field_data.pop("parents", None):
95
+ if isinstance(parents, str):
96
+ parents = [parents]
97
+ for parent in parents:
98
+ if "parents" not in model_fields:
99
+ continue
100
+ # New model: set parents MTM field to IPFabricTransformMap
101
+ app, model = parent.split(".")
102
+ try:
103
+ parent_tm = IPFabricTransformMap.objects.using(db_alias).get(
104
+ target_model=ContentType.objects.using(db_alias).get(
105
+ app_label=app, model=model
106
+ ),
107
+ group__isnull=True,
108
+ )
109
+ tm_parents.append(parent_tm)
110
+ except IPFabricTransformMap.DoesNotExist:
111
+ raise ValueError(f"Parent Transform Map '{parent}' not found")
112
+
43
113
  tm_obj = IPFabricTransformMap.objects.using(db_alias).create(**field_data)
114
+ # Old migrations may not have parents field
115
+ if hasattr(tm_obj, "parents"):
116
+ tm_obj.parents.set(tm_parents)
44
117
  for fm in tm["field_maps"]:
45
118
  field_data = build_fields(fm, apps, db_alias)
46
119
  IPFabricTransformField.objects.using(db_alias).create(
@@ -108,12 +181,15 @@ class RelationshipRecord(Record):
108
181
  class TransformMapRecord:
109
182
  def __init__(
110
183
  self,
111
- source_model: str,
112
184
  target_model: str,
113
185
  fields: tuple[FieldRecord, ...] = tuple(),
114
186
  relationships: tuple[RelationshipRecord, ...] = tuple(),
187
+ # Support both source_model string and source_endpoint string for backward compatibility
188
+ source_model: str | None = None,
189
+ source_endpoint: str | None = None,
115
190
  ):
116
191
  self.source_model = source_model
192
+ self.source_endpoint = source_endpoint
117
193
  self.target_model = target_model
118
194
  self.fields = fields
119
195
  self.relationships = relationships
@@ -135,10 +211,20 @@ def do_change(
135
211
  for change in changes:
136
212
  app, model = change.target_model.split(".")
137
213
  try:
138
- transform_map = IPFabricTransformMap.objects.get(
139
- source_model=change.source_model,
140
- target_model=ContentType.objects.get(app_label=app, model=model),
141
- )
214
+ if change.source_model:
215
+ transform_map = IPFabricTransformMap.objects.get(
216
+ source_model=change.source_model,
217
+ target_model=ContentType.objects.get(
218
+ app_label=app, model=model
219
+ ),
220
+ )
221
+ else:
222
+ transform_map = IPFabricTransformMap.objects.get(
223
+ source_endpoint__endpoint=change.source_endpoint,
224
+ target_model=ContentType.objects.get(
225
+ app_label=app, model=model
226
+ ),
227
+ )
142
228
  except IPFabricTransformMap.DoesNotExist:
143
229
  continue
144
230
 
@@ -231,3 +317,56 @@ def do_change(
231
317
 
232
318
 
233
319
  # endregion
320
+
321
+ # region Cycle Detection
322
+
323
+
324
+ def has_cycle_dfs(
325
+ node_id: int,
326
+ get_parents_func: Callable[[int, Any], Any],
327
+ parent_override: Any = None,
328
+ visited: set[int] | None = None,
329
+ rec_stack: set[int] | None = None,
330
+ ) -> bool:
331
+ """
332
+ DFS helper to detect cycles in transform map parent relationships.
333
+
334
+ Args:
335
+ node_id: The ID of the current node being checked
336
+ get_parents_func: Function that takes (node_id, parent_override) and returns parent objects
337
+ parent_override: Optional override for parents of a specific node (used for validation)
338
+ visited: Set of already visited node IDs (created automatically if not provided)
339
+ rec_stack: Set of node IDs in the current recursion stack (created automatically if not provided)
340
+
341
+ Returns:
342
+ True if a cycle is detected, False otherwise
343
+ """
344
+ if visited is None:
345
+ visited = set()
346
+ if rec_stack is None:
347
+ rec_stack = set()
348
+
349
+ visited.add(node_id)
350
+ rec_stack.add(node_id)
351
+
352
+ try:
353
+ parents = get_parents_func(node_id, parent_override)
354
+
355
+ for parent in parents:
356
+ parent_pk = parent.pk if hasattr(parent, "pk") else parent.id
357
+ if parent_pk not in visited:
358
+ if has_cycle_dfs(
359
+ parent_pk, get_parents_func, visited=visited, rec_stack=rec_stack
360
+ ):
361
+ return True
362
+ elif parent_pk in rec_stack:
363
+ # Found a back edge - cycle detected
364
+ return True
365
+ except Exception as err:
366
+ logger.warning(f"Error applying Transform map updates: {err}")
367
+
368
+ rec_stack.remove(node_id)
369
+ return False
370
+
371
+
372
+ # endregion