ipfabric_netbox 4.2.2b3__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.
- {ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/PKG-INFO +5 -3
- {ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/README.md +3 -1
- {ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/__init__.py +2 -2
- {ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/forms.py +82 -19
- {ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/migrations/0001_initial_squashed_0013_switch_to_branching_plugin.py +37 -17
- ipfabric_netbox-4.2.2b4/ipfabric_netbox/migrations/0019_alter_ipfabrictransformmap_options_and_more.py +19 -0
- {ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/models.py +17 -6
- {ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/template_content.py +1 -1
- {ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/templates/ipfabric_netbox/inc/site_topology_button.html +0 -2
- {ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/templates/ipfabric_netbox/ipfabricsync.html +2 -1
- {ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/tests/api/test_api.py +1 -1
- {ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/tests/test_forms.py +111 -18
- {ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/tests/test_views.py +14 -1
- {ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/utilities/transform_map.py +12 -10
- {ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/views.py +70 -86
- {ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/pyproject.toml +2 -2
- {ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/api/__init__.py +0 -0
- {ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/api/serializers.py +0 -0
- {ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/api/urls.py +0 -0
- {ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/api/views.py +0 -0
- {ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/choices.py +0 -0
- {ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/data/transform_map.json +0 -0
- {ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/exceptions.py +0 -0
- {ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/filtersets.py +0 -0
- {ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/graphql/__init__.py +0 -0
- {ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/graphql/enums.py +0 -0
- {ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/graphql/filters.py +0 -0
- {ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/graphql/schema.py +0 -0
- {ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/graphql/types.py +0 -0
- {ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/jobs.py +0 -0
- {ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/migrations/0001_initial.py +0 -0
- {ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/migrations/0002_ipfabricsnapshot_status.py +0 -0
- {ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/migrations/0003_ipfabricsource_type_and_more.py +0 -0
- {ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/migrations/0004_ipfabricsync_auto_merge.py +0 -0
- {ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/migrations/0005_alter_ipfabricrelationshipfield_source_model_and_more.py +0 -0
- {ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/migrations/0006_alter_ipfabrictransformmap_target_model.py +0 -0
- {ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/migrations/0007_prepare_custom_fields.py +0 -0
- {ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/migrations/0008_prepare_transform_maps.py +0 -0
- {ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/migrations/0009_transformmap_changes_for_netbox_v4_2.py +0 -0
- {ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/migrations/0010_remove_uuid_from_get_or_create.py +0 -0
- {ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/migrations/0011_update_part_number_DCIM_inventory_item_template.py +0 -0
- {ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/migrations/0012_remove_status_field.py +0 -0
- {ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/migrations/0013_switch_to_branching_plugin.py +0 -0
- {ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/migrations/0014_ipfabrictransformmapgroup_ipfabrictransformmap_group.py +0 -0
- {ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/migrations/0015_ipfabricingestionissue.py +0 -0
- {ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/migrations/0016_tags_and_changelog_for_snapshots.py +0 -0
- {ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/migrations/0017_ipfabricsync_update_custom_fields.py +0 -0
- {ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/migrations/0018_remove_type_field.py +0 -0
- {ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/migrations/__init__.py +0 -0
- {ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/navigation.py +0 -0
- {ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/signals.py +0 -0
- {ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/tables.py +0 -0
- {ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/templates/ipfabric_netbox/inc/clone_form.html +0 -0
- {ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/templates/ipfabric_netbox/inc/diff.html +0 -0
- {ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/templates/ipfabric_netbox/inc/json.html +0 -0
- {ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/templates/ipfabric_netbox/inc/logs_pending.html +0 -0
- {ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/templates/ipfabric_netbox/inc/merge_form.html +0 -0
- {ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/templates/ipfabric_netbox/inc/site_topology_modal.html +0 -0
- {ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/templates/ipfabric_netbox/inc/snapshotdata.html +0 -0
- {ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/templates/ipfabric_netbox/inc/transform_map_field_map.html +0 -0
- {ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/templates/ipfabric_netbox/inc/transform_map_relationship_map.html +0 -0
- {ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/templates/ipfabric_netbox/ipfabric_table.html +0 -0
- {ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/templates/ipfabric_netbox/ipfabricingestion.html +0 -0
- {ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/templates/ipfabric_netbox/ipfabricsnapshot.html +0 -0
- {ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/templates/ipfabric_netbox/ipfabricsource.html +0 -0
- {ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/templates/ipfabric_netbox/ipfabrictransformmap.html +0 -0
- {ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/templates/ipfabric_netbox/ipfabrictransformmap_list.html +0 -0
- {ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/templates/ipfabric_netbox/ipfabrictransformmap_restore.html +0 -0
- {ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/templates/ipfabric_netbox/ipfabrictransformmapgroup.html +0 -0
- {ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/templates/ipfabric_netbox/partials/ingestion_all.html +0 -0
- {ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/templates/ipfabric_netbox/partials/ingestion_progress.html +0 -0
- {ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/templates/ipfabric_netbox/partials/ingestion_statistics.html +0 -0
- {ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/templates/ipfabric_netbox/partials/ingestion_status.html +0 -0
- {ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/templates/ipfabric_netbox/partials/job_logs.html +0 -0
- {ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/templates/ipfabric_netbox/partials/object_tabs.html +0 -0
- {ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/templates/ipfabric_netbox/partials/sync_last_ingestion.html +0 -0
- {ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/templates/static/ipfabric_netbox/css/rack.css +0 -0
- {ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/templatetags/__init__.py +0 -0
- {ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/templatetags/ipfabric_netbox_helpers.py +0 -0
- {ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/tests/__init__.py +0 -0
- {ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/tests/api/__init__.py +0 -0
- {ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/tests/test_models.py +0 -0
- {ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/urls.py +0 -0
- {ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/utilities/__init__.py +0 -0
- {ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/utilities/ipfutils.py +0 -0
- {ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/utilities/logging.py +0 -0
- {ipfabric_netbox-4.2.2b3 → 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.
|
|
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 (
|
|
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.
|
|
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.
|
|
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.
|
|
9
|
+
version = "4.2.2b4"
|
|
10
10
|
base_url = "ipfabric"
|
|
11
|
-
min_version = "4.
|
|
11
|
+
min_version = "4.4.0"
|
|
12
12
|
|
|
13
13
|
def ready(self):
|
|
14
14
|
super().ready()
|
|
@@ -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
|
-
|
|
536
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
620
|
+
self.fields[field_name] = copy.deepcopy(form_field)
|
|
575
621
|
if self.instance and self.instance.parameters:
|
|
576
|
-
|
|
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
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
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, _ =
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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(
|
|
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, _ =
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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("
|
|
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 =
|
|
99
|
-
|
|
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(
|
|
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
|
|
@@ -35,10 +36,10 @@ from netbox.models import NetBoxModel
|
|
|
35
36
|
from netbox.models import PrimaryModel
|
|
36
37
|
from netbox.models.features import JobsMixin
|
|
37
38
|
from netbox.models.features import TagsMixin
|
|
38
|
-
from netbox.registry import registry
|
|
39
39
|
from netbox_branching.choices import BranchStatusChoices
|
|
40
40
|
from netbox_branching.contextvars import active_branch
|
|
41
41
|
from netbox_branching.models import Branch
|
|
42
|
+
from netbox_branching.utilities import supports_branching
|
|
42
43
|
from utilities.querysets import RestrictedQuerySet
|
|
43
44
|
from utilities.request import NetBoxFakeRequest
|
|
44
45
|
|
|
@@ -176,13 +177,17 @@ class IPFabricTransformMap(NetBoxModel):
|
|
|
176
177
|
)
|
|
177
178
|
return cleaned_data
|
|
178
179
|
|
|
180
|
+
@functools.cache
|
|
179
181
|
def get_models(self):
|
|
180
182
|
_context = dict()
|
|
181
183
|
|
|
182
|
-
for app,
|
|
184
|
+
for app, app_models in apps.all_models.items():
|
|
183
185
|
_context.setdefault(app, {})
|
|
184
|
-
for
|
|
185
|
-
|
|
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
|
|
186
191
|
_context[app][model.__name__] = model
|
|
187
192
|
_context["contenttypes"] = {}
|
|
188
193
|
_context["contenttypes"]["ContentType"] = ContentType
|
|
@@ -249,10 +254,16 @@ class IPFabricTransformMap(NetBoxModel):
|
|
|
249
254
|
# See NetBox docs Customization -> Custom Scripts -> Change Logging
|
|
250
255
|
instance = queryset.get(**context)
|
|
251
256
|
instance.snapshot()
|
|
257
|
+
changed = False
|
|
252
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
|
|
253
263
|
setattr(instance, attr, value)
|
|
254
|
-
|
|
255
|
-
|
|
264
|
+
if changed:
|
|
265
|
+
instance.full_clean()
|
|
266
|
+
instance.save(using=connection_name)
|
|
256
267
|
except target_class.DoesNotExist:
|
|
257
268
|
for field in list(context.keys()):
|
|
258
269
|
# When assigning we need to replace `field__isnull=True` with `field=None`
|
|
@@ -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>
|
|
@@ -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">
|
|
@@ -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=
|
|
623
|
+
auto_merge=False,
|
|
624
624
|
)
|
|
625
625
|
IPFabricSync.objects.create(
|
|
626
626
|
name="Sync Test F",
|
|
@@ -808,13 +808,6 @@ class IPFabricSyncFormTestCase(TestCase):
|
|
|
808
808
|
)
|
|
809
809
|
self.assertTrue(form.is_valid(), form.errors)
|
|
810
810
|
|
|
811
|
-
def test_form_initialization_with_source_no_data(self):
|
|
812
|
-
"""Test source handling without data"""
|
|
813
|
-
form = IPFabricSyncForm(initial={"source": self.source.pk})
|
|
814
|
-
|
|
815
|
-
# Verify that source_type is set when there's a source and no data
|
|
816
|
-
self.assertEqual(form.source_type, IPFabricSourceTypeChoices.LOCAL)
|
|
817
|
-
|
|
818
811
|
def test_form_initialization_with_sites_no_data(self):
|
|
819
812
|
"""Test sites handling without data"""
|
|
820
813
|
form = IPFabricSyncForm(initial={"sites": ["site1", "site2"]})
|
|
@@ -905,9 +898,6 @@ class IPFabricSyncFormTestCase(TestCase):
|
|
|
905
898
|
# Test form initialization with existing instance but no data
|
|
906
899
|
form = IPFabricSyncForm(instance=sync_instance)
|
|
907
900
|
|
|
908
|
-
# Verify that source_type is set from the instance
|
|
909
|
-
self.assertEqual(form.source_type, IPFabricSourceTypeChoices.LOCAL)
|
|
910
|
-
|
|
911
901
|
# Verify that initial values are set from instance parameters
|
|
912
902
|
self.assertEqual(form.initial["source"], self.source)
|
|
913
903
|
self.assertEqual(form.initial["sites"], ["site1", "site2"])
|
|
@@ -932,18 +922,121 @@ class IPFabricSyncFormTestCase(TestCase):
|
|
|
932
922
|
instance=sync_instance, initial={"name": "Override Name"}
|
|
933
923
|
)
|
|
934
924
|
|
|
935
|
-
#
|
|
936
|
-
|
|
937
|
-
self.
|
|
938
|
-
|
|
939
|
-
# initial should not be set from instance when initial kwargs are provided
|
|
940
|
-
self.assertNotIn("source", form.initial)
|
|
941
|
-
self.assertNotIn("sites", form.initial)
|
|
942
|
-
self.assertNotIn("groups", form.initial)
|
|
925
|
+
# These should be set from instance even when not in initial kwarg
|
|
926
|
+
self.assertIn("source", form.initial)
|
|
927
|
+
self.assertIn("sites", form.initial)
|
|
928
|
+
self.assertIn("groups", form.initial)
|
|
943
929
|
|
|
944
930
|
# But the provided initial value should be present
|
|
945
931
|
self.assertEqual(form.initial.get("name"), "Override Name")
|
|
946
932
|
|
|
933
|
+
def test_sites_initial_value_set_from_form_initial(self):
|
|
934
|
+
"""Test that sites field initial is set from self.initial["sites"]"""
|
|
935
|
+
# Create an existing sync instance with sites in parameters
|
|
936
|
+
sync_instance = IPFabricSync.objects.create(
|
|
937
|
+
name="Existing Sync",
|
|
938
|
+
snapshot_data=self.snapshot,
|
|
939
|
+
parameters={
|
|
940
|
+
"sites": ["site1", "site2"],
|
|
941
|
+
"groups": [self.transform_map_group.pk],
|
|
942
|
+
},
|
|
943
|
+
)
|
|
944
|
+
|
|
945
|
+
# Initialize form with existing instance and additional initial data for sites
|
|
946
|
+
# This will trigger the else branch where self.initial["sites"] is used
|
|
947
|
+
form = IPFabricSyncForm(
|
|
948
|
+
instance=sync_instance,
|
|
949
|
+
initial={"sites": ["override_site1", "override_site2"]},
|
|
950
|
+
)
|
|
951
|
+
|
|
952
|
+
# Verify sites field initial is set from self.initial
|
|
953
|
+
self.assertEqual(
|
|
954
|
+
form.fields["sites"].initial, ["override_site1", "override_site2"]
|
|
955
|
+
)
|
|
956
|
+
|
|
957
|
+
# Also verify that self.initial contains the expected sites
|
|
958
|
+
self.assertEqual(form.initial["sites"], ["override_site1", "override_site2"])
|
|
959
|
+
|
|
960
|
+
def test_htmx_boolean_field_list_values_handled(self):
|
|
961
|
+
"""Test sanitizing HTMX BooleanField list values like ['', 'on']"""
|
|
962
|
+
# Simulate HTMX request where BooleanField values become lists
|
|
963
|
+
# This happens when `source` field value is changed and form is re-drawn via HTMX
|
|
964
|
+
form = IPFabricSyncForm(
|
|
965
|
+
initial={
|
|
966
|
+
"auto_merge": ["", "on"], # HTMX sends BooleanField as list
|
|
967
|
+
"update_custom_fields": ["", "on"], # Another BooleanField as list
|
|
968
|
+
"ipf_site": ["", "on"], # ipf_ prefixed field as list
|
|
969
|
+
"name": "Test Sync", # Normal field (not affected)
|
|
970
|
+
},
|
|
971
|
+
data={
|
|
972
|
+
"name": "Test Sync HTMX",
|
|
973
|
+
"source": self.source.pk,
|
|
974
|
+
"snapshot_data": self.snapshot.pk,
|
|
975
|
+
},
|
|
976
|
+
)
|
|
977
|
+
|
|
978
|
+
# The last value from ['', 'on'] should be 'on' which evaluates to True for BooleanFields
|
|
979
|
+
self.assertEqual(form.initial["auto_merge"], "on")
|
|
980
|
+
self.assertEqual(form.initial["update_custom_fields"], "on")
|
|
981
|
+
self.assertEqual(form.initial["ipf_site"], "on")
|
|
982
|
+
self.assertEqual(form.initial["name"], "Test Sync") # Normal field unchanged
|
|
983
|
+
|
|
984
|
+
# Verify the form is still valid and processes correctly
|
|
985
|
+
self.assertTrue(form.is_valid(), form.errors)
|
|
986
|
+
|
|
987
|
+
def test_htmx_boolean_field_single_values_unchanged(self):
|
|
988
|
+
"""Test that normal single values are not affected by the HTMX list handling"""
|
|
989
|
+
# Test with normal single values (not lists)
|
|
990
|
+
form = IPFabricSyncForm(
|
|
991
|
+
initial={
|
|
992
|
+
"auto_merge": True, # Normal boolean value
|
|
993
|
+
"update_custom_fields": False, # Normal boolean value
|
|
994
|
+
"ipf_site": "on", # Normal string value
|
|
995
|
+
"name": "Test Sync", # Normal string value
|
|
996
|
+
},
|
|
997
|
+
data={
|
|
998
|
+
"name": "Test Sync Normal",
|
|
999
|
+
"source": self.source.pk,
|
|
1000
|
+
"snapshot_data": self.snapshot.pk,
|
|
1001
|
+
},
|
|
1002
|
+
)
|
|
1003
|
+
|
|
1004
|
+
# Verify that single values are not processed by value sanitization
|
|
1005
|
+
self.assertEqual(form.initial["auto_merge"], True)
|
|
1006
|
+
self.assertEqual(form.initial["update_custom_fields"], False)
|
|
1007
|
+
self.assertEqual(form.initial["ipf_site"], "on")
|
|
1008
|
+
self.assertEqual(form.initial["name"], "Test Sync")
|
|
1009
|
+
|
|
1010
|
+
# Verify the form is still valid
|
|
1011
|
+
self.assertTrue(form.is_valid(), form.errors)
|
|
1012
|
+
|
|
1013
|
+
def test_clean_snapshot_does_not_belong_to_source(self):
|
|
1014
|
+
"""Test form validation when snapshot doesn't belong to the selected source"""
|
|
1015
|
+
# Create a second source
|
|
1016
|
+
different_source = IPFabricSource.objects.create(
|
|
1017
|
+
name="Different Source",
|
|
1018
|
+
type=IPFabricSourceTypeChoices.LOCAL,
|
|
1019
|
+
url="https://different.ipfabric.local",
|
|
1020
|
+
status=DataSourceStatusChoices.NEW,
|
|
1021
|
+
)
|
|
1022
|
+
|
|
1023
|
+
# Try to use self.snapshot (which belongs to self.source) with different_source
|
|
1024
|
+
form = IPFabricSyncForm(
|
|
1025
|
+
data={
|
|
1026
|
+
"name": "Test Sync Mismatched Source",
|
|
1027
|
+
"source": different_source.pk,
|
|
1028
|
+
"snapshot_data": self.snapshot.pk, # This snapshot belongs to self.source, not different_source
|
|
1029
|
+
}
|
|
1030
|
+
)
|
|
1031
|
+
|
|
1032
|
+
# Form should be invalid due to snapshot/source mismatch validation
|
|
1033
|
+
self.assertFalse(form.is_valid(), form.errors)
|
|
1034
|
+
self.assertIn("snapshot_data", form.errors)
|
|
1035
|
+
self.assertTrue(
|
|
1036
|
+
"Snapshot does not belong to the selected source"
|
|
1037
|
+
in str(form.errors["snapshot_data"])
|
|
1038
|
+
)
|
|
1039
|
+
|
|
947
1040
|
def test_clean_sites_not_part_of_snapshot(self):
|
|
948
1041
|
"""Test form validation when selected sites are not part of the snapshot"""
|
|
949
1042
|
form = IPFabricSyncForm(
|
|
@@ -37,6 +37,7 @@ from ipfabric_netbox.models import IPFabricSync
|
|
|
37
37
|
from ipfabric_netbox.models import IPFabricTransformField
|
|
38
38
|
from ipfabric_netbox.models import IPFabricTransformMap
|
|
39
39
|
from ipfabric_netbox.models import IPFabricTransformMapGroup
|
|
40
|
+
from ipfabric_netbox.tables import DeviceIPFTable
|
|
40
41
|
|
|
41
42
|
|
|
42
43
|
class PluginPathMixin:
|
|
@@ -1971,7 +1972,19 @@ class IPFabricTableViewTestCase(PluginPathMixin, ModelTestCase):
|
|
|
1971
1972
|
# Validate context variables
|
|
1972
1973
|
self.assertEqual(response.context["object"], self.device)
|
|
1973
1974
|
self.assertIsNotNone(response.context["form"])
|
|
1974
|
-
|
|
1975
|
+
|
|
1976
|
+
table = response.context["table"]
|
|
1977
|
+
self.assertIsInstance(table, DeviceIPFTable)
|
|
1978
|
+
|
|
1979
|
+
# Verify the table has the expected structure for empty data scenario
|
|
1980
|
+
self.assertEqual(len(table.data), 0) # Should be empty when no source
|
|
1981
|
+
self.assertIn(
|
|
1982
|
+
"hostname", [col.name for col in table.columns]
|
|
1983
|
+
) # Should have default hostname column
|
|
1984
|
+
|
|
1985
|
+
# Verify table meta attributes
|
|
1986
|
+
self.assertEqual(table.empty_text, "No results found")
|
|
1987
|
+
self.assertIn("table-hover", table.attrs.get("class", ""))
|
|
1975
1988
|
|
|
1976
1989
|
# When no source is available, source should be None
|
|
1977
1990
|
self.assertIsNone(response.context["source"])
|
{ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/utilities/transform_map.py
RENAMED
|
@@ -8,10 +8,10 @@ from django.apps import apps as django_apps
|
|
|
8
8
|
# see https://docs.djangoproject.com/en/5.1/topics/migrations/#historical-models
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
def build_fields(data, apps):
|
|
11
|
+
def build_fields(data, apps, db_alias):
|
|
12
12
|
ContentType = apps.get_model("contenttypes", "ContentType")
|
|
13
13
|
if "target_model" in data:
|
|
14
|
-
ct = ContentType.objects.get_for_model(
|
|
14
|
+
ct = ContentType.objects.db_manager(db_alias).get_for_model(
|
|
15
15
|
apps.get_model(
|
|
16
16
|
data["target_model"]["app_label"],
|
|
17
17
|
data["target_model"]["model"],
|
|
@@ -19,7 +19,7 @@ def build_fields(data, apps):
|
|
|
19
19
|
)
|
|
20
20
|
data["target_model"] = ct
|
|
21
21
|
elif "source_model" in data:
|
|
22
|
-
ct = ContentType.objects.get_for_model(
|
|
22
|
+
ct = ContentType.objects.db_manager(db_alias).get_for_model(
|
|
23
23
|
apps.get_model(
|
|
24
24
|
data["source_model"]["app_label"],
|
|
25
25
|
data["source_model"]["model"],
|
|
@@ -29,7 +29,7 @@ def build_fields(data, apps):
|
|
|
29
29
|
return data
|
|
30
30
|
|
|
31
31
|
|
|
32
|
-
def build_transform_maps(data, apps: django_apps = None):
|
|
32
|
+
def build_transform_maps(data, apps: django_apps = None, db_alias: str = "default"):
|
|
33
33
|
apps = apps or django_apps
|
|
34
34
|
IPFabricTransformMap = apps.get_model("ipfabric_netbox", "IPFabricTransformMap")
|
|
35
35
|
IPFabricTransformField = apps.get_model("ipfabric_netbox", "IPFabricTransformField")
|
|
@@ -37,14 +37,16 @@ def build_transform_maps(data, apps: django_apps = None):
|
|
|
37
37
|
"ipfabric_netbox", "IPFabricRelationshipField"
|
|
38
38
|
)
|
|
39
39
|
for tm in data:
|
|
40
|
-
field_data = build_fields(tm["data"], apps)
|
|
41
|
-
tm_obj = IPFabricTransformMap.objects.create(**field_data)
|
|
40
|
+
field_data = build_fields(tm["data"], apps, db_alias)
|
|
41
|
+
tm_obj = IPFabricTransformMap.objects.using(db_alias).create(**field_data)
|
|
42
42
|
for fm in tm["field_maps"]:
|
|
43
|
-
field_data = build_fields(fm, apps)
|
|
44
|
-
IPFabricTransformField.objects.create(
|
|
43
|
+
field_data = build_fields(fm, apps, db_alias)
|
|
44
|
+
IPFabricTransformField.objects.using(db_alias).create(
|
|
45
|
+
transform_map=tm_obj, **field_data
|
|
46
|
+
)
|
|
45
47
|
for rm in tm["relationship_maps"]:
|
|
46
|
-
relationship_data = build_fields(rm, apps)
|
|
47
|
-
IPFabricRelationshipField.objects.create(
|
|
48
|
+
relationship_data = build_fields(rm, apps, db_alias)
|
|
49
|
+
IPFabricRelationshipField.objects.using(db_alias).create(
|
|
48
50
|
transform_map=tm_obj, **relationship_data
|
|
49
51
|
)
|
|
50
52
|
|
|
@@ -544,13 +544,13 @@ class IPFabricSyncEditView(generic.ObjectEditView):
|
|
|
544
544
|
@register_model_view(IPFabricSync)
|
|
545
545
|
class IPFabricSyncView(generic.ObjectView):
|
|
546
546
|
queryset = IPFabricSync.objects.all()
|
|
547
|
-
actions = ("edit",)
|
|
548
547
|
|
|
549
548
|
def get(self, request, **kwargs):
|
|
550
|
-
|
|
551
|
-
last_ingestion = instance.ipfabricingestion_set.last()
|
|
552
|
-
|
|
549
|
+
# Handle HTMX requests separately
|
|
553
550
|
if request.htmx:
|
|
551
|
+
instance = self.get_object(**kwargs)
|
|
552
|
+
last_ingestion = instance.ipfabricingestion_set.last()
|
|
553
|
+
|
|
554
554
|
response = render(
|
|
555
555
|
request,
|
|
556
556
|
"ipfabric_netbox/partials/sync_last_ingestion.html",
|
|
@@ -565,15 +565,8 @@ class IPFabricSyncView(generic.ObjectView):
|
|
|
565
565
|
response["HX-Refresh"] = "true"
|
|
566
566
|
return response
|
|
567
567
|
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
self.get_template_name(),
|
|
571
|
-
{
|
|
572
|
-
"object": instance,
|
|
573
|
-
"tab": self.tab,
|
|
574
|
-
**self.get_extra_context(request, instance),
|
|
575
|
-
},
|
|
576
|
-
)
|
|
568
|
+
# For regular requests, use the parent method which includes actions
|
|
569
|
+
return super().get(request, **kwargs)
|
|
577
570
|
|
|
578
571
|
def get_extra_context(self, request, instance):
|
|
579
572
|
if request.GET.get("format") in ["json", "yaml"]:
|
|
@@ -641,6 +634,26 @@ class IPFabricTransformMapTabView(generic.ObjectChildrenView):
|
|
|
641
634
|
)
|
|
642
635
|
|
|
643
636
|
|
|
637
|
+
@register_model_view(IPFabricSync, "ingestion")
|
|
638
|
+
class IPFabricIngestionTabView(generic.ObjectChildrenView):
|
|
639
|
+
queryset = IPFabricSync.objects.all()
|
|
640
|
+
child_model = IPFabricIngestion
|
|
641
|
+
table = IPFabricIngestionTable
|
|
642
|
+
filterset = IPFabricIngestionFilterSet
|
|
643
|
+
tab = ViewTab(
|
|
644
|
+
label="Ingestions",
|
|
645
|
+
badge=lambda obj: IPFabricIngestion.objects.filter(sync=obj).count(),
|
|
646
|
+
permission="ipfabric_netbox.view_ipfabricingestion",
|
|
647
|
+
)
|
|
648
|
+
|
|
649
|
+
def get_children(self, request, parent):
|
|
650
|
+
return self.child_model.objects.filter(sync=parent).annotate(
|
|
651
|
+
description=models.F("branch__description"),
|
|
652
|
+
user=models.F("sync__user__username"),
|
|
653
|
+
staged_changes=models.Count(models.F("branch__changediff")),
|
|
654
|
+
)
|
|
655
|
+
|
|
656
|
+
|
|
644
657
|
# Ingestion
|
|
645
658
|
class IPFabricIngestionListView(generic.ObjectListView):
|
|
646
659
|
queryset = IPFabricIngestion.objects.annotate(
|
|
@@ -870,34 +883,15 @@ class IPFabricIngestionDeleteView(generic.ObjectDeleteView):
|
|
|
870
883
|
queryset = IPFabricIngestion.objects.all()
|
|
871
884
|
|
|
872
885
|
|
|
873
|
-
@register_model_view(IPFabricSync, "ingestion")
|
|
874
|
-
class IPFabricIngestionTabView(generic.ObjectChildrenView):
|
|
875
|
-
queryset = IPFabricSync.objects.all()
|
|
876
|
-
child_model = IPFabricIngestion
|
|
877
|
-
table = IPFabricIngestionTable
|
|
878
|
-
filterset = IPFabricIngestionFilterSet
|
|
879
|
-
template_name = "generic/object_children.html"
|
|
880
|
-
tab = ViewTab(
|
|
881
|
-
label="Ingestions",
|
|
882
|
-
badge=lambda obj: IPFabricIngestion.objects.filter(sync=obj).count(),
|
|
883
|
-
permission="ipfabric_netbox.view_ipfabricingestion",
|
|
884
|
-
)
|
|
885
|
-
|
|
886
|
-
def get_children(self, request, parent):
|
|
887
|
-
return self.child_model.objects.filter(sync=parent).annotate(
|
|
888
|
-
description=models.F("branch__description"),
|
|
889
|
-
user=models.F("sync__user__username"),
|
|
890
|
-
staged_changes=models.Count(models.F("branch__changediff")),
|
|
891
|
-
)
|
|
892
|
-
|
|
893
|
-
|
|
894
886
|
@register_model_view(Device, "ipfabric")
|
|
895
|
-
class IPFabricTable(
|
|
887
|
+
class IPFabricTable(generic.ObjectView):
|
|
896
888
|
template_name = "ipfabric_netbox/ipfabric_table.html"
|
|
897
889
|
tab = ViewTab("IP Fabric", permission="ipfabric_netbox.view_devicetable")
|
|
890
|
+
queryset = Device.objects.all()
|
|
898
891
|
|
|
899
|
-
def
|
|
900
|
-
|
|
892
|
+
def get_extra_context(self, request, instance):
|
|
893
|
+
"""Process form and prepare table data for the template."""
|
|
894
|
+
device = instance
|
|
901
895
|
form = (
|
|
902
896
|
IPFabricTableForm(request.GET)
|
|
903
897
|
if "table" in request.GET
|
|
@@ -908,7 +902,7 @@ class IPFabricTable(View):
|
|
|
908
902
|
source = None
|
|
909
903
|
|
|
910
904
|
if form.is_valid():
|
|
911
|
-
|
|
905
|
+
table_name = form.cleaned_data["table"]
|
|
912
906
|
test = {
|
|
913
907
|
"True": True,
|
|
914
908
|
"False": False,
|
|
@@ -929,36 +923,24 @@ class IPFabricTable(View):
|
|
|
929
923
|
snapshot_id = form.cleaned_data["snapshot_data"].snapshot_id
|
|
930
924
|
source = source or form.cleaned_data["snapshot_data"].source
|
|
931
925
|
|
|
932
|
-
if source is None:
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
)
|
|
951
|
-
if cache_enable:
|
|
952
|
-
data = cache.get(cache_key)
|
|
953
|
-
|
|
954
|
-
if not data:
|
|
955
|
-
try:
|
|
956
|
-
ipf = IPFabric(parameters=source.parameters)
|
|
957
|
-
raw_data, columns = ipf.get_table_data(table=table, device=device)
|
|
958
|
-
data = {"data": raw_data, "columns": columns}
|
|
959
|
-
cache.set(cache_key, data, 60 * 60 * 24)
|
|
960
|
-
except Exception as e:
|
|
961
|
-
messages.error(request, e)
|
|
926
|
+
if source is not None:
|
|
927
|
+
source.parameters["snapshot_id"] = snapshot_id
|
|
928
|
+
source.parameters["base_url"] = source.url
|
|
929
|
+
|
|
930
|
+
cache_key = f"ipfabric_{table_name}_{device.serial}_{source.parameters['snapshot_id']}"
|
|
931
|
+
if cache_enable:
|
|
932
|
+
data = cache.get(cache_key)
|
|
933
|
+
|
|
934
|
+
if not data:
|
|
935
|
+
try:
|
|
936
|
+
ipf = IPFabric(parameters=source.parameters)
|
|
937
|
+
raw_data, columns = ipf.get_table_data(
|
|
938
|
+
table=table_name, device=device
|
|
939
|
+
)
|
|
940
|
+
data = {"data": raw_data, "columns": columns}
|
|
941
|
+
cache.set(cache_key, data, 60 * 60 * 24)
|
|
942
|
+
except Exception as e:
|
|
943
|
+
messages.error(request, e)
|
|
962
944
|
|
|
963
945
|
if not data:
|
|
964
946
|
data = {"data": [], "columns": []}
|
|
@@ -973,32 +955,34 @@ class IPFabricTable(View):
|
|
|
973
955
|
},
|
|
974
956
|
).configure(table)
|
|
975
957
|
|
|
958
|
+
if not source:
|
|
959
|
+
if source_id := device.custom_field_data.get("ipfabric_source"):
|
|
960
|
+
source = IPFabricSource.objects.filter(pk=source_id).first()
|
|
961
|
+
else:
|
|
962
|
+
source = IPFabricSource.get_for_site(device.site).first()
|
|
963
|
+
|
|
964
|
+
return {
|
|
965
|
+
"source": source,
|
|
966
|
+
"form": form,
|
|
967
|
+
"table": table,
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
def get(self, request, **kwargs):
|
|
971
|
+
"""Handle GET requests, with special handling for HTMX table updates."""
|
|
972
|
+
# For HTMX requests, we only need to return the table HTML
|
|
976
973
|
if request.htmx:
|
|
974
|
+
device = get_object_or_404(Device, pk=kwargs.get("pk"))
|
|
975
|
+
context = self.get_extra_context(request, device)
|
|
977
976
|
return render(
|
|
978
977
|
request,
|
|
979
978
|
"htmx/table.html",
|
|
980
979
|
{
|
|
981
|
-
"table": table,
|
|
980
|
+
"table": context["table"],
|
|
982
981
|
},
|
|
983
982
|
)
|
|
984
983
|
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
source = IPFabricSource.objects.filter(pk=source_id).first()
|
|
988
|
-
else:
|
|
989
|
-
source = IPFabricSource.get_for_site(device.site).first()
|
|
990
|
-
|
|
991
|
-
return render(
|
|
992
|
-
request,
|
|
993
|
-
self.template_name,
|
|
994
|
-
{
|
|
995
|
-
"object": device,
|
|
996
|
-
"source": source,
|
|
997
|
-
"tab": self.tab,
|
|
998
|
-
"form": form,
|
|
999
|
-
"table": table,
|
|
1000
|
-
},
|
|
1001
|
-
)
|
|
984
|
+
# For regular requests, use the parent's get() method which will call get_extra_context()
|
|
985
|
+
return super().get(request, **kwargs)
|
|
1002
986
|
|
|
1003
987
|
|
|
1004
988
|
@register_model_view(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "ipfabric_netbox"
|
|
3
|
-
version = "4.2.
|
|
3
|
+
version = "4.2.2b4"
|
|
4
4
|
description = "NetBox plugin to sync IP Fabric data into NetBox"
|
|
5
5
|
authors = ["Solution Architecture <solution.architecture@ipfabric.io>"]
|
|
6
6
|
license = "MIT"
|
|
@@ -29,7 +29,7 @@ ipfabric = [
|
|
|
29
29
|
{version = "~=7.2.0", markers = "extra != 'ipfabric_6_10' and extra != 'ipfabric_7_0' and extra == 'ipfabric_7_2' and extra != 'ipfabric_7_3'", optional = true},
|
|
30
30
|
{version = "~=7.3.0", markers = "extra != 'ipfabric_6_10' and extra != 'ipfabric_7_0' and extra != 'ipfabric_7_2' and extra == 'ipfabric_7_3'", optional = true},
|
|
31
31
|
]
|
|
32
|
-
netboxlabs-netbox-branching = "
|
|
32
|
+
netboxlabs-netbox-branching = "0.7.0"
|
|
33
33
|
netutils = "*"
|
|
34
34
|
|
|
35
35
|
[tool.poetry.extras]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/migrations/0001_initial.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ipfabric_netbox-4.2.2b3 → ipfabric_netbox-4.2.2b4}/ipfabric_netbox/templatetags/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|