ipfabric_netbox 4.3.2b9__py3-none-any.whl → 4.3.2b10__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 (49) hide show
  1. ipfabric_netbox/__init__.py +2 -2
  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 +72 -40
  6. ipfabric_netbox/data/endpoint.json +47 -0
  7. ipfabric_netbox/data/filters.json +51 -0
  8. ipfabric_netbox/data/transform_map.json +188 -174
  9. ipfabric_netbox/exceptions.py +7 -5
  10. ipfabric_netbox/filtersets.py +310 -41
  11. ipfabric_netbox/forms.py +324 -79
  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 +5 -1
  18. ipfabric_netbox/migrations/0022_prepare_for_filters.py +182 -0
  19. ipfabric_netbox/migrations/0023_populate_filters_data.py +279 -0
  20. ipfabric_netbox/migrations/0024_finish_filters.py +29 -0
  21. ipfabric_netbox/models.py +384 -12
  22. ipfabric_netbox/navigation.py +98 -24
  23. ipfabric_netbox/tables.py +194 -9
  24. ipfabric_netbox/templates/ipfabric_netbox/htmx_list.html +5 -0
  25. ipfabric_netbox/templates/ipfabric_netbox/inc/combined_expressions.html +59 -0
  26. ipfabric_netbox/templates/ipfabric_netbox/inc/combined_expressions_content.html +39 -0
  27. ipfabric_netbox/templates/ipfabric_netbox/inc/endpoint_filters_with_selector.html +54 -0
  28. ipfabric_netbox/templates/ipfabric_netbox/ipfabricendpoint.html +39 -0
  29. ipfabric_netbox/templates/ipfabric_netbox/ipfabricfilter.html +51 -0
  30. ipfabric_netbox/templates/ipfabric_netbox/ipfabricfilterexpression.html +39 -0
  31. ipfabric_netbox/templates/ipfabric_netbox/ipfabricfilterexpression_edit.html +150 -0
  32. ipfabric_netbox/templates/ipfabric_netbox/ipfabricsync.html +1 -1
  33. ipfabric_netbox/templates/ipfabric_netbox/ipfabrictransformmap.html +16 -2
  34. ipfabric_netbox/templatetags/ipfabric_netbox_helpers.py +65 -0
  35. ipfabric_netbox/tests/api/test_api.py +333 -13
  36. ipfabric_netbox/tests/test_filtersets.py +2592 -0
  37. ipfabric_netbox/tests/test_forms.py +1256 -74
  38. ipfabric_netbox/tests/test_models.py +242 -34
  39. ipfabric_netbox/tests/test_views.py +2030 -25
  40. ipfabric_netbox/urls.py +35 -0
  41. ipfabric_netbox/utilities/endpoint.py +30 -0
  42. ipfabric_netbox/utilities/filters.py +88 -0
  43. ipfabric_netbox/utilities/ipfutils.py +254 -316
  44. ipfabric_netbox/utilities/logging.py +7 -7
  45. ipfabric_netbox/utilities/transform_map.py +126 -0
  46. ipfabric_netbox/views.py +719 -5
  47. {ipfabric_netbox-4.3.2b9.dist-info → ipfabric_netbox-4.3.2b10.dist-info}/METADATA +3 -2
  48. {ipfabric_netbox-4.3.2b9.dist-info → ipfabric_netbox-4.3.2b10.dist-info}/RECORD +49 -33
  49. {ipfabric_netbox-4.3.2b9.dist-info → ipfabric_netbox-4.3.2b10.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(
@@ -231,3 +304,56 @@ def do_change(
231
304
 
232
305
 
233
306
  # endregion
307
+
308
+ # region Cycle Detection
309
+
310
+
311
+ def has_cycle_dfs(
312
+ node_id: int,
313
+ get_parents_func: Callable[[int, Any], Any],
314
+ parent_override: Any = None,
315
+ visited: set[int] | None = None,
316
+ rec_stack: set[int] | None = None,
317
+ ) -> bool:
318
+ """
319
+ DFS helper to detect cycles in transform map parent relationships.
320
+
321
+ Args:
322
+ node_id: The ID of the current node being checked
323
+ get_parents_func: Function that takes (node_id, parent_override) and returns parent objects
324
+ parent_override: Optional override for parents of a specific node (used for validation)
325
+ visited: Set of already visited node IDs (created automatically if not provided)
326
+ rec_stack: Set of node IDs in the current recursion stack (created automatically if not provided)
327
+
328
+ Returns:
329
+ True if a cycle is detected, False otherwise
330
+ """
331
+ if visited is None:
332
+ visited = set()
333
+ if rec_stack is None:
334
+ rec_stack = set()
335
+
336
+ visited.add(node_id)
337
+ rec_stack.add(node_id)
338
+
339
+ try:
340
+ parents = get_parents_func(node_id, parent_override)
341
+
342
+ for parent in parents:
343
+ parent_pk = parent.pk if hasattr(parent, "pk") else parent.id
344
+ if parent_pk not in visited:
345
+ if has_cycle_dfs(
346
+ parent_pk, get_parents_func, visited=visited, rec_stack=rec_stack
347
+ ):
348
+ return True
349
+ elif parent_pk in rec_stack:
350
+ # Found a back edge - cycle detected
351
+ return True
352
+ except Exception as err:
353
+ logger.warning(f"Error applying Transform map updates: {err}")
354
+
355
+ rec_stack.remove(node_id)
356
+ return False
357
+
358
+
359
+ # endregion