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.

Files changed (31) hide show
  1. ipfabric_netbox/__init__.py +1 -1
  2. ipfabric_netbox/api/__init__.py +1 -0
  3. ipfabric_netbox/api/nested_serializers.py +78 -0
  4. ipfabric_netbox/api/serializers.py +90 -147
  5. ipfabric_netbox/api/urls.py +4 -4
  6. ipfabric_netbox/api/views.py +19 -18
  7. ipfabric_netbox/choices.py +12 -0
  8. ipfabric_netbox/filtersets.py +4 -67
  9. ipfabric_netbox/forms.py +140 -92
  10. ipfabric_netbox/models.py +10 -10
  11. ipfabric_netbox/tables.py +9 -30
  12. ipfabric_netbox/template_content.py +20 -3
  13. ipfabric_netbox/templates/ipfabric_netbox/inc/site_topology_button.html +10 -3
  14. ipfabric_netbox/templates/ipfabric_netbox/ipfabricsync_list.html +71 -0
  15. ipfabric_netbox/tests/test_models.py +11 -47
  16. ipfabric_netbox/utilities/ipfutils.py +23 -43
  17. ipfabric_netbox/views.py +8 -6
  18. {ipfabric_netbox-4.2.0b9.dist-info → ipfabric_netbox-4.2.1.dist-info}/METADATA +6 -6
  19. {ipfabric_netbox-4.2.0b9.dist-info → ipfabric_netbox-4.2.1.dist-info}/RECORD +20 -29
  20. ipfabric_netbox/graphql/__init__.py +0 -23
  21. ipfabric_netbox/graphql/enums.py +0 -35
  22. ipfabric_netbox/graphql/filters.py +0 -317
  23. ipfabric_netbox/graphql/schema.py +0 -101
  24. ipfabric_netbox/graphql/types.py +0 -216
  25. ipfabric_netbox/migrations/0016_tags_and_changelog_for_snapshots.py +0 -31
  26. ipfabric_netbox/migrations/0017_ipfabricsync_update_custom_fields.py +0 -17
  27. ipfabric_netbox/migrations/0018_remove_type_field.py +0 -17
  28. ipfabric_netbox/tests/api/__init__.py +0 -0
  29. ipfabric_netbox/tests/api/test_api.py +0 -879
  30. ipfabric_netbox/tests/test_forms.py +0 -1440
  31. {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
- required=False, label=_("Interfaces"), initial=False
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"), initial=False),
77
- "vrf": forms.BooleanField(required=False, label=_("VRFs"), initial=False),
78
- "prefix": forms.BooleanField(required=False, label=_("Prefixes"), initial=False),
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: str) -> list[tuple[str, str]]:
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
- # This should never happen, but better be safe than sorry
92
- choices = [] # pragma: no cover
99
+ choices = []
93
100
  return choices
94
101
 
95
102
 
96
- def str_to_list(_str: str | list) -> list[str]:
97
- if not isinstance(_str, list):
98
- return [_str]
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 _str
114
+ return str
101
115
 
102
116
 
103
- def list_to_choices(choices: list[str]) -> tuple[tuple[str, str], ...]:
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.get("initial", {}).get("transform_map", None):
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.get("initial", {}).get("transform_map", None):
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 = {"source": HTMXSelect()}
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 sync_parameters.items():
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
- self.fields["sites"].choices = list_to_choices(str_to_list(sites))
600
- if sites and "snapshot_data" in self.cleaned_data:
601
- snapshot = self.cleaned_data["snapshot_data"]
602
- # Check if all sites are valid - fail if any site is not found in snapshot.sites
603
- if not all(
604
- any(site in snapshot_site for snapshot_site in snapshot.sites)
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(TagsMixin, ChangeLoggedModel):
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
- try:
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 IPFabricSyncTable(NetBoxTable):
146
- name = tables.Column(linkify=True)
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 render_last_ingestion(self, value: IPFabricIngestion):
160
- return getattr(value, "name", "---") if value else "---"
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
- "auto_merge",
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
- return self.render(
9
- "ipfabric_netbox/inc/site_topology_button.html", extra_context={}
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=object.pk site=object.site.pk %}?snapshot=$last&source={{ object.custom_field_data.ipfabric_source }}"
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=object.pk site=object.site.pk %}?snapshot=$prev&source={{ object.custom_field_data.ipfabric_source }}"
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></li>
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 %}