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
ipfabric_netbox/views.py
ADDED
|
@@ -0,0 +1,845 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
from dcim.models import Device
|
|
3
|
+
from dcim.models import Site
|
|
4
|
+
from django.contrib import messages
|
|
5
|
+
from django.contrib.auth.mixins import LoginRequiredMixin
|
|
6
|
+
from django.core.cache import cache
|
|
7
|
+
from django.db import models
|
|
8
|
+
from django.shortcuts import get_object_or_404
|
|
9
|
+
from django.shortcuts import redirect
|
|
10
|
+
from django.shortcuts import render
|
|
11
|
+
from django.urls import reverse
|
|
12
|
+
from django.utils import timezone
|
|
13
|
+
from django.views.generic import View
|
|
14
|
+
from django_tables2 import RequestConfig
|
|
15
|
+
from extras.choices import ChangeActionChoices
|
|
16
|
+
from ipfabric.diagrams import Network
|
|
17
|
+
from ipfabric.diagrams import NetworkSettings
|
|
18
|
+
from netbox.staging import StagedChange
|
|
19
|
+
from netbox.views import generic
|
|
20
|
+
from netbox.views.generic.base import BaseObjectView
|
|
21
|
+
from utilities.data import shallow_compare_dict
|
|
22
|
+
from utilities.forms import ConfirmationForm
|
|
23
|
+
from utilities.paginator import EnhancedPaginator
|
|
24
|
+
from utilities.paginator import get_paginate_count
|
|
25
|
+
from utilities.query import count_related
|
|
26
|
+
from utilities.serialization import serialize_object
|
|
27
|
+
from utilities.views import get_viewname
|
|
28
|
+
from utilities.views import register_model_view
|
|
29
|
+
from utilities.views import ViewTab
|
|
30
|
+
|
|
31
|
+
from .filtersets import IPFabricBranchFilterSet
|
|
32
|
+
from .filtersets import IPFabricDataFilterSet
|
|
33
|
+
from .filtersets import IPFabricSnapshotFilterSet
|
|
34
|
+
from .filtersets import IPFabricSourceFilterSet
|
|
35
|
+
from .filtersets import IPFabricStagedChangeFilterSet
|
|
36
|
+
from .forms import IPFabricBranchFilterForm
|
|
37
|
+
from .forms import IPFabricRelationshipFieldForm
|
|
38
|
+
from .forms import IPFabricSnapshotFilterForm
|
|
39
|
+
from .forms import IPFabricSourceFilterForm
|
|
40
|
+
from .forms import IPFabricSourceForm
|
|
41
|
+
from .forms import IPFabricSyncForm
|
|
42
|
+
from .forms import IPFabricTableForm
|
|
43
|
+
from .forms import IPFabricTransformFieldForm
|
|
44
|
+
from .forms import IPFabricTransformMapForm
|
|
45
|
+
from .models import IPFabricBranch
|
|
46
|
+
from .models import IPFabricData
|
|
47
|
+
from .models import IPFabricRelationshipField
|
|
48
|
+
from .models import IPFabricSnapshot
|
|
49
|
+
from .models import IPFabricSource
|
|
50
|
+
from .models import IPFabricSync
|
|
51
|
+
from .models import IPFabricTransformField
|
|
52
|
+
from .models import IPFabricTransformMap
|
|
53
|
+
from .tables import BranchTable
|
|
54
|
+
from .tables import DeviceIPFTable
|
|
55
|
+
from .tables import IPFabricDataTable
|
|
56
|
+
from .tables import IPFabricRelationshipFieldTable
|
|
57
|
+
from .tables import IPFabricSnapshotTable
|
|
58
|
+
from .tables import IPFabricSourceTable
|
|
59
|
+
from .tables import IPFabricTransformFieldTable
|
|
60
|
+
from .tables import IPFabricTransformMapTable
|
|
61
|
+
from .tables import StagedChangesTable
|
|
62
|
+
from .utilities.ipfutils import IPFabric
|
|
63
|
+
from .utilities.transform_map import BuildTransformMaps
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# Transform Map Relationship Field
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@register_model_view(IPFabricRelationshipField, "edit")
|
|
70
|
+
class IPFabricRelationshipFieldEditView(generic.ObjectEditView):
|
|
71
|
+
queryset = IPFabricRelationshipField.objects.all()
|
|
72
|
+
form = IPFabricRelationshipFieldForm
|
|
73
|
+
default_return_url = "plugins:ipfabric_netbox:ipfabricrelationshipfield_list"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class IPFabricRelationshipFieldDeleteView(generic.ObjectDeleteView):
|
|
77
|
+
queryset = IPFabricRelationshipField.objects.all()
|
|
78
|
+
default_return_url = "plugins:ipfabric_netbox:ipfabricrelationshipfield_list"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@register_model_view(IPFabricTransformMap, "relationships")
|
|
82
|
+
class IPFabricTransformRelationshipView(generic.ObjectChildrenView):
|
|
83
|
+
queryset = IPFabricTransformMap.objects.all()
|
|
84
|
+
child_model = IPFabricRelationshipField
|
|
85
|
+
table = IPFabricRelationshipFieldTable
|
|
86
|
+
template_name = "ipfabric_netbox/inc/transform_map_relationship_map.html"
|
|
87
|
+
tab = ViewTab(
|
|
88
|
+
label="Relationship Maps",
|
|
89
|
+
badge=lambda obj: IPFabricRelationshipField.objects.filter(
|
|
90
|
+
transform_map=obj
|
|
91
|
+
).count(),
|
|
92
|
+
permission="ipfabric_netbox.view_ipfabricrelationshipfield",
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
def get_children(self, request, parent):
|
|
96
|
+
return self.child_model.objects.filter(transform_map=parent)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class IPFabricRelationshipFieldListView(generic.ObjectListView):
|
|
100
|
+
queryset = IPFabricRelationshipField.objects.all()
|
|
101
|
+
table = IPFabricRelationshipFieldTable
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# Transform Map
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class IPFabricTransformMapListView(generic.ObjectListView):
|
|
108
|
+
queryset = IPFabricTransformMap.objects.all()
|
|
109
|
+
table = IPFabricTransformMapTable
|
|
110
|
+
template_name = "ipfabric_netbox/ipfabrictransformmap_list.html"
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class IPFabricTransformMapRestoreView(generic.ObjectListView):
|
|
114
|
+
queryset = IPFabricTransformMap.objects.all()
|
|
115
|
+
table = IPFabricTransformMapTable
|
|
116
|
+
|
|
117
|
+
def get_required_permission(self):
|
|
118
|
+
return "ipfabric_netbox.tm_restore"
|
|
119
|
+
|
|
120
|
+
def get(self, request):
|
|
121
|
+
if request.htmx:
|
|
122
|
+
viewname = get_viewname(self.queryset.model, action="restore")
|
|
123
|
+
form_url = reverse(viewname)
|
|
124
|
+
form = ConfirmationForm(initial=request.GET)
|
|
125
|
+
dependent_objects = {
|
|
126
|
+
IPFabricTransformMap: IPFabricTransformMap.objects.all(),
|
|
127
|
+
IPFabricTransformField: IPFabricTransformField.objects.all(),
|
|
128
|
+
IPFabricRelationshipField: IPFabricRelationshipField.objects.all(),
|
|
129
|
+
}
|
|
130
|
+
print(dependent_objects)
|
|
131
|
+
return render(
|
|
132
|
+
request,
|
|
133
|
+
"ipfabric_netbox/ipfabrictransformmap_restore.html",
|
|
134
|
+
{
|
|
135
|
+
"form": form,
|
|
136
|
+
"form_url": form_url,
|
|
137
|
+
"dependent_objects": dependent_objects,
|
|
138
|
+
},
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
def post(self, request):
|
|
142
|
+
IPFabricTransformMap.objects.all().delete()
|
|
143
|
+
try:
|
|
144
|
+
data = requests.get(
|
|
145
|
+
"https://gitlab.com/ip-fabric/integrations/ipfabric-netbox/-/raw/main/scripts/transform_map.json"
|
|
146
|
+
).json()
|
|
147
|
+
except Exception as e:
|
|
148
|
+
messages.error(request, e)
|
|
149
|
+
BuildTransformMaps(data=data)
|
|
150
|
+
return redirect("plugins:ipfabric_netbox:ipfabrictransformmap_list")
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@register_model_view(IPFabricTransformMap, "edit")
|
|
154
|
+
class IPFabricTransformMapEditView(generic.ObjectEditView):
|
|
155
|
+
queryset = IPFabricTransformMap.objects.all()
|
|
156
|
+
form = IPFabricTransformMapForm
|
|
157
|
+
default_return_url = "plugins:ipfabric_netbox:ipfabrictransformmap_list"
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class IPFabricTransformMapDeleteView(generic.ObjectDeleteView):
|
|
161
|
+
queryset = IPFabricTransformMap.objects.all()
|
|
162
|
+
default_return_url = "plugins:ipfabric_netbox:ipfabrictransformmap_list"
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class IPFabricTransformMapBulkDeleteView(generic.BulkDeleteView):
|
|
166
|
+
queryset = IPFabricTransformMap.objects.all()
|
|
167
|
+
table = IPFabricTransformMapTable
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@register_model_view(IPFabricTransformMap)
|
|
171
|
+
class IPFabricTransformMapView(generic.ObjectView):
|
|
172
|
+
queryset = IPFabricTransformMap.objects.all()
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
# Transform Map Field
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class IPFabricTransformFieldListView(generic.ObjectListView):
|
|
179
|
+
queryset = IPFabricTransformField.objects.all()
|
|
180
|
+
table = IPFabricTransformFieldTable
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
@register_model_view(IPFabricTransformField, "edit")
|
|
184
|
+
class IPFabricTransformFieldEditView(generic.ObjectEditView):
|
|
185
|
+
queryset = IPFabricTransformField.objects.all()
|
|
186
|
+
form = IPFabricTransformFieldForm
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class IPFabricTransformFieldDeleteView(generic.ObjectDeleteView):
|
|
190
|
+
queryset = IPFabricTransformField.objects.all()
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
@register_model_view(IPFabricTransformMap, "fields")
|
|
194
|
+
class IPFabricTransformFieldView(generic.ObjectChildrenView):
|
|
195
|
+
queryset = IPFabricTransformMap.objects.all()
|
|
196
|
+
child_model = IPFabricTransformField
|
|
197
|
+
table = IPFabricTransformFieldTable
|
|
198
|
+
template_name = "ipfabric_netbox/inc/transform_map_field_map.html"
|
|
199
|
+
tab = ViewTab(
|
|
200
|
+
label="Field Maps",
|
|
201
|
+
badge=lambda obj: IPFabricTransformField.objects.filter(
|
|
202
|
+
transform_map=obj
|
|
203
|
+
).count(),
|
|
204
|
+
permission="ipfabric_netbox.view_ipfabrictransformfield",
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
def get_children(self, request, parent):
|
|
208
|
+
return self.child_model.objects.filter(transform_map=parent)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
# Snapshot
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
class IPFabricSnapshotListView(generic.ObjectListView):
|
|
215
|
+
queryset = IPFabricSnapshot.objects.all()
|
|
216
|
+
table = IPFabricSnapshotTable
|
|
217
|
+
filterset = IPFabricSnapshotFilterSet
|
|
218
|
+
filterset_form = IPFabricSnapshotFilterForm
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
@register_model_view(IPFabricSnapshot)
|
|
222
|
+
class IPFabricSnapshotView(generic.ObjectView):
|
|
223
|
+
queryset = IPFabricSnapshot.objects.all()
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class IPFabricSnapshotDeleteView(generic.ObjectDeleteView):
|
|
227
|
+
queryset = IPFabricSnapshot.objects.all()
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
class IPFabricSnapshotBulkDeleteView(generic.BulkDeleteView):
|
|
231
|
+
queryset = IPFabricSnapshot.objects.all()
|
|
232
|
+
filterset = IPFabricSnapshotFilterSet
|
|
233
|
+
table = IPFabricSnapshotTable
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
@register_model_view(IPFabricSnapshot, "data")
|
|
237
|
+
class IPFabricSnapshotRawView(generic.ObjectChildrenView):
|
|
238
|
+
queryset = IPFabricSnapshot.objects.all()
|
|
239
|
+
child_model = IPFabricData
|
|
240
|
+
table = IPFabricDataTable
|
|
241
|
+
template_name = "ipfabric_netbox/inc/snapshotdata.html"
|
|
242
|
+
tab = ViewTab(
|
|
243
|
+
label="Raw Data",
|
|
244
|
+
badge=lambda obj: IPFabricData.objects.filter(snapshot_data=obj).count(),
|
|
245
|
+
permission="ipfabric_netbox.view_ipfabricsnapshot",
|
|
246
|
+
hide_if_empty=True,
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
def get_children(self, request, parent):
|
|
250
|
+
return self.child_model.objects.filter(snapshot_data=parent)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
class IPFabricSnapshotDataDeleteView(generic.ObjectDeleteView):
|
|
254
|
+
queryset = IPFabricData.objects.all()
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
class IPFabricSnapshotDataBulkDeleteView(generic.BulkDeleteView):
|
|
258
|
+
queryset = IPFabricData.objects.all()
|
|
259
|
+
filterset = IPFabricDataFilterSet
|
|
260
|
+
table = IPFabricDataTable
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
@register_model_view(
|
|
264
|
+
IPFabricData,
|
|
265
|
+
name="data",
|
|
266
|
+
path="json",
|
|
267
|
+
kwargs={},
|
|
268
|
+
)
|
|
269
|
+
class IPFabricSnapshotDataJSONView(LoginRequiredMixin, View):
|
|
270
|
+
template_name = "ipfabric_netbox/inc/json.html"
|
|
271
|
+
|
|
272
|
+
def get(self, request, **kwargs):
|
|
273
|
+
print(kwargs)
|
|
274
|
+
# change_id = kwargs.get("change_pk", None)
|
|
275
|
+
|
|
276
|
+
if request.htmx:
|
|
277
|
+
data = get_object_or_404(IPFabricData, pk=kwargs.get("pk"))
|
|
278
|
+
return render(
|
|
279
|
+
request,
|
|
280
|
+
self.template_name,
|
|
281
|
+
{
|
|
282
|
+
"object": data,
|
|
283
|
+
},
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
# return render(
|
|
287
|
+
# request,
|
|
288
|
+
# self.template_name,
|
|
289
|
+
# {
|
|
290
|
+
# "change": change,
|
|
291
|
+
# "prechange_data": prechange_data,
|
|
292
|
+
# "postchange_data": postchange_data,
|
|
293
|
+
# "diff_added": diff_added,
|
|
294
|
+
# "diff_removed": diff_removed,
|
|
295
|
+
# "size": "lg",
|
|
296
|
+
# },
|
|
297
|
+
# )
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
# Source
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
class IPFabricSourceListView(generic.ObjectListView):
|
|
304
|
+
queryset = IPFabricSource.objects.annotate(
|
|
305
|
+
snapshot_count=count_related(IPFabricSnapshot, "source")
|
|
306
|
+
)
|
|
307
|
+
filterset = IPFabricSourceFilterSet
|
|
308
|
+
filterset_form = IPFabricSourceFilterForm
|
|
309
|
+
table = IPFabricSourceTable
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
@register_model_view(IPFabricSource, "edit")
|
|
313
|
+
class IPFabricSourceEditView(generic.ObjectEditView):
|
|
314
|
+
queryset = IPFabricSource.objects.all()
|
|
315
|
+
form = IPFabricSourceForm
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
@register_model_view(IPFabricSource)
|
|
319
|
+
class IPFabricSourceView(generic.ObjectView):
|
|
320
|
+
queryset = IPFabricSource.objects.all()
|
|
321
|
+
|
|
322
|
+
def get_extra_context(self, request, instance):
|
|
323
|
+
related_models = (
|
|
324
|
+
(
|
|
325
|
+
IPFabricSnapshot.objects.restrict(request.user, "view").filter(
|
|
326
|
+
source=instance
|
|
327
|
+
),
|
|
328
|
+
"source_id",
|
|
329
|
+
),
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
job = instance.jobs.order_by("id").last()
|
|
333
|
+
data = {"related_models": related_models, "job": job}
|
|
334
|
+
if job:
|
|
335
|
+
data["job_results"] = job.data
|
|
336
|
+
return data
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
@register_model_view(IPFabricSource, "sync")
|
|
340
|
+
class IPFabricSourceSyncView(BaseObjectView):
|
|
341
|
+
queryset = IPFabricSource.objects.all()
|
|
342
|
+
|
|
343
|
+
def get_required_permission(self):
|
|
344
|
+
return "ipfabric_netbox.sync_source"
|
|
345
|
+
|
|
346
|
+
def get(self, request, pk):
|
|
347
|
+
ipfabricsource = get_object_or_404(self.queryset, pk=pk)
|
|
348
|
+
return redirect(ipfabricsource.get_absolute_url())
|
|
349
|
+
|
|
350
|
+
def post(self, request, pk):
|
|
351
|
+
ipfabricsource = get_object_or_404(self.queryset, pk=pk)
|
|
352
|
+
job = ipfabricsource.enqueue_sync_job(request=request)
|
|
353
|
+
|
|
354
|
+
messages.success(request, f"Queued job #{job.pk} to sync {ipfabricsource}")
|
|
355
|
+
return redirect(ipfabricsource.get_absolute_url())
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
@register_model_view(IPFabricSource, "delete")
|
|
359
|
+
class IPFabricSourceDeleteView(generic.ObjectDeleteView):
|
|
360
|
+
queryset = IPFabricSource.objects.all()
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
class IPFabricSourceBulkDeleteView(generic.BulkDeleteView):
|
|
364
|
+
queryset = IPFabricSource.objects.all()
|
|
365
|
+
filterset = IPFabricSourceFilterSet
|
|
366
|
+
table = IPFabricSourceTable
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
# Sync
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
class IPFabricSyncListView(View):
|
|
373
|
+
def get(self, request):
|
|
374
|
+
syncs = IPFabricSync.objects.prefetch_related("snapshot_data")
|
|
375
|
+
return render(
|
|
376
|
+
request,
|
|
377
|
+
"ipfabric_netbox/sync_list.html",
|
|
378
|
+
{"model": IPFabricSync, "syncs": syncs},
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
@register_model_view(IPFabricSync, "edit")
|
|
383
|
+
class IPFabricSyncEditView(generic.ObjectEditView):
|
|
384
|
+
queryset = IPFabricSync.objects.all()
|
|
385
|
+
form = IPFabricSyncForm
|
|
386
|
+
|
|
387
|
+
def alter_object(self, obj, request, url_args, url_kwargs):
|
|
388
|
+
obj.user = request.user
|
|
389
|
+
return obj
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
@register_model_view(IPFabricSync)
|
|
393
|
+
class IPFabricSyncView(generic.ObjectView):
|
|
394
|
+
queryset = IPFabricSync.objects.all()
|
|
395
|
+
actions = ("edit",)
|
|
396
|
+
|
|
397
|
+
def get(self, request, **kwargs):
|
|
398
|
+
instance = self.get_object(**kwargs)
|
|
399
|
+
last_branch = instance.ipfabricbranch_set.last()
|
|
400
|
+
|
|
401
|
+
if request.htmx:
|
|
402
|
+
response = render(
|
|
403
|
+
request,
|
|
404
|
+
"ipfabric_netbox/partials/sync_last_branch.html",
|
|
405
|
+
{"last_branch": last_branch},
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
if instance.status not in ["queued", "syncing"]:
|
|
409
|
+
messages.success(
|
|
410
|
+
request,
|
|
411
|
+
f"Ingestion ({instance.name}) {instance.status}. Branch {last_branch.name} {last_branch.job.status}.",
|
|
412
|
+
)
|
|
413
|
+
response["HX-Refresh"] = "true"
|
|
414
|
+
return response
|
|
415
|
+
|
|
416
|
+
return render(
|
|
417
|
+
request,
|
|
418
|
+
self.get_template_name(),
|
|
419
|
+
{
|
|
420
|
+
"object": instance,
|
|
421
|
+
"tab": self.tab,
|
|
422
|
+
**self.get_extra_context(request, instance),
|
|
423
|
+
},
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
def get_extra_context(self, request, instance):
|
|
427
|
+
last_branch = instance.ipfabricbranch_set.last()
|
|
428
|
+
|
|
429
|
+
if request.GET.get("format") in ["json", "yaml"]:
|
|
430
|
+
format = request.GET.get("format")
|
|
431
|
+
if request.user.is_authenticated:
|
|
432
|
+
request.user.config.set("data_format", format, commit=True)
|
|
433
|
+
elif request.user.is_authenticated:
|
|
434
|
+
format = request.user.config.get("data_format", "json")
|
|
435
|
+
else:
|
|
436
|
+
format = "json"
|
|
437
|
+
|
|
438
|
+
last_branch = instance.ipfabricbranch_set.last()
|
|
439
|
+
|
|
440
|
+
return {"format": format, "last_branch": last_branch}
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
@register_model_view(IPFabricSync, "delete")
|
|
444
|
+
class IPFabricSyncDeleteView(generic.ObjectDeleteView):
|
|
445
|
+
queryset = IPFabricSync.objects.all()
|
|
446
|
+
default_return_url = "plugins:ipfabric_netbox:ipfabricsync_list"
|
|
447
|
+
|
|
448
|
+
def get(self, request, pk):
|
|
449
|
+
obj = get_object_or_404(self.queryset, pk=pk)
|
|
450
|
+
|
|
451
|
+
if request.htmx:
|
|
452
|
+
viewname = get_viewname(self.queryset.model, action="delete")
|
|
453
|
+
form_url = reverse(viewname, kwargs={"pk": obj.pk})
|
|
454
|
+
form = ConfirmationForm(initial=request.GET)
|
|
455
|
+
return render(
|
|
456
|
+
request,
|
|
457
|
+
"ipfabric_netbox/inc/sync_delete.html",
|
|
458
|
+
{
|
|
459
|
+
"object": obj,
|
|
460
|
+
"object_type": self.queryset.model._meta.verbose_name,
|
|
461
|
+
"form": form,
|
|
462
|
+
"form_url": form_url,
|
|
463
|
+
**self.get_extra_context(request, obj),
|
|
464
|
+
},
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
class IPFabricSyncBulkDeleteView(generic.BulkDeleteView):
|
|
469
|
+
queryset = IPFabricSync.objects.all()
|
|
470
|
+
filterset = IPFabricSnapshotFilterSet
|
|
471
|
+
table = IPFabricSnapshotTable
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
# Ingestion
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
@register_model_view(IPFabricSync, "sync")
|
|
478
|
+
class IPFabricIngestSyncView(BaseObjectView):
|
|
479
|
+
queryset = IPFabricSync.objects.all()
|
|
480
|
+
|
|
481
|
+
def get_required_permission(self):
|
|
482
|
+
return "ipfabric_netbox.sync_ingest"
|
|
483
|
+
|
|
484
|
+
def get(self, request, pk):
|
|
485
|
+
ipfabric = get_object_or_404(self.queryset, pk=pk)
|
|
486
|
+
return redirect(ipfabric.get_absolute_url())
|
|
487
|
+
|
|
488
|
+
def post(self, request, pk):
|
|
489
|
+
ipfabric = get_object_or_404(self.queryset, pk=pk)
|
|
490
|
+
job = ipfabric.enqueue_sync_job(user=request.user, adhoc=True)
|
|
491
|
+
|
|
492
|
+
messages.success(request, f"Queued job #{job.pk} to sync {ipfabric}")
|
|
493
|
+
return redirect(ipfabric.get_absolute_url())
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
# Branch
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
class IPFabricBranchListView(generic.ObjectListView):
|
|
500
|
+
queryset = IPFabricBranch.objects.all()
|
|
501
|
+
filterset = IPFabricBranchFilterSet
|
|
502
|
+
filterset_form = IPFabricBranchFilterForm
|
|
503
|
+
table = BranchTable
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
@register_model_view(
|
|
507
|
+
IPFabricBranch,
|
|
508
|
+
name="logs",
|
|
509
|
+
path="logs",
|
|
510
|
+
)
|
|
511
|
+
class IPFabricBranchLogView(LoginRequiredMixin, View):
|
|
512
|
+
template_name = "ipfabric_netbox/partials/branch_all.html"
|
|
513
|
+
|
|
514
|
+
def get(self, request, **kwargs):
|
|
515
|
+
branch_id = kwargs.get("pk")
|
|
516
|
+
if request.htmx:
|
|
517
|
+
branch = IPFabricBranch.objects.get(pk=branch_id)
|
|
518
|
+
data = branch.get_statistics()
|
|
519
|
+
data["object"] = branch
|
|
520
|
+
data["job"] = branch.job
|
|
521
|
+
response = render(
|
|
522
|
+
request,
|
|
523
|
+
self.template_name,
|
|
524
|
+
data,
|
|
525
|
+
)
|
|
526
|
+
if branch.job.completed:
|
|
527
|
+
response["HX-Refresh"] = "true"
|
|
528
|
+
return response
|
|
529
|
+
else:
|
|
530
|
+
return response
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
@register_model_view(IPFabricBranch)
|
|
534
|
+
class IPFabricBranchView(generic.ObjectView):
|
|
535
|
+
queryset = IPFabricBranch.objects.annotate(
|
|
536
|
+
num_created=models.Count(
|
|
537
|
+
"staged_changes",
|
|
538
|
+
filter=models.Q(staged_changes__action=ChangeActionChoices.ACTION_CREATE)
|
|
539
|
+
& ~models.Q(staged_changes__object_type__model="objectchange"),
|
|
540
|
+
),
|
|
541
|
+
num_updated=models.Count(
|
|
542
|
+
"staged_changes",
|
|
543
|
+
filter=models.Q(staged_changes__action=ChangeActionChoices.ACTION_UPDATE)
|
|
544
|
+
& ~models.Q(staged_changes__object_type__model="objectchange"),
|
|
545
|
+
),
|
|
546
|
+
num_deleted=models.Count(
|
|
547
|
+
"staged_changes",
|
|
548
|
+
filter=models.Q(staged_changes__action=ChangeActionChoices.ACTION_DELETE)
|
|
549
|
+
& ~models.Q(staged_changes__object_type__model="objectchange"),
|
|
550
|
+
),
|
|
551
|
+
)
|
|
552
|
+
|
|
553
|
+
def get_extra_context(self, request, instance):
|
|
554
|
+
data = instance.get_statistics()
|
|
555
|
+
return data
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
@register_model_view(IPFabricBranch, "merge")
|
|
559
|
+
class IPFabricBranchMergeView(BaseObjectView):
|
|
560
|
+
queryset = IPFabricBranch.objects.all()
|
|
561
|
+
template_name = "ipfabric_netbox/inc/merge_form.html"
|
|
562
|
+
|
|
563
|
+
def get_required_permission(self):
|
|
564
|
+
return "ipfabric_netbox.merge_branch"
|
|
565
|
+
|
|
566
|
+
def get(self, request, pk):
|
|
567
|
+
obj = get_object_or_404(self.queryset, pk=pk)
|
|
568
|
+
|
|
569
|
+
if request.htmx:
|
|
570
|
+
viewname = get_viewname(self.queryset.model, action="merge")
|
|
571
|
+
form_url = reverse(viewname, kwargs={"pk": obj.pk})
|
|
572
|
+
form = ConfirmationForm(initial=request.GET)
|
|
573
|
+
return render(
|
|
574
|
+
request,
|
|
575
|
+
"ipfabric_netbox/inc/merge_form.html",
|
|
576
|
+
{
|
|
577
|
+
"object": obj,
|
|
578
|
+
"object_type": self.queryset.model._meta.verbose_name,
|
|
579
|
+
"form": form,
|
|
580
|
+
"form_url": form_url,
|
|
581
|
+
**self.get_extra_context(request, obj),
|
|
582
|
+
},
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
return redirect(obj.get_absolute_url())
|
|
586
|
+
|
|
587
|
+
def post(self, request, pk):
|
|
588
|
+
ipfabric = get_object_or_404(self.queryset, pk=pk)
|
|
589
|
+
job = ipfabric.enqueue_merge_job(user=request.user)
|
|
590
|
+
|
|
591
|
+
messages.success(request, f"Queued job #{job.pk} to sync {ipfabric}")
|
|
592
|
+
return redirect(ipfabric.get_absolute_url())
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
@register_model_view(
|
|
596
|
+
IPFabricBranch,
|
|
597
|
+
name="change_diff",
|
|
598
|
+
path="change/<int:change_pk>",
|
|
599
|
+
kwargs={"model": IPFabricBranch},
|
|
600
|
+
)
|
|
601
|
+
class IPFabricBranchChangesDiffView(LoginRequiredMixin, View):
|
|
602
|
+
template_name = "ipfabric_netbox/inc/diff.html"
|
|
603
|
+
|
|
604
|
+
def get(self, request, model, **kwargs):
|
|
605
|
+
change_id = kwargs.get("change_pk", None)
|
|
606
|
+
|
|
607
|
+
if request.htmx:
|
|
608
|
+
if change_id:
|
|
609
|
+
change = StagedChange.objects.get(pk=change_id)
|
|
610
|
+
if hasattr(change.object, "pk"):
|
|
611
|
+
prechange_data = serialize_object(change.object, resolve_tags=False)
|
|
612
|
+
prechange_data = dict(sorted(prechange_data.items()))
|
|
613
|
+
else:
|
|
614
|
+
prechange_data = None
|
|
615
|
+
|
|
616
|
+
if hasattr(change, "data"):
|
|
617
|
+
postchange_data = dict(sorted(change.data.items()))
|
|
618
|
+
|
|
619
|
+
if prechange_data and postchange_data:
|
|
620
|
+
diff_added = shallow_compare_dict(
|
|
621
|
+
prechange_data or dict(),
|
|
622
|
+
postchange_data or dict(),
|
|
623
|
+
exclude=["last_updated"],
|
|
624
|
+
)
|
|
625
|
+
diff_removed = (
|
|
626
|
+
{x: prechange_data.get(x) for x in diff_added}
|
|
627
|
+
if prechange_data
|
|
628
|
+
else {}
|
|
629
|
+
)
|
|
630
|
+
else:
|
|
631
|
+
diff_added = None
|
|
632
|
+
diff_removed = None
|
|
633
|
+
|
|
634
|
+
return render(
|
|
635
|
+
request,
|
|
636
|
+
self.template_name,
|
|
637
|
+
{
|
|
638
|
+
"change": change,
|
|
639
|
+
"prechange_data": prechange_data,
|
|
640
|
+
"postchange_data": postchange_data,
|
|
641
|
+
"diff_added": diff_added,
|
|
642
|
+
"diff_removed": diff_removed,
|
|
643
|
+
"size": "lg",
|
|
644
|
+
},
|
|
645
|
+
)
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
@register_model_view(IPFabricBranch, "change")
|
|
649
|
+
class IPFabricBranchChangesView(generic.ObjectChildrenView):
|
|
650
|
+
queryset = IPFabricBranch.objects.all()
|
|
651
|
+
child_model = StagedChange
|
|
652
|
+
table = StagedChangesTable
|
|
653
|
+
filterset = IPFabricStagedChangeFilterSet
|
|
654
|
+
template_name = "generic/object_children.html"
|
|
655
|
+
tab = ViewTab(
|
|
656
|
+
label="Changes",
|
|
657
|
+
badge=lambda obj: StagedChange.objects.filter(branch=obj).count(),
|
|
658
|
+
permission="ipfabric_netbox.view_ipfabricbranch",
|
|
659
|
+
)
|
|
660
|
+
|
|
661
|
+
def get_children(self, request, parent):
|
|
662
|
+
return self.child_model.objects.filter(branch=parent)
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
@register_model_view(IPFabricBranch, "delete")
|
|
666
|
+
class IPFabricBranchDeleteView(generic.ObjectDeleteView):
|
|
667
|
+
queryset = IPFabricBranch.objects.all()
|
|
668
|
+
|
|
669
|
+
|
|
670
|
+
@register_model_view(IPFabricSync, "branch")
|
|
671
|
+
class IPFabricBranchTabView(generic.ObjectChildrenView):
|
|
672
|
+
queryset = IPFabricSync.objects.all()
|
|
673
|
+
child_model = IPFabricBranch
|
|
674
|
+
table = BranchTable
|
|
675
|
+
filterset = IPFabricBranchFilterSet
|
|
676
|
+
template_name = "generic/object_children.html"
|
|
677
|
+
tab = ViewTab(
|
|
678
|
+
label="Branches",
|
|
679
|
+
badge=lambda obj: IPFabricBranch.objects.filter(sync=obj).count(),
|
|
680
|
+
permission="ipfabric_netbox.view_ipfabricbranch",
|
|
681
|
+
)
|
|
682
|
+
|
|
683
|
+
def get_children(self, request, parent):
|
|
684
|
+
return self.child_model.objects.filter(sync=parent)
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
@register_model_view(Device, "ipfabric")
|
|
688
|
+
class IPFabricTable(View):
|
|
689
|
+
template_name = "ipfabric_netbox/ipfabric_table.html"
|
|
690
|
+
tab = ViewTab("IP Fabric", permission="ipfabric_netbox.view_devicetable")
|
|
691
|
+
|
|
692
|
+
def get(self, request, pk):
|
|
693
|
+
device = get_object_or_404(Device, pk=pk)
|
|
694
|
+
form = (
|
|
695
|
+
IPFabricTableForm(request.GET)
|
|
696
|
+
if "table" in request.GET
|
|
697
|
+
else IPFabricTableForm()
|
|
698
|
+
)
|
|
699
|
+
data = None
|
|
700
|
+
|
|
701
|
+
if form.is_valid():
|
|
702
|
+
table = form.cleaned_data["table"]
|
|
703
|
+
test = {
|
|
704
|
+
"True": True,
|
|
705
|
+
"False": False,
|
|
706
|
+
}
|
|
707
|
+
cache_enable = test.get(form.cleaned_data["cache_enable"])
|
|
708
|
+
snapshot_id = ""
|
|
709
|
+
|
|
710
|
+
if not form.cleaned_data["snapshot_data"]:
|
|
711
|
+
snapshot_id = "$last"
|
|
712
|
+
source = IPFabricSource.objects.get(
|
|
713
|
+
pk=device.custom_field_data["ipfabric_source"]
|
|
714
|
+
)
|
|
715
|
+
|
|
716
|
+
else:
|
|
717
|
+
snapshot_id = form.cleaned_data["snapshot_data"].snapshot_id
|
|
718
|
+
source = form.cleaned_data["snapshot_data"].source
|
|
719
|
+
|
|
720
|
+
source.parameters["snapshot_id"] = snapshot_id
|
|
721
|
+
source.parameters["base_url"] = source.url
|
|
722
|
+
|
|
723
|
+
cache_key = (
|
|
724
|
+
f"ipfabric_{table}_{device.serial}_{source.parameters['snapshot_id']}"
|
|
725
|
+
)
|
|
726
|
+
if cache_enable:
|
|
727
|
+
data = cache.get(cache_key)
|
|
728
|
+
|
|
729
|
+
if not data:
|
|
730
|
+
try:
|
|
731
|
+
ipf = IPFabric(parameters=source.parameters)
|
|
732
|
+
raw_data, columns = ipf.get_table_data(table=table, device=device)
|
|
733
|
+
data = {"data": raw_data, "columns": columns}
|
|
734
|
+
cache.set(cache_key, data, 60 * 60 * 24)
|
|
735
|
+
except Exception as e:
|
|
736
|
+
messages.error(request, e)
|
|
737
|
+
|
|
738
|
+
if not data:
|
|
739
|
+
data = {"data": [], "columns": []}
|
|
740
|
+
|
|
741
|
+
table = DeviceIPFTable(data["data"], extra_columns=data["columns"])
|
|
742
|
+
|
|
743
|
+
RequestConfig(
|
|
744
|
+
request,
|
|
745
|
+
{
|
|
746
|
+
"paginator_class": EnhancedPaginator,
|
|
747
|
+
"per_page": get_paginate_count(request),
|
|
748
|
+
},
|
|
749
|
+
).configure(table)
|
|
750
|
+
|
|
751
|
+
if request.htmx:
|
|
752
|
+
return render(
|
|
753
|
+
request,
|
|
754
|
+
"htmx/table.html",
|
|
755
|
+
{
|
|
756
|
+
"table": table,
|
|
757
|
+
},
|
|
758
|
+
)
|
|
759
|
+
|
|
760
|
+
source = None
|
|
761
|
+
|
|
762
|
+
if source_id := device.custom_field_data["ipfabric_source"]:
|
|
763
|
+
source = IPFabricSource.objects.get(pk=source_id)
|
|
764
|
+
|
|
765
|
+
return render(
|
|
766
|
+
request,
|
|
767
|
+
self.template_name,
|
|
768
|
+
{
|
|
769
|
+
"object": device,
|
|
770
|
+
"source": source,
|
|
771
|
+
"tab": self.tab,
|
|
772
|
+
"form": form,
|
|
773
|
+
"table": table,
|
|
774
|
+
},
|
|
775
|
+
)
|
|
776
|
+
|
|
777
|
+
|
|
778
|
+
@register_model_view(
|
|
779
|
+
IPFabricSource,
|
|
780
|
+
name="topology",
|
|
781
|
+
path="topology/<int:site>",
|
|
782
|
+
kwargs={"snapshot": ""},
|
|
783
|
+
)
|
|
784
|
+
class IPFabricSourceTopology(LoginRequiredMixin, View):
|
|
785
|
+
template_name = "ipfabric_netbox/inc/site_topology_modal.html"
|
|
786
|
+
|
|
787
|
+
def get(self, request, pk, site, **kwargs):
|
|
788
|
+
if request.htmx:
|
|
789
|
+
try:
|
|
790
|
+
site = get_object_or_404(Site, pk=site)
|
|
791
|
+
source_id = request.GET.get("source")
|
|
792
|
+
if not source_id:
|
|
793
|
+
raise Exception("Source ID not available in request.")
|
|
794
|
+
source = get_object_or_404(IPFabricSource, pk=source_id)
|
|
795
|
+
snapshot = request.GET.get("snapshot")
|
|
796
|
+
if not snapshot:
|
|
797
|
+
raise Exception("Snapshot ID not available in request.")
|
|
798
|
+
|
|
799
|
+
source.parameters.update(
|
|
800
|
+
{"snapshot_id": snapshot, "base_url": source.url}
|
|
801
|
+
)
|
|
802
|
+
|
|
803
|
+
ipf = IPFabric(parameters=source.parameters)
|
|
804
|
+
snapshot_data = ipf.ipf.snapshots.get(snapshot)
|
|
805
|
+
if not snapshot_data:
|
|
806
|
+
raise Exception(
|
|
807
|
+
f"Snapshot ({snapshot}) not available in IP Fabric." # noqa E713
|
|
808
|
+
)
|
|
809
|
+
|
|
810
|
+
sites = ipf.ipf.inventory.sites.all(
|
|
811
|
+
filters={"siteName": ["eq", site.name]}
|
|
812
|
+
)
|
|
813
|
+
if not sites:
|
|
814
|
+
raise Exception(
|
|
815
|
+
f"{site.name} not available in snapshot ({snapshot})." # noqa E713
|
|
816
|
+
)
|
|
817
|
+
|
|
818
|
+
net = Network(sites=site.name, all_network=False)
|
|
819
|
+
settings = NetworkSettings()
|
|
820
|
+
settings.hide_protocol("xdp")
|
|
821
|
+
settings.hiddenDeviceTypes.extend(["transit", "cloud"])
|
|
822
|
+
|
|
823
|
+
link = ipf.ipf.diagram.share_link(net, graph_settings=settings)
|
|
824
|
+
svg_data = ipf.ipf.diagram.svg(net, graph_settings=settings).decode(
|
|
825
|
+
"utf-8"
|
|
826
|
+
)
|
|
827
|
+
error = None
|
|
828
|
+
except Exception as e:
|
|
829
|
+
error = e
|
|
830
|
+
svg_data = link = None
|
|
831
|
+
|
|
832
|
+
return render(
|
|
833
|
+
request,
|
|
834
|
+
self.template_name,
|
|
835
|
+
{
|
|
836
|
+
"site": site,
|
|
837
|
+
"source": source,
|
|
838
|
+
"svg": svg_data,
|
|
839
|
+
"size": "xl",
|
|
840
|
+
"link": link,
|
|
841
|
+
"time": timezone.now(),
|
|
842
|
+
"snapshot": snapshot_data,
|
|
843
|
+
"error": error,
|
|
844
|
+
},
|
|
845
|
+
)
|