ipfabric_netbox 3.1.2__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 +42 -0
- ipfabric_netbox/api/__init__.py +2 -0
- ipfabric_netbox/api/nested_serializers.py +99 -0
- ipfabric_netbox/api/serializers.py +160 -0
- ipfabric_netbox/api/urls.py +21 -0
- ipfabric_netbox/api/views.py +111 -0
- ipfabric_netbox/choices.py +226 -0
- ipfabric_netbox/filtersets.py +125 -0
- ipfabric_netbox/forms.py +1063 -0
- ipfabric_netbox/jobs.py +95 -0
- ipfabric_netbox/migrations/0001_initial.py +342 -0
- ipfabric_netbox/migrations/0002_ipfabricsnapshot_status.py +17 -0
- ipfabric_netbox/migrations/0003_ipfabricsource_type_and_more.py +49 -0
- ipfabric_netbox/migrations/0004_ipfabricsync_auto_merge.py +17 -0
- ipfabric_netbox/migrations/0005_alter_ipfabricrelationshipfield_source_model_and_more.py +64 -0
- ipfabric_netbox/migrations/0006_alter_ipfabrictransformmap_target_model.py +48 -0
- ipfabric_netbox/migrations/__init__.py +0 -0
- ipfabric_netbox/models.py +874 -0
- ipfabric_netbox/navigation.py +62 -0
- ipfabric_netbox/signals.py +68 -0
- ipfabric_netbox/tables.py +208 -0
- ipfabric_netbox/template_content.py +13 -0
- ipfabric_netbox/templates/ipfabric_netbox/inc/diff.html +72 -0
- ipfabric_netbox/templates/ipfabric_netbox/inc/json.html +20 -0
- ipfabric_netbox/templates/ipfabric_netbox/inc/logs_pending.html +6 -0
- ipfabric_netbox/templates/ipfabric_netbox/inc/merge_form.html +22 -0
- ipfabric_netbox/templates/ipfabric_netbox/inc/site_topology_button.html +70 -0
- ipfabric_netbox/templates/ipfabric_netbox/inc/site_topology_modal.html +61 -0
- ipfabric_netbox/templates/ipfabric_netbox/inc/snapshotdata.html +60 -0
- ipfabric_netbox/templates/ipfabric_netbox/inc/sync_delete.html +19 -0
- ipfabric_netbox/templates/ipfabric_netbox/inc/transform_map_field_map.html +11 -0
- ipfabric_netbox/templates/ipfabric_netbox/inc/transform_map_relationship_map.html +11 -0
- ipfabric_netbox/templates/ipfabric_netbox/ipfabric_table.html +55 -0
- ipfabric_netbox/templates/ipfabric_netbox/ipfabricbranch.html +141 -0
- ipfabric_netbox/templates/ipfabric_netbox/ipfabricsnapshot.html +105 -0
- ipfabric_netbox/templates/ipfabric_netbox/ipfabricsource.html +111 -0
- ipfabric_netbox/templates/ipfabric_netbox/ipfabricsync.html +103 -0
- ipfabric_netbox/templates/ipfabric_netbox/ipfabrictransformmap.html +41 -0
- ipfabric_netbox/templates/ipfabric_netbox/ipfabrictransformmap_list.html +17 -0
- ipfabric_netbox/templates/ipfabric_netbox/ipfabrictransformmap_restore.html +59 -0
- ipfabric_netbox/templates/ipfabric_netbox/partials/branch_all.html +10 -0
- ipfabric_netbox/templates/ipfabric_netbox/partials/branch_progress.html +19 -0
- ipfabric_netbox/templates/ipfabric_netbox/partials/branch_status.html +1 -0
- ipfabric_netbox/templates/ipfabric_netbox/partials/job_logs.html +53 -0
- ipfabric_netbox/templates/ipfabric_netbox/partials/sync_last_branch.html +1 -0
- ipfabric_netbox/templates/ipfabric_netbox/sync_list.html +126 -0
- ipfabric_netbox/templates/static/ipfabric_netbox/css/rack.css +9 -0
- ipfabric_netbox/tests/__init__.py +0 -0
- ipfabric_netbox/tests/test_models.py +1340 -0
- ipfabric_netbox/urls.py +141 -0
- ipfabric_netbox/utilities/__init__.py +0 -0
- ipfabric_netbox/utilities/ipfutils.py +591 -0
- ipfabric_netbox/utilities/logging.py +93 -0
- ipfabric_netbox/utilities/nbutils.py +105 -0
- ipfabric_netbox/utilities/transform_map.py +35 -0
- ipfabric_netbox/views.py +845 -0
- ipfabric_netbox-3.1.2.dist-info/METADATA +88 -0
- ipfabric_netbox-3.1.2.dist-info/RECORD +59 -0
- ipfabric_netbox-3.1.2.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from netbox.plugins import PluginMenu
|
|
2
|
+
from netbox.plugins import PluginMenuButton
|
|
3
|
+
from netbox.plugins import PluginMenuItem
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
sync_buttons = [
|
|
7
|
+
PluginMenuButton(
|
|
8
|
+
link="plugins:ipfabric_netbox:ipfabricsync_add",
|
|
9
|
+
title="Add",
|
|
10
|
+
icon_class="mdi mdi-plus-thick",
|
|
11
|
+
permissions=["ipfabric_netbox.add_ipfabricsync"],
|
|
12
|
+
)
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
source_buttons = [
|
|
16
|
+
PluginMenuButton(
|
|
17
|
+
link="plugins:ipfabric_netbox:ipfabricsource_add",
|
|
18
|
+
title="Add",
|
|
19
|
+
icon_class="mdi mdi-plus-thick",
|
|
20
|
+
permissions=["ipfabric_netbox.add_ipfabricsource"],
|
|
21
|
+
)
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
source = PluginMenuItem(
|
|
25
|
+
link="plugins:ipfabric_netbox:ipfabricsource_list",
|
|
26
|
+
link_text="Sources",
|
|
27
|
+
buttons=source_buttons,
|
|
28
|
+
permissions=["ipfabric_netbox.view_ipfabricsource"],
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
snapshot = PluginMenuItem(
|
|
32
|
+
link="plugins:ipfabric_netbox:ipfabricsnapshot_list",
|
|
33
|
+
link_text="Snapshots",
|
|
34
|
+
permissions=["ipfabric_netbox.view_ipfabricsnapshot"],
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
ingestion = PluginMenuItem(
|
|
39
|
+
link="plugins:ipfabric_netbox:ipfabricsync_list",
|
|
40
|
+
link_text="Ingestion",
|
|
41
|
+
buttons=sync_buttons,
|
|
42
|
+
permissions=["ipfabric_netbox.view_ipfabricsync"],
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
tm = PluginMenuItem(
|
|
46
|
+
link="plugins:ipfabric_netbox:ipfabrictransformmap_list",
|
|
47
|
+
link_text="Transform Maps",
|
|
48
|
+
permissions=["ipfabric_netbox.view_ipfabrictransformmap"],
|
|
49
|
+
buttons=[
|
|
50
|
+
PluginMenuButton(
|
|
51
|
+
link="plugins:ipfabric_netbox:ipfabrictransformmap_add",
|
|
52
|
+
title="Add",
|
|
53
|
+
icon_class="mdi mdi-plus-thick",
|
|
54
|
+
permissions=["ipfabric_netbox.add_ipfabrictransformmap"],
|
|
55
|
+
)
|
|
56
|
+
],
|
|
57
|
+
)
|
|
58
|
+
menu = PluginMenu(
|
|
59
|
+
label="IP Fabric",
|
|
60
|
+
icon_class="mdi mdi-cloud-sync",
|
|
61
|
+
groups=(("IP Fabric", (source, snapshot, ingestion, tm)),),
|
|
62
|
+
)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
from core.models import ObjectType
|
|
4
|
+
from dcim.models import Device
|
|
5
|
+
from dcim.models import Site
|
|
6
|
+
from extras.choices import CustomFieldTypeChoices
|
|
7
|
+
from extras.choices import CustomFieldUIEditableChoices
|
|
8
|
+
from extras.choices import CustomFieldUIVisibleChoices
|
|
9
|
+
from extras.choices import CustomLinkButtonClassChoices
|
|
10
|
+
from extras.models import CustomField
|
|
11
|
+
from extras.models import CustomLink
|
|
12
|
+
|
|
13
|
+
from .models import IPFabricBranch
|
|
14
|
+
from .models import IPFabricSource
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def create_custom_field(
|
|
18
|
+
field_name: str,
|
|
19
|
+
label: str,
|
|
20
|
+
models: list,
|
|
21
|
+
object_type=None,
|
|
22
|
+
cf_type: Optional[str] = "type_text",
|
|
23
|
+
ui_visibility: Optional[str] = "VISIBILITY_READ_ONLY",
|
|
24
|
+
):
|
|
25
|
+
defaults = {
|
|
26
|
+
"label": label,
|
|
27
|
+
"related_object_type": ObjectType.objects.get_for_model(object_type)
|
|
28
|
+
if object_type
|
|
29
|
+
else None,
|
|
30
|
+
"ui_visible": getattr(CustomFieldUIVisibleChoices, "ALWAYS"),
|
|
31
|
+
"ui_editable": getattr(CustomFieldUIEditableChoices, "NO"),
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
custom_field, _ = CustomField.objects.update_or_create(
|
|
35
|
+
type=getattr(CustomFieldTypeChoices, cf_type.upper()),
|
|
36
|
+
name=field_name,
|
|
37
|
+
defaults=defaults,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
for model in models:
|
|
41
|
+
custom_field.object_types.add(ObjectType.objects.get_for_model(model))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def ipfabric_netbox_init():
|
|
45
|
+
create_custom_field(
|
|
46
|
+
"ipfabric_source",
|
|
47
|
+
"IP Fabric Source",
|
|
48
|
+
[Device, Site],
|
|
49
|
+
cf_type="type_object",
|
|
50
|
+
object_type=IPFabricSource,
|
|
51
|
+
)
|
|
52
|
+
create_custom_field(
|
|
53
|
+
"ipfabric_branch",
|
|
54
|
+
"IP Fabric Last Sync",
|
|
55
|
+
[Device, Site],
|
|
56
|
+
cf_type="type_object",
|
|
57
|
+
object_type=IPFabricBranch,
|
|
58
|
+
)
|
|
59
|
+
cl, _ = CustomLink.objects.update_or_create(
|
|
60
|
+
defaults={
|
|
61
|
+
"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 %}",
|
|
62
|
+
"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%}',
|
|
63
|
+
"new_window": True,
|
|
64
|
+
"button_class": CustomLinkButtonClassChoices.BLUE,
|
|
65
|
+
},
|
|
66
|
+
name="ipfabric",
|
|
67
|
+
)
|
|
68
|
+
cl.object_types.add(ObjectType.objects.get_for_model(Device))
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import django_tables2 as tables
|
|
2
|
+
from django.utils.translation import gettext_lazy as _
|
|
3
|
+
from django_tables2 import Column
|
|
4
|
+
from extras.models import StagedChange
|
|
5
|
+
from netbox.tables import columns
|
|
6
|
+
from netbox.tables import NetBoxTable
|
|
7
|
+
|
|
8
|
+
from .models import IPFabricBranch
|
|
9
|
+
from .models import IPFabricData
|
|
10
|
+
from .models import IPFabricRelationshipField
|
|
11
|
+
from .models import IPFabricSnapshot
|
|
12
|
+
from .models import IPFabricSource
|
|
13
|
+
from .models import IPFabricSync
|
|
14
|
+
from .models import IPFabricTransformField
|
|
15
|
+
from .models import IPFabricTransformMap
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
DIFF_BUTTON = """
|
|
19
|
+
<a href="#"
|
|
20
|
+
hx-get="{% url 'plugins:ipfabric_netbox:ipfabricbranch_change_diff' pk=record.branch.pk change_pk=record.pk %}"
|
|
21
|
+
hx-target="#htmx-modal-content"
|
|
22
|
+
data-bs-toggle="modal"
|
|
23
|
+
data-bs-target="#htmx-modal"
|
|
24
|
+
class="btn btn-success btn-sm"
|
|
25
|
+
>
|
|
26
|
+
<i class="mdi mdi-code-tags">Diff</i>
|
|
27
|
+
</a>
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
DATA_BUTTON = """
|
|
31
|
+
<a href="#"
|
|
32
|
+
hx-get="{% url 'plugins:ipfabric_netbox:ipfabricdata_data' pk=record.pk %}"
|
|
33
|
+
hx-target="#htmx-modal-content"
|
|
34
|
+
data-bs-toggle="modal"
|
|
35
|
+
data-bs-target="#htmx-modal"
|
|
36
|
+
class="btn btn-success btn-sm"
|
|
37
|
+
>
|
|
38
|
+
<i class="mdi mdi-code-tags">JSON</i>
|
|
39
|
+
</a>
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class IPFabricRelationshipFieldTable(NetBoxTable):
|
|
44
|
+
actions = columns.ActionsColumn(actions=("edit", "delete"))
|
|
45
|
+
|
|
46
|
+
class Meta(NetBoxTable.Meta):
|
|
47
|
+
model = IPFabricRelationshipField
|
|
48
|
+
fields = ("source_model", "target_field", "coalesce", "actions")
|
|
49
|
+
default_columns = ("source_model", "target_field", "coalesce", "actions")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class IPFabricTransformFieldTable(NetBoxTable):
|
|
53
|
+
id = tables.Column()
|
|
54
|
+
actions = columns.ActionsColumn(actions=("edit", "delete"))
|
|
55
|
+
|
|
56
|
+
class Meta(NetBoxTable.Meta):
|
|
57
|
+
model = IPFabricTransformField
|
|
58
|
+
fields = ("id", "source_field", "target_field", "coalesce", "actions")
|
|
59
|
+
default_columns = ("source_field", "target_field", "coalesce", "actions")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class IPFabricTransformMapTable(NetBoxTable):
|
|
63
|
+
name = tables.Column(linkify=True)
|
|
64
|
+
|
|
65
|
+
class Meta(NetBoxTable.Meta):
|
|
66
|
+
model = IPFabricTransformMap
|
|
67
|
+
fields = ("name", "source_model", "target_model", "status")
|
|
68
|
+
default_columns = ("name", "source_model", "target_model", "status")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class BranchTable(NetBoxTable):
|
|
72
|
+
name = tables.Column(linkify=True)
|
|
73
|
+
sync = tables.Column(verbose_name="IP Fabric Sync", linkify=True)
|
|
74
|
+
changes = tables.Column(accessor="staged_changes", verbose_name="Number of Changes")
|
|
75
|
+
actions = columns.ActionsColumn(actions=("delete",))
|
|
76
|
+
|
|
77
|
+
def render_changes(self, value):
|
|
78
|
+
return value.count()
|
|
79
|
+
|
|
80
|
+
class Meta(NetBoxTable.Meta):
|
|
81
|
+
model = IPFabricBranch
|
|
82
|
+
fields = ("name", "sync", "description", "user", "changes")
|
|
83
|
+
default_columns = ("name", "sync", "description", "user", "changes")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class IPFabricSnapshotTable(NetBoxTable):
|
|
87
|
+
name = tables.Column(linkify=True)
|
|
88
|
+
source = tables.Column(linkify=True)
|
|
89
|
+
tags = columns.TagColumn(url_name="core:datasource_list")
|
|
90
|
+
actions = columns.ActionsColumn(actions=("delete",))
|
|
91
|
+
status = columns.ChoiceFieldColumn()
|
|
92
|
+
|
|
93
|
+
class Meta(NetBoxTable.Meta):
|
|
94
|
+
model = IPFabricSnapshot
|
|
95
|
+
fields = (
|
|
96
|
+
"pk",
|
|
97
|
+
"id",
|
|
98
|
+
"name",
|
|
99
|
+
"snapshot_id",
|
|
100
|
+
"status",
|
|
101
|
+
"date",
|
|
102
|
+
"created",
|
|
103
|
+
"last_updated",
|
|
104
|
+
)
|
|
105
|
+
default_columns = ("pk", "name", "source", "snapshot_id", "status", "date")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class IPFabricSourceTable(NetBoxTable):
|
|
109
|
+
name = tables.Column(linkify=True)
|
|
110
|
+
status = columns.ChoiceFieldColumn()
|
|
111
|
+
snapshot_count = tables.Column(verbose_name="Snapshots")
|
|
112
|
+
tags = columns.TagColumn(url_name="core:datasource_list")
|
|
113
|
+
|
|
114
|
+
class Meta(NetBoxTable.Meta):
|
|
115
|
+
model = IPFabricSource
|
|
116
|
+
fields = (
|
|
117
|
+
"pk",
|
|
118
|
+
"id",
|
|
119
|
+
"name",
|
|
120
|
+
"status",
|
|
121
|
+
"description",
|
|
122
|
+
"comments",
|
|
123
|
+
"created",
|
|
124
|
+
"last_updated",
|
|
125
|
+
)
|
|
126
|
+
default_columns = ("pk", "name", "status", "description", "snapshot_count")
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class SyncTable(NetBoxTable):
|
|
130
|
+
actions = None
|
|
131
|
+
status = columns.ChoiceFieldColumn()
|
|
132
|
+
snapshot_name = tables.Column(
|
|
133
|
+
verbose_name="Snapshot Name", accessor="snapshot_data"
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
def render_snapshot_name(self, value):
|
|
137
|
+
return value.get("name", "---")
|
|
138
|
+
|
|
139
|
+
class Meta(NetBoxTable.Meta):
|
|
140
|
+
model = IPFabricSync
|
|
141
|
+
fields = ("id", "status", "snapshot_name")
|
|
142
|
+
default_columns = ("id", "status", "snapshot_name")
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class StagedChangesTable(NetBoxTable):
|
|
146
|
+
pk = None
|
|
147
|
+
object_type = tables.Column(
|
|
148
|
+
accessor="object_type.model", verbose_name="Object Type"
|
|
149
|
+
)
|
|
150
|
+
name = tables.Column(accessor="object_type.name", verbose_name="Name")
|
|
151
|
+
actions = columns.TemplateColumn(template_code=DIFF_BUTTON)
|
|
152
|
+
|
|
153
|
+
def render_name(self, value, record):
|
|
154
|
+
models_with_names = [
|
|
155
|
+
"site",
|
|
156
|
+
"manufacturer",
|
|
157
|
+
"device role",
|
|
158
|
+
"device",
|
|
159
|
+
"interface",
|
|
160
|
+
"platform",
|
|
161
|
+
"inventory item",
|
|
162
|
+
"VLAN",
|
|
163
|
+
]
|
|
164
|
+
if record.data:
|
|
165
|
+
if value in models_with_names:
|
|
166
|
+
name = record.data["name"]
|
|
167
|
+
elif value == "device type":
|
|
168
|
+
name = record.data["model"]
|
|
169
|
+
elif value == "IP address":
|
|
170
|
+
name = record.data["address"]
|
|
171
|
+
elif value == "tagged item":
|
|
172
|
+
name = f"Tagging object ({record.data['object_id']})"
|
|
173
|
+
elif value == "prefix":
|
|
174
|
+
name = f"{record.data['prefix']} ({record.data['vrf']})"
|
|
175
|
+
else:
|
|
176
|
+
name = record.data
|
|
177
|
+
else:
|
|
178
|
+
name = record.object.__str__()
|
|
179
|
+
return name
|
|
180
|
+
|
|
181
|
+
class Meta(NetBoxTable.Meta):
|
|
182
|
+
model = StagedChange
|
|
183
|
+
name = "staged_changes"
|
|
184
|
+
fields = ("name", "action", "object_type", "actions")
|
|
185
|
+
default_columns = ("name", "action", "object_type", "actions")
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class DeviceIPFTable(tables.Table):
|
|
189
|
+
hostname = Column()
|
|
190
|
+
|
|
191
|
+
class Meta:
|
|
192
|
+
attrs = {
|
|
193
|
+
"class": "table table-hover object-list",
|
|
194
|
+
}
|
|
195
|
+
empty_text = _("No results found")
|
|
196
|
+
|
|
197
|
+
def __init__(self, data, **kwargs):
|
|
198
|
+
super().__init__(data, **kwargs)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
class IPFabricDataTable(NetBoxTable):
|
|
202
|
+
JSON = columns.TemplateColumn(template_code=DATA_BUTTON)
|
|
203
|
+
actions = columns.ActionsColumn(actions=("delete",))
|
|
204
|
+
|
|
205
|
+
class Meta(NetBoxTable.Meta):
|
|
206
|
+
model = IPFabricData
|
|
207
|
+
fields = ("snapshot_data", "type", "JSON")
|
|
208
|
+
default_columns = ("snapshot_data", "type", "JSON")
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from netbox.plugins import PluginTemplateExtension
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class SiteTopologyButtons(PluginTemplateExtension):
|
|
5
|
+
model = "dcim.site"
|
|
6
|
+
|
|
7
|
+
def buttons(self):
|
|
8
|
+
return self.render(
|
|
9
|
+
"ipfabric_netbox/inc/site_topology_button.html", extra_context={}
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
template_extensions = [SiteTopologyButtons]
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
<div class="modal-content" id="htmx-modal-content">
|
|
2
|
+
<div class="modal-header">
|
|
3
|
+
<h5 class="modal-title">Change Diff {{ change }}</h5>
|
|
4
|
+
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
5
|
+
</div>
|
|
6
|
+
<div class="modal-body">
|
|
7
|
+
<div class="row mb-3">
|
|
8
|
+
<div class="col-6">
|
|
9
|
+
<div class="card">
|
|
10
|
+
<h5 class="card-header">Pre-Change Data</h5>
|
|
11
|
+
<div class="card-body">
|
|
12
|
+
{% if prechange_data %}
|
|
13
|
+
<pre class="change-data">{% for k, v in prechange_data.items %}{% spaceless %}
|
|
14
|
+
<span{% if k in diff_removed %} class="removed"{% endif %}>{{ k }}: {{ v|json }}</span>
|
|
15
|
+
{% endspaceless %}{% endfor %}
|
|
16
|
+
</pre>
|
|
17
|
+
{% else %}
|
|
18
|
+
<span class="text-muted">None</span>
|
|
19
|
+
{% endif %}
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
<div class="col-6">
|
|
24
|
+
<div class="card">
|
|
25
|
+
<h5 class="card-header">Post-Change Data</h5>
|
|
26
|
+
<div class="card-body">
|
|
27
|
+
{% if postchange_data %}
|
|
28
|
+
<pre class="change-data">{% for k, v in postchange_data.items %}{% spaceless %}
|
|
29
|
+
<span{% if k in diff_removed %} class="added"{% endif %}>{{ k }}: {{ v|json }}</span>
|
|
30
|
+
{% endspaceless %}{% endfor %}{% endif %}
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
<div class="row">
|
|
36
|
+
<div class="col">
|
|
37
|
+
<div class="card">
|
|
38
|
+
<div class="card-header">
|
|
39
|
+
<h5 class="d-inline">Difference</h5>
|
|
40
|
+
<!-- <div class="btn-group btn-group-sm float-end noprint">
|
|
41
|
+
<a {% if prev_change %}href="{% url 'extras:objectchange' pk=prev_change.pk %}"{% else %}disabled{% endif %} class="btn btn-outline-secondary">
|
|
42
|
+
<i class="mdi mdi-chevron-left" aria-hidden="true"></i> Previous
|
|
43
|
+
</a>
|
|
44
|
+
<a {% if next_change %}href="{% url 'extras:objectchange' pk=next_change.pk %}"{% else %}disabled{% endif %} class="btn btn-outline-secondary">
|
|
45
|
+
Next <i class="mdi mdi-chevron-right" aria-hidden="true"></i>
|
|
46
|
+
</a>
|
|
47
|
+
</div> -->
|
|
48
|
+
</div>
|
|
49
|
+
<div class="card-body">
|
|
50
|
+
{% if diff_added == diff_removed %}
|
|
51
|
+
<span class="text-muted" style="margin-left: 10px;">
|
|
52
|
+
{% if object.action == 'create' %}
|
|
53
|
+
Object Created
|
|
54
|
+
{% elif object.action == 'delete' %}
|
|
55
|
+
Object Deleted
|
|
56
|
+
{% else %}
|
|
57
|
+
No Changes
|
|
58
|
+
{% endif %}
|
|
59
|
+
</span>
|
|
60
|
+
{% else %}
|
|
61
|
+
<pre class="change-diff change-removed">{{ diff_removed|json }}</pre>
|
|
62
|
+
<pre class="change-diff change-added">{{ diff_added|json }}</pre>
|
|
63
|
+
{% endif %}
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<div class="modal-footer">
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<div class="modal-header">
|
|
2
|
+
<h5 class="modal-title">Raw Data {{ object.pk }}</h5>
|
|
3
|
+
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
4
|
+
</div>
|
|
5
|
+
<div class="modal-body">
|
|
6
|
+
<div class="row mb-3">
|
|
7
|
+
<div class="col-12">
|
|
8
|
+
<div class="card">
|
|
9
|
+
<h5 class="card-header">JSON Output</h5>
|
|
10
|
+
<div class="card-body">
|
|
11
|
+
<pre>{{ object.data | json }}</pre>
|
|
12
|
+
</div>
|
|
13
|
+
</div>
|
|
14
|
+
</div>
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<div class="modal-footer">
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{# Indicates that a job result is still pending; used for HTMX requests #}
|
|
2
|
+
<div class="spinner-border float-start me-1" id="spinner" role="status">
|
|
3
|
+
<span class="visually-hidden">Loading...</span>
|
|
4
|
+
</div>
|
|
5
|
+
<h3>Logs pending...</h3>
|
|
6
|
+
<small class="text-muted">Last updated {% now "H:i:s" %}</small>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{% load form_helpers %}
|
|
2
|
+
|
|
3
|
+
<form action="{{ form_url }}" method="post">
|
|
4
|
+
{% csrf_token %}
|
|
5
|
+
|
|
6
|
+
<div class="modal-header">
|
|
7
|
+
<h5 class="modal-title">Confirm Merge</h5>
|
|
8
|
+
</div>
|
|
9
|
+
<div class="modal-body">
|
|
10
|
+
<p>Are you sure you want to <strong class="text-danger">merge</strong> {{ object_type }} <strong>{{ object }}</strong>?</p>
|
|
11
|
+
{% render_form form %}
|
|
12
|
+
</div>
|
|
13
|
+
<div class="modal-footer">
|
|
14
|
+
{% if return_url %}
|
|
15
|
+
<a href="{{ return_url }}" class="btn btn-outline-secondary">Cancel</a>
|
|
16
|
+
{% else %}
|
|
17
|
+
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
18
|
+
{% endif %}
|
|
19
|
+
<button type="submit" class="btn btn-danger">Merge</button>
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
</form>
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{% if object.custom_field_data.ipfabric_source %}
|
|
2
|
+
<div class="btn-group" role="group" aria-label="Button group with nested dropdown">
|
|
3
|
+
<div class="btn-group" role="group">
|
|
4
|
+
<button id="btnGroupDrop1" type="button" class="btn btn-sm btn-success dropdown-toggle" data-bs-toggle="dropdown"
|
|
5
|
+
aria-expanded="false">
|
|
6
|
+
IP Fabric Topology
|
|
7
|
+
</button>
|
|
8
|
+
<ul class="dropdown-menu" aria-labelledby="btnGroupDrop1">
|
|
9
|
+
<li><a href="#"
|
|
10
|
+
hx-get="{% url 'plugins:ipfabric_netbox:ipfabricsource_topology' pk=object.pk site=object.pk %}?snapshot=$last&source={{ object.custom_field_data.ipfabric_source }}"
|
|
11
|
+
hx-target="#topology-modal-content" data-bs-toggle="modal" data-bs-target="#topology-modal"
|
|
12
|
+
class="dropdown-item">
|
|
13
|
+
Last Snapshot
|
|
14
|
+
</a>
|
|
15
|
+
</li>
|
|
16
|
+
<li><a href="#"
|
|
17
|
+
hx-get="{% url 'plugins:ipfabric_netbox:ipfabricsource_topology' pk=object.pk site=object.pk %}?snapshot=$prev&source={{ object.custom_field_data.ipfabric_source }}"
|
|
18
|
+
hx-target="#topology-modal-content" data-bs-toggle="modal" data-bs-target="#topology-modal"
|
|
19
|
+
class="dropdown-item">
|
|
20
|
+
Previous Snapshot
|
|
21
|
+
</a></li>
|
|
22
|
+
</ul>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<div class="modal fade" id="topology-modal" tabindex="-1" aria-hidden="true">
|
|
27
|
+
<div class="modal-dialog modal-xl">
|
|
28
|
+
<div class="modal-content">
|
|
29
|
+
<div class="modal-header">
|
|
30
|
+
<h5 class="modal-title">{{ object.name }} Topology</h5>
|
|
31
|
+
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
32
|
+
</div>
|
|
33
|
+
<div id="topology-modal-content">
|
|
34
|
+
<div class="modal-body">
|
|
35
|
+
<div class="d-flex align-items-center">
|
|
36
|
+
<strong role="status">Loading...</strong>
|
|
37
|
+
<div class="spinner-border ms-auto" aria-hidden="true"></div>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
{% endif %}
|
|
45
|
+
|
|
46
|
+
{% block javascript %}
|
|
47
|
+
<script>
|
|
48
|
+
document.addEventListener('DOMContentLoaded', function () {
|
|
49
|
+
var myModal = document.getElementById('topology-modal');
|
|
50
|
+
var modalBody = document.getElementById('topology-modal-content');
|
|
51
|
+
console.log(myModal);
|
|
52
|
+
myModal.addEventListener('hidden.bs.modal', function () {
|
|
53
|
+
// Clear modal content on close
|
|
54
|
+
console.log("Modal close");
|
|
55
|
+
modalBody.innerHTML = createLoadingContent();
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
function createLoadingContent() {
|
|
60
|
+
return `
|
|
61
|
+
<div class="modal-body">
|
|
62
|
+
<div class="d-flex align-items-center">
|
|
63
|
+
<strong role="status">Loading...</strong>
|
|
64
|
+
<div class="spinner-border ms-auto" aria-hidden="true"></div>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
`;
|
|
68
|
+
}
|
|
69
|
+
</script>
|
|
70
|
+
{% endblock %}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{% load helpers %}
|
|
2
|
+
|
|
3
|
+
<div class="modal-body">
|
|
4
|
+
<div class="d-flex flex-row m-2"">
|
|
5
|
+
{% if svg %}
|
|
6
|
+
<div class=" d-flex flex-row flex-grow-1 align-items-center">
|
|
7
|
+
{{svg|safe}}
|
|
8
|
+
</div>
|
|
9
|
+
<div>
|
|
10
|
+
<div class="position-absolute top-0 end-0 m-2 border rounded p-2">
|
|
11
|
+
<ul class="list-unstyled">
|
|
12
|
+
<li><span>Fetched: {{ time|isodatetime }}</span></li>
|
|
13
|
+
<li>Snapshot Name: {{ snapshot.name }}</li>
|
|
14
|
+
<li>Snapshot ID: {{ snapshot.snapshot_id }}</li>
|
|
15
|
+
<li>Snapshot Date: {{ snapshot.end|isodatetime }}</li>
|
|
16
|
+
</ul>
|
|
17
|
+
</div>
|
|
18
|
+
</div>
|
|
19
|
+
{% else %}
|
|
20
|
+
<div class="d-flex flex-row flex-grow-1 align-items-center">
|
|
21
|
+
{{ error }}
|
|
22
|
+
</div>
|
|
23
|
+
{% if snapshot %}
|
|
24
|
+
<div>
|
|
25
|
+
<div class="border rounded p-2">
|
|
26
|
+
<ul class="list-unstyled">
|
|
27
|
+
<li><span>Fetched: {{ time|isodatetime }}</span></li>
|
|
28
|
+
<li>Snapshot Name: {{ snapshot.name }}</li>
|
|
29
|
+
<li>Snapshot ID: {{ snapshot.snapshot_id }}</li>
|
|
30
|
+
<li>Snapshot Date: {{ snapshot.end|isodatetime }}</li>
|
|
31
|
+
</ul>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
{% endif %}
|
|
35
|
+
{% endif %}
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
{% if link %}
|
|
39
|
+
<div class="modal-footer">
|
|
40
|
+
<a href="#" hx-get="{% url 'plugins:ipfabric_netbox:ipfabricsource_topology' pk=source.pk site=site.pk %}?snapshot={{ snapshot.snapshot_id }}"
|
|
41
|
+
hx-target="#topology-modal-content" class="btn btn-success" hx-indicator=".my-indicator"><span
|
|
42
|
+
class="spinner-border spinner-border-sm loader my-indicator" role="status" aria-hidden="true"></span>
|
|
43
|
+
Refresh</a>
|
|
44
|
+
<a href="{{ link }}" class="btn btn-primary" target="_blank">Open in IP Fabric</a>
|
|
45
|
+
</div>
|
|
46
|
+
{% endif %}
|
|
47
|
+
|
|
48
|
+
<style type="text/css">
|
|
49
|
+
.my-indicator {
|
|
50
|
+
display: none !important;
|
|
51
|
+
;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.htmx-request .my-indicator {
|
|
55
|
+
display: inline-block !important;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.htmx-request.my-indicator {
|
|
59
|
+
display: inline-block !important;
|
|
60
|
+
}
|
|
61
|
+
</style>
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{% extends base_template %}
|
|
2
|
+
{% load helpers %}
|
|
3
|
+
{% load i18n %}
|
|
4
|
+
|
|
5
|
+
{% block content %}
|
|
6
|
+
{% block table_controls %}
|
|
7
|
+
{% include 'inc/table_controls_htmx.html' with table_modal=table_config %}
|
|
8
|
+
{% endblock table_controls %}
|
|
9
|
+
<form method="post">
|
|
10
|
+
{% csrf_token %}
|
|
11
|
+
<div class="card">
|
|
12
|
+
<div class="card-body htmx-container table-responsive" id="object_list">
|
|
13
|
+
{% include 'htmx/table.html' %}
|
|
14
|
+
</div>
|
|
15
|
+
</div>
|
|
16
|
+
<div class="noprint bulk-buttons">
|
|
17
|
+
{% block bulk_controls %}
|
|
18
|
+
<div class="bulk-button-group">
|
|
19
|
+
<div class="btn-group" role="group">
|
|
20
|
+
{# Bulk edit buttons #}
|
|
21
|
+
{% block bulk_edit_controls %}
|
|
22
|
+
{% with bulk_edit_view=child_model|validated_viewname:"bulk_edit" %}
|
|
23
|
+
{% if 'bulk_edit' in actions and bulk_edit_view %}
|
|
24
|
+
<button type="submit" name="_edit"
|
|
25
|
+
formaction="{% url bulk_edit_view %}?return_url={{ return_url }}"
|
|
26
|
+
class="btn btn-warning btn-sm">
|
|
27
|
+
<i class="mdi mdi-pencil" aria-hidden="true"></i> {% trans "Edit Selected" %}
|
|
28
|
+
</button>
|
|
29
|
+
{% endif %}
|
|
30
|
+
{% endwith %}
|
|
31
|
+
{% endblock bulk_edit_controls %}
|
|
32
|
+
</div>
|
|
33
|
+
<div class="btn-group" role="group">
|
|
34
|
+
{# Bulk delete buttons #}
|
|
35
|
+
{% block bulk_delete_controls %}
|
|
36
|
+
{% with bulk_delete_view=child_model|validated_viewname:"bulk_delete" %}
|
|
37
|
+
{% if 'bulk_delete' in actions and bulk_delete_view %}
|
|
38
|
+
<button type="submit"
|
|
39
|
+
formaction="{% url bulk_delete_view %}?return_url={{ return_url }}"
|
|
40
|
+
class="btn btn-danger btn-sm">
|
|
41
|
+
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> {% trans "Delete Selected" %}
|
|
42
|
+
</button>
|
|
43
|
+
{% endif %}
|
|
44
|
+
{% endwith %}
|
|
45
|
+
{% endblock bulk_delete_controls %}
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
<div class="bulk-button-group">
|
|
49
|
+
{# Other bulk action buttons #}
|
|
50
|
+
{% block bulk_extra_controls %}{% endblock %}
|
|
51
|
+
</div>
|
|
52
|
+
{% endblock bulk_controls %}
|
|
53
|
+
</div>
|
|
54
|
+
</form>
|
|
55
|
+
{% endblock content %}
|
|
56
|
+
|
|
57
|
+
{% block modals %}
|
|
58
|
+
{{ block.super }}
|
|
59
|
+
{% table_config_form table %}
|
|
60
|
+
{% endblock modals %}
|