ipfabric_netbox 4.2.0b9__py3-none-any.whl → 4.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of ipfabric_netbox might be problematic. Click here for more details.
- ipfabric_netbox/__init__.py +1 -1
- ipfabric_netbox/api/__init__.py +1 -0
- ipfabric_netbox/api/nested_serializers.py +78 -0
- ipfabric_netbox/api/serializers.py +90 -147
- ipfabric_netbox/api/urls.py +4 -4
- ipfabric_netbox/api/views.py +19 -18
- ipfabric_netbox/choices.py +12 -0
- ipfabric_netbox/filtersets.py +4 -67
- ipfabric_netbox/forms.py +140 -92
- ipfabric_netbox/models.py +10 -10
- ipfabric_netbox/tables.py +9 -30
- ipfabric_netbox/template_content.py +20 -3
- ipfabric_netbox/templates/ipfabric_netbox/inc/site_topology_button.html +10 -3
- ipfabric_netbox/templates/ipfabric_netbox/ipfabricsync_list.html +71 -0
- ipfabric_netbox/tests/test_models.py +11 -47
- ipfabric_netbox/utilities/ipfutils.py +23 -43
- ipfabric_netbox/views.py +8 -6
- {ipfabric_netbox-4.2.0b9.dist-info → ipfabric_netbox-4.2.1.dist-info}/METADATA +6 -6
- {ipfabric_netbox-4.2.0b9.dist-info → ipfabric_netbox-4.2.1.dist-info}/RECORD +20 -29
- ipfabric_netbox/graphql/__init__.py +0 -23
- ipfabric_netbox/graphql/enums.py +0 -35
- ipfabric_netbox/graphql/filters.py +0 -317
- ipfabric_netbox/graphql/schema.py +0 -101
- ipfabric_netbox/graphql/types.py +0 -216
- ipfabric_netbox/migrations/0016_tags_and_changelog_for_snapshots.py +0 -31
- ipfabric_netbox/migrations/0017_ipfabricsync_update_custom_fields.py +0 -17
- ipfabric_netbox/migrations/0018_remove_type_field.py +0 -17
- ipfabric_netbox/tests/api/__init__.py +0 -0
- ipfabric_netbox/tests/api/test_api.py +0 -879
- ipfabric_netbox/tests/test_forms.py +0 -1440
- {ipfabric_netbox-4.2.0b9.dist-info → ipfabric_netbox-4.2.1.dist-info}/WHEEL +0 -0
ipfabric_netbox/forms.py
CHANGED
|
@@ -46,6 +46,22 @@ exclude_fields = [
|
|
|
46
46
|
"status",
|
|
47
47
|
]
|
|
48
48
|
|
|
49
|
+
|
|
50
|
+
class IPFSiteChoiceField(forms.MultipleChoiceField):
|
|
51
|
+
def valid_value(self, value):
|
|
52
|
+
"""Check to see if the provided value is a valid choice."""
|
|
53
|
+
text_value = str(value)
|
|
54
|
+
for k, v in self.choices:
|
|
55
|
+
if isinstance(v, (list, tuple)):
|
|
56
|
+
for k2, v2 in v:
|
|
57
|
+
if value == k2 or text_value == str(k2):
|
|
58
|
+
return True
|
|
59
|
+
else:
|
|
60
|
+
if value == k or text_value == str(k):
|
|
61
|
+
return True
|
|
62
|
+
return False
|
|
63
|
+
|
|
64
|
+
|
|
49
65
|
dcim_parameters = {
|
|
50
66
|
"site": forms.BooleanField(required=False, label=_("Sites"), initial=True),
|
|
51
67
|
"manufacturer": forms.BooleanField(
|
|
@@ -62,45 +78,43 @@ dcim_parameters = {
|
|
|
62
78
|
"virtualchassis": forms.BooleanField(
|
|
63
79
|
required=False, label=_("Virtual Chassis"), initial=True
|
|
64
80
|
),
|
|
65
|
-
"interface": forms.BooleanField(
|
|
66
|
-
|
|
67
|
-
),
|
|
68
|
-
"macaddress": forms.BooleanField(
|
|
69
|
-
required=False, label=_("MAC Addresses"), initial=False
|
|
70
|
-
),
|
|
71
|
-
"inventoryitem": forms.BooleanField(
|
|
72
|
-
required=False, label=_("Part Numbers"), initial=False
|
|
73
|
-
),
|
|
81
|
+
"interface": forms.BooleanField(required=False, label=_("Interfaces")),
|
|
82
|
+
"macaddress": forms.BooleanField(required=False, label=_("MAC Addresses")),
|
|
83
|
+
"inventoryitem": forms.BooleanField(required=False, label=_("Part Numbers")),
|
|
74
84
|
}
|
|
75
85
|
ipam_parameters = {
|
|
76
|
-
"vlan": forms.BooleanField(required=False, label=_("VLANs")
|
|
77
|
-
"vrf": forms.BooleanField(required=False, label=_("VRFs")
|
|
78
|
-
"prefix": forms.BooleanField(required=False, label=_("Prefixes")
|
|
79
|
-
"ipaddress": forms.BooleanField(
|
|
80
|
-
required=False, label=_("IP Addresses"), initial=False
|
|
81
|
-
),
|
|
86
|
+
"vlan": forms.BooleanField(required=False, label=_("VLANs")),
|
|
87
|
+
"vrf": forms.BooleanField(required=False, label=_("VRFs")),
|
|
88
|
+
"prefix": forms.BooleanField(required=False, label=_("Prefixes")),
|
|
89
|
+
"ipaddress": forms.BooleanField(required=False, label=_("IP Addresses")),
|
|
82
90
|
}
|
|
83
91
|
sync_parameters = {"dcim": dcim_parameters, "ipam": ipam_parameters}
|
|
84
92
|
|
|
85
93
|
|
|
86
|
-
def source_column_choices(model
|
|
94
|
+
def source_column_choices(model):
|
|
87
95
|
columns = transform_field_source_columns.get(model, None)
|
|
88
96
|
if columns:
|
|
89
97
|
choices = [(f, f) for f in transform_field_source_columns.get(model)]
|
|
90
98
|
else:
|
|
91
|
-
|
|
92
|
-
choices = [] # pragma: no cover
|
|
99
|
+
choices = []
|
|
93
100
|
return choices
|
|
94
101
|
|
|
95
102
|
|
|
96
|
-
def
|
|
97
|
-
|
|
98
|
-
|
|
103
|
+
def add_all_sites(choices):
|
|
104
|
+
"""
|
|
105
|
+
Add a blank choice to the beginning of a choices list.
|
|
106
|
+
"""
|
|
107
|
+
return ((None, "All Sites"),) + tuple(choices)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def str_to_list(str):
|
|
111
|
+
if not isinstance(str, list):
|
|
112
|
+
return [str]
|
|
99
113
|
else:
|
|
100
|
-
return
|
|
114
|
+
return str
|
|
101
115
|
|
|
102
116
|
|
|
103
|
-
def list_to_choices(choices
|
|
117
|
+
def list_to_choices(choices):
|
|
104
118
|
new_choices = ()
|
|
105
119
|
for choice in choices:
|
|
106
120
|
new_choices = new_choices + ((choice, choice),)
|
|
@@ -162,7 +176,7 @@ class IPFabricRelationshipFieldForm(NetBoxModelForm):
|
|
|
162
176
|
)
|
|
163
177
|
self.fields["target_field"].widget.initial = self.instance.target_field
|
|
164
178
|
else:
|
|
165
|
-
if kwargs
|
|
179
|
+
if kwargs["initial"].get("transform_map", None):
|
|
166
180
|
transform_map_id = kwargs["initial"]["transform_map"]
|
|
167
181
|
transform_map = IPFabricTransformMap.objects.get(
|
|
168
182
|
pk=transform_map_id
|
|
@@ -244,7 +258,7 @@ class IPFabricTransformFieldForm(NetBoxModelForm):
|
|
|
244
258
|
source_column_choices(source_fields)
|
|
245
259
|
)
|
|
246
260
|
else:
|
|
247
|
-
if kwargs
|
|
261
|
+
if kwargs["initial"].get("transform_map", None):
|
|
248
262
|
transform_map_id = kwargs["initial"]["transform_map"]
|
|
249
263
|
transform_map = IPFabricTransformMap.objects.get(
|
|
250
264
|
pk=transform_map_id
|
|
@@ -361,22 +375,22 @@ class IPFabricSourceForm(NetBoxModelForm):
|
|
|
361
375
|
"type": HTMXSelect(),
|
|
362
376
|
}
|
|
363
377
|
|
|
378
|
+
@property
|
|
379
|
+
def fieldsets(self):
|
|
380
|
+
fieldsets = [
|
|
381
|
+
FieldSet("name", "type", "url", name=_("Source")),
|
|
382
|
+
FieldSet("timeout", name=_("Parameters")),
|
|
383
|
+
]
|
|
384
|
+
|
|
385
|
+
if self.source_type == "local":
|
|
386
|
+
fieldsets[1] = FieldSet("auth", "verify", "timeout", name=_("Parameters"))
|
|
387
|
+
|
|
388
|
+
return fieldsets
|
|
389
|
+
|
|
364
390
|
def __init__(self, *args, **kwargs):
|
|
365
391
|
super().__init__(*args, **kwargs)
|
|
366
392
|
self.source_type = get_field_value(self, "type")
|
|
367
393
|
|
|
368
|
-
# Set fieldsets dynamically based on source_type
|
|
369
|
-
if self.source_type == "local":
|
|
370
|
-
self.fieldsets = [
|
|
371
|
-
FieldSet("name", "type", "url", name=_("Source")),
|
|
372
|
-
FieldSet("auth", "verify", "timeout", name=_("Parameters")),
|
|
373
|
-
]
|
|
374
|
-
else:
|
|
375
|
-
self.fieldsets = [
|
|
376
|
-
FieldSet("name", "type", "url", name=_("Source")),
|
|
377
|
-
FieldSet("timeout", name=_("Parameters")),
|
|
378
|
-
]
|
|
379
|
-
|
|
380
394
|
self.fields["timeout"] = forms.IntegerField(
|
|
381
395
|
required=False,
|
|
382
396
|
label=_("Timeout"),
|
|
@@ -437,9 +451,6 @@ class OrderedModelMultipleChoiceField(forms.ModelMultipleChoiceField):
|
|
|
437
451
|
|
|
438
452
|
def clean(self, value):
|
|
439
453
|
qs = super().clean(value)
|
|
440
|
-
# Handle None or empty values
|
|
441
|
-
if not value:
|
|
442
|
-
return qs
|
|
443
454
|
clauses = " ".join(
|
|
444
455
|
["WHEN id=%s THEN %s" % (pk, i) for i, pk in enumerate(value)]
|
|
445
456
|
)
|
|
@@ -477,18 +488,11 @@ class IPFabricSyncForm(NetBoxModelForm):
|
|
|
477
488
|
sites = forms.MultipleChoiceField(
|
|
478
489
|
required=False,
|
|
479
490
|
label=_("Sites"),
|
|
480
|
-
help_text=_("Defaults to all sites if none selected."),
|
|
481
491
|
widget=APISelectMultiple(
|
|
482
492
|
api_url="/api/plugins/ipfabric/snapshot/{{snapshot_data}}/sites/",
|
|
483
493
|
),
|
|
484
494
|
)
|
|
485
495
|
|
|
486
|
-
update_custom_fields = forms.BooleanField(
|
|
487
|
-
required=False,
|
|
488
|
-
label=_("Custom Fields Updating"),
|
|
489
|
-
help_text=_("Update object custom fields where applicable."),
|
|
490
|
-
)
|
|
491
|
-
|
|
492
496
|
scheduled = forms.DateTimeField(
|
|
493
497
|
required=False,
|
|
494
498
|
widget=DateTimePicker(),
|
|
@@ -515,13 +519,41 @@ class IPFabricSyncForm(NetBoxModelForm):
|
|
|
515
519
|
"source",
|
|
516
520
|
"snapshot_data",
|
|
517
521
|
"auto_merge",
|
|
518
|
-
"update_custom_fields",
|
|
519
522
|
"sites",
|
|
523
|
+
"type",
|
|
520
524
|
"tags",
|
|
521
525
|
"scheduled",
|
|
522
526
|
"interval",
|
|
523
527
|
)
|
|
524
|
-
widgets = {
|
|
528
|
+
widgets = {
|
|
529
|
+
"source": HTMXSelect(),
|
|
530
|
+
"type": HTMXSelect(),
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
@property
|
|
534
|
+
def fieldsets(self):
|
|
535
|
+
fieldsets = [
|
|
536
|
+
FieldSet("name", "source", "groups", name=_("IP Fabric Source")),
|
|
537
|
+
]
|
|
538
|
+
if self.source_type == "local":
|
|
539
|
+
fieldsets.append(
|
|
540
|
+
FieldSet("snapshot_data", "sites", name=_("Snapshot Information")),
|
|
541
|
+
)
|
|
542
|
+
else:
|
|
543
|
+
fieldsets.append(
|
|
544
|
+
FieldSet("snapshot_data", name=_("Snapshot Information")),
|
|
545
|
+
)
|
|
546
|
+
fieldsets.append(FieldSet("type", name=_("Ingestion Type")))
|
|
547
|
+
if self.backend_fields:
|
|
548
|
+
for k, v in self.backend_fields.items():
|
|
549
|
+
fieldsets.append(FieldSet(*v, name=f"{k.upper()} Parameters"))
|
|
550
|
+
fieldsets.append(
|
|
551
|
+
FieldSet("scheduled", "interval", name=_("Ingestion Execution Parameters"))
|
|
552
|
+
)
|
|
553
|
+
fieldsets.append(FieldSet("auto_merge", name=_("Extras")))
|
|
554
|
+
fieldsets.append(FieldSet("tags", name=_("Tags")))
|
|
555
|
+
|
|
556
|
+
return fieldsets
|
|
525
557
|
|
|
526
558
|
def __init__(self, *args, **kwargs):
|
|
527
559
|
super().__init__(*args, **kwargs)
|
|
@@ -553,13 +585,20 @@ class IPFabricSyncForm(NetBoxModelForm):
|
|
|
553
585
|
self.initial["sites"] = self.instance.parameters.get("sites", [])
|
|
554
586
|
self.initial["groups"] = self.instance.parameters.get("groups", [])
|
|
555
587
|
|
|
588
|
+
backend_type = get_field_value(self, "type")
|
|
589
|
+
backend = {}
|
|
590
|
+
if backend_type == "all":
|
|
591
|
+
backend = sync_parameters
|
|
592
|
+
else:
|
|
593
|
+
backend[backend_type] = sync_parameters.get(backend_type)
|
|
594
|
+
|
|
556
595
|
now = local_now().strftime("%Y-%m-%d %H:%M:%S")
|
|
557
596
|
self.fields["scheduled"].help_text += f" (current time: <strong>{now}</strong>)"
|
|
558
597
|
|
|
559
598
|
# Add backend-specific form fields
|
|
560
599
|
self.backend_fields = {}
|
|
561
600
|
|
|
562
|
-
for k, v in
|
|
601
|
+
for k, v in backend.items():
|
|
563
602
|
self.backend_fields[k] = []
|
|
564
603
|
for name, form_field in v.items():
|
|
565
604
|
field_name = f"ipf_{name}"
|
|
@@ -568,52 +607,17 @@ class IPFabricSyncForm(NetBoxModelForm):
|
|
|
568
607
|
if self.instance and self.instance.parameters:
|
|
569
608
|
self.fields[field_name].initial = self.instance.parameters.get(name)
|
|
570
609
|
|
|
571
|
-
# Set fieldsets dynamically based and backend_fields
|
|
572
|
-
fieldsets = [
|
|
573
|
-
FieldSet("name", "source", "groups", name=_("IP Fabric Source")),
|
|
574
|
-
]
|
|
575
|
-
if self.source_type == "local":
|
|
576
|
-
fieldsets.append(
|
|
577
|
-
FieldSet("snapshot_data", "sites", name=_("Snapshot Information")),
|
|
578
|
-
)
|
|
579
|
-
else:
|
|
580
|
-
fieldsets.append(
|
|
581
|
-
FieldSet("snapshot_data", name=_("Snapshot Information")),
|
|
582
|
-
)
|
|
583
|
-
for k, v in self.backend_fields.items():
|
|
584
|
-
fieldsets.append(FieldSet(*v, name=f"{k.upper()} Parameters"))
|
|
585
|
-
fieldsets.append(
|
|
586
|
-
FieldSet("scheduled", "interval", name=_("Ingestion Execution Parameters"))
|
|
587
|
-
)
|
|
588
|
-
fieldsets.append(
|
|
589
|
-
FieldSet("auto_merge", "update_custom_fields", name=_("Extras"))
|
|
590
|
-
)
|
|
591
|
-
fieldsets.append(FieldSet("tags", name=_("Tags")))
|
|
592
|
-
|
|
593
|
-
self.fieldsets = fieldsets
|
|
594
|
-
|
|
595
610
|
def clean(self):
|
|
596
611
|
super().clean()
|
|
612
|
+
snapshot = self.cleaned_data["snapshot_data"]
|
|
597
613
|
|
|
598
614
|
sites = self.data.get("sites")
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
if not
|
|
604
|
-
|
|
605
|
-
for site in sites
|
|
606
|
-
):
|
|
607
|
-
invalid_sites = [
|
|
608
|
-
site
|
|
609
|
-
for site in sites
|
|
610
|
-
if not any(
|
|
611
|
-
site in snapshot_site for snapshot_site in snapshot.sites
|
|
612
|
-
)
|
|
613
|
-
]
|
|
614
|
-
raise ValidationError(
|
|
615
|
-
{"sites": f"Sites {invalid_sites} not part of the snapshot."}
|
|
616
|
-
)
|
|
615
|
+
choices = list_to_choices(str_to_list(sites))
|
|
616
|
+
self.fields["sites"].choices = choices
|
|
617
|
+
|
|
618
|
+
if sites:
|
|
619
|
+
if not any(y in x for x in snapshot.sites for y in sites):
|
|
620
|
+
raise ValidationError({"sites": f"{sites} not part of the snapshot."})
|
|
617
621
|
|
|
618
622
|
scheduled_time = self.cleaned_data.get("scheduled")
|
|
619
623
|
if scheduled_time and scheduled_time < local_now():
|
|
@@ -661,6 +665,50 @@ class IPFabricSyncForm(NetBoxModelForm):
|
|
|
661
665
|
return object
|
|
662
666
|
|
|
663
667
|
|
|
668
|
+
# class SyncForm(forms.Form):
|
|
669
|
+
# def __init__(self, *args, **kwargs):
|
|
670
|
+
# self.snapshots = kwargs.pop("snapshot_choices", None)
|
|
671
|
+
# self.sites = kwargs.pop("site_choices", None)
|
|
672
|
+
# super(SyncForm, self).__init__(*args, **kwargs)
|
|
673
|
+
# if self.snapshots:
|
|
674
|
+
# snapshot_choices = [
|
|
675
|
+
# (snapshot_id, snapshot_name)
|
|
676
|
+
# for snapshot_name, snapshot_id in self.snapshots.values()
|
|
677
|
+
# ]
|
|
678
|
+
# self.fields["snapshot"] = forms.ChoiceField(
|
|
679
|
+
# label="Snapshot",
|
|
680
|
+
# required=True,
|
|
681
|
+
# choices=snapshot_choices,
|
|
682
|
+
# help_text="IPFabric snapshot to sync from. Defaults to $last",
|
|
683
|
+
# widget=forms.Select(
|
|
684
|
+
# attrs={
|
|
685
|
+
# "hx-get": reverse("plugins:ipfabric_netbox:ipfabricsync_add"),
|
|
686
|
+
# "hx-trigger": "change",
|
|
687
|
+
# "hx-target": "#modules",
|
|
688
|
+
# "class": "form-control",
|
|
689
|
+
# }
|
|
690
|
+
# ),
|
|
691
|
+
# )
|
|
692
|
+
# if self.sites:
|
|
693
|
+
# site_choices = [(site, site) for site in self.sites]
|
|
694
|
+
# self.fields["site"] = forms.ChoiceField(
|
|
695
|
+
# label="Site",
|
|
696
|
+
# required=False,
|
|
697
|
+
# choices=add_blank_choice(site_choices),
|
|
698
|
+
# help_text="Sites available within snapshot",
|
|
699
|
+
# widget=forms.Select(attrs={"class": "form-control"}),
|
|
700
|
+
# )
|
|
701
|
+
# else:
|
|
702
|
+
# self.fields["site"] = forms.ChoiceField(
|
|
703
|
+
# label="Site",
|
|
704
|
+
# required=False,
|
|
705
|
+
# choices=add_blank_choice([]),
|
|
706
|
+
# help_text="Sites available within snapshot",
|
|
707
|
+
# widget=forms.Select(
|
|
708
|
+
# attrs={"class": "form-control", "disabled": "disabled"}
|
|
709
|
+
# ),
|
|
710
|
+
# )
|
|
711
|
+
|
|
664
712
|
tableChoices = [
|
|
665
713
|
("eol_details", "Inventory - EOL_DETAILS"),
|
|
666
714
|
("fans", "Inventory - FANS"),
|
ipfabric_netbox/models.py
CHANGED
|
@@ -44,6 +44,7 @@ from utilities.request import NetBoxFakeRequest
|
|
|
44
44
|
from .choices import IPFabricRawDataTypeChoices
|
|
45
45
|
from .choices import IPFabricSnapshotStatusModelChoices
|
|
46
46
|
from .choices import IPFabricSourceTypeChoices
|
|
47
|
+
from .choices import IPFabricSyncTypeChoices
|
|
47
48
|
from .choices import IPFabricTransformMapSourceModelChoices
|
|
48
49
|
from .choices import required_transform_map_contenttypes
|
|
49
50
|
from .signals import clear_other_primary_ip
|
|
@@ -544,7 +545,9 @@ class IPFabricSource(IPFabricClient, JobsMixin, PrimaryModel):
|
|
|
544
545
|
# post_sync.send(sender=self.__class__, instance=self)
|
|
545
546
|
|
|
546
547
|
|
|
547
|
-
class IPFabricSnapshot(
|
|
548
|
+
class IPFabricSnapshot(models.Model):
|
|
549
|
+
created = models.DateTimeField(auto_now_add=True)
|
|
550
|
+
last_updated = models.DateTimeField(editable=False)
|
|
548
551
|
source = models.ForeignKey(
|
|
549
552
|
to=IPFabricSource,
|
|
550
553
|
on_delete=models.CASCADE,
|
|
@@ -597,6 +600,11 @@ class IPFabricSync(IPFabricClient, JobsMixin, TagsMixin, ChangeLoggedModel):
|
|
|
597
600
|
on_delete=models.CASCADE,
|
|
598
601
|
related_name="snapshots",
|
|
599
602
|
)
|
|
603
|
+
type = models.CharField(
|
|
604
|
+
max_length=50,
|
|
605
|
+
choices=IPFabricSyncTypeChoices,
|
|
606
|
+
default=IPFabricSyncTypeChoices.DCIM,
|
|
607
|
+
)
|
|
600
608
|
status = models.CharField(
|
|
601
609
|
max_length=50,
|
|
602
610
|
choices=DataSourceStatusChoices,
|
|
@@ -605,7 +613,6 @@ class IPFabricSync(IPFabricClient, JobsMixin, TagsMixin, ChangeLoggedModel):
|
|
|
605
613
|
)
|
|
606
614
|
parameters = models.JSONField(blank=True, null=True)
|
|
607
615
|
auto_merge = models.BooleanField(default=False)
|
|
608
|
-
update_custom_fields = models.BooleanField(default=True)
|
|
609
616
|
last_synced = models.DateTimeField(blank=True, null=True, editable=False)
|
|
610
617
|
scheduled = models.DateTimeField(null=True, blank=True)
|
|
611
618
|
interval = models.PositiveIntegerField(
|
|
@@ -661,10 +668,6 @@ class IPFabricSync(IPFabricClient, JobsMixin, TagsMixin, ChangeLoggedModel):
|
|
|
661
668
|
else:
|
|
662
669
|
return False
|
|
663
670
|
|
|
664
|
-
@property
|
|
665
|
-
def last_ingestion(self):
|
|
666
|
-
return self.ipfabricingestion_set.last()
|
|
667
|
-
|
|
668
671
|
@staticmethod
|
|
669
672
|
def get_transform_maps(group_ids=None):
|
|
670
673
|
"""
|
|
@@ -886,10 +889,7 @@ class IPFabricIngestion(JobsMixin, models.Model):
|
|
|
886
889
|
def name(self):
|
|
887
890
|
if self.branch:
|
|
888
891
|
return self.branch.name
|
|
889
|
-
|
|
890
|
-
return f"{self.sync.name} (Ingestion {self.pk})"
|
|
891
|
-
except IPFabricIngestion.sync.RelatedObjectDoesNotExist:
|
|
892
|
-
return f"Ingestion {self.pk} (No Sync)"
|
|
892
|
+
return f"{self.sync.name} (Ingestion {self.pk})"
|
|
893
893
|
|
|
894
894
|
def get_absolute_url(self):
|
|
895
895
|
return reverse("plugins:ipfabric_netbox:ipfabricingestion", args=[self.pk])
|
ipfabric_netbox/tables.py
CHANGED
|
@@ -142,41 +142,20 @@ class IPFabricSourceTable(NetBoxTable):
|
|
|
142
142
|
default_columns = ("pk", "name", "status", "description", "snapshot_count")
|
|
143
143
|
|
|
144
144
|
|
|
145
|
-
class
|
|
146
|
-
|
|
145
|
+
class SyncTable(NetBoxTable):
|
|
146
|
+
actions = None
|
|
147
147
|
status = columns.ChoiceFieldColumn()
|
|
148
148
|
snapshot_name = tables.Column(
|
|
149
|
-
verbose_name="Snapshot Name",
|
|
150
|
-
accessor="snapshot_data",
|
|
151
|
-
linkify=True,
|
|
152
|
-
)
|
|
153
|
-
last_ingestion = tables.Column(
|
|
154
|
-
accessor="last_ingestion",
|
|
155
|
-
verbose_name="Last Ingestion",
|
|
156
|
-
linkify=True,
|
|
149
|
+
verbose_name="Snapshot Name", accessor="snapshot_data"
|
|
157
150
|
)
|
|
158
151
|
|
|
159
|
-
def
|
|
160
|
-
return
|
|
161
|
-
|
|
162
|
-
def render_snapshot_name(self, value: IPFabricSnapshot):
|
|
163
|
-
return getattr(value, "name", "---") if value else "---"
|
|
152
|
+
def render_snapshot_name(self, value):
|
|
153
|
+
return value.get("name", "---")
|
|
164
154
|
|
|
165
155
|
class Meta(NetBoxTable.Meta):
|
|
166
156
|
model = IPFabricSync
|
|
167
|
-
fields = (
|
|
168
|
-
|
|
169
|
-
"id",
|
|
170
|
-
"interval",
|
|
171
|
-
"last_synced",
|
|
172
|
-
"last_ingestion",
|
|
173
|
-
"name",
|
|
174
|
-
"scheduled",
|
|
175
|
-
"status",
|
|
176
|
-
"snapshot_name",
|
|
177
|
-
"user",
|
|
178
|
-
)
|
|
179
|
-
default_columns = ("name", "status", "last_ingestion", "snapshot_name")
|
|
157
|
+
fields = ("id", "status", "snapshot_name")
|
|
158
|
+
default_columns = ("id", "status", "snapshot_name")
|
|
180
159
|
|
|
181
160
|
|
|
182
161
|
class IPFabricIngestionChangesTable(NetBoxTable):
|
|
@@ -262,5 +241,5 @@ class IPFabricDataTable(NetBoxTable):
|
|
|
262
241
|
|
|
263
242
|
class Meta(NetBoxTable.Meta):
|
|
264
243
|
model = IPFabricData
|
|
265
|
-
fields = ("snapshot_data", "JSON")
|
|
266
|
-
default_columns = ("snapshot_data", "JSON")
|
|
244
|
+
fields = ("snapshot_data", "type", "JSON")
|
|
245
|
+
default_columns = ("snapshot_data", "type", "JSON")
|
|
@@ -1,13 +1,30 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
1
3
|
from netbox.plugins import PluginTemplateExtension
|
|
2
4
|
|
|
5
|
+
from ipfabric_netbox.models import IPFabricSnapshot
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger("ipfabric_netbox.template_content")
|
|
8
|
+
|
|
3
9
|
|
|
4
10
|
class SiteTopologyButtons(PluginTemplateExtension):
|
|
5
11
|
model = "dcim.site"
|
|
6
12
|
|
|
7
13
|
def buttons(self):
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
14
|
+
try:
|
|
15
|
+
site = self.context.get("object")
|
|
16
|
+
source = None
|
|
17
|
+
for snapshot in IPFabricSnapshot.objects.all():
|
|
18
|
+
# `Site.name` is unique in DB, so we can use it to match against IPF snapshots
|
|
19
|
+
if site.name in snapshot.sites:
|
|
20
|
+
source = snapshot.source
|
|
21
|
+
return self.render(
|
|
22
|
+
"ipfabric_netbox/inc/site_topology_button.html",
|
|
23
|
+
extra_context={"source": source},
|
|
24
|
+
)
|
|
25
|
+
except Exception as e:
|
|
26
|
+
logger.error(f"Could not render topology button: {e}.")
|
|
27
|
+
return "render error"
|
|
11
28
|
|
|
12
29
|
|
|
13
30
|
template_extensions = [SiteTopologyButtons]
|
|
@@ -6,19 +6,26 @@
|
|
|
6
6
|
IP Fabric Topology
|
|
7
7
|
</button>
|
|
8
8
|
<ul class="dropdown-menu" aria-labelledby="btnGroupDrop1">
|
|
9
|
+
{% if source is not None %}
|
|
9
10
|
<li><a href="#"
|
|
10
|
-
hx-get="{% url 'plugins:ipfabric_netbox:ipfabricsource_topology' pk=
|
|
11
|
+
hx-get="{% url 'plugins:ipfabric_netbox:ipfabricsource_topology' pk=source.pk site=object.pk %}?snapshot=$last&source={{ source.id }}"
|
|
11
12
|
hx-target="#topology-modal-content" data-bs-toggle="modal" data-bs-target="#topology-modal"
|
|
12
13
|
class="dropdown-item">
|
|
13
14
|
Last Snapshot
|
|
14
15
|
</a>
|
|
15
16
|
</li>
|
|
16
17
|
<li><a href="#"
|
|
17
|
-
hx-get="{% url 'plugins:ipfabric_netbox:ipfabricsource_topology' pk=
|
|
18
|
+
hx-get="{% url 'plugins:ipfabric_netbox:ipfabricsource_topology' pk=source.pk site=object.pk %}?snapshot=$prev&source={{ source.id }}"
|
|
18
19
|
hx-target="#topology-modal-content" data-bs-toggle="modal" data-bs-target="#topology-modal"
|
|
19
20
|
class="dropdown-item">
|
|
20
21
|
Previous Snapshot
|
|
21
|
-
</a
|
|
22
|
+
</a>
|
|
23
|
+
</li>
|
|
24
|
+
{% else %}
|
|
25
|
+
<li>
|
|
26
|
+
<span class="dropdown-item text-danger">No IP Fabric Source Found for this Site</span>
|
|
27
|
+
</li>
|
|
28
|
+
{% endif %}
|
|
22
29
|
</ul>
|
|
23
30
|
</div>
|
|
24
31
|
</div>
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{% extends 'base/layout.html' %}
|
|
2
|
+
{% load buttons %}
|
|
3
|
+
{% load helpers %}
|
|
4
|
+
{% load perms %}
|
|
5
|
+
|
|
6
|
+
{% block title %}Ingestion{% endblock %}
|
|
7
|
+
|
|
8
|
+
{% block tabs %}
|
|
9
|
+
<ul class="nav nav-tabs px-3">
|
|
10
|
+
<li class="nav-item" role="presentation">
|
|
11
|
+
<a class="nav-link active" role="tab">Ingestion</a>
|
|
12
|
+
</li>
|
|
13
|
+
</ul>
|
|
14
|
+
{% endblock tabs %}
|
|
15
|
+
|
|
16
|
+
{% block controls %}
|
|
17
|
+
<div class="controls">
|
|
18
|
+
<div class="control-group">
|
|
19
|
+
{% block extra_controls %}{% endblock %}
|
|
20
|
+
{% add_button model %}
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
{% endblock controls %}
|
|
24
|
+
|
|
25
|
+
{% block content %}
|
|
26
|
+
<div class="tab-content">
|
|
27
|
+
{% for sync in syncs %}
|
|
28
|
+
<div class="card">
|
|
29
|
+
<h5 class="card-header d-flex justify-content-between" id="module{{ module.pk }}">
|
|
30
|
+
<div>
|
|
31
|
+
<i class="mdi mdi-cloud-sync"></i><i class="mdi mdi-sync"></i> <a href="{% url 'plugins:ipfabric_netbox:ipfabricsync' pk=sync.pk %}">{{ sync.name}}</a>
|
|
32
|
+
</div>
|
|
33
|
+
{% if perms.ipfabric_netbox.delete_ipfabricsync %}
|
|
34
|
+
<a href="{% url 'plugins:ipfabric_netbox:ipfabricsync_delete' pk=sync.pk %}" class="btn btn-danger btn-sm">
|
|
35
|
+
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
|
|
36
|
+
</a>
|
|
37
|
+
{% endif %}
|
|
38
|
+
|
|
39
|
+
</h5>
|
|
40
|
+
<div class="card-body">
|
|
41
|
+
{% include 'inc/sync_warning.html' with object=module %}
|
|
42
|
+
<table class="table table-hover table-headings reports">
|
|
43
|
+
<thead>
|
|
44
|
+
<tr>
|
|
45
|
+
<th width="200">Source</th>
|
|
46
|
+
<th width="400">Snapshot</th>
|
|
47
|
+
<th>Status</th>
|
|
48
|
+
<th>Last Run</th>
|
|
49
|
+
</tr>
|
|
50
|
+
</thead>
|
|
51
|
+
<tbody>
|
|
52
|
+
<tr>
|
|
53
|
+
<td><a href="{% url 'plugins:ipfabric_netbox:ipfabricsource' pk=sync.snapshot_data.source.pk %}">{{ sync.snapshot_data.source.name }}</a></td>
|
|
54
|
+
<td><a href="{% url 'plugins:ipfabric_netbox:ipfabricsnapshot' pk=sync.snapshot_data.pk %}">{{ sync.snapshot_data.name}}</a></td>
|
|
55
|
+
<td>{% badge sync.get_status_display last_job.get_status_color %}</td>
|
|
56
|
+
<td>{{sync.last_synced}}</td>
|
|
57
|
+
</tr>
|
|
58
|
+
</tbody>
|
|
59
|
+
</table>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
{% empty %}
|
|
63
|
+
<div class="alert alert-info" role="alert">
|
|
64
|
+
<h4 class="alert-heading">Sync Jobs Settings Found</h4>
|
|
65
|
+
{% if perms.extras.add_reportmodule %}
|
|
66
|
+
Get started by <a href="{% url 'plugins:ipfabric_netbox:ipfabricsync_add' %}">creating a sync</a> from an IP Fabric source.
|
|
67
|
+
{% endif %}
|
|
68
|
+
</div>
|
|
69
|
+
{% endfor %}
|
|
70
|
+
</div>
|
|
71
|
+
{% endblock content %}
|