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
|
@@ -7,7 +7,11 @@ from django.core import serializers
|
|
|
7
7
|
from django.test import TestCase
|
|
8
8
|
from django.utils import timezone
|
|
9
9
|
|
|
10
|
+
from ipfabric_netbox.choices import IPFabricFilterTypeChoices
|
|
10
11
|
from ipfabric_netbox.choices import IPFabricSnapshotStatusModelChoices
|
|
12
|
+
from ipfabric_netbox.models import IPFabricEndpoint
|
|
13
|
+
from ipfabric_netbox.models import IPFabricFilter
|
|
14
|
+
from ipfabric_netbox.models import IPFabricFilterExpression
|
|
11
15
|
from ipfabric_netbox.models import IPFabricIngestion
|
|
12
16
|
from ipfabric_netbox.models import IPFabricSnapshot
|
|
13
17
|
from ipfabric_netbox.models import IPFabricSource
|
|
@@ -123,19 +127,19 @@ class IPFabricTransformMapModelTestCase(TestCase):
|
|
|
123
127
|
snapshot_data=snapshot,
|
|
124
128
|
update_custom_fields=True,
|
|
125
129
|
parameters={
|
|
126
|
-
"vrf": False,
|
|
127
|
-
"site": True,
|
|
128
|
-
"vlan": False,
|
|
129
|
-
"
|
|
130
|
-
"device": True,
|
|
131
|
-
"prefix": False,
|
|
132
|
-
"platform": True,
|
|
133
|
-
"interface": False,
|
|
134
|
-
"ipaddress": False,
|
|
135
|
-
"devicerole": True,
|
|
136
|
-
"devicetype": True,
|
|
137
|
-
"manufacturer": True,
|
|
138
|
-
"virtualchassis": False,
|
|
130
|
+
"ipam.vrf": False,
|
|
131
|
+
"dcim.site": True,
|
|
132
|
+
"ipam.vlan": False,
|
|
133
|
+
"groups": [],
|
|
134
|
+
"dcim.device": True,
|
|
135
|
+
"ipam.prefix": False,
|
|
136
|
+
"dcim.platform": True,
|
|
137
|
+
"dcim.interface": False,
|
|
138
|
+
"ipam.ipaddress": False,
|
|
139
|
+
"dcim.devicerole": True,
|
|
140
|
+
"dcim.devicetype": True,
|
|
141
|
+
"dcim.manufacturer": True,
|
|
142
|
+
"dcim.virtualchassis": False,
|
|
139
143
|
},
|
|
140
144
|
)
|
|
141
145
|
|
|
@@ -143,15 +147,15 @@ class IPFabricTransformMapModelTestCase(TestCase):
|
|
|
143
147
|
|
|
144
148
|
runner = IPFabricSyncRunner(
|
|
145
149
|
settings={
|
|
146
|
-
"site": True,
|
|
147
|
-
"
|
|
148
|
-
"device": True,
|
|
149
|
-
"platform": True,
|
|
150
|
-
"interface": False,
|
|
151
|
-
"devicerole": True,
|
|
152
|
-
"devicetype": True,
|
|
153
|
-
"manufacturer": True,
|
|
154
|
-
"virtualchassis": True,
|
|
150
|
+
"dcim.site": True,
|
|
151
|
+
"groups": [],
|
|
152
|
+
"dcim.device": True,
|
|
153
|
+
"dcim.platform": True,
|
|
154
|
+
"dcim.interface": False,
|
|
155
|
+
"dcim.devicerole": True,
|
|
156
|
+
"dcim.devicetype": True,
|
|
157
|
+
"dcim.manufacturer": True,
|
|
158
|
+
"dcim.virtualchassis": True,
|
|
155
159
|
"snapshot_id": "12dd8c61-129c-431a-b98b-4c9211571f89",
|
|
156
160
|
},
|
|
157
161
|
sync=sync,
|
|
@@ -251,7 +255,10 @@ class IPFabricTransformMapModelTestCase(TestCase):
|
|
|
251
255
|
def test_transform_map(self):
|
|
252
256
|
site_transform_map = IPFabricTransformMap.objects.get(name="Site Transform Map")
|
|
253
257
|
self.assertEqual(site_transform_map.name, "Site Transform Map")
|
|
254
|
-
self.
|
|
258
|
+
self.assertIsNotNone(site_transform_map.source_endpoint)
|
|
259
|
+
self.assertEqual(
|
|
260
|
+
site_transform_map.source_endpoint.endpoint, "/inventory/sites/overview"
|
|
261
|
+
)
|
|
255
262
|
self.assertEqual(
|
|
256
263
|
site_transform_map.target_model,
|
|
257
264
|
ContentType.objects.filter(app_label="dcim", model="site")[0],
|
|
@@ -287,7 +294,7 @@ class IPFabricTransformMapModelTestCase(TestCase):
|
|
|
287
294
|
"pk": site_transform_map.pk,
|
|
288
295
|
"fields": {
|
|
289
296
|
"name": "Site Transform Map",
|
|
290
|
-
"
|
|
297
|
+
"source_endpoint": site_transform_map.source_endpoint.pk,
|
|
291
298
|
"target_model": ContentType.objects.get(
|
|
292
299
|
app_label="dcim", model="site"
|
|
293
300
|
).pk,
|
|
@@ -358,15 +365,15 @@ class IPFabricTransformMapModelTestCase(TestCase):
|
|
|
358
365
|
|
|
359
366
|
runner = IPFabricSyncRunner(
|
|
360
367
|
settings={
|
|
361
|
-
"site": True,
|
|
362
|
-
"
|
|
363
|
-
"device": True,
|
|
364
|
-
"platform": True,
|
|
365
|
-
"interface": False,
|
|
366
|
-
"devicerole": True,
|
|
367
|
-
"devicetype": True,
|
|
368
|
-
"manufacturer": True,
|
|
369
|
-
"virtualchassis": True,
|
|
368
|
+
"dcim.site": True,
|
|
369
|
+
"groups": [],
|
|
370
|
+
"dcim.device": True,
|
|
371
|
+
"dcim.platform": True,
|
|
372
|
+
"dcim.interface": False,
|
|
373
|
+
"dcim.devicerole": True,
|
|
374
|
+
"dcim.devicetype": True,
|
|
375
|
+
"dcim.manufacturer": True,
|
|
376
|
+
"dcim.virtualchassis": True,
|
|
370
377
|
"snapshot_id": "12dd8c61-129c-431a-b98b-4c9211571f89",
|
|
371
378
|
},
|
|
372
379
|
sync=sync,
|
|
@@ -409,11 +416,12 @@ class IPFabricTransformMapModelTestCase(TestCase):
|
|
|
409
416
|
"slug": None,
|
|
410
417
|
}
|
|
411
418
|
|
|
419
|
+
device_endpoint = IPFabricEndpoint.objects.get(endpoint="/inventory/devices")
|
|
412
420
|
transform_field = IPFabricTransformField.objects.get(
|
|
413
421
|
source_field="hostname",
|
|
414
422
|
target_field="name",
|
|
415
423
|
transform_map=IPFabricTransformMap.objects.get(
|
|
416
|
-
|
|
424
|
+
source_endpoint=device_endpoint,
|
|
417
425
|
target_model=ContentType.objects.get(app_label="dcim", model="device"),
|
|
418
426
|
),
|
|
419
427
|
)
|
|
@@ -426,3 +434,203 @@ class IPFabricTransformMapModelTestCase(TestCase):
|
|
|
426
434
|
cf=sync.update_custom_fields,
|
|
427
435
|
)
|
|
428
436
|
self.assertEqual(device_object.name, "L21PE152 - test")
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
class IPFabricFilterCombinationTestCase(TestCase):
|
|
440
|
+
"""Test cases for recursive filter merging with nested and/or structures."""
|
|
441
|
+
|
|
442
|
+
@classmethod
|
|
443
|
+
def setUpTestData(cls):
|
|
444
|
+
# Create source
|
|
445
|
+
cls.source = IPFabricSource.objects.create(
|
|
446
|
+
name="Test Source",
|
|
447
|
+
url="https://test.ipfabric.io",
|
|
448
|
+
status="active",
|
|
449
|
+
parameters={"auth": "test_token", "verify": False},
|
|
450
|
+
)
|
|
451
|
+
# Create snapshot
|
|
452
|
+
cls.snapshot = IPFabricSnapshot.objects.create(
|
|
453
|
+
name="Test Snapshot",
|
|
454
|
+
source=cls.source,
|
|
455
|
+
snapshot_id="test-snapshot-123",
|
|
456
|
+
status=IPFabricSnapshotStatusModelChoices.STATUS_LOADED,
|
|
457
|
+
data={"sites": ["Site1", "Site2"]},
|
|
458
|
+
)
|
|
459
|
+
# Create sync
|
|
460
|
+
cls.sync = IPFabricSync.objects.create(
|
|
461
|
+
name="Test Sync",
|
|
462
|
+
snapshot_data=cls.snapshot,
|
|
463
|
+
status="new",
|
|
464
|
+
)
|
|
465
|
+
# Get or create endpoint
|
|
466
|
+
cls.endpoint, _ = IPFabricEndpoint.objects.get_or_create(
|
|
467
|
+
endpoint="/inventory/devices",
|
|
468
|
+
defaults={"name": "Devices Endpoint"},
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
def test_combine_filters_merges_same_nested_or_structure(self):
|
|
472
|
+
"""Test that filters with same nested 'or' structure merge correctly."""
|
|
473
|
+
# Create two AND-type filters
|
|
474
|
+
filter1 = IPFabricFilter.objects.create(
|
|
475
|
+
name="Filter with router1",
|
|
476
|
+
filter_type=IPFabricFilterTypeChoices.AND,
|
|
477
|
+
)
|
|
478
|
+
filter1.endpoints.add(self.endpoint)
|
|
479
|
+
filter1.syncs.add(self.sync)
|
|
480
|
+
|
|
481
|
+
filter2 = IPFabricFilter.objects.create(
|
|
482
|
+
name="Filter with router2",
|
|
483
|
+
filter_type=IPFabricFilterTypeChoices.AND,
|
|
484
|
+
)
|
|
485
|
+
filter2.endpoints.add(self.endpoint)
|
|
486
|
+
filter2.syncs.add(self.sync)
|
|
487
|
+
|
|
488
|
+
# Create expressions with same nested structure
|
|
489
|
+
expr1 = IPFabricFilterExpression.objects.create(
|
|
490
|
+
name="Expression router1",
|
|
491
|
+
expression=[{"or": [{"hostname": ["like", "router1"]}]}],
|
|
492
|
+
)
|
|
493
|
+
expr1.filters.add(filter1)
|
|
494
|
+
|
|
495
|
+
expr2 = IPFabricFilterExpression.objects.create(
|
|
496
|
+
name="Expression router2",
|
|
497
|
+
expression=[{"or": [{"hostname": ["like", "router2"]}]}],
|
|
498
|
+
)
|
|
499
|
+
expr2.filters.add(filter2)
|
|
500
|
+
|
|
501
|
+
# Combine filters
|
|
502
|
+
result = self.endpoint.combine_filters(sync=self.sync)
|
|
503
|
+
|
|
504
|
+
# Should have merged into single nested structure
|
|
505
|
+
self.assertIn("and", result)
|
|
506
|
+
self.assertEqual(len(result["and"]), 1)
|
|
507
|
+
self.assertIn("or", result["and"][0])
|
|
508
|
+
self.assertEqual(len(result["and"][0]["or"]), 2)
|
|
509
|
+
|
|
510
|
+
# Verify both hostnames are present
|
|
511
|
+
hostnames = [item["hostname"][1] for item in result["and"][0]["or"]]
|
|
512
|
+
self.assertIn("router1", hostnames)
|
|
513
|
+
self.assertIn("router2", hostnames)
|
|
514
|
+
|
|
515
|
+
def test_combine_filters_deeply_nested_structures(self):
|
|
516
|
+
"""Test that deeply nested structures (3+ levels) merge correctly."""
|
|
517
|
+
# Create two AND-type filters with 3-level nesting
|
|
518
|
+
filter1 = IPFabricFilter.objects.create(
|
|
519
|
+
name="Deep filter cisco",
|
|
520
|
+
filter_type=IPFabricFilterTypeChoices.AND,
|
|
521
|
+
)
|
|
522
|
+
filter1.endpoints.add(self.endpoint)
|
|
523
|
+
filter1.syncs.add(self.sync)
|
|
524
|
+
|
|
525
|
+
filter2 = IPFabricFilter.objects.create(
|
|
526
|
+
name="Deep filter juniper",
|
|
527
|
+
filter_type=IPFabricFilterTypeChoices.AND,
|
|
528
|
+
)
|
|
529
|
+
filter2.endpoints.add(self.endpoint)
|
|
530
|
+
filter2.syncs.add(self.sync)
|
|
531
|
+
|
|
532
|
+
# Create deeply nested expressions
|
|
533
|
+
expr1 = IPFabricFilterExpression.objects.create(
|
|
534
|
+
name="Deep expression cisco",
|
|
535
|
+
expression=[{"or": [{"and": [{"or": [{"vendor": ["eq", "cisco"]}]}]}]}],
|
|
536
|
+
)
|
|
537
|
+
expr1.filters.add(filter1)
|
|
538
|
+
|
|
539
|
+
expr2 = IPFabricFilterExpression.objects.create(
|
|
540
|
+
name="Deep expression juniper",
|
|
541
|
+
expression=[{"or": [{"and": [{"or": [{"vendor": ["eq", "juniper"]}]}]}]}],
|
|
542
|
+
)
|
|
543
|
+
expr2.filters.add(filter2)
|
|
544
|
+
|
|
545
|
+
# Combine filters
|
|
546
|
+
result = self.endpoint.combine_filters(sync=self.sync)
|
|
547
|
+
|
|
548
|
+
# Should have merged at all levels
|
|
549
|
+
self.assertIn("and", result)
|
|
550
|
+
self.assertEqual(len(result["and"]), 1)
|
|
551
|
+
self.assertIn("or", result["and"][0])
|
|
552
|
+
self.assertEqual(len(result["and"][0]["or"]), 1)
|
|
553
|
+
self.assertIn("and", result["and"][0]["or"][0])
|
|
554
|
+
self.assertEqual(len(result["and"][0]["or"][0]["and"]), 1)
|
|
555
|
+
self.assertIn("or", result["and"][0]["or"][0]["and"][0])
|
|
556
|
+
|
|
557
|
+
# Verify both vendors are in innermost or array
|
|
558
|
+
innermost_or = result["and"][0]["or"][0]["and"][0]["or"]
|
|
559
|
+
self.assertEqual(len(innermost_or), 2)
|
|
560
|
+
vendors = [item["vendor"][1] for item in innermost_or]
|
|
561
|
+
self.assertIn("cisco", vendors)
|
|
562
|
+
self.assertIn("juniper", vendors)
|
|
563
|
+
|
|
564
|
+
def test_combine_filters_different_structures_no_merge(self):
|
|
565
|
+
"""Test that filters with different structures don't merge."""
|
|
566
|
+
# Create two AND-type filters with different nested structures
|
|
567
|
+
filter1 = IPFabricFilter.objects.create(
|
|
568
|
+
name="Filter with or structure",
|
|
569
|
+
filter_type=IPFabricFilterTypeChoices.AND,
|
|
570
|
+
)
|
|
571
|
+
filter1.endpoints.add(self.endpoint)
|
|
572
|
+
filter1.syncs.add(self.sync)
|
|
573
|
+
|
|
574
|
+
filter2 = IPFabricFilter.objects.create(
|
|
575
|
+
name="Filter with and structure",
|
|
576
|
+
filter_type=IPFabricFilterTypeChoices.AND,
|
|
577
|
+
)
|
|
578
|
+
filter2.endpoints.add(self.endpoint)
|
|
579
|
+
filter2.syncs.add(self.sync)
|
|
580
|
+
|
|
581
|
+
# Create expressions with different structures
|
|
582
|
+
expr1 = IPFabricFilterExpression.objects.create(
|
|
583
|
+
name="Expression with or",
|
|
584
|
+
expression=[{"or": [{"hostname": ["like", "router1"]}]}],
|
|
585
|
+
)
|
|
586
|
+
expr1.filters.add(filter1)
|
|
587
|
+
|
|
588
|
+
expr2 = IPFabricFilterExpression.objects.create(
|
|
589
|
+
name="Expression with and",
|
|
590
|
+
expression=[{"and": [{"hostname": ["like", "router2"]}]}],
|
|
591
|
+
)
|
|
592
|
+
expr2.filters.add(filter2)
|
|
593
|
+
|
|
594
|
+
# Combine filters
|
|
595
|
+
result = self.endpoint.combine_filters(sync=self.sync)
|
|
596
|
+
|
|
597
|
+
# Should have two separate structures (not merged)
|
|
598
|
+
self.assertIn("and", result)
|
|
599
|
+
self.assertEqual(len(result["and"]), 2)
|
|
600
|
+
|
|
601
|
+
# One should have 'or', the other should have 'and'
|
|
602
|
+
structures = [list(item.keys()) for item in result["and"]]
|
|
603
|
+
self.assertIn(["or"], structures)
|
|
604
|
+
self.assertIn(["and"], structures)
|
|
605
|
+
|
|
606
|
+
def test_combine_filters_multiple_filters_same_structure(self):
|
|
607
|
+
"""Test that 3+ filters with same structure all merge correctly."""
|
|
608
|
+
# Create three AND-type filters with same nested structure
|
|
609
|
+
for i in range(1, 4):
|
|
610
|
+
filter_obj = IPFabricFilter.objects.create(
|
|
611
|
+
name=f"Filter with router{i}",
|
|
612
|
+
filter_type=IPFabricFilterTypeChoices.AND,
|
|
613
|
+
)
|
|
614
|
+
filter_obj.endpoints.add(self.endpoint)
|
|
615
|
+
filter_obj.syncs.add(self.sync)
|
|
616
|
+
|
|
617
|
+
expr = IPFabricFilterExpression.objects.create(
|
|
618
|
+
name=f"Expression router{i}",
|
|
619
|
+
expression=[{"or": [{"hostname": ["like", f"router{i}"]}]}],
|
|
620
|
+
)
|
|
621
|
+
expr.filters.add(filter_obj)
|
|
622
|
+
|
|
623
|
+
# Combine filters
|
|
624
|
+
result = self.endpoint.combine_filters(sync=self.sync)
|
|
625
|
+
|
|
626
|
+
# Should have merged all three into single nested structure
|
|
627
|
+
self.assertIn("and", result)
|
|
628
|
+
self.assertEqual(len(result["and"]), 1)
|
|
629
|
+
self.assertIn("or", result["and"][0])
|
|
630
|
+
self.assertEqual(len(result["and"][0]["or"]), 3)
|
|
631
|
+
|
|
632
|
+
# Verify all three hostnames are present
|
|
633
|
+
hostnames = [item["hostname"][1] for item in result["and"][0]["or"]]
|
|
634
|
+
self.assertIn("router1", hostnames)
|
|
635
|
+
self.assertIn("router2", hostnames)
|
|
636
|
+
self.assertIn("router3", hostnames)
|