ipfabric_netbox 4.0.1__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.
- {ipfabric_netbox-4.0.1 → ipfabric_netbox-4.0.1b1}/PKG-INFO +1 -1
- ipfabric_netbox-4.0.1b1/ipfabric_netbox/__init__.py +25 -0
- {ipfabric_netbox-4.0.1 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/api/serializers.py +13 -0
- {ipfabric_netbox-4.0.1 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/api/urls.py +2 -0
- {ipfabric_netbox-4.0.1 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/api/views.py +7 -0
- {ipfabric_netbox-4.0.1 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/choices.py +17 -0
- {ipfabric_netbox-4.0.1 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/filtersets.py +38 -0
- {ipfabric_netbox-4.0.1 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/forms.py +79 -3
- {ipfabric_netbox-4.0.1 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/migrations/0001_initial_squashed_0013_switch_to_branching_plugin.py +1 -1
- ipfabric_netbox-4.0.1b1/ipfabric_netbox/migrations/0014_ipfabrictransformmapgroup_ipfabrictransformmap_group.py +65 -0
- {ipfabric_netbox-4.0.1 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/models.py +85 -11
- {ipfabric_netbox-4.0.1 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/navigation.py +15 -1
- {ipfabric_netbox-4.0.1 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/signals.py +17 -0
- {ipfabric_netbox-4.0.1 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/tables.py +18 -2
- ipfabric_netbox-4.0.1b1/ipfabric_netbox/templates/ipfabric_netbox/inc/clone_form.html +27 -0
- {ipfabric_netbox-4.0.1 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/templates/ipfabric_netbox/ipfabricsync.html +17 -2
- ipfabric_netbox-4.0.1b1/ipfabric_netbox/templates/ipfabric_netbox/ipfabrictransformmap.html +52 -0
- ipfabric_netbox-4.0.1/ipfabric_netbox/templates/ipfabric_netbox/ipfabrictransformmap.html → ipfabric_netbox-4.0.1b1/ipfabric_netbox/templates/ipfabric_netbox/ipfabrictransformmapgroup.html +6 -8
- ipfabric_netbox-4.0.1b1/ipfabric_netbox/templatetags/ipfabric_netbox_helpers.py +17 -0
- {ipfabric_netbox-4.0.1 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/tests/test_models.py +0 -2
- {ipfabric_netbox-4.0.1 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/urls.py +25 -0
- ipfabric_netbox-4.0.1b1/ipfabric_netbox/utilities/__init__.py +0 -0
- {ipfabric_netbox-4.0.1 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/utilities/ipfutils.py +8 -12
- {ipfabric_netbox-4.0.1 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/utilities/transform_map.py +7 -6
- {ipfabric_netbox-4.0.1 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/views.py +191 -5
- {ipfabric_netbox-4.0.1 → ipfabric_netbox-4.0.1b1}/pyproject.toml +1 -1
- ipfabric_netbox-4.0.1/ipfabric_netbox/__init__.py +0 -13
- {ipfabric_netbox-4.0.1 → ipfabric_netbox-4.0.1b1}/README.md +0 -0
- {ipfabric_netbox-4.0.1 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/api/__init__.py +0 -0
- {ipfabric_netbox-4.0.1 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/api/nested_serializers.py +0 -0
- {ipfabric_netbox-4.0.1 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/data/transform_map.json +0 -0
- {ipfabric_netbox-4.0.1 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/exceptions.py +0 -0
- {ipfabric_netbox-4.0.1 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/jobs.py +0 -0
- {ipfabric_netbox-4.0.1 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/migrations/0001_initial.py +0 -0
- {ipfabric_netbox-4.0.1 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/migrations/0002_ipfabricsnapshot_status.py +0 -0
- {ipfabric_netbox-4.0.1 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/migrations/0003_ipfabricsource_type_and_more.py +0 -0
- {ipfabric_netbox-4.0.1 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/migrations/0004_ipfabricsync_auto_merge.py +0 -0
- {ipfabric_netbox-4.0.1 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/migrations/0005_alter_ipfabricrelationshipfield_source_model_and_more.py +0 -0
- {ipfabric_netbox-4.0.1 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/migrations/0006_alter_ipfabrictransformmap_target_model.py +0 -0
- {ipfabric_netbox-4.0.1 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/migrations/0007_prepare_custom_fields.py +0 -0
- {ipfabric_netbox-4.0.1 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/migrations/0008_prepare_transform_maps.py +0 -0
- {ipfabric_netbox-4.0.1 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/migrations/0009_transformmap_changes_for_netbox_v4_2.py +0 -0
- {ipfabric_netbox-4.0.1 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/migrations/0010_remove_uuid_from_get_or_create.py +0 -0
- {ipfabric_netbox-4.0.1 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/migrations/0011_update_part_number_DCIM_inventory_item_template.py +0 -0
- {ipfabric_netbox-4.0.1 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/migrations/0012_remove_status_field.py +0 -0
- {ipfabric_netbox-4.0.1 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/migrations/0013_switch_to_branching_plugin.py +0 -0
- {ipfabric_netbox-4.0.1 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/migrations/__init__.py +0 -0
- {ipfabric_netbox-4.0.1 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/template_content.py +0 -0
- {ipfabric_netbox-4.0.1 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/templates/ipfabric_netbox/inc/diff.html +0 -0
- {ipfabric_netbox-4.0.1 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/templates/ipfabric_netbox/inc/json.html +0 -0
- {ipfabric_netbox-4.0.1 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/templates/ipfabric_netbox/inc/logs_pending.html +0 -0
- {ipfabric_netbox-4.0.1 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/templates/ipfabric_netbox/inc/merge_form.html +0 -0
- {ipfabric_netbox-4.0.1 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/templates/ipfabric_netbox/inc/site_topology_button.html +0 -0
- {ipfabric_netbox-4.0.1 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/templates/ipfabric_netbox/inc/site_topology_modal.html +0 -0
- {ipfabric_netbox-4.0.1 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/templates/ipfabric_netbox/inc/snapshotdata.html +0 -0
- {ipfabric_netbox-4.0.1 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/templates/ipfabric_netbox/inc/transform_map_field_map.html +0 -0
- {ipfabric_netbox-4.0.1 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/templates/ipfabric_netbox/inc/transform_map_relationship_map.html +0 -0
- {ipfabric_netbox-4.0.1 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/templates/ipfabric_netbox/ipfabric_table.html +0 -0
- {ipfabric_netbox-4.0.1 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/templates/ipfabric_netbox/ipfabricingestion.html +0 -0
- {ipfabric_netbox-4.0.1 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/templates/ipfabric_netbox/ipfabricsnapshot.html +0 -0
- {ipfabric_netbox-4.0.1 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/templates/ipfabric_netbox/ipfabricsource.html +0 -0
- {ipfabric_netbox-4.0.1 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/templates/ipfabric_netbox/ipfabricsync_list.html +0 -0
- {ipfabric_netbox-4.0.1 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/templates/ipfabric_netbox/ipfabrictransformmap_list.html +0 -0
- {ipfabric_netbox-4.0.1 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/templates/ipfabric_netbox/ipfabrictransformmap_restore.html +0 -0
- {ipfabric_netbox-4.0.1 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/templates/ipfabric_netbox/partials/ingestion_all.html +0 -0
- {ipfabric_netbox-4.0.1 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/templates/ipfabric_netbox/partials/ingestion_progress.html +0 -0
- {ipfabric_netbox-4.0.1 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/templates/ipfabric_netbox/partials/ingestion_status.html +0 -0
- {ipfabric_netbox-4.0.1 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/templates/ipfabric_netbox/partials/job_logs.html +0 -0
- {ipfabric_netbox-4.0.1 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/templates/ipfabric_netbox/partials/sync_last_ingestion.html +0 -0
- {ipfabric_netbox-4.0.1 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/templates/static/ipfabric_netbox/css/rack.css +0 -0
- {ipfabric_netbox-4.0.1/ipfabric_netbox/tests → ipfabric_netbox-4.0.1b1/ipfabric_netbox/templatetags}/__init__.py +0 -0
- {ipfabric_netbox-4.0.1/ipfabric_netbox/utilities → ipfabric_netbox-4.0.1b1/ipfabric_netbox/tests}/__init__.py +0 -0
- {ipfabric_netbox-4.0.1 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/utilities/logging.py +0 -0
- {ipfabric_netbox-4.0.1 → ipfabric_netbox-4.0.1b1}/ipfabric_netbox/utilities/nbutils.py +0 -0
|
@@ -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
|
|
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
|
|
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
|
|
382
|
+
def get_client(self, parameters):
|
|
337
383
|
try:
|
|
338
|
-
|
|
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):
|