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.
- ipfabric_netbox/__init__.py +1 -1
- 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 +74 -40
- ipfabric_netbox/data/endpoint.json +52 -0
- ipfabric_netbox/data/filters.json +51 -0
- ipfabric_netbox/data/transform_map.json +190 -176
- ipfabric_netbox/exceptions.py +7 -5
- ipfabric_netbox/filtersets.py +310 -41
- ipfabric_netbox/forms.py +330 -80
- 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 +12 -1
- ipfabric_netbox/migrations/0022_prepare_for_filters.py +182 -0
- ipfabric_netbox/migrations/0023_populate_filters_data.py +303 -0
- ipfabric_netbox/migrations/0024_finish_filters.py +29 -0
- ipfabric_netbox/migrations/0025_add_vss_chassis_endpoint.py +166 -0
- ipfabric_netbox/models.py +432 -17
- 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 +68 -0
- ipfabric_netbox/tests/api/test_api.py +333 -13
- ipfabric_netbox/tests/test_filtersets.py +2592 -0
- ipfabric_netbox/tests/test_forms.py +1349 -74
- ipfabric_netbox/tests/test_models.py +242 -34
- ipfabric_netbox/tests/test_views.py +2031 -26
- ipfabric_netbox/urls.py +35 -0
- ipfabric_netbox/utilities/endpoint.py +83 -0
- ipfabric_netbox/utilities/filters.py +88 -0
- ipfabric_netbox/utilities/ipfutils.py +393 -377
- ipfabric_netbox/utilities/logging.py +7 -7
- ipfabric_netbox/utilities/transform_map.py +144 -5
- ipfabric_netbox/views.py +719 -5
- {ipfabric_netbox-4.3.2b9.dist-info → ipfabric_netbox-4.3.2b11.dist-info}/METADATA +2 -2
- {ipfabric_netbox-4.3.2b9.dist-info → ipfabric_netbox-4.3.2b11.dist-info}/RECORD +50 -33
- {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,
|
|
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(
|
|
@@ -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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|