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
@@ -0,0 +1,303 @@
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
+ ContentType = apps.get_model("contenttypes", "ContentType")
49
+
50
+ source_model_to_endpoint = {
51
+ "site": "/inventory/sites/overview",
52
+ "device": "/inventory/devices",
53
+ "virtualchassis": "/technology/platforms/stack/members",
54
+ "interface": "/inventory/interfaces",
55
+ "part_number": "/inventory/part-numbers",
56
+ "vlan": "/technology/vlans/site-summary",
57
+ "vrf": "/technology/routing/vrf/detail",
58
+ "prefix": "/technology/networks/managed-networks",
59
+ "ipaddress": "/technology/addressing/managed-ip/ipv4",
60
+ "inventory": "/inventory/part-numbers",
61
+ }
62
+
63
+ # Get first endpoint as fallback to make sure it's always set
64
+ fallback_endpoint = IPFabricEndpoint.objects.using(
65
+ schema_editor.connection.alias
66
+ ).first()
67
+
68
+ for transform_map in IPFabricTransformMap.objects.using(
69
+ schema_editor.connection.alias
70
+ ).all():
71
+ endpoint_value = source_model_to_endpoint.get(transform_map.source_model)
72
+
73
+ # Special case: source_model="device" with target_model="dcim.virtualchassis"
74
+ # should map to /technology/platforms/stack/members, not /inventory/devices
75
+ if transform_map.source_model == "device" and transform_map.target_model:
76
+ virtualchassis_ct = ContentType.objects.using(
77
+ schema_editor.connection.alias
78
+ ).get(app_label="dcim", model="virtualchassis")
79
+ if transform_map.target_model_id == virtualchassis_ct.id:
80
+ endpoint_value = "/technology/platforms/stack/members"
81
+
82
+ # If no mapping exists, use fallback and mark the name for manual fix
83
+ if not endpoint_value:
84
+ if not transform_map.name.startswith("[NEEDS CORRECTION]"):
85
+ transform_map.name = f"[NEEDS CORRECTION - Unknown source_model: {transform_map.source_model}] {transform_map.name}"
86
+ transform_map.source_endpoint = fallback_endpoint
87
+ transform_map.save()
88
+ continue
89
+
90
+ # Try to get the mapped endpoint
91
+ try:
92
+ endpoint = IPFabricEndpoint.objects.using(
93
+ schema_editor.connection.alias
94
+ ).get(endpoint=endpoint_value)
95
+ transform_map.source_endpoint = endpoint
96
+ except IPFabricEndpoint.DoesNotExist:
97
+ # Use fallback endpoint and mark the name
98
+ if not transform_map.name.startswith("[NEEDS CORRECTION]"):
99
+ transform_map.name = f"[NEEDS CORRECTION - Expected endpoint '{endpoint_value}' not found] {transform_map.name}"
100
+ transform_map.source_endpoint = fallback_endpoint
101
+ transform_map.save()
102
+
103
+
104
+ def migrate_endpoint_to_source_model(
105
+ apps: "apps_type", schema_editor: "BaseDatabaseSchemaEditor"
106
+ ):
107
+ """Reverse migration: migrate IPFabricTransformMap source_endpoint data back to source_model."""
108
+ IPFabricTransformMap = apps.get_model("ipfabric_netbox", "IPFabricTransformMap")
109
+ IPFabricEndpoint = apps.get_model("ipfabric_netbox", "IPFabricEndpoint")
110
+ ContentType = apps.get_model("contenttypes", "ContentType")
111
+
112
+ endpoint_to_source_model = {
113
+ "/inventory/sites/overview": "site",
114
+ "/inventory/devices": "device",
115
+ "/technology/platforms/stack/members": "virtualchassis",
116
+ "/inventory/interfaces": "interface",
117
+ "/inventory/part-numbers": "part_number",
118
+ "/technology/vlans/site-summary": "vlan",
119
+ "/technology/routing/vrf/detail": "vrf",
120
+ "/technology/networks/managed-networks": "prefix",
121
+ "/technology/addressing/managed-ip/ipv4": "ipaddress",
122
+ }
123
+
124
+ for transform_map in IPFabricTransformMap.objects.using(
125
+ schema_editor.connection.alias
126
+ ).all():
127
+ if not transform_map.source_endpoint:
128
+ continue
129
+
130
+ # Get the endpoint and map it back to source_model
131
+ try:
132
+ endpoint = IPFabricEndpoint.objects.using(
133
+ schema_editor.connection.alias
134
+ ).get(pk=transform_map.source_endpoint_id)
135
+ source_model_value = endpoint_to_source_model.get(endpoint.endpoint)
136
+
137
+ # Special case: source_endpoint="/technology/platforms/stack/members" with
138
+ # target_model="dcim.virtualchassis" should map back to source_model="device"
139
+ if (
140
+ endpoint.endpoint == "/technology/platforms/stack/members"
141
+ and transform_map.target_model
142
+ ):
143
+ virtualchassis_ct = ContentType.objects.using(
144
+ schema_editor.connection.alias
145
+ ).get(app_label="dcim", model="virtualchassis")
146
+ if transform_map.target_model_id == virtualchassis_ct.id:
147
+ source_model_value = "device"
148
+
149
+ transform_map.source_model = source_model_value
150
+ except IPFabricEndpoint.DoesNotExist:
151
+ pass
152
+
153
+ # Restore original name by removing correction markers
154
+ if transform_map.name.startswith("[NEEDS CORRECTION"):
155
+ # Extract original name after the correction marker
156
+ import re
157
+
158
+ match = re.match(r"\[NEEDS CORRECTION[^]]*]\s*(.+)", transform_map.name)
159
+ if match:
160
+ transform_map.name = match.group(1)
161
+ transform_map.save()
162
+
163
+
164
+ transform_keys = {
165
+ "site": "dcim.site",
166
+ "vlan": "ipam.vlan",
167
+ "manufacturer": "dcim.manufacturer",
168
+ "vrf": "ipam.vrf",
169
+ "platform": "dcim.platform",
170
+ "devicerole": "dcim.devicerole",
171
+ "devicetype": "dcim.devicetype",
172
+ "prefix": "ipam.prefix",
173
+ "device": "dcim.device",
174
+ "virtualchassis": "dcim.virtualchassis",
175
+ "ipaddress": "ipam.ipaddress",
176
+ "interface": "dcim.interface",
177
+ "inventoryitem": "dcim.inventoryitem",
178
+ "macaddress": "dcim.macaddress",
179
+ }
180
+
181
+
182
+ def correct_sync_parameters(
183
+ apps: "apps_type", schema_editor: "BaseDatabaseSchemaEditor"
184
+ ):
185
+ """Correct existing sync parameters to match new structure."""
186
+ IPFabricSync = apps.get_model("ipfabric_netbox", "IPFabricSync")
187
+ for sync in IPFabricSync.objects.using(schema_editor.connection.alias).all():
188
+ for old_key, new_key in transform_keys.items():
189
+ if old_key not in sync.parameters:
190
+ continue
191
+ sync.parameters[new_key] = sync.parameters.pop(old_key)
192
+ sync.save()
193
+
194
+
195
+ def return_sync_parameters(
196
+ apps: "apps_type", schema_editor: "BaseDatabaseSchemaEditor"
197
+ ):
198
+ """Reverse the key renaming in sync parameters to restore original structure."""
199
+ IPFabricSync = apps.get_model("ipfabric_netbox", "IPFabricSync")
200
+ reverse_transform_keys = {v: k for k, v in transform_keys.items()}
201
+ for sync in IPFabricSync.objects.using(schema_editor.connection.alias).all():
202
+ for new_key, old_key in reverse_transform_keys.items():
203
+ if new_key not in sync.parameters:
204
+ continue
205
+ sync.parameters[old_key] = sync.parameters.pop(new_key)
206
+ sync.save()
207
+
208
+
209
+ def assign_parent_transform_maps(
210
+ apps: "apps_type", schema_editor: "BaseDatabaseSchemaEditor"
211
+ ):
212
+ """Assign parent relationships to transform maps based on transform_map.json data."""
213
+
214
+ IPFabricTransformMap = apps.get_model("ipfabric_netbox", "IPFabricTransformMap")
215
+ ContentType = apps.get_model("contenttypes", "ContentType")
216
+ transform_map_data = get_transform_map()
217
+
218
+ # Process each transform map from JSON
219
+ for tm_data in transform_map_data:
220
+ data = tm_data.get("data", {})
221
+
222
+ parents_value = data.get("parents")
223
+ if not parents_value:
224
+ continue
225
+
226
+ # Handle both string (single parent) and array (multiple parents) formats
227
+ if isinstance(parents_value, str):
228
+ parents_value = [parents_value]
229
+
230
+ try:
231
+ # Find the transform map (without group) matching this target model
232
+ target_model_data = data.get("target_model", {})
233
+ target_model = ContentType.objects.using(
234
+ schema_editor.connection.alias
235
+ ).get(
236
+ app_label=target_model_data.get("app_label"),
237
+ model=target_model_data.get("model"),
238
+ )
239
+ transform_map = (
240
+ IPFabricTransformMap.objects.using(schema_editor.connection.alias)
241
+ .filter(group__isnull=True, target_model=target_model)
242
+ .first()
243
+ )
244
+ if not transform_map:
245
+ continue
246
+
247
+ # Add each parent to the M2M relationship
248
+ for parent_str in parents_value:
249
+ parent_app_label, parent_model = parent_str.split(".")
250
+
251
+ try:
252
+ parent_content_type = ContentType.objects.using(
253
+ schema_editor.connection.alias
254
+ ).get(app_label=parent_app_label, model=parent_model)
255
+ parent_transform_map = (
256
+ IPFabricTransformMap.objects.using(
257
+ schema_editor.connection.alias
258
+ )
259
+ .filter(group__isnull=True, target_model=parent_content_type)
260
+ .first()
261
+ )
262
+ if parent_transform_map:
263
+ transform_map.parents.add(parent_transform_map)
264
+
265
+ except ContentType.DoesNotExist:
266
+ continue
267
+
268
+ except ContentType.DoesNotExist:
269
+ continue
270
+
271
+
272
+ class Migration(migrations.Migration):
273
+ dependencies = [
274
+ ("extras", "0132_configcontextprofile"),
275
+ ("ipfabric_netbox", "0022_prepare_for_filters"),
276
+ ]
277
+
278
+ operations = [
279
+ migrations.RunPython(
280
+ prepare_endpoints,
281
+ migrations.RunPython.noop,
282
+ ),
283
+ migrations.RunPython(
284
+ prepare_filters,
285
+ migrations.RunPython.noop,
286
+ ),
287
+ migrations.RunPython(
288
+ assign_filters_to_syncs,
289
+ migrations.RunPython.noop,
290
+ ),
291
+ migrations.RunPython(
292
+ migrate_source_model_to_endpoint,
293
+ migrate_endpoint_to_source_model,
294
+ ),
295
+ migrations.RunPython(
296
+ correct_sync_parameters,
297
+ return_sync_parameters,
298
+ ),
299
+ migrations.RunPython(
300
+ assign_parent_transform_maps,
301
+ migrations.RunPython.noop,
302
+ ),
303
+ ]
@@ -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
+ ]
@@ -0,0 +1,166 @@
1
+ from typing import TYPE_CHECKING
2
+
3
+ from django.db import migrations
4
+
5
+ from ipfabric_netbox.utilities.endpoint import do_endpoint_change
6
+ from ipfabric_netbox.utilities.endpoint import EndpointRecord
7
+ from ipfabric_netbox.utilities.transform_map import do_change
8
+ from ipfabric_netbox.utilities.transform_map import FieldRecord
9
+ from ipfabric_netbox.utilities.transform_map import RelationshipRecord
10
+ from ipfabric_netbox.utilities.transform_map import TransformMapRecord
11
+
12
+ if TYPE_CHECKING:
13
+ from django.apps import apps as apps_type
14
+ from django.db.backends.base.schema import BaseDatabaseSchemaEditor
15
+
16
+ ENDPOINTS = (
17
+ EndpointRecord(
18
+ name="Default VSS Chassis Endpoint",
19
+ description="",
20
+ endpoint="/technology/platforms/vss/chassis",
21
+ ),
22
+ )
23
+
24
+ TRANSFORM_MAP_CHANGES = (
25
+ TransformMapRecord(
26
+ source_endpoint="/technology/platforms/stack/members",
27
+ target_model="dcim.virtualchassis",
28
+ fields=(
29
+ FieldRecord(
30
+ source_field="master",
31
+ target_field="name",
32
+ old_template="",
33
+ new_template="{% if object.master is defined and object.master %}{{ object.master }}{% else %}{{ object.hostname }}{% endif %}",
34
+ ),
35
+ ),
36
+ ),
37
+ TransformMapRecord(
38
+ source_endpoint="/inventory/devices",
39
+ target_model="dcim.device",
40
+ fields=(
41
+ FieldRecord(
42
+ source_field="hostname",
43
+ target_field="vc_position",
44
+ old_template="{% if object.virtual_chassis %}{{ object.virtual_chassis.member }}{% else %}None{% endif %}",
45
+ new_template="{% if object.virtual_chassis %}{% if object.virtual_chassis.member is defined and object.virtual_chassis.member %}{{ object.virtual_chassis.member }}{% else %}{{ object.virtual_chassis.chassisId }}{% endif %}{% else %}None{% endif %}",
46
+ ),
47
+ ),
48
+ relationships=(
49
+ RelationshipRecord(
50
+ source_model="dcim.virtualchassis",
51
+ target_field="virtual_chassis",
52
+ old_template="{% if object.virtual_chassis %}{{ dcim.VirtualChassis.objects.filter(name=object.virtual_chassis.master).first().pk }}{% endif %}",
53
+ new_template="{% if object.virtual_chassis %}{% if object.virtual_chassis.master is defined and object.virtual_chassis.master %}{{ dcim.VirtualChassis.objects.filter(name=object.virtual_chassis.master).first().pk }}{% else %}{{ dcim.VirtualChassis.objects.filter(name=object.virtual_chassis.hostname).first().pk }}{% endif %}{% endif %}",
54
+ ),
55
+ ),
56
+ ),
57
+ )
58
+
59
+
60
+ def add_vss_chassis_endpoint(
61
+ apps: "apps_type", schema_editor: "BaseDatabaseSchemaEditor"
62
+ ):
63
+ """Add VSS chassis endpoint."""
64
+ do_endpoint_change(apps, schema_editor, ENDPOINTS, forward=True)
65
+
66
+
67
+ def remove_vss_chassis_endpoint(
68
+ apps: "apps_type", schema_editor: "BaseDatabaseSchemaEditor"
69
+ ):
70
+ """Remove VSS chassis endpoint."""
71
+ do_endpoint_change(apps, schema_editor, ENDPOINTS, forward=False)
72
+
73
+
74
+ def forward_transform_maps_change(
75
+ apps: "apps_type", schema_editor: "BaseDatabaseSchemaEditor"
76
+ ):
77
+ """Replace old template with updated version."""
78
+ do_change(apps, schema_editor, changes=TRANSFORM_MAP_CHANGES, forward=True)
79
+
80
+
81
+ def revert_transform_maps_change(
82
+ apps: "apps_type", schema_editor: "BaseDatabaseSchemaEditor"
83
+ ):
84
+ """Revert template back to the previous exact template."""
85
+ do_change(apps, schema_editor, changes=TRANSFORM_MAP_CHANGES, forward=False)
86
+
87
+
88
+ def add_vss_to_child_filter(
89
+ apps: "apps_type", schema_editor: "BaseDatabaseSchemaEditor"
90
+ ):
91
+ """Add VSS chassis endpoint to Default Child Devices Filter idempotently."""
92
+ IPFabricFilter = apps.get_model("ipfabric_netbox", "IPFabricFilter")
93
+ IPFabricEndpoint = apps.get_model("ipfabric_netbox", "IPFabricEndpoint")
94
+
95
+ # Get the filter
96
+ try:
97
+ child_filter = IPFabricFilter.objects.using(schema_editor.connection.alias).get(
98
+ name="Default Child Devices Filter"
99
+ )
100
+ except IPFabricFilter.DoesNotExist:
101
+ # Filter doesn't exist, nothing to update
102
+ return
103
+
104
+ # Get the VSS chassis endpoint
105
+ try:
106
+ vss_endpoint = IPFabricEndpoint.objects.using(
107
+ schema_editor.connection.alias
108
+ ).get(endpoint="/technology/platforms/vss/chassis")
109
+ except IPFabricEndpoint.DoesNotExist:
110
+ # Endpoint doesn't exist yet, skip (will be created by add_vss_chassis_endpoint)
111
+ return
112
+
113
+ # Add the endpoint to the filter if not already present
114
+ if not child_filter.endpoints.filter(pk=vss_endpoint.pk).exists():
115
+ child_filter.endpoints.add(vss_endpoint)
116
+
117
+
118
+ def remove_vss_from_child_filter(
119
+ apps: "apps_type", schema_editor: "BaseDatabaseSchemaEditor"
120
+ ):
121
+ """Remove VSS chassis endpoint from Default Child Devices Filter."""
122
+ IPFabricFilter = apps.get_model("ipfabric_netbox", "IPFabricFilter")
123
+ IPFabricEndpoint = apps.get_model("ipfabric_netbox", "IPFabricEndpoint")
124
+
125
+ # Get the filter
126
+ try:
127
+ child_filter = IPFabricFilter.objects.using(schema_editor.connection.alias).get(
128
+ name="Default Child Devices Filter"
129
+ )
130
+ except IPFabricFilter.DoesNotExist:
131
+ # Filter doesn't exist, nothing to update
132
+ return
133
+
134
+ # Get the VSS chassis endpoint
135
+ try:
136
+ vss_endpoint = IPFabricEndpoint.objects.using(
137
+ schema_editor.connection.alias
138
+ ).get(endpoint="/technology/platforms/vss/chassis")
139
+ except IPFabricEndpoint.DoesNotExist:
140
+ # Endpoint doesn't exist, nothing to remove
141
+ return
142
+
143
+ # Remove the endpoint from the filter if present
144
+ if child_filter.endpoints.filter(pk=vss_endpoint.pk).exists():
145
+ child_filter.endpoints.remove(vss_endpoint)
146
+
147
+
148
+ class Migration(migrations.Migration):
149
+ dependencies = [
150
+ ("ipfabric_netbox", "0024_finish_filters"),
151
+ ]
152
+
153
+ operations = [
154
+ migrations.RunPython(
155
+ add_vss_chassis_endpoint,
156
+ remove_vss_chassis_endpoint,
157
+ ),
158
+ migrations.RunPython(
159
+ forward_transform_maps_change,
160
+ revert_transform_maps_change,
161
+ ),
162
+ migrations.RunPython(
163
+ add_vss_to_child_filter,
164
+ remove_vss_from_child_filter,
165
+ ),
166
+ ]