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.
- 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 +5 -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.2b9.dist-info → ipfabric_netbox-4.3.2b10.dist-info}/METADATA +3 -2
- {ipfabric_netbox-4.3.2b9.dist-info → ipfabric_netbox-4.3.2b10.dist-info}/RECORD +49 -33
- {ipfabric_netbox-4.3.2b9.dist-info → ipfabric_netbox-4.3.2b10.dist-info}/WHEEL +1 -1
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING
|
|
2
|
+
|
|
3
|
+
from django.db import migrations
|
|
4
|
+
|
|
5
|
+
from ipfabric_netbox.utilities.endpoint import build_endpoints
|
|
6
|
+
from ipfabric_netbox.utilities.endpoint import get_endpoint_data
|
|
7
|
+
from ipfabric_netbox.utilities.filters import build_filters
|
|
8
|
+
from ipfabric_netbox.utilities.filters import get_filter_data
|
|
9
|
+
from ipfabric_netbox.utilities.transform_map import get_transform_map
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from django.apps import apps as apps_type
|
|
13
|
+
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def prepare_endpoints(apps: "apps_type", schema_editor: "BaseDatabaseSchemaEditor"):
|
|
17
|
+
"""Create endpoints if they do not exist yet."""
|
|
18
|
+
build_endpoints(
|
|
19
|
+
data=get_endpoint_data(), apps=apps, db_alias=schema_editor.connection.alias
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def prepare_filters(apps: "apps_type", schema_editor: "BaseDatabaseSchemaEditor"):
|
|
24
|
+
"""Create filters if they do not exist yet."""
|
|
25
|
+
build_filters(
|
|
26
|
+
data=get_filter_data(), apps=apps, db_alias=schema_editor.connection.alias
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def assign_filters_to_syncs(
|
|
31
|
+
apps: "apps_type", schema_editor: "BaseDatabaseSchemaEditor"
|
|
32
|
+
):
|
|
33
|
+
"""Assign all created IPFabricFilters to all existing IPFabricSync objects."""
|
|
34
|
+
IPFabricSync = apps.get_model("ipfabric_netbox", "IPFabricSync")
|
|
35
|
+
IPFabricFilter = apps.get_model("ipfabric_netbox", "IPFabricFilter")
|
|
36
|
+
|
|
37
|
+
all_filters = IPFabricFilter.objects.using(schema_editor.connection.alias).all()
|
|
38
|
+
for sync in IPFabricSync.objects.using(schema_editor.connection.alias).all():
|
|
39
|
+
sync.filters.set(all_filters)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def migrate_source_model_to_endpoint(
|
|
43
|
+
apps: "apps_type", schema_editor: "BaseDatabaseSchemaEditor"
|
|
44
|
+
):
|
|
45
|
+
"""Migrate IPFabricTransformMap source_model data to source_endpoint."""
|
|
46
|
+
IPFabricTransformMap = apps.get_model("ipfabric_netbox", "IPFabricTransformMap")
|
|
47
|
+
IPFabricEndpoint = apps.get_model("ipfabric_netbox", "IPFabricEndpoint")
|
|
48
|
+
|
|
49
|
+
source_model_to_endpoint = {
|
|
50
|
+
"site": "/inventory/sites/overview",
|
|
51
|
+
"device": "/inventory/devices",
|
|
52
|
+
"virtualchassis": "/technology/platforms/stack/members",
|
|
53
|
+
"interface": "/inventory/interfaces",
|
|
54
|
+
"part_number": "/inventory/part-numbers",
|
|
55
|
+
"vlan": "/technology/vlans/site-summary",
|
|
56
|
+
"vrf": "/technology/routing/vrf/detail",
|
|
57
|
+
"prefix": "/technology/networks/managed-networks",
|
|
58
|
+
"ipaddress": "/technology/addressing/managed-ip/ipv4",
|
|
59
|
+
"inventory": "/inventory/part-numbers",
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
# Get first endpoint as fallback to make sure it's always set
|
|
63
|
+
fallback_endpoint = IPFabricEndpoint.objects.using(
|
|
64
|
+
schema_editor.connection.alias
|
|
65
|
+
).first()
|
|
66
|
+
|
|
67
|
+
for transform_map in IPFabricTransformMap.objects.using(
|
|
68
|
+
schema_editor.connection.alias
|
|
69
|
+
).all():
|
|
70
|
+
endpoint_value = source_model_to_endpoint.get(transform_map.source_model)
|
|
71
|
+
|
|
72
|
+
# If no mapping exists, use fallback and mark the name for manual fix
|
|
73
|
+
if not endpoint_value:
|
|
74
|
+
if not transform_map.name.startswith("[NEEDS CORRECTION]"):
|
|
75
|
+
transform_map.name = f"[NEEDS CORRECTION - Unknown source_model: {transform_map.source_model}] {transform_map.name}"
|
|
76
|
+
transform_map.source_endpoint = fallback_endpoint
|
|
77
|
+
transform_map.save()
|
|
78
|
+
continue
|
|
79
|
+
|
|
80
|
+
# Try to get the mapped endpoint
|
|
81
|
+
try:
|
|
82
|
+
endpoint = IPFabricEndpoint.objects.using(
|
|
83
|
+
schema_editor.connection.alias
|
|
84
|
+
).get(endpoint=endpoint_value)
|
|
85
|
+
transform_map.source_endpoint = endpoint
|
|
86
|
+
except IPFabricEndpoint.DoesNotExist:
|
|
87
|
+
# Use fallback endpoint and mark the name
|
|
88
|
+
if not transform_map.name.startswith("[NEEDS CORRECTION]"):
|
|
89
|
+
transform_map.name = f"[NEEDS CORRECTION - Expected endpoint '{endpoint_value}' not found] {transform_map.name}"
|
|
90
|
+
transform_map.source_endpoint = fallback_endpoint
|
|
91
|
+
transform_map.save()
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def migrate_endpoint_to_source_model(
|
|
95
|
+
apps: "apps_type", schema_editor: "BaseDatabaseSchemaEditor"
|
|
96
|
+
):
|
|
97
|
+
"""Reverse migration: migrate IPFabricTransformMap source_endpoint data back to source_model."""
|
|
98
|
+
IPFabricTransformMap = apps.get_model("ipfabric_netbox", "IPFabricTransformMap")
|
|
99
|
+
IPFabricEndpoint = apps.get_model("ipfabric_netbox", "IPFabricEndpoint")
|
|
100
|
+
|
|
101
|
+
endpoint_to_source_model = {
|
|
102
|
+
"/inventory/sites/overview": "site",
|
|
103
|
+
"/inventory/devices": "device",
|
|
104
|
+
"/technology/platforms/stack/members": "virtualchassis",
|
|
105
|
+
"/inventory/interfaces": "interface",
|
|
106
|
+
"/inventory/part-numbers": "part_number",
|
|
107
|
+
"/technology/vlans/site-summary": "vlan",
|
|
108
|
+
"/technology/routing/vrf/detail": "vrf",
|
|
109
|
+
"/technology/networks/managed-networks": "prefix",
|
|
110
|
+
"/technology/addressing/managed-ip/ipv4": "ipaddress",
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
for transform_map in IPFabricTransformMap.objects.using(
|
|
114
|
+
schema_editor.connection.alias
|
|
115
|
+
).all():
|
|
116
|
+
if not transform_map.source_endpoint:
|
|
117
|
+
continue
|
|
118
|
+
|
|
119
|
+
# Get the endpoint and map it back to source_model
|
|
120
|
+
try:
|
|
121
|
+
endpoint = IPFabricEndpoint.objects.using(
|
|
122
|
+
schema_editor.connection.alias
|
|
123
|
+
).get(pk=transform_map.source_endpoint_id)
|
|
124
|
+
source_model_value = endpoint_to_source_model.get(endpoint.endpoint)
|
|
125
|
+
transform_map.source_model = source_model_value
|
|
126
|
+
except IPFabricEndpoint.DoesNotExist:
|
|
127
|
+
pass
|
|
128
|
+
|
|
129
|
+
# Restore original name by removing correction markers
|
|
130
|
+
if transform_map.name.startswith("[NEEDS CORRECTION"):
|
|
131
|
+
# Extract original name after the correction marker
|
|
132
|
+
import re
|
|
133
|
+
|
|
134
|
+
match = re.match(r"\[NEEDS CORRECTION[^]]*]\s*(.+)", transform_map.name)
|
|
135
|
+
if match:
|
|
136
|
+
transform_map.name = match.group(1)
|
|
137
|
+
transform_map.save()
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
transform_keys = {
|
|
141
|
+
"site": "dcim.site",
|
|
142
|
+
"vlan": "ipam.vlan",
|
|
143
|
+
"manufacturer": "dcim.manufacturer",
|
|
144
|
+
"vrf": "ipam.vrf",
|
|
145
|
+
"platform": "dcim.platform",
|
|
146
|
+
"devicerole": "dcim.devicerole",
|
|
147
|
+
"devicetype": "dcim.devicetype",
|
|
148
|
+
"prefix": "ipam.prefix",
|
|
149
|
+
"device": "dcim.device",
|
|
150
|
+
"virtualchassis": "dcim.virtualchassis",
|
|
151
|
+
"ipaddress": "ipam.ipaddress",
|
|
152
|
+
"interface": "dcim.interface",
|
|
153
|
+
"inventoryitem": "dcim.inventoryitem",
|
|
154
|
+
"macaddress": "dcim.macaddress",
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def correct_sync_parameters(
|
|
159
|
+
apps: "apps_type", schema_editor: "BaseDatabaseSchemaEditor"
|
|
160
|
+
):
|
|
161
|
+
"""Correct existing sync parameters to match new structure."""
|
|
162
|
+
IPFabricSync = apps.get_model("ipfabric_netbox", "IPFabricSync")
|
|
163
|
+
for sync in IPFabricSync.objects.using(schema_editor.connection.alias).all():
|
|
164
|
+
for old_key, new_key in transform_keys.items():
|
|
165
|
+
if old_key not in sync.parameters:
|
|
166
|
+
continue
|
|
167
|
+
sync.parameters[new_key] = sync.parameters.pop(old_key)
|
|
168
|
+
sync.save()
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def return_sync_parameters(
|
|
172
|
+
apps: "apps_type", schema_editor: "BaseDatabaseSchemaEditor"
|
|
173
|
+
):
|
|
174
|
+
"""Reverse the key renaming in sync parameters to restore original structure."""
|
|
175
|
+
IPFabricSync = apps.get_model("ipfabric_netbox", "IPFabricSync")
|
|
176
|
+
reverse_transform_keys = {v: k for k, v in transform_keys.items()}
|
|
177
|
+
for sync in IPFabricSync.objects.using(schema_editor.connection.alias).all():
|
|
178
|
+
for new_key, old_key in reverse_transform_keys.items():
|
|
179
|
+
if new_key not in sync.parameters:
|
|
180
|
+
continue
|
|
181
|
+
sync.parameters[old_key] = sync.parameters.pop(new_key)
|
|
182
|
+
sync.save()
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def assign_parent_transform_maps(
|
|
186
|
+
apps: "apps_type", schema_editor: "BaseDatabaseSchemaEditor"
|
|
187
|
+
):
|
|
188
|
+
"""Assign parent relationships to transform maps based on transform_map.json data."""
|
|
189
|
+
|
|
190
|
+
IPFabricTransformMap = apps.get_model("ipfabric_netbox", "IPFabricTransformMap")
|
|
191
|
+
ContentType = apps.get_model("contenttypes", "ContentType")
|
|
192
|
+
transform_map_data = get_transform_map()
|
|
193
|
+
|
|
194
|
+
# Process each transform map from JSON
|
|
195
|
+
for tm_data in transform_map_data:
|
|
196
|
+
data = tm_data.get("data", {})
|
|
197
|
+
|
|
198
|
+
parents_value = data.get("parents")
|
|
199
|
+
if not parents_value:
|
|
200
|
+
continue
|
|
201
|
+
|
|
202
|
+
# Handle both string (single parent) and array (multiple parents) formats
|
|
203
|
+
if isinstance(parents_value, str):
|
|
204
|
+
parents_value = [parents_value]
|
|
205
|
+
|
|
206
|
+
try:
|
|
207
|
+
# Find the transform map (without group) matching this target model
|
|
208
|
+
target_model_data = data.get("target_model", {})
|
|
209
|
+
target_model = ContentType.objects.using(
|
|
210
|
+
schema_editor.connection.alias
|
|
211
|
+
).get(
|
|
212
|
+
app_label=target_model_data.get("app_label"),
|
|
213
|
+
model=target_model_data.get("model"),
|
|
214
|
+
)
|
|
215
|
+
transform_map = (
|
|
216
|
+
IPFabricTransformMap.objects.using(schema_editor.connection.alias)
|
|
217
|
+
.filter(group__isnull=True, target_model=target_model)
|
|
218
|
+
.first()
|
|
219
|
+
)
|
|
220
|
+
if not transform_map:
|
|
221
|
+
continue
|
|
222
|
+
|
|
223
|
+
# Add each parent to the M2M relationship
|
|
224
|
+
for parent_str in parents_value:
|
|
225
|
+
parent_app_label, parent_model = parent_str.split(".")
|
|
226
|
+
|
|
227
|
+
try:
|
|
228
|
+
parent_content_type = ContentType.objects.using(
|
|
229
|
+
schema_editor.connection.alias
|
|
230
|
+
).get(app_label=parent_app_label, model=parent_model)
|
|
231
|
+
parent_transform_map = (
|
|
232
|
+
IPFabricTransformMap.objects.using(
|
|
233
|
+
schema_editor.connection.alias
|
|
234
|
+
)
|
|
235
|
+
.filter(group__isnull=True, target_model=parent_content_type)
|
|
236
|
+
.first()
|
|
237
|
+
)
|
|
238
|
+
if parent_transform_map:
|
|
239
|
+
transform_map.parents.add(parent_transform_map)
|
|
240
|
+
|
|
241
|
+
except ContentType.DoesNotExist:
|
|
242
|
+
continue
|
|
243
|
+
|
|
244
|
+
except ContentType.DoesNotExist:
|
|
245
|
+
continue
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
class Migration(migrations.Migration):
|
|
249
|
+
dependencies = [
|
|
250
|
+
("extras", "0132_configcontextprofile"),
|
|
251
|
+
("ipfabric_netbox", "0022_prepare_for_filters"),
|
|
252
|
+
]
|
|
253
|
+
|
|
254
|
+
operations = [
|
|
255
|
+
migrations.RunPython(
|
|
256
|
+
prepare_endpoints,
|
|
257
|
+
migrations.RunPython.noop,
|
|
258
|
+
),
|
|
259
|
+
migrations.RunPython(
|
|
260
|
+
prepare_filters,
|
|
261
|
+
migrations.RunPython.noop,
|
|
262
|
+
),
|
|
263
|
+
migrations.RunPython(
|
|
264
|
+
assign_filters_to_syncs,
|
|
265
|
+
migrations.RunPython.noop,
|
|
266
|
+
),
|
|
267
|
+
migrations.RunPython(
|
|
268
|
+
migrate_source_model_to_endpoint,
|
|
269
|
+
migrate_endpoint_to_source_model,
|
|
270
|
+
),
|
|
271
|
+
migrations.RunPython(
|
|
272
|
+
correct_sync_parameters,
|
|
273
|
+
return_sync_parameters,
|
|
274
|
+
),
|
|
275
|
+
migrations.RunPython(
|
|
276
|
+
assign_parent_transform_maps,
|
|
277
|
+
migrations.RunPython.noop,
|
|
278
|
+
),
|
|
279
|
+
]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Generated by Django 5.2.5 on 2025-12-02 12:06
|
|
2
|
+
import django.db.models.deletion
|
|
3
|
+
from django.db import migrations
|
|
4
|
+
from django.db import models
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Migration(migrations.Migration):
|
|
8
|
+
dependencies = [
|
|
9
|
+
("extras", "0132_configcontextprofile"),
|
|
10
|
+
("ipfabric_netbox", "0023_populate_filters_data"),
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
operations = [
|
|
14
|
+
# The new endpoint is populated, so we can make the field non-nullable
|
|
15
|
+
migrations.AlterField(
|
|
16
|
+
model_name="ipfabrictransformmap",
|
|
17
|
+
name="source_endpoint",
|
|
18
|
+
field=models.ForeignKey(
|
|
19
|
+
on_delete=django.db.models.deletion.PROTECT,
|
|
20
|
+
related_name="transform_maps",
|
|
21
|
+
to="ipfabric_netbox.ipfabricendpoint",
|
|
22
|
+
),
|
|
23
|
+
),
|
|
24
|
+
# Remove the old source_model field as it's been replaced by source_endpoint
|
|
25
|
+
migrations.RemoveField(
|
|
26
|
+
model_name="ipfabrictransformmap",
|
|
27
|
+
name="source_model",
|
|
28
|
+
),
|
|
29
|
+
]
|