ipfabric_netbox 4.3.2b8__py3-none-any.whl → 4.3.2b10__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 +2 -2
- ipfabric_netbox/api/serializers.py +112 -7
- ipfabric_netbox/api/urls.py +6 -0
- ipfabric_netbox/api/views.py +23 -0
- ipfabric_netbox/choices.py +72 -40
- ipfabric_netbox/data/endpoint.json +47 -0
- ipfabric_netbox/data/filters.json +51 -0
- ipfabric_netbox/data/transform_map.json +188 -174
- ipfabric_netbox/exceptions.py +7 -5
- ipfabric_netbox/filtersets.py +310 -41
- ipfabric_netbox/forms.py +324 -79
- ipfabric_netbox/graphql/__init__.py +6 -0
- ipfabric_netbox/graphql/enums.py +5 -5
- ipfabric_netbox/graphql/filters.py +56 -4
- ipfabric_netbox/graphql/schema.py +28 -0
- ipfabric_netbox/graphql/types.py +61 -1
- ipfabric_netbox/jobs.py +18 -1
- ipfabric_netbox/migrations/0022_prepare_for_filters.py +182 -0
- ipfabric_netbox/migrations/0023_populate_filters_data.py +279 -0
- ipfabric_netbox/migrations/0024_finish_filters.py +29 -0
- ipfabric_netbox/models.py +384 -12
- ipfabric_netbox/navigation.py +98 -24
- ipfabric_netbox/tables.py +194 -9
- ipfabric_netbox/templates/ipfabric_netbox/htmx_list.html +5 -0
- ipfabric_netbox/templates/ipfabric_netbox/inc/combined_expressions.html +59 -0
- ipfabric_netbox/templates/ipfabric_netbox/inc/combined_expressions_content.html +39 -0
- ipfabric_netbox/templates/ipfabric_netbox/inc/endpoint_filters_with_selector.html +54 -0
- ipfabric_netbox/templates/ipfabric_netbox/ipfabricendpoint.html +39 -0
- ipfabric_netbox/templates/ipfabric_netbox/ipfabricfilter.html +51 -0
- ipfabric_netbox/templates/ipfabric_netbox/ipfabricfilterexpression.html +39 -0
- ipfabric_netbox/templates/ipfabric_netbox/ipfabricfilterexpression_edit.html +150 -0
- ipfabric_netbox/templates/ipfabric_netbox/ipfabricsync.html +1 -1
- ipfabric_netbox/templates/ipfabric_netbox/ipfabrictransformmap.html +16 -2
- ipfabric_netbox/templatetags/ipfabric_netbox_helpers.py +65 -0
- ipfabric_netbox/tests/api/test_api.py +333 -13
- ipfabric_netbox/tests/test_filtersets.py +2592 -0
- ipfabric_netbox/tests/test_forms.py +1256 -74
- ipfabric_netbox/tests/test_models.py +242 -34
- ipfabric_netbox/tests/test_views.py +2030 -25
- ipfabric_netbox/urls.py +35 -0
- ipfabric_netbox/utilities/endpoint.py +30 -0
- ipfabric_netbox/utilities/filters.py +88 -0
- ipfabric_netbox/utilities/ipfutils.py +254 -316
- ipfabric_netbox/utilities/logging.py +7 -7
- ipfabric_netbox/utilities/transform_map.py +126 -0
- ipfabric_netbox/views.py +719 -5
- {ipfabric_netbox-4.3.2b8.dist-info → ipfabric_netbox-4.3.2b10.dist-info}/METADATA +3 -2
- {ipfabric_netbox-4.3.2b8.dist-info → ipfabric_netbox-4.3.2b10.dist-info}/RECORD +49 -33
- {ipfabric_netbox-4.3.2b8.dist-info → ipfabric_netbox-4.3.2b10.dist-info}/WHEEL +1 -1
ipfabric_netbox/forms.py
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import copy
|
|
2
|
-
|
|
3
1
|
from core.choices import JobIntervalChoices
|
|
4
2
|
from django import forms
|
|
5
3
|
from django.contrib.contenttypes.models import ContentType
|
|
@@ -29,13 +27,17 @@ from utilities.forms.widgets import DateTimePicker
|
|
|
29
27
|
from utilities.forms.widgets import HTMXSelect
|
|
30
28
|
from utilities.forms.widgets import NumberWithOptions
|
|
31
29
|
|
|
30
|
+
from .choices import IPFabricEndpointChoices
|
|
31
|
+
from .choices import IPFabricFilterTypeChoices
|
|
32
32
|
from .choices import IPFabricSnapshotStatusModelChoices
|
|
33
33
|
from .choices import IPFabricSourceStatusChoices
|
|
34
34
|
from .choices import IPFabricSourceTypeChoices
|
|
35
35
|
from .choices import IPFabricSyncStatusChoices
|
|
36
|
-
from .choices import IPFabricTransformMapSourceModelChoices
|
|
37
36
|
from .choices import required_transform_map_contenttypes
|
|
38
37
|
from .choices import transform_field_source_columns
|
|
38
|
+
from .models import IPFabricEndpoint
|
|
39
|
+
from .models import IPFabricFilter
|
|
40
|
+
from .models import IPFabricFilterExpression
|
|
39
41
|
from .models import IPFabricIngestion
|
|
40
42
|
from .models import IPFabricRelationshipField
|
|
41
43
|
from .models import IPFabricSnapshot
|
|
@@ -45,7 +47,8 @@ from .models import IPFabricSync
|
|
|
45
47
|
from .models import IPFabricTransformField
|
|
46
48
|
from .models import IPFabricTransformMap
|
|
47
49
|
from .models import IPFabricTransformMapGroup
|
|
48
|
-
|
|
50
|
+
from .utilities.filters import get_filter_expression_test_candidates
|
|
51
|
+
from .utilities.transform_map import has_cycle_dfs
|
|
49
52
|
|
|
50
53
|
exclude_fields = [
|
|
51
54
|
"id",
|
|
@@ -56,47 +59,11 @@ exclude_fields = [
|
|
|
56
59
|
"status",
|
|
57
60
|
]
|
|
58
61
|
|
|
59
|
-
dcim_parameters = {
|
|
60
|
-
"site": forms.BooleanField(required=False, label=_("Sites"), initial=True),
|
|
61
|
-
"manufacturer": forms.BooleanField(
|
|
62
|
-
required=False, label=_("Manufacturers"), initial=True
|
|
63
|
-
),
|
|
64
|
-
"devicetype": forms.BooleanField(
|
|
65
|
-
required=False, label=_("Device Types"), initial=True
|
|
66
|
-
),
|
|
67
|
-
"devicerole": forms.BooleanField(
|
|
68
|
-
required=False, label=_("Device Roles"), initial=True
|
|
69
|
-
),
|
|
70
|
-
"platform": forms.BooleanField(required=False, label=_("Platforms"), initial=True),
|
|
71
|
-
"device": forms.BooleanField(required=False, label=_("Devices"), initial=True),
|
|
72
|
-
"virtualchassis": forms.BooleanField(
|
|
73
|
-
required=False, label=_("Virtual Chassis"), initial=True
|
|
74
|
-
),
|
|
75
|
-
"interface": forms.BooleanField(
|
|
76
|
-
required=False, label=_("Interfaces"), initial=True
|
|
77
|
-
),
|
|
78
|
-
"macaddress": forms.BooleanField(
|
|
79
|
-
required=False, label=_("MAC Addresses"), initial=True
|
|
80
|
-
),
|
|
81
|
-
"inventoryitem": forms.BooleanField(
|
|
82
|
-
required=False, label=_("Part Numbers"), initial=True
|
|
83
|
-
),
|
|
84
|
-
}
|
|
85
|
-
ipam_parameters = {
|
|
86
|
-
"vlan": forms.BooleanField(required=False, label=_("VLANs"), initial=True),
|
|
87
|
-
"vrf": forms.BooleanField(required=False, label=_("VRFs"), initial=True),
|
|
88
|
-
"prefix": forms.BooleanField(required=False, label=_("Prefixes"), initial=True),
|
|
89
|
-
"ipaddress": forms.BooleanField(
|
|
90
|
-
required=False, label=_("IP Addresses"), initial=True
|
|
91
|
-
),
|
|
92
|
-
}
|
|
93
|
-
sync_parameters = {"dcim": dcim_parameters, "ipam": ipam_parameters}
|
|
94
|
-
|
|
95
62
|
|
|
96
|
-
def source_column_choices(
|
|
97
|
-
columns = transform_field_source_columns.get(
|
|
63
|
+
def source_column_choices(endpoint: str) -> list[tuple[str, str]]:
|
|
64
|
+
columns = transform_field_source_columns.get(endpoint, None)
|
|
98
65
|
if columns:
|
|
99
|
-
choices = [(f, f) for f in transform_field_source_columns.get(
|
|
66
|
+
choices = [(f, f) for f in transform_field_source_columns.get(endpoint)]
|
|
100
67
|
else:
|
|
101
68
|
# This should never happen, but better be safe than sorry
|
|
102
69
|
choices = [] # pragma: no cover
|
|
@@ -113,10 +80,37 @@ def str_to_list(_str: str | list) -> list[str]:
|
|
|
113
80
|
def list_to_choices(choices: list[str]) -> tuple[tuple[str, str], ...]:
|
|
114
81
|
new_choices = ()
|
|
115
82
|
for choice in choices:
|
|
116
|
-
new_choices
|
|
83
|
+
new_choices += ((choice, choice),)
|
|
117
84
|
return new_choices
|
|
118
85
|
|
|
119
86
|
|
|
87
|
+
class IPFabricEndpointForm(NetBoxModelForm):
|
|
88
|
+
endpoint = CSVChoiceField(
|
|
89
|
+
label=_("Endpoint"),
|
|
90
|
+
choices=IPFabricEndpointChoices,
|
|
91
|
+
help_text=_("API endpoints available in IP Fabric to pull data from"),
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
class Meta:
|
|
95
|
+
model = IPFabricEndpoint
|
|
96
|
+
fields = ("name", "description", "endpoint")
|
|
97
|
+
widgets = {
|
|
98
|
+
"endpoint": HTMXSelect(),
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class IPFabricEndpointBulkEditForm(NetBoxModelBulkEditForm):
|
|
103
|
+
model = IPFabricEndpoint
|
|
104
|
+
fields = ("endpoint",)
|
|
105
|
+
nullable_fields = ("endpoint",)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class IPFabricEndpointBulkImportForm(NetBoxModelImportForm):
|
|
109
|
+
class Meta:
|
|
110
|
+
model = IPFabricEndpoint
|
|
111
|
+
fields = ("name", "description", "endpoint")
|
|
112
|
+
|
|
113
|
+
|
|
120
114
|
class IPFabricRelationshipFieldForm(NetBoxModelForm):
|
|
121
115
|
coalesce = forms.BooleanField(required=False, initial=False)
|
|
122
116
|
target_field = forms.CharField(
|
|
@@ -241,7 +235,6 @@ class IPFabricTransformFieldForm(NetBoxModelForm):
|
|
|
241
235
|
fields = (
|
|
242
236
|
self.instance.transform_map.target_model.model_class()._meta.fields
|
|
243
237
|
)
|
|
244
|
-
source_fields = self.instance.transform_map.source_model
|
|
245
238
|
self.fields["target_field"].widget.choices = add_blank_choice(
|
|
246
239
|
[
|
|
247
240
|
(f.name, f.verbose_name)
|
|
@@ -251,7 +244,9 @@ class IPFabricTransformFieldForm(NetBoxModelForm):
|
|
|
251
244
|
)
|
|
252
245
|
self.fields["target_field"].widget.initial = self.instance.target_field
|
|
253
246
|
self.fields["source_field"].widget.choices = add_blank_choice(
|
|
254
|
-
source_column_choices(
|
|
247
|
+
source_column_choices(
|
|
248
|
+
self.instance.transform_map.source_endpoint.endpoint
|
|
249
|
+
)
|
|
255
250
|
)
|
|
256
251
|
else:
|
|
257
252
|
if kwargs.get("initial", {}).get("transform_map", None):
|
|
@@ -269,7 +264,7 @@ class IPFabricTransformFieldForm(NetBoxModelForm):
|
|
|
269
264
|
choices
|
|
270
265
|
)
|
|
271
266
|
self.fields["source_field"].widget.choices = add_blank_choice(
|
|
272
|
-
source_column_choices(transform_map.
|
|
267
|
+
source_column_choices(transform_map.source_endpoint.endpoint)
|
|
273
268
|
)
|
|
274
269
|
|
|
275
270
|
|
|
@@ -294,13 +289,68 @@ class IPFabricTransformMapGroupBulkImportForm(NetBoxModelImportForm):
|
|
|
294
289
|
|
|
295
290
|
|
|
296
291
|
class IPFabricTransformMapForm(NetBoxModelForm):
|
|
292
|
+
parents = DynamicModelMultipleChoiceField(
|
|
293
|
+
queryset=IPFabricTransformMap.objects.all(),
|
|
294
|
+
required=False,
|
|
295
|
+
label=_("Parents"),
|
|
296
|
+
help_text=_(
|
|
297
|
+
"Parent transform maps that must be processed before this one. "
|
|
298
|
+
"This transform map will only be processed after ALL selected parents complete. "
|
|
299
|
+
"Example: IP Address requires both Interface and VRF as parents."
|
|
300
|
+
),
|
|
301
|
+
)
|
|
302
|
+
|
|
297
303
|
class Meta:
|
|
298
304
|
model = IPFabricTransformMap
|
|
299
|
-
fields = ("name", "group", "
|
|
305
|
+
fields = ("name", "group", "source_endpoint", "target_model", "parents")
|
|
300
306
|
widgets = {
|
|
301
307
|
"target_model": HTMXSelect(hx_url="/plugins/ipfabric/transform-map/add"),
|
|
302
308
|
}
|
|
303
309
|
|
|
310
|
+
def __init__(self, *args, **kwargs):
|
|
311
|
+
super().__init__(*args, **kwargs)
|
|
312
|
+
|
|
313
|
+
if self.instance and self.instance.pk:
|
|
314
|
+
self.fields["parents"].widget.add_query_param("id__n", self.instance.pk)
|
|
315
|
+
self.fields["parents"].queryset = self.fields["parents"].queryset.exclude(
|
|
316
|
+
pk=self.instance.pk
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
def clean(self):
|
|
320
|
+
super().clean()
|
|
321
|
+
|
|
322
|
+
# Validate circular dependencies with the new parent selection
|
|
323
|
+
if self.instance and self.instance.pk:
|
|
324
|
+
new_parents = self.cleaned_data.get("parents", [])
|
|
325
|
+
if new_parents:
|
|
326
|
+
self._validate_no_circular_dependency_with_parents(new_parents)
|
|
327
|
+
|
|
328
|
+
def _validate_no_circular_dependency_with_parents(self, new_parents):
|
|
329
|
+
"""
|
|
330
|
+
Check if adding these parents would create a circular dependency.
|
|
331
|
+
Uses DFS to detect cycles considering the new parent set.
|
|
332
|
+
"""
|
|
333
|
+
|
|
334
|
+
def get_parents(node_id: int, parent_override: list | None):
|
|
335
|
+
"""Get parents for a node, with optional override for the instance being modified."""
|
|
336
|
+
if node_id == self.instance.pk and parent_override is not None:
|
|
337
|
+
# Use the new parents for the current node
|
|
338
|
+
return parent_override
|
|
339
|
+
else:
|
|
340
|
+
# Use existing parents for other nodes
|
|
341
|
+
node = IPFabricTransformMap.objects.get(pk=node_id)
|
|
342
|
+
return node.parents.all()
|
|
343
|
+
|
|
344
|
+
if has_cycle_dfs(self.instance.pk, get_parents, parent_override=new_parents):
|
|
345
|
+
raise forms.ValidationError(
|
|
346
|
+
{
|
|
347
|
+
"parents": _(
|
|
348
|
+
"The selected parents create a circular dependency. "
|
|
349
|
+
"A transform map cannot be an ancestor of itself."
|
|
350
|
+
)
|
|
351
|
+
}
|
|
352
|
+
)
|
|
353
|
+
|
|
304
354
|
|
|
305
355
|
class IPFabricTransformMapBulkEditForm(NetBoxModelBulkEditForm):
|
|
306
356
|
group = forms.ModelChoiceField(
|
|
@@ -308,16 +358,23 @@ class IPFabricTransformMapBulkEditForm(NetBoxModelBulkEditForm):
|
|
|
308
358
|
required=False,
|
|
309
359
|
label=_("Target Group"),
|
|
310
360
|
)
|
|
361
|
+
parents = forms.ModelMultipleChoiceField(
|
|
362
|
+
queryset=IPFabricTransformMap.objects.all(),
|
|
363
|
+
required=False,
|
|
364
|
+
label=_("Parents"),
|
|
365
|
+
)
|
|
366
|
+
|
|
311
367
|
model = IPFabricTransformMap
|
|
312
|
-
fields = ("group",)
|
|
313
|
-
nullable_fields = ("group",)
|
|
368
|
+
fields = ("group", "parents")
|
|
369
|
+
nullable_fields = ("group", "parents")
|
|
314
370
|
|
|
315
371
|
|
|
316
372
|
class IPFabricTransformMapBulkImportForm(NetBoxModelImportForm):
|
|
317
|
-
|
|
318
|
-
label=_("
|
|
319
|
-
|
|
320
|
-
|
|
373
|
+
source_endpoint = CSVModelChoiceField(
|
|
374
|
+
label=_("Endpoints"),
|
|
375
|
+
queryset=IPFabricEndpoint.objects.all(),
|
|
376
|
+
required=True,
|
|
377
|
+
to_field_name="name",
|
|
321
378
|
)
|
|
322
379
|
target_model = CSVContentTypeField(
|
|
323
380
|
queryset=ContentType.objects.filter(IPFabricSupportedSyncModels),
|
|
@@ -337,7 +394,7 @@ class IPFabricTransformMapBulkImportForm(NetBoxModelImportForm):
|
|
|
337
394
|
|
|
338
395
|
class Meta:
|
|
339
396
|
model = IPFabricTransformMap
|
|
340
|
-
fields = ("name", "
|
|
397
|
+
fields = ("name", "source_endpoint", "target_model", "group", "parents")
|
|
341
398
|
|
|
342
399
|
|
|
343
400
|
class IPFabricTransformMapCloneForm(forms.Form):
|
|
@@ -398,7 +455,7 @@ class IPFabricIngestionFilterForm(SavedFiltersMixin, FilterForm):
|
|
|
398
455
|
)
|
|
399
456
|
model = IPFabricIngestion
|
|
400
457
|
sync_id = DynamicModelMultipleChoiceField(
|
|
401
|
-
queryset=IPFabricSync.objects.all(), required=False, label=_("
|
|
458
|
+
queryset=IPFabricSync.objects.all(), required=False, label=_("Syncs")
|
|
402
459
|
)
|
|
403
460
|
|
|
404
461
|
|
|
@@ -573,6 +630,12 @@ class IPFabricSyncForm(NetBoxModelForm):
|
|
|
573
630
|
),
|
|
574
631
|
)
|
|
575
632
|
|
|
633
|
+
filters = forms.ModelMultipleChoiceField(
|
|
634
|
+
queryset=IPFabricFilter.objects.all(),
|
|
635
|
+
required=False,
|
|
636
|
+
widget=forms.SelectMultiple(),
|
|
637
|
+
)
|
|
638
|
+
|
|
576
639
|
update_custom_fields = forms.BooleanField(
|
|
577
640
|
required=False,
|
|
578
641
|
label=_("Custom Fields Updating"),
|
|
@@ -607,6 +670,7 @@ class IPFabricSyncForm(NetBoxModelForm):
|
|
|
607
670
|
"auto_merge",
|
|
608
671
|
"update_custom_fields",
|
|
609
672
|
"sites",
|
|
673
|
+
"filters",
|
|
610
674
|
"tags",
|
|
611
675
|
"scheduled",
|
|
612
676
|
"interval",
|
|
@@ -620,11 +684,8 @@ class IPFabricSyncForm(NetBoxModelForm):
|
|
|
620
684
|
for name, value in initial.items():
|
|
621
685
|
if (
|
|
622
686
|
(
|
|
623
|
-
name.
|
|
624
|
-
|
|
625
|
-
name in self.base_fields
|
|
626
|
-
and isinstance(self.base_fields[name], forms.BooleanField)
|
|
627
|
-
)
|
|
687
|
+
name in self.base_fields
|
|
688
|
+
and isinstance(self.base_fields[name], forms.BooleanField)
|
|
628
689
|
)
|
|
629
690
|
and isinstance(value, list)
|
|
630
691
|
and len(value) > 1
|
|
@@ -688,34 +749,66 @@ class IPFabricSyncForm(NetBoxModelForm):
|
|
|
688
749
|
else:
|
|
689
750
|
self.fields["sites"].initial = self.initial["sites"]
|
|
690
751
|
|
|
752
|
+
if "filters" not in self.initial:
|
|
753
|
+
self.initial["filters"] = self.instance.filters.all()
|
|
754
|
+
else:
|
|
755
|
+
# For new instances, populate default filters (filters with names starting with "Default ")
|
|
756
|
+
if "filters" not in self.initial or not self.initial.get("filters"):
|
|
757
|
+
self.initial["filters"] = IPFabricFilter.objects.filter(
|
|
758
|
+
name__istartswith="Default "
|
|
759
|
+
)
|
|
760
|
+
|
|
691
761
|
now = local_now().strftime("%Y-%m-%d %H:%M:%S")
|
|
692
762
|
self.fields["scheduled"].help_text += f" (current time: <strong>{now}</strong>)"
|
|
693
763
|
|
|
694
764
|
# Add backend-specific form fields
|
|
695
765
|
self.backend_fields = {}
|
|
766
|
+
for transform_map in IPFabricSync.get_transform_maps():
|
|
767
|
+
field = transform_map.target_model
|
|
768
|
+
if field.app_label not in self.backend_fields:
|
|
769
|
+
self.backend_fields[field.app_label] = []
|
|
770
|
+
self.backend_fields[field.app_label].append(
|
|
771
|
+
f"{field.app_label}.{field.model}"
|
|
772
|
+
)
|
|
696
773
|
|
|
697
|
-
# Prepare buttons for each target Model
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
for
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
774
|
+
# Prepare buttons for each target Model, order according to model hierarchy
|
|
775
|
+
hierarchy = [
|
|
776
|
+
f"{m.app_label}.{m.model}"
|
|
777
|
+
for m in IPFabricSync.get_model_hierarchy(
|
|
778
|
+
group_ids=self.initial.get("groups", [])
|
|
779
|
+
)
|
|
780
|
+
]
|
|
781
|
+
for k, v in self.backend_fields.items():
|
|
782
|
+
self.backend_fields[k] = [
|
|
783
|
+
f for f in hierarchy if f in self.backend_fields[k]
|
|
784
|
+
]
|
|
785
|
+
# Now that it's sorted, we can add those fields to have them in correct order
|
|
786
|
+
for field in self.backend_fields[k]:
|
|
787
|
+
self.fields[field] = forms.BooleanField(
|
|
788
|
+
required=False,
|
|
789
|
+
label=field.split(".", maxsplit=1).pop(),
|
|
790
|
+
initial=True,
|
|
791
|
+
)
|
|
704
792
|
if self.instance and self.instance.parameters:
|
|
705
|
-
value = self.instance.parameters.get(
|
|
706
|
-
self.fields[
|
|
793
|
+
value = self.instance.parameters.get(field)
|
|
794
|
+
self.fields[field].initial = value
|
|
707
795
|
|
|
708
796
|
# Set fieldsets dynamically based and backend_fields
|
|
709
797
|
fieldsets = [
|
|
710
798
|
FieldSet("name", "source", "groups", name=_("IP Fabric Source")),
|
|
711
799
|
]
|
|
712
|
-
# Only show snapshot and
|
|
800
|
+
# Only show snapshot, sites and filters if source is selected
|
|
713
801
|
if source:
|
|
714
802
|
if isinstance(source, str) or isinstance(source, int):
|
|
715
803
|
source = IPFabricSource.objects.get(pk=source)
|
|
716
804
|
if source.type == "local":
|
|
717
805
|
fieldsets.append(
|
|
718
|
-
FieldSet(
|
|
806
|
+
FieldSet(
|
|
807
|
+
"snapshot_data",
|
|
808
|
+
"sites",
|
|
809
|
+
"filters",
|
|
810
|
+
name=_("Snapshot Information"),
|
|
811
|
+
),
|
|
719
812
|
)
|
|
720
813
|
else:
|
|
721
814
|
fieldsets.append(
|
|
@@ -749,6 +842,10 @@ class IPFabricSyncForm(NetBoxModelForm):
|
|
|
749
842
|
{"snapshot_data": _("Snapshot does not belong to the selected source.")}
|
|
750
843
|
)
|
|
751
844
|
|
|
845
|
+
scheduled_time = self.cleaned_data.get("scheduled")
|
|
846
|
+
if scheduled_time and scheduled_time < local_now():
|
|
847
|
+
raise forms.ValidationError(_("Scheduled time must be in the future."))
|
|
848
|
+
|
|
752
849
|
sites = self.data.get("sites")
|
|
753
850
|
self.fields["sites"].choices = list_to_choices(str_to_list(sites))
|
|
754
851
|
if sites and "snapshot_data" in self.cleaned_data:
|
|
@@ -768,10 +865,6 @@ class IPFabricSyncForm(NetBoxModelForm):
|
|
|
768
865
|
{"sites": _(f"Sites {invalid_sites} not part of the snapshot.")}
|
|
769
866
|
)
|
|
770
867
|
|
|
771
|
-
scheduled_time = self.cleaned_data.get("scheduled")
|
|
772
|
-
if scheduled_time and scheduled_time < local_now():
|
|
773
|
-
raise forms.ValidationError(_("Scheduled time must be in the future."))
|
|
774
|
-
|
|
775
868
|
# When interval is used without schedule at, schedule for the current time
|
|
776
869
|
if self.cleaned_data.get("interval") and not scheduled_time:
|
|
777
870
|
self.cleaned_data["scheduled"] = local_now()
|
|
@@ -796,16 +889,19 @@ class IPFabricSyncForm(NetBoxModelForm):
|
|
|
796
889
|
|
|
797
890
|
def save(self, *args, **kwargs):
|
|
798
891
|
parameters = {}
|
|
892
|
+
backend_fields_values = {
|
|
893
|
+
item for lst in self.backend_fields.values() for item in lst
|
|
894
|
+
}
|
|
799
895
|
for name in self.fields:
|
|
800
|
-
if name
|
|
801
|
-
parameters[name
|
|
896
|
+
if name in backend_fields_values:
|
|
897
|
+
parameters[name] = self.cleaned_data[name]
|
|
802
898
|
if name == "sites":
|
|
803
899
|
parameters["sites"] = self.cleaned_data["sites"]
|
|
804
900
|
if name == "groups":
|
|
805
901
|
parameters["groups"] = [
|
|
806
902
|
group.pk for group in self.cleaned_data["groups"]
|
|
807
903
|
]
|
|
808
|
-
self.instance.parameters = parameters
|
|
904
|
+
self.instance.parameters = dict(sorted(parameters.items()))
|
|
809
905
|
self.instance.status = IPFabricSyncStatusChoices.NEW
|
|
810
906
|
return super().save(*args, **kwargs)
|
|
811
907
|
|
|
@@ -869,6 +965,155 @@ class IPFabricSyncBulkEditForm(NetBoxModelBulkEditForm):
|
|
|
869
965
|
)
|
|
870
966
|
|
|
871
967
|
|
|
968
|
+
class IPFabricFilterForm(NetBoxModelForm):
|
|
969
|
+
endpoints = DynamicModelMultipleChoiceField(
|
|
970
|
+
label=_("Endpoints"),
|
|
971
|
+
queryset=IPFabricEndpoint.objects.all(),
|
|
972
|
+
required=False,
|
|
973
|
+
widget=APISelectMultiple(
|
|
974
|
+
api_url="/api/plugins/ipfabric/endpoint/",
|
|
975
|
+
),
|
|
976
|
+
)
|
|
977
|
+
filter_type = CSVChoiceField(
|
|
978
|
+
label=_("Filter Type"),
|
|
979
|
+
choices=IPFabricFilterTypeChoices,
|
|
980
|
+
help_text=_(
|
|
981
|
+
"Top-level merging of filter, where this will be used along other filters."
|
|
982
|
+
),
|
|
983
|
+
)
|
|
984
|
+
expressions = DynamicModelMultipleChoiceField(
|
|
985
|
+
queryset=IPFabricFilterExpression.objects.all(),
|
|
986
|
+
label=_("Filter Expressions"),
|
|
987
|
+
required=False,
|
|
988
|
+
widget=APISelectMultiple(
|
|
989
|
+
api_url="/api/plugins/ipfabric/filter-expression/",
|
|
990
|
+
),
|
|
991
|
+
)
|
|
992
|
+
|
|
993
|
+
class Meta:
|
|
994
|
+
model = IPFabricFilter
|
|
995
|
+
fields = (
|
|
996
|
+
"name",
|
|
997
|
+
"description",
|
|
998
|
+
"endpoints",
|
|
999
|
+
"filter_type",
|
|
1000
|
+
"syncs",
|
|
1001
|
+
"expressions",
|
|
1002
|
+
)
|
|
1003
|
+
widgets = {
|
|
1004
|
+
"endpoints": forms.SelectMultiple(),
|
|
1005
|
+
"filter_type": forms.Select(),
|
|
1006
|
+
"syncs": forms.SelectMultiple(),
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
def __init__(self, data=None, instance=None, *args, **kwargs):
|
|
1010
|
+
super().__init__(data=data, instance=instance, *args, **kwargs)
|
|
1011
|
+
|
|
1012
|
+
if self.instance and self.instance.pk is not None:
|
|
1013
|
+
self.fields[
|
|
1014
|
+
"expressions"
|
|
1015
|
+
].initial = self.instance.expressions.all().values_list("id", flat=True)
|
|
1016
|
+
|
|
1017
|
+
def save(self, *args, **kwargs):
|
|
1018
|
+
instance = super().save(*args, **kwargs)
|
|
1019
|
+
instance.expressions.set(self.cleaned_data["expressions"])
|
|
1020
|
+
return instance
|
|
1021
|
+
|
|
1022
|
+
|
|
1023
|
+
class IPFabricFilterBulkEditForm(NetBoxModelBulkEditForm):
|
|
1024
|
+
endpoints = DynamicModelMultipleChoiceField(
|
|
1025
|
+
queryset=IPFabricEndpoint.objects.all(),
|
|
1026
|
+
required=False,
|
|
1027
|
+
label=_("Endpoints"),
|
|
1028
|
+
)
|
|
1029
|
+
filter_type = forms.ChoiceField(
|
|
1030
|
+
choices=add_blank_choice(IPFabricFilterTypeChoices),
|
|
1031
|
+
required=False,
|
|
1032
|
+
label=_("Filter Type"),
|
|
1033
|
+
)
|
|
1034
|
+
syncs = DynamicModelMultipleChoiceField(
|
|
1035
|
+
queryset=IPFabricSync.objects.all(),
|
|
1036
|
+
required=False,
|
|
1037
|
+
label=_("Syncs"),
|
|
1038
|
+
)
|
|
1039
|
+
|
|
1040
|
+
model = IPFabricFilter
|
|
1041
|
+
fields = ("endpoints", "filter_type", "syncs")
|
|
1042
|
+
nullable_fields = ("syncs", "endpoints")
|
|
1043
|
+
|
|
1044
|
+
|
|
1045
|
+
class IPFabricFilterBulkImportForm(NetBoxModelImportForm):
|
|
1046
|
+
class Meta:
|
|
1047
|
+
model = IPFabricFilter
|
|
1048
|
+
fields = ("name", "description", "endpoints", "filter_type", "syncs")
|
|
1049
|
+
|
|
1050
|
+
|
|
1051
|
+
class IPFabricFilterExpressionForm(NetBoxModelForm):
|
|
1052
|
+
filters = forms.ModelMultipleChoiceField(
|
|
1053
|
+
queryset=IPFabricFilter.objects.all(),
|
|
1054
|
+
required=False,
|
|
1055
|
+
widget=forms.SelectMultiple(),
|
|
1056
|
+
)
|
|
1057
|
+
|
|
1058
|
+
# Test expression fields - for testing the filter against IP Fabric API
|
|
1059
|
+
test_source = DynamicModelChoiceField(
|
|
1060
|
+
queryset=IPFabricSource.objects.filter(type=IPFabricSourceTypeChoices.LOCAL),
|
|
1061
|
+
required=False,
|
|
1062
|
+
label=_("Test Source"),
|
|
1063
|
+
help_text=_(
|
|
1064
|
+
"IP Fabric source to test against. Auto-detected from associated filters if available."
|
|
1065
|
+
),
|
|
1066
|
+
)
|
|
1067
|
+
test_endpoint = DynamicModelChoiceField(
|
|
1068
|
+
queryset=IPFabricEndpoint.objects.all(),
|
|
1069
|
+
required=False,
|
|
1070
|
+
label=_("Test Endpoint"),
|
|
1071
|
+
help_text=_(
|
|
1072
|
+
"Endpoint to query. Auto-detected from associated filters if available."
|
|
1073
|
+
),
|
|
1074
|
+
)
|
|
1075
|
+
|
|
1076
|
+
fieldsets = (
|
|
1077
|
+
FieldSet("name", "description", name=_("Filter Expression")),
|
|
1078
|
+
FieldSet("filters", "expression", name=_("Configuration")),
|
|
1079
|
+
FieldSet("test_source", "test_endpoint", name=_("Test Expression")),
|
|
1080
|
+
)
|
|
1081
|
+
|
|
1082
|
+
class Meta:
|
|
1083
|
+
model = IPFabricFilterExpression
|
|
1084
|
+
fields = ("name", "description", "filters", "expression")
|
|
1085
|
+
widgets = {
|
|
1086
|
+
"filters": forms.SelectMultiple(),
|
|
1087
|
+
"expression": forms.Textarea(attrs={"class": "font-monospace"}),
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
def __init__(self, *args, **kwargs):
|
|
1091
|
+
super().__init__(*args, **kwargs)
|
|
1092
|
+
|
|
1093
|
+
# Auto-detect test parameters from associated filters if editing existing expression
|
|
1094
|
+
if self.instance and self.instance.pk:
|
|
1095
|
+
# Get unique sources and endpoints from associated filters
|
|
1096
|
+
sources, endpoints = get_filter_expression_test_candidates(self.instance)
|
|
1097
|
+
|
|
1098
|
+
# Set smart defaults if only one option exists
|
|
1099
|
+
if sources and len(sources) == 1:
|
|
1100
|
+
self.fields["test_source"].initial = list(sources)[0]
|
|
1101
|
+
if endpoints and len(endpoints) == 1:
|
|
1102
|
+
self.fields["test_endpoint"].initial = list(endpoints)[0]
|
|
1103
|
+
|
|
1104
|
+
|
|
1105
|
+
class IPFabricFilterExpressionBulkEditForm(NetBoxModelBulkEditForm):
|
|
1106
|
+
model = IPFabricFilterExpression
|
|
1107
|
+
fields = ("description", "filters")
|
|
1108
|
+
nullable_fields = ("description", "filters")
|
|
1109
|
+
|
|
1110
|
+
|
|
1111
|
+
class IPFabricFilterExpressionBulkImportForm(NetBoxModelImportForm):
|
|
1112
|
+
class Meta:
|
|
1113
|
+
model = IPFabricFilterExpression
|
|
1114
|
+
fields = ("name", "description", "expression")
|
|
1115
|
+
|
|
1116
|
+
|
|
872
1117
|
tableChoices = [
|
|
873
1118
|
("eol_details", "Inventory - EOL_DETAILS"),
|
|
874
1119
|
("fans", "Inventory - FANS"),
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
from .schema import IPFabricDataQuery
|
|
2
|
+
from .schema import IPFabricEndpointQuery
|
|
3
|
+
from .schema import IPFabricFilterExpressionQuery
|
|
4
|
+
from .schema import IPFabricFilterQuery
|
|
2
5
|
from .schema import IPFabricIngestionIssueQuery
|
|
3
6
|
from .schema import IPFabricIngestionQuery
|
|
4
7
|
from .schema import IPFabricRelationshipFieldQuery
|
|
@@ -20,4 +23,7 @@ schema = [
|
|
|
20
23
|
IPFabricIngestionQuery,
|
|
21
24
|
IPFabricIngestionIssueQuery,
|
|
22
25
|
IPFabricDataQuery,
|
|
26
|
+
IPFabricEndpointQuery,
|
|
27
|
+
IPFabricFilterExpressionQuery,
|
|
28
|
+
IPFabricFilterQuery,
|
|
23
29
|
]
|
ipfabric_netbox/graphql/enums.py
CHANGED
|
@@ -2,22 +2,22 @@ import strawberry
|
|
|
2
2
|
from core.choices import JobStatusChoices
|
|
3
3
|
from netbox_branching.choices import BranchStatusChoices
|
|
4
4
|
|
|
5
|
+
from ipfabric_netbox.choices import IPFabricFilterTypeChoices
|
|
5
6
|
from ipfabric_netbox.choices import IPFabricRawDataTypeChoices
|
|
6
7
|
from ipfabric_netbox.choices import IPFabricSnapshotStatusModelChoices
|
|
7
8
|
from ipfabric_netbox.choices import IPFabricSourceStatusChoices
|
|
8
9
|
from ipfabric_netbox.choices import IPFabricSourceTypeChoices
|
|
9
10
|
from ipfabric_netbox.choices import IPFabricSyncStatusChoices
|
|
10
|
-
from ipfabric_netbox.choices import IPFabricTransformMapSourceModelChoices
|
|
11
11
|
|
|
12
12
|
__all__ = (
|
|
13
13
|
"IPFabricSourceStatusEnum",
|
|
14
14
|
"IPFabricSyncStatusEnum",
|
|
15
|
-
"IPFabricTransformMapSourceModelEnum",
|
|
16
15
|
"IPFabricSourceTypeEnum",
|
|
17
16
|
"IPFabricSnapshotStatusModelEnum",
|
|
18
17
|
"IPFabricRawDataTypeEnum",
|
|
19
18
|
"BranchStatusEnum",
|
|
20
19
|
"JobStatusEnum",
|
|
20
|
+
"IPFabricFilterTypeEnum",
|
|
21
21
|
)
|
|
22
22
|
|
|
23
23
|
IPFabricSourceStatusEnum = strawberry.enum(
|
|
@@ -26,9 +26,6 @@ IPFabricSourceStatusEnum = strawberry.enum(
|
|
|
26
26
|
IPFabricSyncStatusEnum = strawberry.enum(
|
|
27
27
|
IPFabricSyncStatusChoices.as_enum(prefix="type")
|
|
28
28
|
)
|
|
29
|
-
IPFabricTransformMapSourceModelEnum = strawberry.enum(
|
|
30
|
-
IPFabricTransformMapSourceModelChoices.as_enum(prefix="type")
|
|
31
|
-
)
|
|
32
29
|
IPFabricSourceTypeEnum = strawberry.enum(
|
|
33
30
|
IPFabricSourceTypeChoices.as_enum(prefix="type")
|
|
34
31
|
)
|
|
@@ -40,3 +37,6 @@ IPFabricRawDataTypeEnum = strawberry.enum(
|
|
|
40
37
|
)
|
|
41
38
|
BranchStatusEnum = strawberry.enum(BranchStatusChoices.as_enum(prefix="type"))
|
|
42
39
|
JobStatusEnum = strawberry.enum(JobStatusChoices.as_enum(prefix="type"))
|
|
40
|
+
IPFabricFilterTypeEnum = strawberry.enum(
|
|
41
|
+
IPFabricFilterTypeChoices.as_enum(prefix="type")
|
|
42
|
+
)
|