ipfabric_netbox 4.3.2b8__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.
- ipfabric_netbox/__init__.py +2 -2
- ipfabric_netbox/api/serializers.py +112 -7
- ipfabric_netbox/api/urls.py +6 -0
- ipfabric_netbox/api/views.py +23 -0
- ipfabric_netbox/choices.py +72 -40
- ipfabric_netbox/data/endpoint.json +47 -0
- ipfabric_netbox/data/filters.json +51 -0
- ipfabric_netbox/data/transform_map.json +188 -174
- ipfabric_netbox/exceptions.py +7 -5
- ipfabric_netbox/filtersets.py +310 -41
- ipfabric_netbox/forms.py +324 -79
- ipfabric_netbox/graphql/__init__.py +6 -0
- ipfabric_netbox/graphql/enums.py +5 -5
- ipfabric_netbox/graphql/filters.py +56 -4
- ipfabric_netbox/graphql/schema.py +28 -0
- ipfabric_netbox/graphql/types.py +61 -1
- ipfabric_netbox/jobs.py +18 -1
- ipfabric_netbox/migrations/0022_prepare_for_filters.py +182 -0
- ipfabric_netbox/migrations/0023_populate_filters_data.py +279 -0
- ipfabric_netbox/migrations/0024_finish_filters.py +29 -0
- ipfabric_netbox/models.py +384 -12
- ipfabric_netbox/navigation.py +98 -24
- ipfabric_netbox/tables.py +194 -9
- ipfabric_netbox/templates/ipfabric_netbox/htmx_list.html +5 -0
- ipfabric_netbox/templates/ipfabric_netbox/inc/combined_expressions.html +59 -0
- ipfabric_netbox/templates/ipfabric_netbox/inc/combined_expressions_content.html +39 -0
- ipfabric_netbox/templates/ipfabric_netbox/inc/endpoint_filters_with_selector.html +54 -0
- ipfabric_netbox/templates/ipfabric_netbox/ipfabricendpoint.html +39 -0
- ipfabric_netbox/templates/ipfabric_netbox/ipfabricfilter.html +51 -0
- ipfabric_netbox/templates/ipfabric_netbox/ipfabricfilterexpression.html +39 -0
- ipfabric_netbox/templates/ipfabric_netbox/ipfabricfilterexpression_edit.html +150 -0
- ipfabric_netbox/templates/ipfabric_netbox/ipfabricsync.html +1 -1
- ipfabric_netbox/templates/ipfabric_netbox/ipfabrictransformmap.html +16 -2
- ipfabric_netbox/templatetags/ipfabric_netbox_helpers.py +65 -0
- ipfabric_netbox/tests/api/test_api.py +333 -13
- ipfabric_netbox/tests/test_filtersets.py +2592 -0
- ipfabric_netbox/tests/test_forms.py +1256 -74
- ipfabric_netbox/tests/test_models.py +242 -34
- ipfabric_netbox/tests/test_views.py +2030 -25
- ipfabric_netbox/urls.py +35 -0
- ipfabric_netbox/utilities/endpoint.py +30 -0
- ipfabric_netbox/utilities/filters.py +88 -0
- ipfabric_netbox/utilities/ipfutils.py +254 -316
- ipfabric_netbox/utilities/logging.py +7 -7
- ipfabric_netbox/utilities/transform_map.py +126 -0
- ipfabric_netbox/views.py +719 -5
- {ipfabric_netbox-4.3.2b8.dist-info → ipfabric_netbox-4.3.2b10.dist-info}/METADATA +3 -2
- {ipfabric_netbox-4.3.2b8.dist-info → ipfabric_netbox-4.3.2b10.dist-info}/RECORD +49 -33
- {ipfabric_netbox-4.3.2b8.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,
|
|
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(
|
|
72
|
-
stats = statistics[
|
|
71
|
+
if not statistics.get(model_string):
|
|
72
|
+
stats = statistics[model_string] = {"current": 0, "total": total}
|
|
73
73
|
else:
|
|
74
|
-
stats = statistics.get(
|
|
74
|
+
stats = statistics.get(model_string)
|
|
75
75
|
return stats
|
|
76
76
|
|
|
77
|
-
def increment_statistics(self,
|
|
78
|
-
stats = self.init_statistics(
|
|
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"{
|
|
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
|