ipfabric_netbox 4.0.1b0__tar.gz → 4.0.1b1__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 (74) hide show
  1. {ipfabric_netbox-4.0.1b0 → ipfabric_netbox-4.0.1b1}/PKG-INFO +1 -1
  2. ipfabric_netbox-4.0.1b1/ipfabric_netbox/__init__.py +25 -0
  3. {ipfabric_netbox-4.0.1b0 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/api/serializers.py +13 -0
  4. {ipfabric_netbox-4.0.1b0 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/api/urls.py +2 -0
  5. {ipfabric_netbox-4.0.1b0 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/api/views.py +7 -0
  6. {ipfabric_netbox-4.0.1b0 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/choices.py +17 -0
  7. {ipfabric_netbox-4.0.1b0 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/filtersets.py +38 -0
  8. {ipfabric_netbox-4.0.1b0 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/forms.py +79 -3
  9. {ipfabric_netbox-4.0.1b0 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/migrations/0001_initial_squashed_0013_switch_to_branching_plugin.py +1 -1
  10. ipfabric_netbox-4.0.1b1/ipfabric_netbox/migrations/0014_ipfabrictransformmapgroup_ipfabrictransformmap_group.py +65 -0
  11. {ipfabric_netbox-4.0.1b0 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/models.py +85 -11
  12. {ipfabric_netbox-4.0.1b0 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/navigation.py +15 -1
  13. {ipfabric_netbox-4.0.1b0 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/signals.py +17 -0
  14. {ipfabric_netbox-4.0.1b0 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/tables.py +18 -2
  15. ipfabric_netbox-4.0.1b1/ipfabric_netbox/templates/ipfabric_netbox/inc/clone_form.html +27 -0
  16. {ipfabric_netbox-4.0.1b0 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/templates/ipfabric_netbox/ipfabricsync.html +17 -2
  17. ipfabric_netbox-4.0.1b1/ipfabric_netbox/templates/ipfabric_netbox/ipfabrictransformmap.html +52 -0
  18. ipfabric_netbox-4.0.1b0/ipfabric_netbox/templates/ipfabric_netbox/ipfabrictransformmap.html → ipfabric_netbox-4.0.1b1/ipfabric_netbox/templates/ipfabric_netbox/ipfabrictransformmapgroup.html +6 -8
  19. ipfabric_netbox-4.0.1b1/ipfabric_netbox/templatetags/ipfabric_netbox_helpers.py +17 -0
  20. {ipfabric_netbox-4.0.1b0 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/tests/test_models.py +0 -2
  21. {ipfabric_netbox-4.0.1b0 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/urls.py +25 -0
  22. ipfabric_netbox-4.0.1b1/ipfabric_netbox/utilities/__init__.py +0 -0
  23. {ipfabric_netbox-4.0.1b0 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/utilities/ipfutils.py +8 -12
  24. {ipfabric_netbox-4.0.1b0 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/utilities/transform_map.py +7 -6
  25. {ipfabric_netbox-4.0.1b0 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/views.py +191 -5
  26. {ipfabric_netbox-4.0.1b0 → ipfabric_netbox-4.0.1b1}/pyproject.toml +1 -1
  27. ipfabric_netbox-4.0.1b0/ipfabric_netbox/__init__.py +0 -13
  28. {ipfabric_netbox-4.0.1b0 → ipfabric_netbox-4.0.1b1}/README.md +0 -0
  29. {ipfabric_netbox-4.0.1b0 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/api/__init__.py +0 -0
  30. {ipfabric_netbox-4.0.1b0 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/api/nested_serializers.py +0 -0
  31. {ipfabric_netbox-4.0.1b0 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/data/transform_map.json +0 -0
  32. {ipfabric_netbox-4.0.1b0 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/exceptions.py +0 -0
  33. {ipfabric_netbox-4.0.1b0 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/jobs.py +0 -0
  34. {ipfabric_netbox-4.0.1b0 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/migrations/0001_initial.py +0 -0
  35. {ipfabric_netbox-4.0.1b0 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/migrations/0002_ipfabricsnapshot_status.py +0 -0
  36. {ipfabric_netbox-4.0.1b0 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/migrations/0003_ipfabricsource_type_and_more.py +0 -0
  37. {ipfabric_netbox-4.0.1b0 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/migrations/0004_ipfabricsync_auto_merge.py +0 -0
  38. {ipfabric_netbox-4.0.1b0 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/migrations/0005_alter_ipfabricrelationshipfield_source_model_and_more.py +0 -0
  39. {ipfabric_netbox-4.0.1b0 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/migrations/0006_alter_ipfabrictransformmap_target_model.py +0 -0
  40. {ipfabric_netbox-4.0.1b0 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/migrations/0007_prepare_custom_fields.py +0 -0
  41. {ipfabric_netbox-4.0.1b0 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/migrations/0008_prepare_transform_maps.py +0 -0
  42. {ipfabric_netbox-4.0.1b0 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/migrations/0009_transformmap_changes_for_netbox_v4_2.py +0 -0
  43. {ipfabric_netbox-4.0.1b0 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/migrations/0010_remove_uuid_from_get_or_create.py +0 -0
  44. {ipfabric_netbox-4.0.1b0 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/migrations/0011_update_part_number_DCIM_inventory_item_template.py +0 -0
  45. {ipfabric_netbox-4.0.1b0 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/migrations/0012_remove_status_field.py +0 -0
  46. {ipfabric_netbox-4.0.1b0 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/migrations/0013_switch_to_branching_plugin.py +0 -0
  47. {ipfabric_netbox-4.0.1b0 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/migrations/__init__.py +0 -0
  48. {ipfabric_netbox-4.0.1b0 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/template_content.py +0 -0
  49. {ipfabric_netbox-4.0.1b0 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/templates/ipfabric_netbox/inc/diff.html +0 -0
  50. {ipfabric_netbox-4.0.1b0 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/templates/ipfabric_netbox/inc/json.html +0 -0
  51. {ipfabric_netbox-4.0.1b0 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/templates/ipfabric_netbox/inc/logs_pending.html +0 -0
  52. {ipfabric_netbox-4.0.1b0 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/templates/ipfabric_netbox/inc/merge_form.html +0 -0
  53. {ipfabric_netbox-4.0.1b0 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/templates/ipfabric_netbox/inc/site_topology_button.html +0 -0
  54. {ipfabric_netbox-4.0.1b0 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/templates/ipfabric_netbox/inc/site_topology_modal.html +0 -0
  55. {ipfabric_netbox-4.0.1b0 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/templates/ipfabric_netbox/inc/snapshotdata.html +0 -0
  56. {ipfabric_netbox-4.0.1b0 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/templates/ipfabric_netbox/inc/transform_map_field_map.html +0 -0
  57. {ipfabric_netbox-4.0.1b0 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/templates/ipfabric_netbox/inc/transform_map_relationship_map.html +0 -0
  58. {ipfabric_netbox-4.0.1b0 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/templates/ipfabric_netbox/ipfabric_table.html +0 -0
  59. {ipfabric_netbox-4.0.1b0 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/templates/ipfabric_netbox/ipfabricingestion.html +0 -0
  60. {ipfabric_netbox-4.0.1b0 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/templates/ipfabric_netbox/ipfabricsnapshot.html +0 -0
  61. {ipfabric_netbox-4.0.1b0 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/templates/ipfabric_netbox/ipfabricsource.html +0 -0
  62. {ipfabric_netbox-4.0.1b0 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/templates/ipfabric_netbox/ipfabricsync_list.html +0 -0
  63. {ipfabric_netbox-4.0.1b0 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/templates/ipfabric_netbox/ipfabrictransformmap_list.html +0 -0
  64. {ipfabric_netbox-4.0.1b0 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/templates/ipfabric_netbox/ipfabrictransformmap_restore.html +0 -0
  65. {ipfabric_netbox-4.0.1b0 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/templates/ipfabric_netbox/partials/ingestion_all.html +0 -0
  66. {ipfabric_netbox-4.0.1b0 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/templates/ipfabric_netbox/partials/ingestion_progress.html +0 -0
  67. {ipfabric_netbox-4.0.1b0 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/templates/ipfabric_netbox/partials/ingestion_status.html +0 -0
  68. {ipfabric_netbox-4.0.1b0 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/templates/ipfabric_netbox/partials/job_logs.html +0 -0
  69. {ipfabric_netbox-4.0.1b0 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/templates/ipfabric_netbox/partials/sync_last_ingestion.html +0 -0
  70. {ipfabric_netbox-4.0.1b0 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/templates/static/ipfabric_netbox/css/rack.css +0 -0
  71. {ipfabric_netbox-4.0.1b0/ipfabric_netbox/tests → ipfabric_netbox-4.0.1b1/ipfabric_netbox/templatetags}/__init__.py +0 -0
  72. {ipfabric_netbox-4.0.1b0/ipfabric_netbox/utilities → ipfabric_netbox-4.0.1b1/ipfabric_netbox/tests}/__init__.py +0 -0
  73. {ipfabric_netbox-4.0.1b0 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/utilities/logging.py +0 -0
  74. {ipfabric_netbox-4.0.1b0 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/utilities/nbutils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: ipfabric_netbox
3
- Version: 4.0.1b0
3
+ Version: 4.0.1b1
4
4
  Summary: NetBox plugin to sync IP Fabric data into NetBox
5
5
  License: MIT
6
6
  Keywords: netbox,ipfabric,plugin,sync
@@ -0,0 +1,25 @@
1
+ from django.db.models.signals import post_delete
2
+ from netbox.plugins import PluginConfig
3
+
4
+
5
+ class NetboxIPFabricConfig(PluginConfig):
6
+ name = "ipfabric_netbox"
7
+ verbose_name = "NetBox IP Fabric SoT Plugin"
8
+ description = "Sync IP Fabric into NetBox"
9
+ version = "4.0.1b1"
10
+ base_url = "ipfabric"
11
+ min_version = "4.2.4"
12
+
13
+ def ready(self):
14
+ super().ready()
15
+
16
+ from ipfabric_netbox.signals import remove_group_from_syncs
17
+
18
+ post_delete.connect(
19
+ remove_group_from_syncs,
20
+ sender="ipfabric_netbox.IPFabricTransformMapGroup",
21
+ dispatch_uid="remove_group_from_syncs",
22
+ )
23
+
24
+
25
+ config = NetboxIPFabricConfig
@@ -15,6 +15,7 @@ from ipfabric_netbox.models import IPFabricSource
15
15
  from ipfabric_netbox.models import IPFabricSync
16
16
  from ipfabric_netbox.models import IPFabricTransformField
17
17
  from ipfabric_netbox.models import IPFabricTransformMap
18
+ from ipfabric_netbox.models import IPFabricTransformMapGroup
18
19
 
19
20
  __all__ = (
20
21
  "IPFabricSyncSerializer",
@@ -89,6 +90,18 @@ class IPFabricRelationshipFieldSerializer(NetBoxModelSerializer):
89
90
  ]
90
91
 
91
92
 
93
+ class IPFabricTransformMapGroupSerializer(NetBoxModelSerializer):
94
+ class Meta:
95
+ model = IPFabricTransformMapGroup
96
+ fields = [
97
+ "name",
98
+ "description",
99
+ "transform_maps",
100
+ "created",
101
+ "last_updated",
102
+ ]
103
+
104
+
92
105
  class IPFabricTransformMapSerializer(NetBoxModelSerializer):
93
106
  target_model = ContentTypeField(read_only=True)
94
107
 
@@ -7,12 +7,14 @@ from ipfabric_netbox.api.views import IPFabricSnapshotViewSet
7
7
  from ipfabric_netbox.api.views import IPFabricSourceViewSet
8
8
  from ipfabric_netbox.api.views import IPFabricSyncViewSet
9
9
  from ipfabric_netbox.api.views import IPFabricTransformFieldiewSet
10
+ from ipfabric_netbox.api.views import IPFabricTransformMapGroupViewSet
10
11
  from ipfabric_netbox.api.views import IPFabricTransformMapViewSet
11
12
 
12
13
 
13
14
  router = NetBoxRouter()
14
15
  router.register("source", IPFabricSourceViewSet)
15
16
  router.register("snapshot", IPFabricSnapshotViewSet)
17
+ router.register("transform-map-group", IPFabricTransformMapGroupViewSet)
16
18
  router.register("transform-map", IPFabricTransformMapViewSet)
17
19
  router.register("sync", IPFabricSyncViewSet)
18
20
  router.register("ingestion", IPFabricIngestionViewSet)
@@ -11,6 +11,7 @@ from .serializers import IPFabricSnapshotSerializer
11
11
  from .serializers import IPFabricSourceSerializer
12
12
  from .serializers import IPFabricSyncSerializer
13
13
  from .serializers import IPFabricTransformFieldSerializer
14
+ from .serializers import IPFabricTransformMapGroupSerializer
14
15
  from .serializers import IPFabricTransformMapSerializer
15
16
  from ipfabric_netbox.filtersets import IPFabricSnapshotFilterSet
16
17
  from ipfabric_netbox.filtersets import IPFabricSourceFilterSet
@@ -23,6 +24,12 @@ from ipfabric_netbox.models import IPFabricSource
23
24
  from ipfabric_netbox.models import IPFabricSync
24
25
  from ipfabric_netbox.models import IPFabricTransformField
25
26
  from ipfabric_netbox.models import IPFabricTransformMap
27
+ from ipfabric_netbox.models import IPFabricTransformMapGroup
28
+
29
+
30
+ class IPFabricTransformMapGroupViewSet(NetBoxReadOnlyModelViewSet):
31
+ queryset = IPFabricTransformMapGroup.objects.all()
32
+ serializer_class = IPFabricTransformMapGroupSerializer
26
33
 
27
34
 
28
35
  class IPFabricTransformMapViewSet(NetBoxReadOnlyModelViewSet):
@@ -142,6 +142,23 @@ transform_field_source_columns = {
142
142
  ],
143
143
  }
144
144
 
145
+ required_transform_map_contenttypes = [
146
+ ("dcim", "site"),
147
+ ("dcim", "manufacturer"),
148
+ ("dcim", "platform"),
149
+ ("dcim", "devicerole"),
150
+ ("dcim", "devicetype"),
151
+ ("dcim", "device"),
152
+ ("dcim", "virtualchassis"),
153
+ ("dcim", "interface"),
154
+ ("dcim", "macaddress"),
155
+ ("ipam", "vlan"),
156
+ ("ipam", "vrf"),
157
+ ("ipam", "prefix"),
158
+ ("ipam", "ipaddress"),
159
+ ("dcim", "inventoryitem"),
160
+ ]
161
+
145
162
 
146
163
  class IPFabricTransformMapSourceModelChoices(ChoiceSet):
147
164
  SITE = "site"
@@ -14,6 +14,7 @@ from .models import IPFabricSnapshot
14
14
  from .models import IPFabricSource
15
15
  from .models import IPFabricSync
16
16
  from .models import IPFabricTransformMap
17
+ from .models import IPFabricTransformMapGroup
17
18
 
18
19
 
19
20
  class IPFabricIngestionChangeFilterSet(BaseFilterSet):
@@ -121,6 +122,43 @@ class IPFabricIngestionFilterSet(BaseFilterSet):
121
122
  )
122
123
 
123
124
 
125
+ class IPFabricTransformMapGroupFilterSet(NetBoxModelFilterSet):
126
+ q = django_filters.CharFilter(method="search")
127
+
128
+ class Meta:
129
+ model = IPFabricTransformMapGroup
130
+ fields = ("id", "name", "description")
131
+
132
+ def search(self, queryset, name, value):
133
+ if not value.strip():
134
+ return queryset
135
+ return queryset.filter(
136
+ Q(name__icontains=value) | Q(description__icontains=value)
137
+ )
138
+
139
+
140
+ class IPFabricTransformMapFilterSet(NetBoxModelFilterSet):
141
+ q = django_filters.CharFilter(method="search")
142
+ group_id = django_filters.ModelMultipleChoiceFilter(
143
+ queryset=IPFabricTransformMapGroup.objects.all(),
144
+ label=_("Transform Map Group (ID)"),
145
+ )
146
+ group = django_filters.ModelMultipleChoiceFilter(
147
+ queryset=IPFabricTransformMapGroup.objects.all(), label=_("Transform Map Group")
148
+ )
149
+
150
+ class Meta:
151
+ model = IPFabricTransformMap
152
+ fields = ("id", "name", "group", "source_model", "target_model")
153
+
154
+ def search(self, queryset, name, value):
155
+ if not value.strip():
156
+ return queryset
157
+ return queryset.filter(
158
+ Q(group__name__icontains=value) | Q(name__icontains=value)
159
+ )
160
+
161
+
124
162
  class IPFabricTransformFieldFilterSet(BaseFilterSet):
125
163
  transform_map = django_filters.ModelMultipleChoiceFilter(
126
164
  queryset=IPFabricTransformMap.objects.all(), label=_("Transform Map")
@@ -3,6 +3,7 @@ import copy
3
3
  from core.choices import DataSourceStatusChoices
4
4
  from core.choices import JobIntervalChoices
5
5
  from django import forms
6
+ from django.contrib.contenttypes.models import ContentType
6
7
  from django.core.exceptions import ValidationError
7
8
  from django.utils import timezone
8
9
  from django.utils.translation import gettext_lazy as _
@@ -24,6 +25,7 @@ from utilities.forms.widgets import HTMXSelect
24
25
  from utilities.forms.widgets import NumberWithOptions
25
26
 
26
27
  from .choices import IPFabricSnapshotStatusModelChoices
28
+ from .choices import required_transform_map_contenttypes
27
29
  from .choices import transform_field_source_columns
28
30
  from .models import IPFabricIngestion
29
31
  from .models import IPFabricRelationshipField
@@ -32,6 +34,7 @@ from .models import IPFabricSource
32
34
  from .models import IPFabricSync
33
35
  from .models import IPFabricTransformField
34
36
  from .models import IPFabricTransformMap
37
+ from .models import IPFabricTransformMapGroup
35
38
 
36
39
 
37
40
  exclude_fields = [
@@ -274,15 +277,45 @@ class IPFabricTransformFieldForm(NetBoxModelForm):
274
277
  )
275
278
 
276
279
 
280
+ class IPFabricTransformMapGroupForm(forms.ModelForm):
281
+ class Meta:
282
+ model = IPFabricTransformMapGroup
283
+ fields = ("name", "description")
284
+
285
+
277
286
  class IPFabricTransformMapForm(forms.ModelForm):
278
287
  class Meta:
279
288
  model = IPFabricTransformMap
280
- fields = ("name", "source_model", "target_model")
289
+ fields = ("name", "group", "source_model", "target_model")
281
290
  widgets = {
282
291
  "target_model": HTMXSelect(hx_url="/plugins/ipfabric/transform-map/add"),
283
292
  }
284
293
 
285
294
 
295
+ class IPFabricTransformMapCloneForm(forms.Form):
296
+ name = forms.CharField(
297
+ required=True, label="Name", help_text="Name for the cloned transform map."
298
+ )
299
+ group = forms.ModelChoiceField(
300
+ queryset=IPFabricTransformMapGroup.objects.all(),
301
+ required=False,
302
+ label="Target Group",
303
+ help_text="Select the group to assign the cloned transform map to.",
304
+ )
305
+ clone_fields = forms.BooleanField(
306
+ required=False,
307
+ initial=True,
308
+ label="Clone Child Fields",
309
+ help_text="Clone all child fields of this transform map.",
310
+ )
311
+ clone_relationships = forms.BooleanField(
312
+ required=False,
313
+ initial=True,
314
+ label="Clone Child Relationships",
315
+ help_text="Clone all child relationships of this transform map.",
316
+ )
317
+
318
+
286
319
  class IPFabricSnapshotFilterForm(NetBoxModelFilterSetForm):
287
320
  model = IPFabricSnapshot
288
321
  fieldsets = (
@@ -413,6 +446,19 @@ class IPFabricSourceForm(NetBoxModelForm):
413
446
  return instance
414
447
 
415
448
 
449
+ class OrderedModelMultipleChoiceField(forms.ModelMultipleChoiceField):
450
+ """A ModelMultipleChoiceField that preserves the order of the selected items."""
451
+
452
+ def clean(self, value):
453
+ qs = super().clean(value)
454
+ clauses = " ".join(
455
+ ["WHEN id=%s THEN %s" % (pk, i) for i, pk in enumerate(value)]
456
+ )
457
+ return qs.filter(pk__in=value).extra(
458
+ select={"ordering": "CASE %s END" % clauses}, order_by=("ordering",)
459
+ )
460
+
461
+
416
462
  class IPFabricSyncForm(NetBoxModelForm):
417
463
  source = forms.ModelChoiceField(
418
464
  queryset=IPFabricSource.objects.all(),
@@ -420,6 +466,15 @@ class IPFabricSyncForm(NetBoxModelForm):
420
466
  label=_("IP Fabric Source"),
421
467
  widget=HTMXSelect(),
422
468
  )
469
+ groups = OrderedModelMultipleChoiceField(
470
+ queryset=IPFabricTransformMapGroup.objects.all(),
471
+ required=False,
472
+ label=_("Transform Map Groups"),
473
+ widget=forms.SelectMultiple(attrs={"class": "form-control"}),
474
+ help_text=_(
475
+ "Prioritize transform maps by group in entered order for each NetBox model. Default maps will be used if no group is selected for given model."
476
+ ),
477
+ )
423
478
  snapshot_data = DynamicModelChoiceField(
424
479
  queryset=IPFabricSnapshot.objects.filter(status="loaded"),
425
480
  required=True,
@@ -478,7 +533,7 @@ class IPFabricSyncForm(NetBoxModelForm):
478
533
  @property
479
534
  def fieldsets(self):
480
535
  fieldsets = [
481
- FieldSet("name", "source", name=_("IP Fabric Source")),
536
+ FieldSet("name", "source", "groups", name=_("IP Fabric Source")),
482
537
  ]
483
538
  if self.source_type == "local":
484
539
  fieldsets.append(
@@ -527,7 +582,8 @@ class IPFabricSyncForm(NetBoxModelForm):
527
582
  self.instance.snapshot_data.sites
528
583
  )
529
584
 
530
- self.initial["sites"] = self.instance.parameters["sites"]
585
+ self.initial["sites"] = self.instance.parameters.get("sites", [])
586
+ self.initial["groups"] = self.instance.parameters.get("groups", [])
531
587
 
532
588
  backend_type = get_field_value(self, "type")
533
589
  backend = {}
@@ -571,6 +627,22 @@ class IPFabricSyncForm(NetBoxModelForm):
571
627
  if self.cleaned_data.get("interval") and not scheduled_time:
572
628
  self.cleaned_data["scheduled"] = local_now()
573
629
 
630
+ maps = IPFabricSync.get_transform_maps(self.cleaned_data.get("groups", []))
631
+ missing = []
632
+ for app_label, model in required_transform_map_contenttypes:
633
+ if not maps.filter(
634
+ target_model=ContentType.objects.get(app_label=app_label, model=model)
635
+ ):
636
+ missing.append(f"{app_label}.{model}")
637
+ if missing:
638
+ raise ValidationError(
639
+ {
640
+ "groups": _(
641
+ f"Combination of these transform map groups failed validation. Missing maps: {missing}."
642
+ )
643
+ }
644
+ )
645
+
574
646
  return self.cleaned_data
575
647
 
576
648
  def save(self, *args, **kwargs):
@@ -580,6 +652,10 @@ class IPFabricSyncForm(NetBoxModelForm):
580
652
  parameters[name[4:]] = self.cleaned_data[name]
581
653
  if name == "sites":
582
654
  parameters["sites"] = self.cleaned_data["sites"]
655
+ if name == "groups":
656
+ parameters["groups"] = [
657
+ group.pk for group in self.cleaned_data["groups"]
658
+ ]
583
659
  self.instance.parameters = parameters
584
660
  self.instance.status = DataSourceStatusChoices.NEW
585
661
 
@@ -109,7 +109,7 @@ def prepare_transform_maps(
109
109
  """Create transform maps if they do not exist yet.
110
110
  They used to be created during plugin.ready() so they might be present on older DBs.
111
111
  """
112
- build_transform_maps(data=get_transform_map())
112
+ build_transform_maps(data=get_transform_map(), apps=apps)
113
113
 
114
114
 
115
115
  class Migration(migrations.Migration):
@@ -0,0 +1,65 @@
1
+ # Generated by Django 5.2 on 2025-05-26 08:53
2
+ import django.db.models.deletion
3
+ import taggit.managers
4
+ import utilities.json
5
+ from django.db import migrations
6
+ from django.db import models
7
+
8
+
9
+ class Migration(migrations.Migration):
10
+ dependencies = [
11
+ ("extras", "0128_tableconfig"),
12
+ ("ipfabric_netbox", "0001_initial_squashed_0013_switch_to_branching_plugin"),
13
+ ]
14
+
15
+ operations = [
16
+ migrations.CreateModel(
17
+ name="IPFabricTransformMapGroup",
18
+ fields=[
19
+ (
20
+ "id",
21
+ models.BigAutoField(
22
+ auto_created=True, primary_key=True, serialize=False
23
+ ),
24
+ ),
25
+ ("created", models.DateTimeField(auto_now_add=True, null=True)),
26
+ ("last_updated", models.DateTimeField(auto_now=True, null=True)),
27
+ (
28
+ "custom_field_data",
29
+ models.JSONField(
30
+ blank=True,
31
+ default=dict,
32
+ encoder=utilities.json.CustomFieldJSONEncoder,
33
+ ),
34
+ ),
35
+ ("name", models.CharField(max_length=100, unique=True)),
36
+ ("description", models.TextField(blank=True, null=True)),
37
+ (
38
+ "tags",
39
+ taggit.managers.TaggableManager(
40
+ through="extras.TaggedItem", to="extras.Tag"
41
+ ),
42
+ ),
43
+ ],
44
+ options={
45
+ "verbose_name": "IP Fabric Transform Map Group",
46
+ "verbose_name_plural": "IP Fabric Transform Map Groups",
47
+ },
48
+ ),
49
+ migrations.AddField(
50
+ model_name="ipfabrictransformmap",
51
+ name="group",
52
+ field=models.ForeignKey(
53
+ blank=True,
54
+ null=True,
55
+ on_delete=django.db.models.deletion.CASCADE,
56
+ related_name="transform_maps",
57
+ to="ipfabric_netbox.ipfabrictransformmapgroup",
58
+ ),
59
+ ),
60
+ migrations.AlterField(
61
+ model_name="ipfabrictransformmap",
62
+ name="name",
63
+ field=models.CharField(max_length=100),
64
+ ),
65
+ ]
@@ -18,6 +18,7 @@ from django.apps import apps
18
18
  from django.conf import settings
19
19
  from django.contrib.contenttypes.models import ContentType
20
20
  from django.core.cache import cache
21
+ from django.core.exceptions import ValidationError
21
22
  from django.core.validators import MinValueValidator
22
23
  from django.db import models
23
24
  from django.db import transaction
@@ -45,6 +46,7 @@ from .choices import IPFabricSnapshotStatusModelChoices
45
46
  from .choices import IPFabricSourceTypeChoices
46
47
  from .choices import IPFabricSyncTypeChoices
47
48
  from .choices import IPFabricTransformMapSourceModelChoices
49
+ from .choices import required_transform_map_contenttypes
48
50
  from .signals import clear_other_primary_ip
49
51
  from .utilities.ipfutils import IPFabric
50
52
  from .utilities.ipfutils import IPFabricSyncRunner
@@ -94,8 +96,25 @@ IPFabricRelationshipFieldSourceModels = Q(
94
96
  )
95
97
 
96
98
 
97
- class IPFabricTransformMap(NetBoxModel):
99
+ class IPFabricTransformMapGroup(NetBoxModel):
98
100
  name = models.CharField(max_length=100, unique=True)
101
+ description = models.TextField(blank=True, null=True)
102
+
103
+ class Meta:
104
+ verbose_name = "IP Fabric Transform Map Group"
105
+ verbose_name_plural = "IP Fabric Transform Map Groups"
106
+
107
+ def __str__(self):
108
+ return self.name
109
+
110
+ def get_absolute_url(self):
111
+ return reverse(
112
+ "plugins:ipfabric_netbox:ipfabrictransformmapgroup", args=[self.pk]
113
+ )
114
+
115
+
116
+ class IPFabricTransformMap(NetBoxModel):
117
+ name = models.CharField(max_length=100)
99
118
  source_model = models.CharField(
100
119
  max_length=50, choices=IPFabricTransformMapSourceModelChoices
101
120
  )
@@ -109,6 +128,13 @@ class IPFabricTransformMap(NetBoxModel):
109
128
  blank=False,
110
129
  null=False,
111
130
  )
131
+ group = models.ForeignKey(
132
+ to=IPFabricTransformMapGroup,
133
+ on_delete=models.CASCADE,
134
+ related_name="transform_maps",
135
+ blank=True,
136
+ null=True,
137
+ )
112
138
 
113
139
  class Meta:
114
140
  verbose_name = "IP Fabric Transform Map"
@@ -128,6 +154,26 @@ class IPFabricTransformMap(NetBoxModel):
128
154
  # TODO: Add docs url
129
155
  return ""
130
156
 
157
+ def clean(self):
158
+ cleaned_data = super().clean()
159
+ qs = IPFabricTransformMap.objects.filter(
160
+ group=self.group,
161
+ target_model_id=self.target_model_id,
162
+ )
163
+ if self.pk:
164
+ qs = qs.exclude(pk=self.pk)
165
+ if qs.exists():
166
+ err_msg = _(
167
+ "A transform map with this group and target model already exists."
168
+ )
169
+ raise ValidationError(
170
+ {
171
+ "group": err_msg,
172
+ "target_model": err_msg,
173
+ }
174
+ )
175
+ return cleaned_data
176
+
131
177
  def get_models(self):
132
178
  _context = dict()
133
179
 
@@ -333,12 +379,9 @@ class IPFabricTransformField(models.Model):
333
379
 
334
380
 
335
381
  class IPFabricClient:
336
- def get_client(self, parameters, transform_map=None):
382
+ def get_client(self, parameters):
337
383
  try:
338
- if transform_map:
339
- ipf = IPFabric(parameters=parameters, transform_map=transform_map)
340
- else:
341
- ipf = IPFabric(parameters=parameters)
384
+ ipf = IPFabric(parameters=parameters)
342
385
  return ipf.ipf
343
386
  except httpx.ConnectError as e:
344
387
  if "CERTIFICATE_VERIFY_FAILED" in str(e):
@@ -623,6 +666,25 @@ class IPFabricSync(IPFabricClient, JobsMixin, TagsMixin, ChangeLoggedModel):
623
666
  else:
624
667
  return False
625
668
 
669
+ @staticmethod
670
+ def get_transform_maps(group_ids=None):
671
+ """
672
+ Returns a queryset of IPFabricTransformMap objects that would be used by this sync,
673
+ following group and default precedence logic.
674
+ """
675
+ default_maps = IPFabricTransformMap.objects.filter(group__isnull=True)
676
+ group_ids = group_ids or []
677
+ maps_by_target = {tm.target_model_id: tm for tm in default_maps}
678
+ # Replace default maps with the ones from the groups, in given order.
679
+ if group_ids:
680
+ for group_id in group_ids:
681
+ group_maps = IPFabricTransformMap.objects.filter(group_id=group_id)
682
+ for tm in group_maps:
683
+ maps_by_target[tm.target_model_id] = tm
684
+ return IPFabricTransformMap.objects.filter(
685
+ pk__in=[tm.pk for tm in maps_by_target.values()]
686
+ )
687
+
626
688
  def enqueue_sync_job(self, adhoc=False, user=None):
627
689
  # Set the status to "syncing"
628
690
  self.status = DataSourceStatusChoices.QUEUED
@@ -666,6 +728,22 @@ class IPFabricSync(IPFabricClient, JobsMixin, TagsMixin, ChangeLoggedModel):
666
728
  self.logger = SyncLogging(job=self.pk)
667
729
  user = None
668
730
 
731
+ maps = self.get_transform_maps(self.parameters.get("groups", []))
732
+ missing = []
733
+ for app_label, model in required_transform_map_contenttypes:
734
+ if not maps.filter(
735
+ target_model=ContentType.objects.get(app_label=app_label, model=model)
736
+ ):
737
+ missing.append(f"{app_label}.{model}")
738
+ if missing:
739
+ self.logger.log_failure(
740
+ f"Combination of these transform map groups failed validation. Missing maps: {missing}.",
741
+ obj=self,
742
+ )
743
+ raise SyncError(
744
+ f"Combination of these transform map groups failed validation. Missing maps: {missing}."
745
+ )
746
+
669
747
  if self.status == DataSourceStatusChoices.SYNCING:
670
748
  raise SyncError("Cannot initiate sync; ingestion already in progress.")
671
749
 
@@ -714,10 +792,7 @@ class IPFabricSync(IPFabricClient, JobsMixin, TagsMixin, ChangeLoggedModel):
714
792
  logger.info("Fetching IP Fabric Client")
715
793
 
716
794
  if self.snapshot_data.source.type == IPFabricSourceTypeChoices.LOCAL:
717
- ipf = self.get_client(
718
- parameters=self.snapshot_data.source.parameters,
719
- transform_map=IPFabricTransformMap,
720
- )
795
+ ipf = self.get_client(parameters=self.snapshot_data.source.parameters)
721
796
  if not ipf:
722
797
  logger.debug("Unable to connect to IP Fabric.")
723
798
  raise SyncError("Unable to connect to IP Fabric.")
@@ -728,7 +803,6 @@ class IPFabricSync(IPFabricClient, JobsMixin, TagsMixin, ChangeLoggedModel):
728
803
  client=ipf,
729
804
  ingestion=ingestion,
730
805
  settings=self.parameters,
731
- transform_map=IPFabricTransformMap,
732
806
  sync=self,
733
807
  )
734
808
 
@@ -42,6 +42,20 @@ ingestion = PluginMenuItem(
42
42
  permissions=["ipfabric_netbox.view_ipfabricsync"],
43
43
  )
44
44
 
45
+ tmg = PluginMenuItem(
46
+ link="plugins:ipfabric_netbox:ipfabrictransformmapgroup_list",
47
+ link_text="Transform Map Groups",
48
+ permissions=["ipfabric_netbox.view_ipfabrictransformmapgroup"],
49
+ buttons=[
50
+ PluginMenuButton(
51
+ link="plugins:ipfabric_netbox:ipfabrictransformmapgroup_add",
52
+ title="Add",
53
+ icon_class="mdi mdi-plus-thick",
54
+ permissions=["ipfabric_netbox.add_ipfabrictransformmapgroup"],
55
+ )
56
+ ],
57
+ )
58
+
45
59
  tm = PluginMenuItem(
46
60
  link="plugins:ipfabric_netbox:ipfabrictransformmap_list",
47
61
  link_text="Transform Maps",
@@ -58,5 +72,5 @@ tm = PluginMenuItem(
58
72
  menu = PluginMenu(
59
73
  label="IP Fabric",
60
74
  icon_class="mdi mdi-cloud-sync",
61
- groups=(("IP Fabric", (source, snapshot, ingestion, tm)),),
75
+ groups=(("IP Fabric", (source, snapshot, ingestion, tmg, tm)),),
62
76
  )
@@ -34,3 +34,20 @@ def clear_other_primary_ip(instance: Device, **kwargs) -> None:
34
34
  other_device.save(using=connection_name)
35
35
  except Device.DoesNotExist:
36
36
  pass
37
+
38
+
39
+ def remove_group_from_syncs(instance, **kwargs):
40
+ """
41
+ When an IPFabricTransformMapGroup is deleted, remove its ID from any IPFabricSync.parameters['groups'] list.
42
+ """
43
+ from ipfabric_netbox.models import IPFabricSync
44
+
45
+ group_id = instance.pk
46
+ for sync in IPFabricSync.objects.all():
47
+ params = sync.parameters or {}
48
+ groups = params.get("groups", [])
49
+ if group_id not in groups:
50
+ continue
51
+ params["groups"] = [gid for gid in groups if gid != group_id]
52
+ sync.parameters = params
53
+ sync.save()
@@ -14,6 +14,7 @@ from .models import IPFabricSource
14
14
  from .models import IPFabricSync
15
15
  from .models import IPFabricTransformField
16
16
  from .models import IPFabricTransformMap
17
+ from .models import IPFabricTransformMapGroup
17
18
 
18
19
 
19
20
  DIFF_BUTTON = """
@@ -60,13 +61,28 @@ class IPFabricTransformFieldTable(NetBoxTable):
60
61
  default_columns = ("source_field", "target_field", "coalesce", "actions")
61
62
 
62
63
 
64
+ class IPFabricTransformMapGroupTable(NetBoxTable):
65
+ name = tables.Column(linkify=True)
66
+ maps_count = columns.LinkedCountColumn(
67
+ viewname="plugins:ipfabric_netbox:ipfabrictransformmap_list",
68
+ url_params={"group_id": "pk"},
69
+ verbose_name="Transform Maps",
70
+ )
71
+
72
+ class Meta(NetBoxTable.Meta):
73
+ model = IPFabricTransformMapGroup
74
+ fields = ("name", "description", "maps_count")
75
+ default_columns = ("name", "description", "maps_count")
76
+
77
+
63
78
  class IPFabricTransformMapTable(NetBoxTable):
64
79
  name = tables.Column(linkify=True)
80
+ group = tables.Column(linkify=True)
65
81
 
66
82
  class Meta(NetBoxTable.Meta):
67
83
  model = IPFabricTransformMap
68
- fields = ("name", "source_model", "target_model")
69
- default_columns = ("name", "source_model", "target_model")
84
+ fields = ("name", "group", "source_model", "target_model")
85
+ default_columns = ("name", "group", "source_model", "target_model")
70
86
 
71
87
 
72
88
  class IPFabricIngestionTable(NetBoxTable):