ipfabric_netbox 4.2.2b2__tar.gz → 4.2.2b4__tar.gz

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 (87) hide show
  1. {ipfabric_netbox-4.2.2b2 → ipfabric_netbox-4.2.2b4}/PKG-INFO +5 -3
  2. {ipfabric_netbox-4.2.2b2 → ipfabric_netbox-4.2.2b4}/README.md +3 -1
  3. {ipfabric_netbox-4.2.2b2 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/__init__.py +2 -2
  4. {ipfabric_netbox-4.2.2b2 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/forms.py +84 -21
  5. {ipfabric_netbox-4.2.2b2 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/migrations/0001_initial_squashed_0013_switch_to_branching_plugin.py +37 -17
  6. ipfabric_netbox-4.2.2b4/ipfabric_netbox/migrations/0019_alter_ipfabrictransformmap_options_and_more.py +19 -0
  7. {ipfabric_netbox-4.2.2b2 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/models.py +30 -6
  8. {ipfabric_netbox-4.2.2b2 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/template_content.py +9 -6
  9. {ipfabric_netbox-4.2.2b2 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/templates/ipfabric_netbox/inc/site_topology_button.html +0 -2
  10. {ipfabric_netbox-4.2.2b2 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/templates/ipfabric_netbox/ipfabric_table.html +1 -1
  11. {ipfabric_netbox-4.2.2b2 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/templates/ipfabric_netbox/ipfabricsync.html +2 -1
  12. {ipfabric_netbox-4.2.2b2 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/templates/ipfabric_netbox/ipfabrictransformmap_restore.html +1 -1
  13. {ipfabric_netbox-4.2.2b2 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/tests/api/test_api.py +1 -1
  14. {ipfabric_netbox-4.2.2b2 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/tests/test_forms.py +111 -18
  15. ipfabric_netbox-4.2.2b4/ipfabric_netbox/tests/test_views.py +2166 -0
  16. {ipfabric_netbox-4.2.2b2 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/urls.py +15 -14
  17. {ipfabric_netbox-4.2.2b2 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/utilities/transform_map.py +12 -10
  18. {ipfabric_netbox-4.2.2b2 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/views.py +153 -139
  19. {ipfabric_netbox-4.2.2b2 → ipfabric_netbox-4.2.2b4}/pyproject.toml +2 -2
  20. {ipfabric_netbox-4.2.2b2 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/api/__init__.py +0 -0
  21. {ipfabric_netbox-4.2.2b2 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/api/serializers.py +0 -0
  22. {ipfabric_netbox-4.2.2b2 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/api/urls.py +0 -0
  23. {ipfabric_netbox-4.2.2b2 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/api/views.py +0 -0
  24. {ipfabric_netbox-4.2.2b2 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/choices.py +0 -0
  25. {ipfabric_netbox-4.2.2b2 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/data/transform_map.json +0 -0
  26. {ipfabric_netbox-4.2.2b2 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/exceptions.py +0 -0
  27. {ipfabric_netbox-4.2.2b2 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/filtersets.py +0 -0
  28. {ipfabric_netbox-4.2.2b2 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/graphql/__init__.py +0 -0
  29. {ipfabric_netbox-4.2.2b2 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/graphql/enums.py +0 -0
  30. {ipfabric_netbox-4.2.2b2 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/graphql/filters.py +0 -0
  31. {ipfabric_netbox-4.2.2b2 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/graphql/schema.py +0 -0
  32. {ipfabric_netbox-4.2.2b2 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/graphql/types.py +0 -0
  33. {ipfabric_netbox-4.2.2b2 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/jobs.py +0 -0
  34. {ipfabric_netbox-4.2.2b2 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/migrations/0001_initial.py +0 -0
  35. {ipfabric_netbox-4.2.2b2 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/migrations/0002_ipfabricsnapshot_status.py +0 -0
  36. {ipfabric_netbox-4.2.2b2 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/migrations/0003_ipfabricsource_type_and_more.py +0 -0
  37. {ipfabric_netbox-4.2.2b2 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/migrations/0004_ipfabricsync_auto_merge.py +0 -0
  38. {ipfabric_netbox-4.2.2b2 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/migrations/0005_alter_ipfabricrelationshipfield_source_model_and_more.py +0 -0
  39. {ipfabric_netbox-4.2.2b2 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/migrations/0006_alter_ipfabrictransformmap_target_model.py +0 -0
  40. {ipfabric_netbox-4.2.2b2 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/migrations/0007_prepare_custom_fields.py +0 -0
  41. {ipfabric_netbox-4.2.2b2 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/migrations/0008_prepare_transform_maps.py +0 -0
  42. {ipfabric_netbox-4.2.2b2 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/migrations/0009_transformmap_changes_for_netbox_v4_2.py +0 -0
  43. {ipfabric_netbox-4.2.2b2 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/migrations/0010_remove_uuid_from_get_or_create.py +0 -0
  44. {ipfabric_netbox-4.2.2b2 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/migrations/0011_update_part_number_DCIM_inventory_item_template.py +0 -0
  45. {ipfabric_netbox-4.2.2b2 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/migrations/0012_remove_status_field.py +0 -0
  46. {ipfabric_netbox-4.2.2b2 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/migrations/0013_switch_to_branching_plugin.py +0 -0
  47. {ipfabric_netbox-4.2.2b2 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/migrations/0014_ipfabrictransformmapgroup_ipfabrictransformmap_group.py +0 -0
  48. {ipfabric_netbox-4.2.2b2 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/migrations/0015_ipfabricingestionissue.py +0 -0
  49. {ipfabric_netbox-4.2.2b2 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/migrations/0016_tags_and_changelog_for_snapshots.py +0 -0
  50. {ipfabric_netbox-4.2.2b2 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/migrations/0017_ipfabricsync_update_custom_fields.py +0 -0
  51. {ipfabric_netbox-4.2.2b2 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/migrations/0018_remove_type_field.py +0 -0
  52. {ipfabric_netbox-4.2.2b2 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/migrations/__init__.py +0 -0
  53. {ipfabric_netbox-4.2.2b2 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/navigation.py +0 -0
  54. {ipfabric_netbox-4.2.2b2 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/signals.py +0 -0
  55. {ipfabric_netbox-4.2.2b2 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/tables.py +0 -0
  56. {ipfabric_netbox-4.2.2b2 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/templates/ipfabric_netbox/inc/clone_form.html +0 -0
  57. {ipfabric_netbox-4.2.2b2 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/templates/ipfabric_netbox/inc/diff.html +0 -0
  58. {ipfabric_netbox-4.2.2b2 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/templates/ipfabric_netbox/inc/json.html +0 -0
  59. {ipfabric_netbox-4.2.2b2 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/templates/ipfabric_netbox/inc/logs_pending.html +0 -0
  60. {ipfabric_netbox-4.2.2b2 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/templates/ipfabric_netbox/inc/merge_form.html +0 -0
  61. {ipfabric_netbox-4.2.2b2 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/templates/ipfabric_netbox/inc/site_topology_modal.html +0 -0
  62. {ipfabric_netbox-4.2.2b2 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/templates/ipfabric_netbox/inc/snapshotdata.html +0 -0
  63. {ipfabric_netbox-4.2.2b2 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/templates/ipfabric_netbox/inc/transform_map_field_map.html +0 -0
  64. {ipfabric_netbox-4.2.2b2 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/templates/ipfabric_netbox/inc/transform_map_relationship_map.html +0 -0
  65. {ipfabric_netbox-4.2.2b2 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/templates/ipfabric_netbox/ipfabricingestion.html +0 -0
  66. {ipfabric_netbox-4.2.2b2 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/templates/ipfabric_netbox/ipfabricsnapshot.html +0 -0
  67. {ipfabric_netbox-4.2.2b2 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/templates/ipfabric_netbox/ipfabricsource.html +0 -0
  68. {ipfabric_netbox-4.2.2b2 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/templates/ipfabric_netbox/ipfabrictransformmap.html +0 -0
  69. {ipfabric_netbox-4.2.2b2 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/templates/ipfabric_netbox/ipfabrictransformmap_list.html +0 -0
  70. {ipfabric_netbox-4.2.2b2 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/templates/ipfabric_netbox/ipfabrictransformmapgroup.html +0 -0
  71. {ipfabric_netbox-4.2.2b2 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/templates/ipfabric_netbox/partials/ingestion_all.html +0 -0
  72. {ipfabric_netbox-4.2.2b2 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/templates/ipfabric_netbox/partials/ingestion_progress.html +0 -0
  73. {ipfabric_netbox-4.2.2b2 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/templates/ipfabric_netbox/partials/ingestion_statistics.html +0 -0
  74. {ipfabric_netbox-4.2.2b2 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/templates/ipfabric_netbox/partials/ingestion_status.html +0 -0
  75. {ipfabric_netbox-4.2.2b2 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/templates/ipfabric_netbox/partials/job_logs.html +0 -0
  76. {ipfabric_netbox-4.2.2b2 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/templates/ipfabric_netbox/partials/object_tabs.html +0 -0
  77. {ipfabric_netbox-4.2.2b2 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/templates/ipfabric_netbox/partials/sync_last_ingestion.html +0 -0
  78. {ipfabric_netbox-4.2.2b2 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/templates/static/ipfabric_netbox/css/rack.css +0 -0
  79. {ipfabric_netbox-4.2.2b2 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/templatetags/__init__.py +0 -0
  80. {ipfabric_netbox-4.2.2b2 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/templatetags/ipfabric_netbox_helpers.py +0 -0
  81. {ipfabric_netbox-4.2.2b2 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/tests/__init__.py +0 -0
  82. {ipfabric_netbox-4.2.2b2 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/tests/api/__init__.py +0 -0
  83. {ipfabric_netbox-4.2.2b2 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/tests/test_models.py +0 -0
  84. {ipfabric_netbox-4.2.2b2 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/utilities/__init__.py +0 -0
  85. {ipfabric_netbox-4.2.2b2 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/utilities/ipfutils.py +0 -0
  86. {ipfabric_netbox-4.2.2b2 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/utilities/logging.py +0 -0
  87. {ipfabric_netbox-4.2.2b2 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/utilities/nbutils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ipfabric_netbox
3
- Version: 4.2.2b2
3
+ Version: 4.2.2b4
4
4
  Summary: NetBox plugin to sync IP Fabric data into NetBox
5
5
  License: MIT
6
6
  Keywords: netbox,ipfabric,plugin,sync
@@ -24,7 +24,7 @@ Requires-Dist: ipfabric (>=6.6.4) ; extra != "ipfabric_6_10" and extra != "ipfab
24
24
  Requires-Dist: ipfabric (>=7.0.0,<7.1.0) ; extra != "ipfabric_6_10" and extra == "ipfabric_7_0" and extra != "ipfabric_7_2" and extra != "ipfabric_7_3"
25
25
  Requires-Dist: ipfabric (>=7.2.0,<7.3.0) ; extra != "ipfabric_6_10" and extra != "ipfabric_7_0" and extra == "ipfabric_7_2" and extra != "ipfabric_7_3"
26
26
  Requires-Dist: ipfabric (>=7.3.0,<7.4.0) ; extra != "ipfabric_6_10" and extra != "ipfabric_7_0" and extra != "ipfabric_7_2" and extra == "ipfabric_7_3"
27
- Requires-Dist: netboxlabs-netbox-branching (>=0.5.5,<0.6.0)
27
+ Requires-Dist: netboxlabs-netbox-branching (==0.7.0)
28
28
  Requires-Dist: netutils
29
29
  Project-URL: Bug Tracker, https://gitlab.com/ip-fabric/integrations/ipfabric-netbox-sync/-/issues
30
30
  Project-URL: Homepage, https://gitlab.com/ip-fabric/integrations/ipfabric-netbox-sync
@@ -65,7 +65,9 @@ These are the required NetBox versions for corresponding plugin version. Any oth
65
65
 
66
66
  | Netbox Version | Plugin Version |
67
67
  |----------------|----------------|
68
- | 4.3.0 and up | 4.0.0 and up |
68
+ | 4.4.0 and up | 4.3.0 and up |
69
+ | 4.3.0 - 4.3.7 | 4.2.2 |
70
+ | 4.3.0 - 4.3.6 | 4.0.0 - 4.2.1 |
69
71
  | 4.2.4 - 4.2.9 | 3.2.2 - 3.2.4 |
70
72
  | 4.2.0 - 4.2.3 | 3.2.0 |
71
73
  | 4.1.5 - 4.1.11 | 3.1.1 - 3.1.3 |
@@ -32,7 +32,9 @@ These are the required NetBox versions for corresponding plugin version. Any oth
32
32
 
33
33
  | Netbox Version | Plugin Version |
34
34
  |----------------|----------------|
35
- | 4.3.0 and up | 4.0.0 and up |
35
+ | 4.4.0 and up | 4.3.0 and up |
36
+ | 4.3.0 - 4.3.7 | 4.2.2 |
37
+ | 4.3.0 - 4.3.6 | 4.0.0 - 4.2.1 |
36
38
  | 4.2.4 - 4.2.9 | 3.2.2 - 3.2.4 |
37
39
  | 4.2.0 - 4.2.3 | 3.2.0 |
38
40
  | 4.1.5 - 4.1.11 | 3.1.1 - 3.1.3 |
@@ -6,9 +6,9 @@ class NetboxIPFabricConfig(PluginConfig):
6
6
  name = "ipfabric_netbox"
7
7
  verbose_name = "NetBox IP Fabric SoT Plugin"
8
8
  description = "Sync IP Fabric into NetBox"
9
- version = "4.2.2b2"
9
+ version = "4.2.2b4"
10
10
  base_url = "ipfabric"
11
- min_version = "4.2.4"
11
+ min_version = "4.4.0"
12
12
 
13
13
  def ready(self):
14
14
  super().ready()
@@ -263,13 +263,13 @@ class IPFabricTransformFieldForm(NetBoxModelForm):
263
263
  )
264
264
 
265
265
 
266
- class IPFabricTransformMapGroupForm(forms.ModelForm):
266
+ class IPFabricTransformMapGroupForm(NetBoxModelForm):
267
267
  class Meta:
268
268
  model = IPFabricTransformMapGroup
269
269
  fields = ("name", "description")
270
270
 
271
271
 
272
- class IPFabricTransformMapForm(forms.ModelForm):
272
+ class IPFabricTransformMapForm(NetBoxModelForm):
273
273
  class Meta:
274
274
  model = IPFabricTransformMap
275
275
  fields = ("name", "group", "source_model", "target_model")
@@ -531,13 +531,28 @@ class IPFabricSyncForm(NetBoxModelForm):
531
531
  widgets = {"source": HTMXSelect()}
532
532
 
533
533
  def __init__(self, *args, **kwargs):
534
+ # The initial data for BooleanFields change to list on HTMX requests.
535
+ # This causes URL to bloat, so we need to sanitize it before it's used.
536
+ initial = kwargs.get("initial", {}).copy()
537
+ for name, value in initial.items():
538
+ if (
539
+ (
540
+ name.startswith("ipf_")
541
+ or (
542
+ name in self.base_fields
543
+ and isinstance(self.base_fields[name], forms.BooleanField)
544
+ )
545
+ )
546
+ and isinstance(value, list)
547
+ and len(value) > 1
548
+ ):
549
+ initial[name] = value[-1] # Keep only the last value
550
+ kwargs["initial"] = initial
534
551
  super().__init__(*args, **kwargs)
535
- self.source_type = None
536
- ingestion_type = get_field_value(self, "source")
552
+
553
+ source = get_field_value(self, "source")
537
554
 
538
555
  if not self.data:
539
- if ingestion_type:
540
- self.source_type = IPFabricSource.objects.get(pk=ingestion_type).type
541
556
  if sites := get_field_value(self, "sites"):
542
557
  sites = list_to_choices(str_to_list(sites))
543
558
  self.fields["sites"].choices = sites
@@ -547,46 +562,82 @@ class IPFabricSyncForm(NetBoxModelForm):
547
562
  snapshot_sites = IPFabricSnapshot.objects.get(pk=snapshot_id).sites
548
563
  choices = list_to_choices(str_to_list(snapshot_sites))
549
564
  self.fields["sites"].choices = choices
565
+ source = self.data.get("source")
550
566
 
567
+ # These values are not populated by data on instance, so we need to set them manually
551
568
  if self.instance and self.instance.pk:
552
569
  if not kwargs.get("initial"):
553
- self.source_type = self.instance.snapshot_data.source.type
554
- self.initial["source"] = self.instance.snapshot_data.source
570
+ source = self.instance.snapshot_data.source
555
571
  if not self.data:
556
572
  self.fields["sites"].choices = list_to_choices(
557
573
  self.instance.snapshot_data.sites
558
574
  )
559
-
560
- self.initial["sites"] = self.instance.parameters.get("sites", [])
575
+ else:
576
+ source = kwargs["initial"].get(
577
+ "source", self.instance.snapshot_data.source
578
+ )
579
+ self.initial["source"] = source
580
+ if "groups" not in self.initial:
561
581
  self.initial["groups"] = self.instance.parameters.get("groups", [])
562
582
 
583
+ # Handle sites field initialization
584
+ if "sites" not in self.initial:
585
+ selected_sites = self.instance.parameters.get("sites", [])
586
+ self.initial["sites"] = selected_sites
587
+
588
+ # Ensure the field has the selected sites as both choices and initial values
589
+ if not self.data and selected_sites:
590
+ # Get current choices and ensure selected sites are included
591
+ current_choices = (
592
+ list(self.fields["sites"].choices)
593
+ if hasattr(self.fields["sites"], "choices")
594
+ else []
595
+ )
596
+ current_choice_values = [choice[0] for choice in current_choices]
597
+
598
+ # Add any missing selected sites to choices
599
+ for site in str_to_list(selected_sites):
600
+ if site not in current_choice_values:
601
+ current_choices.append((site, site))
602
+
603
+ self.fields["sites"].choices = current_choices
604
+ self.fields["sites"].initial = selected_sites
605
+ else:
606
+ self.fields["sites"].initial = self.initial["sites"]
607
+
563
608
  now = local_now().strftime("%Y-%m-%d %H:%M:%S")
564
609
  self.fields["scheduled"].help_text += f" (current time: <strong>{now}</strong>)"
565
610
 
566
611
  # Add backend-specific form fields
567
612
  self.backend_fields = {}
568
613
 
614
+ # Prepare buttons for each target Model
569
615
  for k, v in sync_parameters.items():
570
616
  self.backend_fields[k] = []
571
617
  for name, form_field in v.items():
572
618
  field_name = f"ipf_{name}"
573
619
  self.backend_fields[k].append(field_name)
574
- self.fields[field_name] = copy.copy(form_field)
620
+ self.fields[field_name] = copy.deepcopy(form_field)
575
621
  if self.instance and self.instance.parameters:
576
- self.fields[field_name].initial = self.instance.parameters.get(name)
622
+ value = self.instance.parameters.get(name)
623
+ self.fields[field_name].initial = value
577
624
 
578
625
  # Set fieldsets dynamically based and backend_fields
579
626
  fieldsets = [
580
627
  FieldSet("name", "source", "groups", name=_("IP Fabric Source")),
581
628
  ]
582
- if self.source_type == "local":
583
- fieldsets.append(
584
- FieldSet("snapshot_data", "sites", name=_("Snapshot Information")),
585
- )
586
- else:
587
- fieldsets.append(
588
- FieldSet("snapshot_data", name=_("Snapshot Information")),
589
- )
629
+ # Only show snapshot and sites if source is selected
630
+ if source:
631
+ if isinstance(source, str) or isinstance(source, int):
632
+ source = IPFabricSource.objects.get(pk=source)
633
+ if source.type == "local":
634
+ fieldsets.append(
635
+ FieldSet("snapshot_data", "sites", name=_("Snapshot Information")),
636
+ )
637
+ else:
638
+ fieldsets.append(
639
+ FieldSet("snapshot_data", name=_("Snapshot Information")),
640
+ )
590
641
  for k, v in self.backend_fields.items():
591
642
  fieldsets.append(FieldSet(*v, name=f"{k.upper()} Parameters"))
592
643
  fieldsets.append(
@@ -602,10 +653,22 @@ class IPFabricSyncForm(NetBoxModelForm):
602
653
  def clean(self):
603
654
  super().clean()
604
655
 
656
+ source = self.cleaned_data.get("source")
657
+ snapshot = self.cleaned_data.get("snapshot_data")
658
+
659
+ if (
660
+ source
661
+ and snapshot
662
+ and IPFabricSource.objects.get(pk=source.pk)
663
+ != IPFabricSnapshot.objects.get(pk=snapshot.pk).source
664
+ ):
665
+ raise ValidationError(
666
+ {"snapshot_data": _("Snapshot does not belong to the selected source.")}
667
+ )
668
+
605
669
  sites = self.data.get("sites")
606
670
  self.fields["sites"].choices = list_to_choices(str_to_list(sites))
607
671
  if sites and "snapshot_data" in self.cleaned_data:
608
- snapshot = self.cleaned_data["snapshot_data"]
609
672
  # Check if all sites are valid - fail if any site is not found in snapshot.sites
610
673
  if not all(
611
674
  any(site in snapshot_site for snapshot_site in snapshot.sites)
@@ -29,6 +29,7 @@ if TYPE_CHECKING:
29
29
 
30
30
  def create_custom_field(
31
31
  apps: "apps_type",
32
+ db_alias: str,
32
33
  field_name: str,
33
34
  label: str,
34
35
  target_models: list,
@@ -37,6 +38,7 @@ def create_custom_field(
37
38
  ):
38
39
  """Create a single custom field and link it to required models."""
39
40
  ObjectType = apps.get_model("core", "ObjectType")
41
+ ContentType = apps.get_model("contenttypes", "ContentType")
40
42
 
41
43
  defaults = {
42
44
  "label": label,
@@ -47,23 +49,29 @@ def create_custom_field(
47
49
  "ui_editable": getattr(CustomFieldUIEditableChoices, "NO"),
48
50
  }
49
51
 
50
- custom_field, _ = apps.get_model("extras", "CustomField").objects.update_or_create(
51
- type=getattr(CustomFieldTypeChoices, cf_type.upper()),
52
- name=field_name,
53
- defaults=defaults,
52
+ custom_field, _ = (
53
+ apps.get_model("extras", "CustomField")
54
+ .objects.using(db_alias)
55
+ .update_or_create(
56
+ type=getattr(CustomFieldTypeChoices, cf_type.upper()),
57
+ name=field_name,
58
+ defaults=defaults,
59
+ )
54
60
  )
55
61
 
56
62
  for model in target_models:
57
- custom_field.object_types.add(ObjectType.objects.get_for_model(model))
63
+ custom_field.object_types.add(ContentType.objects.get_for_model(model))
58
64
 
59
65
 
60
66
  def prepare_custom_fields(apps: "apps_type", schema_editor: "BaseDatabaseSchemaEditor"):
61
67
  """Forward migration to prepare ipfabric_netbox custom fields and links."""
68
+ db_alias = schema_editor.connection.alias
62
69
  Device = apps.get_model("dcim", "Device")
63
70
  Site = apps.get_model("dcim", "Site")
64
71
 
65
72
  create_custom_field(
66
73
  apps,
74
+ db_alias,
67
75
  "ipfabric_source",
68
76
  "IP Fabric Source",
69
77
  [Device, Site],
@@ -72,31 +80,41 @@ def prepare_custom_fields(apps: "apps_type", schema_editor: "BaseDatabaseSchemaE
72
80
  )
73
81
  create_custom_field(
74
82
  apps,
83
+ db_alias,
75
84
  "ipfabric_ingestion",
76
85
  "IP Fabric Last Ingestion",
77
86
  [Device, Site],
78
87
  cf_type="type_object",
79
88
  object_type=apps.get_model("ipfabric_netbox", "IPFabricIngestion"),
80
89
  )
81
- cl, _ = apps.get_model("extras", "CustomLink").objects.update_or_create(
82
- defaults={
83
- "link_text": "{% if object.custom_field_data.ipfabric_source is defined %}{% set SOURCE_ID = object.custom_field_data.ipfabric_source %}{% if SOURCE_ID %}IP Fabric{% endif %}{% endif %}",
84
- "link_url": '{% if object.custom_field_data.ipfabric_source is defined %}{% set SOURCE_ID = object.custom_field_data.ipfabric_source %}{% if SOURCE_ID %}{% set BASE_URL = object.custom_fields.filter(related_object_type__model="ipfabricsource").first().related_object_type.model_class().objects.get(pk=SOURCE_ID).url %}{{ BASE_URL }}/inventory/devices?options={"filters":{"sn": ["like","{{ object.serial }}"]}}{% endif %}{%endif%}',
85
- "new_window": True,
86
- "button_class": CustomLinkButtonClassChoices.BLUE,
87
- },
88
- name="ipfabric",
90
+ cl, _ = (
91
+ apps.get_model("extras", "CustomLink")
92
+ .objects.using(db_alias)
93
+ .update_or_create(
94
+ defaults={
95
+ "link_text": "{% if object.custom_field_data.ipfabric_source is defined %}{% set SOURCE_ID = object.custom_field_data.ipfabric_source %}{% if SOURCE_ID %}IP Fabric{% endif %}{% endif %}",
96
+ "link_url": '{% if object.custom_field_data.ipfabric_source is defined %}{% set SOURCE_ID = object.custom_field_data.ipfabric_source %}{% if SOURCE_ID %}{% set BASE_URL = object.custom_fields.filter(related_object_type__model="ipfabricsource").first().related_object_type.model_class().objects.get(pk=SOURCE_ID).url %}{{ BASE_URL }}/inventory/devices?options={"filters":{"sn": ["like","{{ object.serial }}"]}}{% endif %}{%endif%}',
97
+ "new_window": True,
98
+ "button_class": CustomLinkButtonClassChoices.BLUE,
99
+ },
100
+ name="ipfabric",
101
+ )
89
102
  )
90
103
  cl.object_types.add(
91
- apps.get_model("core", "ObjectType").objects.get_for_model(Device)
104
+ apps.get_model("contenttypes", "ContentType")
105
+ .objects.db_manager(db_alias)
106
+ .get_for_model(Device)
92
107
  )
93
108
 
94
109
 
95
110
  def cleanup_custom_fields(apps: "apps_type", schema_editor: "BaseDatabaseSchemaEditor"):
96
111
  """Reverse migration to prepare ipfabric_netbox custom fields and links."""
112
+ db_alias = schema_editor.connection.alias
97
113
  for field_name in ["ipfabric_source", "ipfabric_ingestion"]:
98
- custom_field = apps.get_model("extras", "CustomField").objects.get(
99
- name=field_name
114
+ custom_field = (
115
+ apps.get_model("extras", "CustomField")
116
+ .objects.using(db_alias)
117
+ .get(name=field_name)
100
118
  )
101
119
  for model in custom_field.object_types.all()[:]:
102
120
  custom_field.object_types.remove(model)
@@ -109,7 +127,9 @@ def prepare_transform_maps(
109
127
  """Create transform maps if they do not exist yet.
110
128
  They used to be created during plugin.ready() so they might be present on older DBs.
111
129
  """
112
- build_transform_maps(data=get_transform_map(), apps=apps)
130
+ build_transform_maps(
131
+ data=get_transform_map(), apps=apps, db_alias=schema_editor.connection.alias
132
+ )
113
133
 
114
134
 
115
135
  class Migration(migrations.Migration):
@@ -0,0 +1,19 @@
1
+ # Generated by Django 5.2.5 on 2025-09-22 07:43
2
+ from django.db import migrations
3
+
4
+
5
+ class Migration(migrations.Migration):
6
+ dependencies = [
7
+ ("ipfabric_netbox", "0018_remove_type_field"),
8
+ ]
9
+
10
+ operations = [
11
+ migrations.AlterModelOptions(
12
+ name="ipfabrictransformmap",
13
+ options={"ordering": ("pk",)},
14
+ ),
15
+ migrations.AlterModelOptions(
16
+ name="ipfabrictransformmapgroup",
17
+ options={"ordering": ("pk",)},
18
+ ),
19
+ ]
@@ -1,4 +1,5 @@
1
1
  import ast
2
+ import functools
2
3
  import json
3
4
  import logging
4
5
  import traceback
@@ -12,6 +13,7 @@ from core.models import Job
12
13
  from core.models import ObjectType
13
14
  from core.signals import pre_sync
14
15
  from dcim.models import Device
16
+ from dcim.models import Site
15
17
  from dcim.models import VirtualChassis
16
18
  from dcim.signals import assign_virtualchassis_master
17
19
  from django.apps import apps
@@ -34,10 +36,10 @@ from netbox.models import NetBoxModel
34
36
  from netbox.models import PrimaryModel
35
37
  from netbox.models.features import JobsMixin
36
38
  from netbox.models.features import TagsMixin
37
- from netbox.registry import registry
38
39
  from netbox_branching.choices import BranchStatusChoices
39
40
  from netbox_branching.contextvars import active_branch
40
41
  from netbox_branching.models import Branch
42
+ from netbox_branching.utilities import supports_branching
41
43
  from utilities.querysets import RestrictedQuerySet
42
44
  from utilities.request import NetBoxFakeRequest
43
45
 
@@ -100,6 +102,7 @@ class IPFabricTransformMapGroup(NetBoxModel):
100
102
  description = models.TextField(blank=True, null=True)
101
103
 
102
104
  class Meta:
105
+ ordering = ("pk",)
103
106
  verbose_name = "IP Fabric Transform Map Group"
104
107
  verbose_name_plural = "IP Fabric Transform Map Groups"
105
108
 
@@ -136,6 +139,7 @@ class IPFabricTransformMap(NetBoxModel):
136
139
  )
137
140
 
138
141
  class Meta:
142
+ ordering = ("pk",)
139
143
  verbose_name = "IP Fabric Transform Map"
140
144
  verbose_name_plural = "IP Fabric Transform Maps"
141
145
 
@@ -173,13 +177,17 @@ class IPFabricTransformMap(NetBoxModel):
173
177
  )
174
178
  return cleaned_data
175
179
 
180
+ @functools.cache
176
181
  def get_models(self):
177
182
  _context = dict()
178
183
 
179
- for app, model_names in registry["model_features"]["custom_fields"].items():
184
+ for app, app_models in apps.all_models.items():
180
185
  _context.setdefault(app, {})
181
- for model_name in model_names:
182
- model = apps.get_registered_model(app, model_name)
186
+ for model in app_models:
187
+ if isinstance(model, str):
188
+ model = apps.get_registered_model(app, model)
189
+ if not supports_branching(model):
190
+ continue
183
191
  _context[app][model.__name__] = model
184
192
  _context["contenttypes"] = {}
185
193
  _context["contenttypes"]["ContentType"] = ContentType
@@ -246,10 +254,16 @@ class IPFabricTransformMap(NetBoxModel):
246
254
  # See NetBox docs Customization -> Custom Scripts -> Change Logging
247
255
  instance = queryset.get(**context)
248
256
  instance.snapshot()
257
+ changed = False
249
258
  for attr, value in defaults.items():
259
+ # Only run data validation and save if something has changed
260
+ if getattr(instance, attr) == value:
261
+ continue
262
+ changed = True
250
263
  setattr(instance, attr, value)
251
- instance.full_clean()
252
- instance.save(using=connection_name)
264
+ if changed:
265
+ instance.full_clean()
266
+ instance.save(using=connection_name)
253
267
  except target_class.DoesNotExist:
254
268
  for field in list(context.keys()):
255
269
  # When assigning we need to replace `field__isnull=True` with `field=None`
@@ -543,6 +557,13 @@ class IPFabricSource(IPFabricClient, JobsMixin, PrimaryModel):
543
557
  # Emit the post_sync signal
544
558
  # post_sync.send(sender=self.__class__, instance=self)
545
559
 
560
+ @classmethod
561
+ def get_for_site(cls, site: Site):
562
+ """Get all snapshots containing the given site."""
563
+ return cls.objects.filter(
564
+ Q(snapshots__data__sites__contains=[site.name])
565
+ ).distinct()
566
+
546
567
 
547
568
  class IPFabricSnapshot(TagsMixin, ChangeLoggedModel):
548
569
  source = models.ForeignKey(
@@ -1004,3 +1025,6 @@ class IPFabricData(models.Model):
1004
1025
  choices=IPFabricRawDataTypeChoices,
1005
1026
  )
1006
1027
  objects = RestrictedQuerySet.as_manager()
1028
+
1029
+ def get_absolute_url(self):
1030
+ return reverse("plugins:ipfabric_netbox:ipfabricdata_data", args=[self.pk])
@@ -1,23 +1,26 @@
1
1
  import logging
2
2
 
3
+ from dcim.models import Site
3
4
  from netbox.plugins import PluginTemplateExtension
4
5
 
5
- from ipfabric_netbox.models import IPFabricSnapshot
6
+ from ipfabric_netbox.models import IPFabricSource
6
7
 
7
8
  logger = logging.getLogger("ipfabric_netbox.template_content")
8
9
 
9
10
 
10
11
  class SiteTopologyButtons(PluginTemplateExtension):
11
- model = "dcim.site"
12
+ models = ["dcim.site"]
12
13
 
13
14
  def buttons(self):
14
15
  try:
15
16
  site = self.context.get("object")
16
17
  source = None
17
- for snapshot in IPFabricSnapshot.objects.all():
18
- # `Site.name` is unique in DB, so we can use it to match against IPF snapshots
19
- if site.name in snapshot.sites:
20
- source = snapshot.source
18
+ if isinstance(site, Site) and (
19
+ source_id := site.custom_field_data.get("ipfabric_source")
20
+ ):
21
+ source = IPFabricSource.objects.filter(id=source_id).first()
22
+ # `source_id` saved in CF might be obsolete, so always fall back to search by site
23
+ source = source or IPFabricSource.get_for_site(site).first()
21
24
  return self.render(
22
25
  "ipfabric_netbox/inc/site_topology_button.html",
23
26
  extra_context={"source": source},
@@ -1,4 +1,3 @@
1
- {% if object.custom_field_data.ipfabric_source %}
2
1
  <div class="btn-group" role="group" aria-label="Button group with nested dropdown">
3
2
  <div class="btn-group" role="group">
4
3
  <button id="btnGroupDrop1" type="button" class="btn btn-sm btn-success dropdown-toggle" data-bs-toggle="dropdown"
@@ -48,7 +47,6 @@
48
47
  </div>
49
48
  </div>
50
49
  </div>
51
- {% endif %}
52
50
 
53
51
  {% block javascript %}
54
52
  <script>
@@ -47,7 +47,7 @@
47
47
  <div class="row px-3">
48
48
  <div class="card">
49
49
  <div class="card-body">
50
- Device does not have a IP Fabric source configured in the custom fields. The source is used to match devices between NetBox and IP Fabric instance.
50
+ Could not find an IP Fabric Source that could have synced this Device. The source is used to match devices between NetBox and IP Fabric instance.
51
51
  </div>
52
52
  </div>
53
53
  </div>
@@ -3,6 +3,7 @@
3
3
  {% load helpers %}
4
4
  {% load plugins %}
5
5
  {% load render_table from django_tables2 %}
6
+ {% load perms %}
6
7
  {% load ipfabric_netbox_helpers %}
7
8
 
8
9
  {% block extra_controls %}
@@ -22,7 +23,7 @@
22
23
  </span>
23
24
  {% endif %}
24
25
  {% endif %}
25
- {% endblock %}
26
+ {% endblock extra_controls %}
26
27
 
27
28
  {% block content %}
28
29
  <div class="row mb-3">
@@ -9,7 +9,7 @@
9
9
  <div class="modal-body">
10
10
  <p>
11
11
  {% blocktrans trimmed %}
12
- Are you sure you want to <strong class="text-danger">restore</strong> the Transform Maps? This restores from GitLab and will overwrite any changes made in NetBox.
12
+ Are you sure you want to <strong class="text-danger">restore</strong> the Transform Maps? This restores will overwrite any changes made in NetBox to Transform Maps without a group.
13
13
  {% endblocktrans %}
14
14
  </p>
15
15
  {% if dependent_objects %}
@@ -620,7 +620,7 @@ class IPFabricSyncTest(APIViewTestCases.APIViewTestCase):
620
620
  name="Sync Test E",
621
621
  snapshot_data=snapshots[1],
622
622
  parameters={"device": True, "interface": True},
623
- auto_merge=True,
623
+ auto_merge=False,
624
624
  )
625
625
  IPFabricSync.objects.create(
626
626
  name="Sync Test F",