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.

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 +18 -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.2b8.dist-info → ipfabric_netbox-4.3.2b10.dist-info}/METADATA +3 -2
  48. {ipfabric_netbox-4.3.2b8.dist-info → ipfabric_netbox-4.3.2b10.dist-info}/RECORD +49 -33
  49. {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
- "sites": [],
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
- "sites": [],
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.assertEqual(site_transform_map.source_model, "site")
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
- "source_model": "site",
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
- "sites": [],
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
- source_model="device",
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)