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
@@ -3,9 +3,13 @@ from django.utils import timezone
3
3
  from rest_framework import status
4
4
  from utilities.testing import APIViewTestCases
5
5
 
6
+ from ipfabric_netbox.choices import IPFabricFilterTypeChoices
6
7
  from ipfabric_netbox.choices import IPFabricSourceStatusChoices
7
8
  from ipfabric_netbox.choices import IPFabricSyncStatusChoices
8
9
  from ipfabric_netbox.models import IPFabricData
10
+ from ipfabric_netbox.models import IPFabricEndpoint
11
+ from ipfabric_netbox.models import IPFabricFilter
12
+ from ipfabric_netbox.models import IPFabricFilterExpression
9
13
  from ipfabric_netbox.models import IPFabricIngestion
10
14
  from ipfabric_netbox.models import IPFabricIngestionIssue
11
15
  from ipfabric_netbox.models import IPFabricRelationshipField
@@ -25,6 +29,7 @@ class IPFabricTransformMapGroupTest(APIViewTestCases.APIViewTestCase):
25
29
  graphql_base_name = "ipfabric_transform_map_group"
26
30
  brief_fields = [
27
31
  "description",
32
+ "display",
28
33
  "id",
29
34
  "name",
30
35
  ]
@@ -56,10 +61,11 @@ class IPFabricTransformMapTest(APIViewTestCases.APIViewTestCase):
56
61
  model = IPFabricTransformMap
57
62
  graphql_base_name = "ipfabric_transform_map"
58
63
  brief_fields = [
64
+ "display",
59
65
  "group",
60
66
  "id",
61
67
  "name",
62
- "source_model",
68
+ "source_endpoint",
63
69
  "target_model",
64
70
  ]
65
71
 
@@ -77,20 +83,32 @@ class IPFabricTransformMapTest(APIViewTestCases.APIViewTestCase):
77
83
  IPFabricTransformMapGroup.objects.create(name="Group C"),
78
84
  )
79
85
 
86
+ # Get existing endpoints created by migrations
87
+ endpoints = {
88
+ "site": IPFabricEndpoint.objects.get(endpoint="/inventory/sites/overview"),
89
+ "device": IPFabricEndpoint.objects.get(endpoint="/inventory/devices"),
90
+ "ipaddress": IPFabricEndpoint.objects.get(
91
+ endpoint="/technology/addressing/managed-ip/ipv4"
92
+ ),
93
+ "vrf": IPFabricEndpoint.objects.get(
94
+ endpoint="/technology/routing/vrf/detail"
95
+ ),
96
+ }
97
+
80
98
  IPFabricTransformMap.objects.create(
81
99
  name="TransformMap D",
82
- source_model="site",
100
+ source_endpoint=endpoints["site"],
83
101
  target_model=ContentType.objects.get(app_label="dcim", model="site"),
84
102
  )
85
103
  IPFabricTransformMap.objects.create(
86
104
  name="TransformMap E",
87
- source_model="site",
105
+ source_endpoint=endpoints["site"],
88
106
  target_model=ContentType.objects.get(app_label="dcim", model="site"),
89
107
  group=groups[0],
90
108
  )
91
109
  IPFabricTransformMap.objects.create(
92
110
  name="TransformMap F",
93
- source_model="ipaddress",
111
+ source_endpoint=endpoints["ipaddress"],
94
112
  target_model=ContentType.objects.get(app_label="ipam", model="ipaddress"),
95
113
  group=groups[0],
96
114
  )
@@ -98,19 +116,19 @@ class IPFabricTransformMapTest(APIViewTestCases.APIViewTestCase):
98
116
  cls.create_data = [
99
117
  {
100
118
  "name": "Transform Map A",
101
- "source_model": "site",
119
+ "source_endpoint": endpoints["site"].pk,
102
120
  "target_model": "dcim.site",
103
121
  "group": groups[1].pk,
104
122
  },
105
123
  {
106
124
  "name": "Transform Map B",
107
- "source_model": "device",
125
+ "source_endpoint": endpoints["device"].pk,
108
126
  "target_model": "dcim.device",
109
127
  "group": groups[1].pk,
110
128
  },
111
129
  {
112
130
  "name": "Transform Map C",
113
- "source_model": "vrf",
131
+ "source_endpoint": endpoints["vrf"].pk,
114
132
  "target_model": "ipam.vrf",
115
133
  "group": groups[1].pk,
116
134
  },
@@ -126,6 +144,7 @@ class IPFabricTransformFieldTest(APIViewTestCases.APIViewTestCase):
126
144
  # in this case brief fields are the same, but they are needed fot the test
127
145
  brief_fields = [
128
146
  "coalesce",
147
+ "display",
129
148
  "id",
130
149
  "source_field",
131
150
  "target_field",
@@ -148,23 +167,32 @@ class IPFabricTransformFieldTest(APIViewTestCases.APIViewTestCase):
148
167
  IPFabricTransformMapGroup.objects.create(name="Field Test Group C"),
149
168
  )
150
169
 
170
+ # Get existing endpoints created by migrations
171
+ endpoints = {
172
+ "site": IPFabricEndpoint.objects.get(endpoint="/inventory/sites/overview"),
173
+ "device": IPFabricEndpoint.objects.get(endpoint="/inventory/devices"),
174
+ "ipaddress": IPFabricEndpoint.objects.get(
175
+ endpoint="/technology/addressing/managed-ip/ipv4"
176
+ ),
177
+ }
178
+
151
179
  # Create transform maps for the fields to reference
152
180
  transform_maps = [
153
181
  IPFabricTransformMap.objects.create(
154
182
  name="Field Map A",
155
- source_model="site",
183
+ source_endpoint=endpoints["site"],
156
184
  target_model=ContentType.objects.get(app_label="dcim", model="site"),
157
185
  group=groups[0],
158
186
  ),
159
187
  IPFabricTransformMap.objects.create(
160
188
  name="Field Map B",
161
- source_model="device",
189
+ source_endpoint=endpoints["device"],
162
190
  target_model=ContentType.objects.get(app_label="dcim", model="device"),
163
191
  group=groups[0],
164
192
  ),
165
193
  IPFabricTransformMap.objects.create(
166
194
  name="Field Map C",
167
- source_model="ipaddress",
195
+ source_endpoint=endpoints["ipaddress"],
168
196
  target_model=ContentType.objects.get(
169
197
  app_label="ipam", model="ipaddress"
170
198
  ),
@@ -229,6 +257,7 @@ class IPFabricRelationshipFieldTest(APIViewTestCases.APIViewTestCase):
229
257
  # in this case brief fields are the same, but they are needed fot the test
230
258
  brief_fields = [
231
259
  "coalesce",
260
+ "display",
232
261
  "id",
233
262
  "source_model",
234
263
  "target_field",
@@ -251,23 +280,32 @@ class IPFabricRelationshipFieldTest(APIViewTestCases.APIViewTestCase):
251
280
  IPFabricTransformMapGroup.objects.create(name="Relationship Test Group C"),
252
281
  )
253
282
 
283
+ # Get existing endpoints created by migrations
284
+ endpoints = {
285
+ "site": IPFabricEndpoint.objects.get(endpoint="/inventory/sites/overview"),
286
+ "device": IPFabricEndpoint.objects.get(endpoint="/inventory/devices"),
287
+ "ipaddress": IPFabricEndpoint.objects.get(
288
+ endpoint="/technology/addressing/managed-ip/ipv4"
289
+ ),
290
+ }
291
+
254
292
  # Create transform maps for the relationship fields to reference
255
293
  transform_maps = [
256
294
  IPFabricTransformMap.objects.create(
257
295
  name="Relationship Map A",
258
- source_model="site",
296
+ source_endpoint=endpoints["site"],
259
297
  target_model=ContentType.objects.get(app_label="dcim", model="site"),
260
298
  group=groups[0],
261
299
  ),
262
300
  IPFabricTransformMap.objects.create(
263
301
  name="Relationship Map B",
264
- source_model="device",
302
+ source_endpoint=endpoints["device"],
265
303
  target_model=ContentType.objects.get(app_label="dcim", model="device"),
266
304
  group=groups[0],
267
305
  ),
268
306
  IPFabricTransformMap.objects.create(
269
307
  name="Relationship Map C",
270
- source_model="ipaddress",
308
+ source_endpoint=endpoints["ipaddress"],
271
309
  target_model=ContentType.objects.get(
272
310
  app_label="ipam", model="ipaddress"
273
311
  ),
@@ -547,6 +585,30 @@ class IPFabricSnapshotTest(
547
585
  labels = [i["name"].lower() for i in body["results"]]
548
586
  self.assertTrue(all("site" in name for name in labels))
549
587
 
588
+ def test_sites_action_with_no_data(self):
589
+ """Test sites endpoint when snapshot.data is None."""
590
+ self.add_permissions("ipfabric_netbox.view_ipfabricsnapshot")
591
+
592
+ # Create a snapshot with data=None
593
+ source = IPFabricSource.objects.first()
594
+ snapshot_no_data = IPFabricSnapshot.objects.create(
595
+ name="Snapshot No Data",
596
+ source=source,
597
+ snapshot_id="snap-no-data",
598
+ status="unloaded",
599
+ data=None,
600
+ date=timezone.now(),
601
+ last_updated=timezone.now(),
602
+ )
603
+
604
+ # Call sites endpoint on snapshot with no data
605
+ url = f"{BASE}/snapshot/{snapshot_no_data.pk}/sites/"
606
+ resp = self.client.get(url, **self.header)
607
+ self.assertHttpStatus(resp, status.HTTP_200_OK)
608
+ body = resp.json()
609
+ # Should return empty list when data is None
610
+ self.assertEqual(body, [])
611
+
550
612
  def test_raw_patch_and_delete(self):
551
613
  self.add_permissions(
552
614
  "ipfabric_netbox.view_ipfabricsnapshot",
@@ -585,6 +647,7 @@ class IPFabricSyncTest(APIViewTestCases.APIViewTestCase):
585
647
  graphql_base_name = "ipfabric_sync"
586
648
  brief_fields = [
587
649
  "auto_merge",
650
+ "display",
588
651
  "id",
589
652
  "last_synced",
590
653
  "name",
@@ -595,16 +658,19 @@ class IPFabricSyncTest(APIViewTestCases.APIViewTestCase):
595
658
  {
596
659
  "name": "Test Sync A",
597
660
  "parameters": {"site": True, "device": False},
661
+ "filters": [],
598
662
  },
599
663
  {
600
664
  "name": "Test Sync B",
601
665
  "parameters": {"ipaddress": True, "prefix": True},
602
666
  "auto_merge": True,
667
+ "filters": [],
603
668
  },
604
669
  {
605
670
  "name": "Test Sync C",
606
671
  "parameters": {"device": True, "interface": True},
607
672
  "interval": 60,
673
+ "filters": [],
608
674
  },
609
675
  ]
610
676
  bulk_update_data = {
@@ -775,6 +841,7 @@ class IPFabricIngestionTest(
775
841
  graphql_base_name = "ipfabric_ingestion"
776
842
  brief_fields = [
777
843
  "branch",
844
+ "display",
778
845
  "id",
779
846
  "name",
780
847
  "sync",
@@ -874,6 +941,7 @@ class IPFabricIngestionIssueTest(
874
941
  model = IPFabricIngestionIssue
875
942
  graphql_base_name = "ipfabric_ingestion_issue"
876
943
  brief_fields = [
944
+ "display",
877
945
  "exception",
878
946
  "id",
879
947
  "ingestion",
@@ -1005,3 +1073,255 @@ class IPFabricIngestionIssueTest(
1005
1073
  defaults='{"dns_name": ""}',
1006
1074
  exception="IntegrityError: IP address 192.168.1.1/24 already exists",
1007
1075
  )
1076
+
1077
+
1078
+ class IPFabricEndpointTest(
1079
+ APIViewTestCases.GetObjectViewTestCase,
1080
+ APIViewTestCases.ListObjectsViewTestCase,
1081
+ APIViewTestCases.GraphQLTestCase,
1082
+ ):
1083
+ model = IPFabricEndpoint
1084
+ brief_fields = [
1085
+ "display",
1086
+ "endpoint",
1087
+ "id",
1088
+ "name",
1089
+ ]
1090
+ graphql_base_name = "ipfabric_endpoint"
1091
+
1092
+ def _get_list_url(self):
1093
+ return f"{BASE}/endpoint/"
1094
+
1095
+ def _get_detail_url(self, instance):
1096
+ return f"{BASE}/endpoint/{instance.pk}/"
1097
+
1098
+ @classmethod
1099
+ def setUpTestData(cls):
1100
+ # Note: IPFabricEndpoint is read-only in the API (NetBoxReadOnlyModelViewSet)
1101
+ # The endpoints are created by migrations and should not be modified via API
1102
+ # We rely on the existing endpoints created by migrations for these tests
1103
+ pass
1104
+
1105
+
1106
+ class IPFabricFilterTest(
1107
+ APIViewTestCases.GetObjectViewTestCase,
1108
+ APIViewTestCases.ListObjectsViewTestCase,
1109
+ APIViewTestCases.GraphQLTestCase,
1110
+ ):
1111
+ model = IPFabricFilter
1112
+ brief_fields = [
1113
+ "display",
1114
+ "endpoints",
1115
+ "expressions",
1116
+ "filter_type",
1117
+ "id",
1118
+ "name",
1119
+ "syncs",
1120
+ ]
1121
+ graphql_base_name = "ip_fabric_filter"
1122
+
1123
+ def _get_list_url(self):
1124
+ return f"{BASE}/filter/"
1125
+
1126
+ def _get_detail_url(self, instance):
1127
+ return f"{BASE}/filter/{instance.pk}/"
1128
+
1129
+ @classmethod
1130
+ def setUpTestData(cls):
1131
+ # Get existing endpoints created by migrations
1132
+ endpoints = {
1133
+ "site": IPFabricEndpoint.objects.get(endpoint="/inventory/sites/overview"),
1134
+ "device": IPFabricEndpoint.objects.get(endpoint="/inventory/devices"),
1135
+ "ipaddress": IPFabricEndpoint.objects.get(
1136
+ endpoint="/technology/addressing/managed-ip/ipv4"
1137
+ ),
1138
+ "vrf": IPFabricEndpoint.objects.get(
1139
+ endpoint="/technology/routing/vrf/detail"
1140
+ ),
1141
+ }
1142
+
1143
+ # Create sources for snapshots
1144
+ sources = (
1145
+ IPFabricSource.objects.create(
1146
+ name="Filter Test Source A",
1147
+ url="https://filter-a.local",
1148
+ parameters={"auth": "token", "verify": True},
1149
+ last_synced=timezone.now(),
1150
+ ),
1151
+ IPFabricSource.objects.create(
1152
+ name="Filter Test Source B",
1153
+ url="https://filter-b.local",
1154
+ parameters={"auth": "token", "verify": False},
1155
+ last_synced=timezone.now(),
1156
+ ),
1157
+ )
1158
+
1159
+ # Create snapshots for syncs
1160
+ snapshots = (
1161
+ IPFabricSnapshot.objects.create(
1162
+ name="Filter Test Snapshot A",
1163
+ source=sources[0],
1164
+ snapshot_id="filter-snap-a",
1165
+ status="loaded",
1166
+ data={"sites": ["FilterSiteA"]},
1167
+ date=timezone.now(),
1168
+ last_updated=timezone.now(),
1169
+ ),
1170
+ IPFabricSnapshot.objects.create(
1171
+ name="Filter Test Snapshot B",
1172
+ source=sources[1],
1173
+ snapshot_id="filter-snap-b",
1174
+ status="loaded",
1175
+ data={"devices": ["FilterDevice1"]},
1176
+ date=timezone.now(),
1177
+ last_updated=timezone.now(),
1178
+ ),
1179
+ )
1180
+
1181
+ # Create syncs to associate with filters
1182
+ syncs = (
1183
+ IPFabricSync.objects.create(
1184
+ name="Filter Test Sync A",
1185
+ snapshot_data=snapshots[0],
1186
+ parameters={"site": True, "device": False},
1187
+ ),
1188
+ IPFabricSync.objects.create(
1189
+ name="Filter Test Sync B",
1190
+ snapshot_data=snapshots[1],
1191
+ parameters={"device": True, "interface": True},
1192
+ ),
1193
+ )
1194
+
1195
+ # Create existing filters for testing
1196
+ IPFabricFilter.objects.create(
1197
+ name="Filter D",
1198
+ description="Sites and devices filter",
1199
+ filter_type=IPFabricFilterTypeChoices.AND,
1200
+ )
1201
+ IPFabricFilter.objects.create(
1202
+ name="Filter E",
1203
+ description="Device and VRF filter",
1204
+ filter_type=IPFabricFilterTypeChoices.OR,
1205
+ )
1206
+ IPFabricFilter.objects.create(
1207
+ name="Filter F",
1208
+ description="IP address filter",
1209
+ filter_type=IPFabricFilterTypeChoices.AND,
1210
+ )
1211
+
1212
+ cls.create_data = [
1213
+ {
1214
+ "name": "Filter A",
1215
+ "description": "Test filter A",
1216
+ "filter_type": IPFabricFilterTypeChoices.AND,
1217
+ "endpoints": [endpoints["site"].pk, endpoints["device"].pk],
1218
+ "syncs": [syncs[0].pk],
1219
+ },
1220
+ {
1221
+ "name": "Filter B",
1222
+ "description": "Test filter B",
1223
+ "filter_type": IPFabricFilterTypeChoices.OR,
1224
+ "endpoints": [endpoints["device"].pk],
1225
+ "syncs": [syncs[1].pk],
1226
+ },
1227
+ {
1228
+ "name": "Filter C",
1229
+ "description": "Test filter C",
1230
+ "filter_type": IPFabricFilterTypeChoices.AND,
1231
+ "endpoints": [endpoints["ipaddress"].pk, endpoints["vrf"].pk],
1232
+ "syncs": [],
1233
+ },
1234
+ ]
1235
+ cls.bulk_update_data = {
1236
+ "description": "Updated filter description",
1237
+ }
1238
+
1239
+
1240
+ class IPFabricFilterExpressionTest(
1241
+ APIViewTestCases.GetObjectViewTestCase,
1242
+ APIViewTestCases.ListObjectsViewTestCase,
1243
+ APIViewTestCases.GraphQLTestCase,
1244
+ ):
1245
+ model = IPFabricFilterExpression
1246
+ brief_fields = [
1247
+ "display",
1248
+ "expression",
1249
+ "filters",
1250
+ "id",
1251
+ "name",
1252
+ ]
1253
+ graphql_base_name = "ip_fabric_filter_expression"
1254
+
1255
+ def _get_list_url(self):
1256
+ return f"{BASE}/filter-expression/"
1257
+
1258
+ def _get_detail_url(self, instance):
1259
+ return f"{BASE}/filter-expression/{instance.pk}/"
1260
+
1261
+ @classmethod
1262
+ def setUpTestData(cls):
1263
+ # Create filters to associate with expressions
1264
+ filters = (
1265
+ IPFabricFilter.objects.create(
1266
+ name="Expression Test Filter A",
1267
+ description="First expression test filter",
1268
+ filter_type=IPFabricFilterTypeChoices.AND,
1269
+ ),
1270
+ IPFabricFilter.objects.create(
1271
+ name="Expression Test Filter B",
1272
+ description="Second expression test filter",
1273
+ filter_type=IPFabricFilterTypeChoices.OR,
1274
+ ),
1275
+ IPFabricFilter.objects.create(
1276
+ name="Expression Test Filter C",
1277
+ description="Third expression test filter",
1278
+ filter_type=IPFabricFilterTypeChoices.AND,
1279
+ ),
1280
+ )
1281
+
1282
+ # Create existing filter expressions for testing
1283
+ IPFabricFilterExpression.objects.create(
1284
+ name="Expression D",
1285
+ description="Sites expression",
1286
+ expression=[{"siteName": ["eq", "Site1"]}],
1287
+ )
1288
+ IPFabricFilterExpression.objects.create(
1289
+ name="Expression E",
1290
+ description="Devices expression",
1291
+ expression=[{"hostname": ["like", "router%"]}],
1292
+ )
1293
+ IPFabricFilterExpression.objects.create(
1294
+ name="Expression F",
1295
+ description="Complex expression",
1296
+ expression=[
1297
+ {"siteName": ["eq", "Site1"]},
1298
+ {"hostname": ["like", "switch%"]},
1299
+ ],
1300
+ )
1301
+
1302
+ cls.create_data = [
1303
+ {
1304
+ "name": "Expression A",
1305
+ "description": "Test expression A",
1306
+ "expression": [{"siteName": ["eq", "TestSite"]}],
1307
+ "filters": [filters[0].pk],
1308
+ },
1309
+ {
1310
+ "name": "Expression B",
1311
+ "description": "Test expression B",
1312
+ "expression": [{"hostname": ["like", "test-router%"]}],
1313
+ "filters": [filters[1].pk, filters[2].pk],
1314
+ },
1315
+ {
1316
+ "name": "Expression C",
1317
+ "description": "Test expression C",
1318
+ "expression": [
1319
+ {"siteName": ["eq", "TestSite2"]},
1320
+ {"vendor": ["eq", "Cisco"]},
1321
+ ],
1322
+ "filters": [filters[0].pk],
1323
+ },
1324
+ ]
1325
+ cls.bulk_update_data = {
1326
+ "description": "Updated expression description",
1327
+ }