ipfabric_netbox 4.1.0b4__tar.gz → 4.1.0b5__tar.gz
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-4.1.0b4 → ipfabric_netbox-4.1.0b5}/PKG-INFO +1 -1
- {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/__init__.py +1 -1
- {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/api/nested_serializers.py +0 -1
- {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/api/serializers.py +23 -1
- {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/api/urls.py +2 -0
- {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/api/views.py +7 -0
- ipfabric_netbox-4.1.0b5/ipfabric_netbox/exceptions.py +81 -0
- {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/filtersets.py +30 -0
- ipfabric_netbox-4.1.0b5/ipfabric_netbox/migrations/0015_ipfabricingestionissue.py +48 -0
- {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/models.py +25 -0
- {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/tables.py +23 -0
- {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/templates/ipfabric_netbox/ipfabricingestion.html +18 -47
- {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/templates/ipfabric_netbox/partials/ingestion_all.html +6 -1
- ipfabric_netbox-4.1.0b5/ipfabric_netbox/templates/ipfabric_netbox/partials/ingestion_statistics.html +37 -0
- ipfabric_netbox-4.1.0b5/ipfabric_netbox/templates/ipfabric_netbox/partials/ingestion_status.html +1 -0
- ipfabric_netbox-4.1.0b5/ipfabric_netbox/templates/ipfabric_netbox/partials/object_tabs.html +12 -0
- {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/utilities/ipfutils.py +61 -26
- {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/views.py +55 -30
- {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b5}/pyproject.toml +1 -1
- ipfabric_netbox-4.1.0b4/ipfabric_netbox/exceptions.py +0 -24
- ipfabric_netbox-4.1.0b4/ipfabric_netbox/templates/ipfabric_netbox/partials/ingestion_status.html +0 -1
- {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b5}/README.md +0 -0
- {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/api/__init__.py +0 -0
- {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/choices.py +0 -0
- {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/data/transform_map.json +0 -0
- {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/forms.py +0 -0
- {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/jobs.py +0 -0
- {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/migrations/0001_initial.py +0 -0
- {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/migrations/0001_initial_squashed_0013_switch_to_branching_plugin.py +0 -0
- {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/migrations/0002_ipfabricsnapshot_status.py +0 -0
- {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/migrations/0003_ipfabricsource_type_and_more.py +0 -0
- {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/migrations/0004_ipfabricsync_auto_merge.py +0 -0
- {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/migrations/0005_alter_ipfabricrelationshipfield_source_model_and_more.py +0 -0
- {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/migrations/0006_alter_ipfabrictransformmap_target_model.py +0 -0
- {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/migrations/0007_prepare_custom_fields.py +0 -0
- {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/migrations/0008_prepare_transform_maps.py +0 -0
- {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/migrations/0009_transformmap_changes_for_netbox_v4_2.py +0 -0
- {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/migrations/0010_remove_uuid_from_get_or_create.py +0 -0
- {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/migrations/0011_update_part_number_DCIM_inventory_item_template.py +0 -0
- {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/migrations/0012_remove_status_field.py +0 -0
- {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/migrations/0013_switch_to_branching_plugin.py +0 -0
- {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/migrations/0014_ipfabrictransformmapgroup_ipfabrictransformmap_group.py +0 -0
- {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/migrations/__init__.py +0 -0
- {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/navigation.py +0 -0
- {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/signals.py +0 -0
- {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/template_content.py +0 -0
- {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/templates/ipfabric_netbox/inc/clone_form.html +0 -0
- {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/templates/ipfabric_netbox/inc/diff.html +0 -0
- {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/templates/ipfabric_netbox/inc/json.html +0 -0
- {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/templates/ipfabric_netbox/inc/logs_pending.html +0 -0
- {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/templates/ipfabric_netbox/inc/merge_form.html +0 -0
- {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/templates/ipfabric_netbox/inc/site_topology_button.html +0 -0
- {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/templates/ipfabric_netbox/inc/site_topology_modal.html +0 -0
- {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/templates/ipfabric_netbox/inc/snapshotdata.html +0 -0
- {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/templates/ipfabric_netbox/inc/transform_map_field_map.html +0 -0
- {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/templates/ipfabric_netbox/inc/transform_map_relationship_map.html +0 -0
- {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/templates/ipfabric_netbox/ipfabric_table.html +0 -0
- {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/templates/ipfabric_netbox/ipfabricsnapshot.html +0 -0
- {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/templates/ipfabric_netbox/ipfabricsource.html +0 -0
- {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/templates/ipfabric_netbox/ipfabricsync.html +0 -0
- {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/templates/ipfabric_netbox/ipfabricsync_list.html +0 -0
- {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/templates/ipfabric_netbox/ipfabrictransformmap.html +0 -0
- {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/templates/ipfabric_netbox/ipfabrictransformmap_list.html +0 -0
- {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/templates/ipfabric_netbox/ipfabrictransformmap_restore.html +0 -0
- {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/templates/ipfabric_netbox/ipfabrictransformmapgroup.html +0 -0
- {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/templates/ipfabric_netbox/partials/ingestion_progress.html +0 -0
- {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/templates/ipfabric_netbox/partials/job_logs.html +0 -0
- {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/templates/ipfabric_netbox/partials/sync_last_ingestion.html +0 -0
- {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/templates/static/ipfabric_netbox/css/rack.css +0 -0
- {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/templatetags/__init__.py +0 -0
- {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/templatetags/ipfabric_netbox_helpers.py +0 -0
- {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/tests/__init__.py +0 -0
- {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/tests/test_models.py +0 -0
- {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/urls.py +0 -0
- {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/utilities/__init__.py +0 -0
- {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/utilities/logging.py +0 -0
- {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/utilities/nbutils.py +0 -0
- {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/utilities/transform_map.py +0 -0
{ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/api/nested_serializers.py
RENAMED
|
@@ -10,7 +10,6 @@ from ipfabric_netbox.models import IPFabricSource
|
|
|
10
10
|
from ipfabric_netbox.models import IPFabricSync
|
|
11
11
|
from ipfabric_netbox.models import IPFabricTransformMap
|
|
12
12
|
|
|
13
|
-
# from .serializers import IPFabricSyncSerializer
|
|
14
13
|
|
|
15
14
|
__all__ = (
|
|
16
15
|
"NestedIPFabricSourceSerializer",
|
|
@@ -7,8 +7,10 @@ from rest_framework import serializers
|
|
|
7
7
|
|
|
8
8
|
from .nested_serializers import NestedIPFabricSnapshotSerializer
|
|
9
9
|
from .nested_serializers import NestedIPFabricSourceSerializer
|
|
10
|
+
from .nested_serializers import NestedIPFabricSyncSerializer
|
|
10
11
|
from .nested_serializers import NestedIPFabricTransformMapSerializer
|
|
11
12
|
from ipfabric_netbox.models import IPFabricIngestion
|
|
13
|
+
from ipfabric_netbox.models import IPFabricIngestionIssue
|
|
12
14
|
from ipfabric_netbox.models import IPFabricRelationshipField
|
|
13
15
|
from ipfabric_netbox.models import IPFabricSnapshot
|
|
14
16
|
from ipfabric_netbox.models import IPFabricSource
|
|
@@ -23,7 +25,9 @@ __all__ = (
|
|
|
23
25
|
"IPFabricRelationshipFieldSerializer",
|
|
24
26
|
"IPFabricTransformFieldSerializer",
|
|
25
27
|
"IPFabricTransformMapSerializer",
|
|
28
|
+
"IPFabricTransformMapGroupSerializer",
|
|
26
29
|
"IPFabricIngestionSerializer",
|
|
30
|
+
"IPFabricIngestionIssueSerializer",
|
|
27
31
|
"IPFabricSourceSerializer",
|
|
28
32
|
)
|
|
29
33
|
|
|
@@ -133,7 +137,7 @@ class IPFabricTransformFieldSerializer(NetBoxModelSerializer):
|
|
|
133
137
|
|
|
134
138
|
class IPFabricIngestionSerializer(NetBoxModelSerializer):
|
|
135
139
|
branch = BranchSerializer(read_only=True)
|
|
136
|
-
sync =
|
|
140
|
+
sync = NestedIPFabricSyncSerializer(read_only=True)
|
|
137
141
|
|
|
138
142
|
class Meta:
|
|
139
143
|
model = IPFabricIngestion
|
|
@@ -145,6 +149,24 @@ class IPFabricIngestionSerializer(NetBoxModelSerializer):
|
|
|
145
149
|
]
|
|
146
150
|
|
|
147
151
|
|
|
152
|
+
class IPFabricIngestionIssueSerializer(NetBoxModelSerializer):
|
|
153
|
+
ingestion = IPFabricIngestionSerializer(read_only=True)
|
|
154
|
+
|
|
155
|
+
class Meta:
|
|
156
|
+
model = IPFabricIngestionIssue
|
|
157
|
+
fields = [
|
|
158
|
+
"id",
|
|
159
|
+
"ingestion",
|
|
160
|
+
"timestamp",
|
|
161
|
+
"model",
|
|
162
|
+
"message",
|
|
163
|
+
"raw_data",
|
|
164
|
+
"coalesce_fields",
|
|
165
|
+
"defaults",
|
|
166
|
+
"exception",
|
|
167
|
+
]
|
|
168
|
+
|
|
169
|
+
|
|
148
170
|
class IPFabricSourceSerializer(NetBoxModelSerializer):
|
|
149
171
|
status = ChoiceField(choices=DataSourceStatusChoices)
|
|
150
172
|
url = serializers.URLField()
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# api/urls.py
|
|
2
2
|
from netbox.api.routers import NetBoxRouter
|
|
3
3
|
|
|
4
|
+
from ipfabric_netbox.api.views import IPFabricIngestionIssueViewSet
|
|
4
5
|
from ipfabric_netbox.api.views import IPFabricIngestionViewSet
|
|
5
6
|
from ipfabric_netbox.api.views import IPFabricRelationshipFieldiewSet
|
|
6
7
|
from ipfabric_netbox.api.views import IPFabricSnapshotViewSet
|
|
@@ -18,6 +19,7 @@ router.register("transform-map-group", IPFabricTransformMapGroupViewSet)
|
|
|
18
19
|
router.register("transform-map", IPFabricTransformMapViewSet)
|
|
19
20
|
router.register("sync", IPFabricSyncViewSet)
|
|
20
21
|
router.register("ingestion", IPFabricIngestionViewSet)
|
|
22
|
+
router.register("ingestion-issues", IPFabricIngestionIssueViewSet)
|
|
21
23
|
router.register("transform-field", IPFabricTransformFieldiewSet)
|
|
22
24
|
router.register("relationship-field", IPFabricRelationshipFieldiewSet)
|
|
23
25
|
urlpatterns = router.urls
|
|
@@ -5,6 +5,7 @@ from rest_framework.decorators import action
|
|
|
5
5
|
from rest_framework.response import Response
|
|
6
6
|
from utilities.query import count_related
|
|
7
7
|
|
|
8
|
+
from .serializers import IPFabricIngestionIssueSerializer
|
|
8
9
|
from .serializers import IPFabricIngestionSerializer
|
|
9
10
|
from .serializers import IPFabricRelationshipFieldSerializer
|
|
10
11
|
from .serializers import IPFabricSnapshotSerializer
|
|
@@ -18,6 +19,7 @@ from ipfabric_netbox.filtersets import IPFabricSourceFilterSet
|
|
|
18
19
|
from ipfabric_netbox.filtersets import IPFabricTransformFieldFilterSet
|
|
19
20
|
from ipfabric_netbox.models import IPFabricData
|
|
20
21
|
from ipfabric_netbox.models import IPFabricIngestion
|
|
22
|
+
from ipfabric_netbox.models import IPFabricIngestionIssue
|
|
21
23
|
from ipfabric_netbox.models import IPFabricRelationshipField
|
|
22
24
|
from ipfabric_netbox.models import IPFabricSnapshot
|
|
23
25
|
from ipfabric_netbox.models import IPFabricSource
|
|
@@ -59,6 +61,11 @@ class IPFabricIngestionViewSet(NetBoxReadOnlyModelViewSet):
|
|
|
59
61
|
serializer_class = IPFabricIngestionSerializer
|
|
60
62
|
|
|
61
63
|
|
|
64
|
+
class IPFabricIngestionIssueViewSet(NetBoxReadOnlyModelViewSet):
|
|
65
|
+
queryset = IPFabricIngestionIssue.objects.all()
|
|
66
|
+
serializer_class = IPFabricIngestionIssueSerializer
|
|
67
|
+
|
|
68
|
+
|
|
62
69
|
class IPFabricSnapshotViewSet(NetBoxModelViewSet):
|
|
63
70
|
queryset = IPFabricSnapshot.objects.all()
|
|
64
71
|
serializer_class = IPFabricSnapshotSerializer
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING
|
|
2
|
+
|
|
3
|
+
from core.exceptions import SyncError
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from .models import IPFabricIngestionIssue
|
|
7
|
+
from .models import IPFabricIngestion
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class IngestionIssue(Exception):
|
|
11
|
+
"""
|
|
12
|
+
This exception is used to indicate an issue during the ingestion process.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
# Store created issue object ID if it exists for this exception
|
|
16
|
+
issue_id = None
|
|
17
|
+
model: str = ""
|
|
18
|
+
defaults: dict[str, str] = {}
|
|
19
|
+
coalesce_fields: dict[str, str] = {}
|
|
20
|
+
|
|
21
|
+
def __init__(self, model: str, data: dict, context: dict = None, issue_id=None):
|
|
22
|
+
super().__init__()
|
|
23
|
+
self.model = model
|
|
24
|
+
self.data = data
|
|
25
|
+
context = context or {}
|
|
26
|
+
self.defaults = context.pop("defaults", {})
|
|
27
|
+
self.coalesce_fields = context
|
|
28
|
+
self.issue_id = issue_id
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class SearchError(IngestionIssue, LookupError):
|
|
32
|
+
def __init__(self, message: str, *args, **kwargs):
|
|
33
|
+
super().__init__(*args, **kwargs)
|
|
34
|
+
self.message = message
|
|
35
|
+
|
|
36
|
+
def __str__(self):
|
|
37
|
+
return self.message
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class SyncDataError(IngestionIssue, SyncError):
|
|
41
|
+
def __str__(self):
|
|
42
|
+
return f"Sync failed for {self.model}: coalesce_fields={self.coalesce_fields} defaults={self.defaults}."
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class IPAddressDuplicateError(IngestionIssue, SyncError):
|
|
46
|
+
def __str__(self):
|
|
47
|
+
return f"IP address {self.data.get('address')} already exists in {self.model} with coalesce_fields={self.coalesce_fields}."
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def create_or_get_sync_issue(
|
|
51
|
+
exception: Exception,
|
|
52
|
+
ingestion: "IPFabricIngestion",
|
|
53
|
+
message: str = None,
|
|
54
|
+
model: str = None,
|
|
55
|
+
context: dict = None,
|
|
56
|
+
data: dict = None,
|
|
57
|
+
) -> (bool, "IPFabricIngestionIssue"):
|
|
58
|
+
"""
|
|
59
|
+
Helper function to handle sync errors and create IPFabricIngestionIssue if needed.
|
|
60
|
+
"""
|
|
61
|
+
context = context or {}
|
|
62
|
+
|
|
63
|
+
# TODO: This is to prevent circular import issues, clean it up later.
|
|
64
|
+
from .models import IPFabricIngestionIssue
|
|
65
|
+
|
|
66
|
+
if not hasattr(exception, "issue_id") or not exception.issue_id:
|
|
67
|
+
issue = IPFabricIngestionIssue.objects.create(
|
|
68
|
+
ingestion=ingestion,
|
|
69
|
+
exception=exception.__class__.__name__,
|
|
70
|
+
message=message or getattr(exception, "message", str(exception)),
|
|
71
|
+
model=model,
|
|
72
|
+
coalesce_fields={k: v for k, v in context.items() if k not in ["defaults"]},
|
|
73
|
+
defaults=context.get("defaults", dict()),
|
|
74
|
+
raw_data=data or dict(),
|
|
75
|
+
)
|
|
76
|
+
if hasattr(exception, "issue_id"):
|
|
77
|
+
exception.issue_id = issue.id
|
|
78
|
+
return True, issue
|
|
79
|
+
else:
|
|
80
|
+
issue = IPFabricIngestionIssue.objects.get(id=exception.issue_id)
|
|
81
|
+
return False, issue
|
|
@@ -10,6 +10,7 @@ from netbox_branching.models import ChangeDiff
|
|
|
10
10
|
|
|
11
11
|
from .models import IPFabricData
|
|
12
12
|
from .models import IPFabricIngestion
|
|
13
|
+
from .models import IPFabricIngestionIssue
|
|
13
14
|
from .models import IPFabricSnapshot
|
|
14
15
|
from .models import IPFabricSource
|
|
15
16
|
from .models import IPFabricSync
|
|
@@ -37,6 +38,35 @@ class IPFabricIngestionChangeFilterSet(BaseFilterSet):
|
|
|
37
38
|
)
|
|
38
39
|
|
|
39
40
|
|
|
41
|
+
class IPFabricIngestionIssueFilterSet(BaseFilterSet):
|
|
42
|
+
q = django_filters.CharFilter(method="search")
|
|
43
|
+
|
|
44
|
+
class Meta:
|
|
45
|
+
model = IPFabricIngestionIssue
|
|
46
|
+
fields = [
|
|
47
|
+
"model",
|
|
48
|
+
"timestamp",
|
|
49
|
+
"raw_data",
|
|
50
|
+
"coalesce_fields",
|
|
51
|
+
"defaults",
|
|
52
|
+
"exception",
|
|
53
|
+
"message",
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
def search(self, queryset, name, value):
|
|
57
|
+
if not value.strip():
|
|
58
|
+
return queryset
|
|
59
|
+
return queryset.filter(
|
|
60
|
+
Q(model__icontains=value)
|
|
61
|
+
| Q(timestamp__icontains=value)
|
|
62
|
+
| Q(raw_data__icontains=value)
|
|
63
|
+
| Q(coalesce_fields__icontains=value)
|
|
64
|
+
| Q(defaults__icontains=value)
|
|
65
|
+
| Q(exception__icontains=value)
|
|
66
|
+
| Q(message__icontains=value)
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
40
70
|
class IPFabricDataFilterSet(BaseFilterSet):
|
|
41
71
|
q = django_filters.CharFilter(method="search")
|
|
42
72
|
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Generated by Django 5.2 on 2025-07-11 19:39
|
|
2
|
+
import django.db.models.deletion
|
|
3
|
+
import django.utils.timezone
|
|
4
|
+
from django.db import migrations
|
|
5
|
+
from django.db import models
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Migration(migrations.Migration):
|
|
9
|
+
dependencies = [
|
|
10
|
+
(
|
|
11
|
+
"ipfabric_netbox",
|
|
12
|
+
"0014_ipfabrictransformmapgroup_ipfabrictransformmap_group",
|
|
13
|
+
),
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
operations = [
|
|
17
|
+
migrations.CreateModel(
|
|
18
|
+
name="IPFabricIngestionIssue",
|
|
19
|
+
fields=[
|
|
20
|
+
(
|
|
21
|
+
"id",
|
|
22
|
+
models.BigAutoField(
|
|
23
|
+
auto_created=True, primary_key=True, serialize=False
|
|
24
|
+
),
|
|
25
|
+
),
|
|
26
|
+
("timestamp", models.DateTimeField(default=django.utils.timezone.now)),
|
|
27
|
+
("model", models.CharField(blank=True, max_length=100, null=True)),
|
|
28
|
+
("message", models.TextField()),
|
|
29
|
+
("raw_data", models.TextField(blank=True, default="")),
|
|
30
|
+
("coalesce_fields", models.TextField(blank=True, default="")),
|
|
31
|
+
("defaults", models.TextField(blank=True, default="")),
|
|
32
|
+
("exception", models.TextField()),
|
|
33
|
+
(
|
|
34
|
+
"ingestion",
|
|
35
|
+
models.ForeignKey(
|
|
36
|
+
on_delete=django.db.models.deletion.CASCADE,
|
|
37
|
+
related_name="issues",
|
|
38
|
+
to="ipfabric_netbox.ipfabricingestion",
|
|
39
|
+
),
|
|
40
|
+
),
|
|
41
|
+
],
|
|
42
|
+
options={
|
|
43
|
+
"verbose_name": "IP Fabric Ingestion Issue",
|
|
44
|
+
"verbose_name_plural": "IP Fabric Ingestion Issues",
|
|
45
|
+
"ordering": ["timestamp"],
|
|
46
|
+
},
|
|
47
|
+
),
|
|
48
|
+
]
|
|
@@ -236,6 +236,8 @@ class IPFabricTransformMap(NetBoxModel):
|
|
|
236
236
|
target_class = self.target_model.model_class()
|
|
237
237
|
queryset = target_class.objects.using(connection_name)
|
|
238
238
|
|
|
239
|
+
# Don't change context since it's used in case of exception for IPFabricIngestionIssue
|
|
240
|
+
context = deepcopy(context)
|
|
239
241
|
defaults = context.pop("defaults", {})
|
|
240
242
|
|
|
241
243
|
with transaction.atomic(using=connection_name):
|
|
@@ -967,6 +969,29 @@ class IPFabricIngestion(JobsMixin, models.Model):
|
|
|
967
969
|
)
|
|
968
970
|
|
|
969
971
|
|
|
972
|
+
class IPFabricIngestionIssue(models.Model):
|
|
973
|
+
objects = RestrictedQuerySet.as_manager()
|
|
974
|
+
|
|
975
|
+
ingestion = models.ForeignKey(
|
|
976
|
+
to="IPFabricIngestion", on_delete=models.CASCADE, related_name="issues"
|
|
977
|
+
)
|
|
978
|
+
timestamp = models.DateTimeField(default=timezone.now)
|
|
979
|
+
model = models.CharField(max_length=100, blank=True, null=True)
|
|
980
|
+
message = models.TextField()
|
|
981
|
+
raw_data = models.TextField(blank=True, default="")
|
|
982
|
+
coalesce_fields = models.TextField(blank=True, default="")
|
|
983
|
+
defaults = models.TextField(blank=True, default="")
|
|
984
|
+
exception = models.TextField()
|
|
985
|
+
|
|
986
|
+
class Meta:
|
|
987
|
+
ordering = ["timestamp"]
|
|
988
|
+
verbose_name = "IP Fabric Ingestion Issue"
|
|
989
|
+
verbose_name_plural = "IP Fabric Ingestion Issues"
|
|
990
|
+
|
|
991
|
+
def __str__(self):
|
|
992
|
+
return f"[{self.timestamp}] {self.message}"
|
|
993
|
+
|
|
994
|
+
|
|
970
995
|
class IPFabricData(models.Model):
|
|
971
996
|
snapshot_data = models.ForeignKey(
|
|
972
997
|
to=IPFabricSnapshot,
|
|
@@ -8,6 +8,7 @@ from netbox_branching.models import ChangeDiff
|
|
|
8
8
|
|
|
9
9
|
from .models import IPFabricData
|
|
10
10
|
from .models import IPFabricIngestion
|
|
11
|
+
from .models import IPFabricIngestionIssue
|
|
11
12
|
from .models import IPFabricRelationshipField
|
|
12
13
|
from .models import IPFabricSnapshot
|
|
13
14
|
from .models import IPFabricSource
|
|
@@ -199,6 +200,28 @@ class IPFabricIngestionChangesTable(NetBoxTable):
|
|
|
199
200
|
default_columns = ("object", "action", "object_type", "actions")
|
|
200
201
|
|
|
201
202
|
|
|
203
|
+
class IPFabricIngestionIssuesTable(NetBoxTable):
|
|
204
|
+
id = tables.Column(verbose_name=_("ID"))
|
|
205
|
+
exception = tables.Column(verbose_name="Exception Type")
|
|
206
|
+
message = tables.Column(verbose_name="Error Message")
|
|
207
|
+
actions = None
|
|
208
|
+
|
|
209
|
+
class Meta(NetBoxTable.Meta):
|
|
210
|
+
model = IPFabricIngestionIssue
|
|
211
|
+
fields = (
|
|
212
|
+
"model",
|
|
213
|
+
"timestamp",
|
|
214
|
+
"raw_data",
|
|
215
|
+
"coalesce_fields",
|
|
216
|
+
"defaults",
|
|
217
|
+
"exception",
|
|
218
|
+
"message",
|
|
219
|
+
)
|
|
220
|
+
default_columns = ("model", "exception", "message")
|
|
221
|
+
empty_text = _("No Ingestion Issues found")
|
|
222
|
+
order_by = "id"
|
|
223
|
+
|
|
224
|
+
|
|
202
225
|
class DeviceIPFTable(tables.Table):
|
|
203
226
|
hostname = Column()
|
|
204
227
|
|
|
@@ -36,6 +36,11 @@
|
|
|
36
36
|
</div>
|
|
37
37
|
{% endblock control-buttons %}
|
|
38
38
|
|
|
39
|
+
{% block tabs %}
|
|
40
|
+
<div id="object_tabs" hx-swap-oob="true">
|
|
41
|
+
{% include 'ipfabric_netbox/partials/object_tabs.html' with object=object %}
|
|
42
|
+
</div>
|
|
43
|
+
{% endblock tabs %}
|
|
39
44
|
|
|
40
45
|
{% block content %}
|
|
41
46
|
<div class="row mb-3">
|
|
@@ -70,7 +75,6 @@
|
|
|
70
75
|
<th scope="row">Ingestion Status</th>
|
|
71
76
|
<td><div id="ingestion_status" hx-swap-oob="true">{% include 'ipfabric_netbox/partials/ingestion_status.html' with object=object %}</div></td>
|
|
72
77
|
<!-- <td >{% badge object.sync.get_status_display bg_color=object.sync.get_status_color %}</td> -->
|
|
73
|
-
</div>
|
|
74
78
|
</tr>
|
|
75
79
|
<tr>
|
|
76
80
|
<th scope="row">Snapshot</th>
|
|
@@ -88,59 +92,26 @@
|
|
|
88
92
|
{% plugin_left_page object %}
|
|
89
93
|
</div>
|
|
90
94
|
<div class="col col-md-6">
|
|
91
|
-
<div
|
|
92
|
-
|
|
93
|
-
<div class="card-body">
|
|
94
|
-
<table class="table table-hover attr-table">
|
|
95
|
-
<tr>
|
|
96
|
-
<th scope="row">Created</th>
|
|
97
|
-
<td>
|
|
98
|
-
{% if object.num_created %}
|
|
99
|
-
{{ object.num_created }}
|
|
100
|
-
{% else %}
|
|
101
|
-
{{ ''|placeholder }}
|
|
102
|
-
{% endif %}
|
|
103
|
-
</td>
|
|
104
|
-
</tr>
|
|
105
|
-
<tr>
|
|
106
|
-
<th scope="row">Updated</th>
|
|
107
|
-
<td>
|
|
108
|
-
{% if object.num_updated %}
|
|
109
|
-
{{ object.num_updated }}
|
|
110
|
-
{% else %}
|
|
111
|
-
{{ ''|placeholder }}
|
|
112
|
-
{% endif %}
|
|
113
|
-
</td>
|
|
114
|
-
</tr>
|
|
115
|
-
<tr>
|
|
116
|
-
<th scope="row">Deleted</th>
|
|
117
|
-
<td>
|
|
118
|
-
{% if object.num_deleted %}
|
|
119
|
-
{{ object.num_deleted }}
|
|
120
|
-
{% else %}
|
|
121
|
-
{{ ''|placeholder }}
|
|
122
|
-
{% endif %}
|
|
123
|
-
</td>
|
|
124
|
-
</tr>
|
|
125
|
-
</table>
|
|
126
|
-
</div>
|
|
95
|
+
<div id="ingestion_statistics" hx-swap-oob="true">
|
|
96
|
+
{% include 'ipfabric_netbox/partials/ingestion_statistics.html' with object=object %}
|
|
127
97
|
</div>
|
|
128
|
-
<div id="ingestion_progress">
|
|
98
|
+
<div id="ingestion_progress" hx-swap-oob="true">
|
|
129
99
|
{% include 'ipfabric_netbox/partials/ingestion_progress.html' %}
|
|
130
100
|
</div>
|
|
131
101
|
<!-- {% include 'inc/panels/related_objects.html' %} -->
|
|
132
102
|
{% include 'inc/panels/custom_fields.html' %}
|
|
133
103
|
{% plugin_right_page object %}
|
|
134
104
|
</div>
|
|
135
|
-
|
|
136
|
-
<div class="
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
105
|
+
<div class="row mb-3">
|
|
106
|
+
<div class="col col-md-12" {% if not object.job.completed %} hx-get="{% url 'plugins:ipfabric_netbox:ipfabricingestion_logs' pk=object.pk %}?type=info"
|
|
107
|
+
hx-trigger="every 5s" hx-target="#ingestion_logs" hx-select="#ingestion_logs" hx-select-oob="#ingestion_status:innerHTML,#ingestion_progress:innerHTML,#ingestion_statistics:innerHTML,#object_tabs:innerHTML"
|
|
108
|
+
hx-swap="innerHTML" {% endif %}>
|
|
109
|
+
<div id="ingestion_logs">{% include 'ipfabric_netbox/partials/job_logs.html' with job=object.job %}</div>
|
|
110
|
+
</div>
|
|
141
111
|
</div>
|
|
112
|
+
</div>
|
|
142
113
|
{% endblock content %}
|
|
143
114
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
115
|
+
{% block modals %}
|
|
116
|
+
{% include 'inc/htmx_modal.html' with size='lg' %}
|
|
117
|
+
{% endblock modals %}
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
<div id="object_tabs" hx-swap-oob="true">
|
|
2
|
+
{% include 'ipfabric_netbox/partials/object_tabs.html' with object=object %}
|
|
3
|
+
</div>
|
|
4
|
+
<div id="ingestion_statistics" hx-swap-oob="true">
|
|
5
|
+
{% include 'ipfabric_netbox/partials/ingestion_statistics.html' with object=object %}
|
|
6
|
+
</div>
|
|
2
7
|
<div id="ingestion_status" hx-swap-oob="true">
|
|
3
8
|
{% include 'ipfabric_netbox/partials/ingestion_status.html' with object=object %}
|
|
4
9
|
</div>
|
ipfabric_netbox-4.1.0b5/ipfabric_netbox/templates/ipfabric_netbox/partials/ingestion_statistics.html
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<div class="card">
|
|
2
|
+
<h5 class="card-header">Statistics</h5>
|
|
3
|
+
<div class="card-body">
|
|
4
|
+
<table class="table table-hover attr-table">
|
|
5
|
+
<tr>
|
|
6
|
+
<th scope="row">Created</th>
|
|
7
|
+
<td>
|
|
8
|
+
{% if object.num_created %}
|
|
9
|
+
{{ object.num_created }}
|
|
10
|
+
{% else %}
|
|
11
|
+
{{ ''|placeholder }}
|
|
12
|
+
{% endif %}
|
|
13
|
+
</td>
|
|
14
|
+
</tr>
|
|
15
|
+
<tr>
|
|
16
|
+
<th scope="row">Updated</th>
|
|
17
|
+
<td>
|
|
18
|
+
{% if object.num_updated %}
|
|
19
|
+
{{ object.num_updated }}
|
|
20
|
+
{% else %}
|
|
21
|
+
{{ ''|placeholder }}
|
|
22
|
+
{% endif %}
|
|
23
|
+
</td>
|
|
24
|
+
</tr>
|
|
25
|
+
<tr>
|
|
26
|
+
<th scope="row">Deleted</th>
|
|
27
|
+
<td>
|
|
28
|
+
{% if object.num_deleted %}
|
|
29
|
+
{{ object.num_deleted }}
|
|
30
|
+
{% else %}
|
|
31
|
+
{{ ''|placeholder }}
|
|
32
|
+
{% endif %}
|
|
33
|
+
</td>
|
|
34
|
+
</tr>
|
|
35
|
+
</table>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
ipfabric_netbox-4.1.0b5/ipfabric_netbox/templates/ipfabric_netbox/partials/ingestion_status.html
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{% badge object.sync.get_status_display bg_color=object.sync.get_status_color %} {% if object.errors and object.errors.count %}(with {{ object.errors.count }} issues){% endif %}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{% load helpers %}
|
|
2
|
+
{% load tabs %}
|
|
3
|
+
|
|
4
|
+
<ul class="nav nav-tabs" role="presentation">
|
|
5
|
+
{# Primary tab #}
|
|
6
|
+
<li class="nav-item">
|
|
7
|
+
<a class="nav-link{% if not tab %} active{% endif %}" href="{{ object.get_absolute_url }}">{{ object|meta:"verbose_name"|bettertitle }}</a>
|
|
8
|
+
</li>
|
|
9
|
+
|
|
10
|
+
{# Include tabs for registered model views #}
|
|
11
|
+
{% model_view_tabs object %}
|
|
12
|
+
</ul>
|
|
@@ -22,19 +22,14 @@ from netbox.config import get_config
|
|
|
22
22
|
from netutils.utils import jinja2_convenience_function
|
|
23
23
|
|
|
24
24
|
from ..choices import IPFabricSourceTypeChoices
|
|
25
|
+
from ..exceptions import create_or_get_sync_issue
|
|
26
|
+
from ..exceptions import IPAddressDuplicateError
|
|
25
27
|
from ..exceptions import SearchError
|
|
26
28
|
from ..exceptions import SyncDataError
|
|
27
29
|
from .nbutils import device_serial_max_length
|
|
28
30
|
from .nbutils import order_devices
|
|
29
31
|
from .nbutils import order_members
|
|
30
32
|
|
|
31
|
-
FAILED_CREATE_INSTANCE_TEMPLATE = (
|
|
32
|
-
"Failed to create instance of '{model}': <br/>"
|
|
33
|
-
"message: '{err}'<br/>"
|
|
34
|
-
"raw data: '{data}'<br/>"
|
|
35
|
-
"context: '{context}'<br/>"
|
|
36
|
-
)
|
|
37
|
-
|
|
38
33
|
if TYPE_CHECKING:
|
|
39
34
|
from ..models import IPFabricIngestion
|
|
40
35
|
from ipam.models import IPAddress
|
|
@@ -163,6 +158,9 @@ class IPFabricSyncRunner(object):
|
|
|
163
158
|
except Exception as err:
|
|
164
159
|
# Log the error to logger outside of job - console/file
|
|
165
160
|
logger.error(err, exc_info=True)
|
|
161
|
+
if hasattr(err, "issue_id") and err.issue_id:
|
|
162
|
+
# The error is already logged to user, no need to log it again
|
|
163
|
+
return None
|
|
166
164
|
# Logging section for logs inside job - facing user
|
|
167
165
|
self = args[0]
|
|
168
166
|
if isinstance(err, SearchError):
|
|
@@ -176,6 +174,11 @@ class IPFabricSyncRunner(object):
|
|
|
176
174
|
f"Syncing `{err.model}` is disabled in settings, but hit above error trying to find the correct item. Please check your transform maps and/or existing data.",
|
|
177
175
|
obj=self.sync,
|
|
178
176
|
)
|
|
177
|
+
if isinstance(err, IPAddressDuplicateError):
|
|
178
|
+
self.logger.log_warning(
|
|
179
|
+
f"IP Address `{err.data.get('address')}` already exists in `{err.model}` with coalesce fields: `{err.coalesce_fields}`. Please check your transform maps and/or existing data.",
|
|
180
|
+
obj=self.sync,
|
|
181
|
+
)
|
|
179
182
|
else:
|
|
180
183
|
self.logger.log_failure(
|
|
181
184
|
f"Syncing failed with: `{err}`. See above error for more details.",
|
|
@@ -207,13 +210,21 @@ class IPFabricSyncRunner(object):
|
|
|
207
210
|
except Exception as err:
|
|
208
211
|
message = f"Error getting context for `{model}`."
|
|
209
212
|
if isinstance(err, ObjectDoesNotExist):
|
|
210
|
-
message +=
|
|
213
|
+
message += (
|
|
214
|
+
" Could not find related object using template in transform maps."
|
|
215
|
+
)
|
|
211
216
|
elif isinstance(err, MultipleObjectsReturned):
|
|
212
217
|
message += " Multiple objects returned using on template in transform maps, the template is not strict enough."
|
|
213
|
-
|
|
214
|
-
|
|
218
|
+
_, issue = create_or_get_sync_issue(
|
|
219
|
+
exception=err,
|
|
220
|
+
ingestion=self.ingestion,
|
|
221
|
+
message=message,
|
|
222
|
+
model=model,
|
|
223
|
+
data=data,
|
|
215
224
|
)
|
|
216
|
-
raise
|
|
225
|
+
raise SearchError(
|
|
226
|
+
message=message, data=data, model=model, issue_id=issue.id
|
|
227
|
+
) from err
|
|
217
228
|
|
|
218
229
|
queryset = transform_map.target_model.model_class().objects
|
|
219
230
|
|
|
@@ -232,25 +243,50 @@ class IPFabricSyncRunner(object):
|
|
|
232
243
|
context.pop("defaults", None)
|
|
233
244
|
object = queryset.using(connection_name).get(**context)
|
|
234
245
|
except queryset.model.DoesNotExist as err:
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
246
|
+
message = f"Instance of `{model}` not found."
|
|
247
|
+
_, issue = create_or_get_sync_issue(
|
|
248
|
+
exception=err,
|
|
249
|
+
ingestion=self.ingestion,
|
|
250
|
+
message=message,
|
|
251
|
+
model=model,
|
|
252
|
+
context=context,
|
|
253
|
+
data=data,
|
|
238
254
|
)
|
|
239
|
-
raise SearchError(
|
|
255
|
+
raise SearchError(
|
|
256
|
+
message=message,
|
|
257
|
+
model=model,
|
|
258
|
+
context=context,
|
|
259
|
+
data=data,
|
|
260
|
+
issue_id=issue.id,
|
|
261
|
+
) from err
|
|
240
262
|
except queryset.model.MultipleObjectsReturned as err:
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
263
|
+
message = f"Multiple instances of `{model}` found."
|
|
264
|
+
_, issue = create_or_get_sync_issue(
|
|
265
|
+
exception=err,
|
|
266
|
+
ingestion=self.ingestion,
|
|
267
|
+
message=message,
|
|
268
|
+
model=model,
|
|
269
|
+
context=context,
|
|
270
|
+
data=data,
|
|
244
271
|
)
|
|
245
|
-
raise SearchError(
|
|
272
|
+
raise SearchError(
|
|
273
|
+
message=message,
|
|
274
|
+
model=model,
|
|
275
|
+
context=context,
|
|
276
|
+
data=data,
|
|
277
|
+
issue_id=issue.id,
|
|
278
|
+
) from err
|
|
246
279
|
except Exception as err:
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
280
|
+
_, issue = create_or_get_sync_issue(
|
|
281
|
+
exception=err,
|
|
282
|
+
ingestion=self.ingestion,
|
|
283
|
+
model=model,
|
|
284
|
+
context=context,
|
|
285
|
+
data=data,
|
|
252
286
|
)
|
|
253
|
-
raise SyncDataError(
|
|
287
|
+
raise SyncDataError(
|
|
288
|
+
model=model, context=context, data=data, issue_id=issue.id
|
|
289
|
+
) from err
|
|
254
290
|
|
|
255
291
|
return object
|
|
256
292
|
|
|
@@ -440,7 +476,6 @@ class IPFabricSyncRunner(object):
|
|
|
440
476
|
f"{len(data.get('ipaddress', []))} management IP's collected",
|
|
441
477
|
obj=self.sync.snapshot_data.source,
|
|
442
478
|
)
|
|
443
|
-
|
|
444
479
|
self.logger.log_info("Ordering devices", obj=self.sync)
|
|
445
480
|
|
|
446
481
|
members = order_members(data.get("virtualchassis", []))
|
|
@@ -34,6 +34,7 @@ from utilities.views import ViewTab
|
|
|
34
34
|
from .filtersets import IPFabricDataFilterSet
|
|
35
35
|
from .filtersets import IPFabricIngestionChangeFilterSet
|
|
36
36
|
from .filtersets import IPFabricIngestionFilterSet
|
|
37
|
+
from .filtersets import IPFabricIngestionIssueFilterSet
|
|
37
38
|
from .filtersets import IPFabricSnapshotFilterSet
|
|
38
39
|
from .filtersets import IPFabricSourceFilterSet
|
|
39
40
|
from .filtersets import IPFabricTransformMapFilterSet
|
|
@@ -52,6 +53,7 @@ from .forms import IPFabricTransformMapForm
|
|
|
52
53
|
from .forms import IPFabricTransformMapGroupForm
|
|
53
54
|
from .models import IPFabricData
|
|
54
55
|
from .models import IPFabricIngestion
|
|
56
|
+
from .models import IPFabricIngestionIssue
|
|
55
57
|
from .models import IPFabricRelationshipField
|
|
56
58
|
from .models import IPFabricSnapshot
|
|
57
59
|
from .models import IPFabricSource
|
|
@@ -62,6 +64,7 @@ from .models import IPFabricTransformMapGroup
|
|
|
62
64
|
from .tables import DeviceIPFTable
|
|
63
65
|
from .tables import IPFabricDataTable
|
|
64
66
|
from .tables import IPFabricIngestionChangesTable
|
|
67
|
+
from .tables import IPFabricIngestionIssuesTable
|
|
65
68
|
from .tables import IPFabricIngestionTable
|
|
66
69
|
from .tables import IPFabricRelationshipFieldTable
|
|
67
70
|
from .tables import IPFabricSnapshotTable
|
|
@@ -657,36 +660,8 @@ class IPFabricIngestionListView(generic.ObjectListView):
|
|
|
657
660
|
table = IPFabricIngestionTable
|
|
658
661
|
|
|
659
662
|
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
name="logs",
|
|
663
|
-
path="logs",
|
|
664
|
-
)
|
|
665
|
-
class IPFabricIngestionLogView(LoginRequiredMixin, View):
|
|
666
|
-
template_name = "ipfabric_netbox/partials/ingestion_all.html"
|
|
667
|
-
|
|
668
|
-
def get(self, request, **kwargs):
|
|
669
|
-
ingestion_id = kwargs.get("pk")
|
|
670
|
-
if request.htmx:
|
|
671
|
-
ingestion = IPFabricIngestion.objects.get(pk=ingestion_id)
|
|
672
|
-
data = ingestion.get_statistics()
|
|
673
|
-
data["object"] = ingestion
|
|
674
|
-
data["job"] = ingestion.jobs.first()
|
|
675
|
-
response = render(
|
|
676
|
-
request,
|
|
677
|
-
self.template_name,
|
|
678
|
-
data,
|
|
679
|
-
)
|
|
680
|
-
if ingestion.job.completed:
|
|
681
|
-
response["HX-Refresh"] = "true"
|
|
682
|
-
return response
|
|
683
|
-
else:
|
|
684
|
-
return response
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
@register_model_view(IPFabricIngestion)
|
|
688
|
-
class IPFabricIngestionView(generic.ObjectView):
|
|
689
|
-
queryset = IPFabricIngestion.objects.annotate(
|
|
663
|
+
def annotate_statistics(queryset):
|
|
664
|
+
return queryset.annotate(
|
|
690
665
|
num_created=models.Count(
|
|
691
666
|
"branch__changediff",
|
|
692
667
|
filter=models.Q(
|
|
@@ -713,6 +688,39 @@ class IPFabricIngestionView(generic.ObjectView):
|
|
|
713
688
|
staged_changes=models.Count(models.F("branch__changediff")),
|
|
714
689
|
)
|
|
715
690
|
|
|
691
|
+
|
|
692
|
+
@register_model_view(
|
|
693
|
+
IPFabricIngestion,
|
|
694
|
+
name="logs",
|
|
695
|
+
path="logs",
|
|
696
|
+
)
|
|
697
|
+
class IPFabricIngestionLogView(LoginRequiredMixin, View):
|
|
698
|
+
template_name = "ipfabric_netbox/partials/ingestion_all.html"
|
|
699
|
+
|
|
700
|
+
def get(self, request, **kwargs):
|
|
701
|
+
ingestion_id = kwargs.get("pk")
|
|
702
|
+
if request.htmx:
|
|
703
|
+
ingestion = annotate_statistics(IPFabricIngestion.objects).get(
|
|
704
|
+
pk=ingestion_id
|
|
705
|
+
)
|
|
706
|
+
data = ingestion.get_statistics()
|
|
707
|
+
data["object"] = ingestion
|
|
708
|
+
data["job"] = ingestion.jobs.first()
|
|
709
|
+
response = render(
|
|
710
|
+
request,
|
|
711
|
+
self.template_name,
|
|
712
|
+
data,
|
|
713
|
+
)
|
|
714
|
+
if ingestion.job.completed:
|
|
715
|
+
response["HX-Refresh"] = "true"
|
|
716
|
+
return response
|
|
717
|
+
return render(request, self.template_name)
|
|
718
|
+
|
|
719
|
+
|
|
720
|
+
@register_model_view(IPFabricIngestion)
|
|
721
|
+
class IPFabricIngestionView(generic.ObjectView):
|
|
722
|
+
queryset = annotate_statistics(IPFabricIngestion.objects)
|
|
723
|
+
|
|
716
724
|
def get_extra_context(self, request, instance):
|
|
717
725
|
data = instance.get_statistics()
|
|
718
726
|
return data
|
|
@@ -844,6 +852,23 @@ class IPFabricIngestionChangesView(generic.ObjectChildrenView):
|
|
|
844
852
|
return self.child_model.objects.filter(branch=parent.branch)
|
|
845
853
|
|
|
846
854
|
|
|
855
|
+
@register_model_view(IPFabricIngestion, "ingestion_issues")
|
|
856
|
+
class IPFabricIngestionIssuesView(generic.ObjectChildrenView):
|
|
857
|
+
queryset = IPFabricIngestion.objects.all()
|
|
858
|
+
child_model = IPFabricIngestionIssue
|
|
859
|
+
table = IPFabricIngestionIssuesTable
|
|
860
|
+
template_name = "generic/object_children.html"
|
|
861
|
+
filterset = IPFabricIngestionIssueFilterSet
|
|
862
|
+
tab = ViewTab(
|
|
863
|
+
label="Ingestion Issues",
|
|
864
|
+
badge=lambda obj: IPFabricIngestionIssue.objects.filter(ingestion=obj).count(),
|
|
865
|
+
permission="ipfabric_netbox.view_ipfabricingestionissue",
|
|
866
|
+
)
|
|
867
|
+
|
|
868
|
+
def get_children(self, request, parent):
|
|
869
|
+
return IPFabricIngestionIssue.objects.filter(ingestion=parent)
|
|
870
|
+
|
|
871
|
+
|
|
847
872
|
@register_model_view(IPFabricIngestion, "delete")
|
|
848
873
|
class IPFabricIngestionDeleteView(generic.ObjectDeleteView):
|
|
849
874
|
queryset = IPFabricIngestion.objects.all()
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
from core.exceptions import SyncError
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
class ErrorMixin(Exception):
|
|
5
|
-
model: str = ""
|
|
6
|
-
defaults: dict[str, str] = {}
|
|
7
|
-
coalesce_fields: dict[str, str] = {}
|
|
8
|
-
|
|
9
|
-
def __init__(self, model: str, context: dict, data: dict = None):
|
|
10
|
-
super().__init__()
|
|
11
|
-
self.model = model
|
|
12
|
-
self.data = data or {}
|
|
13
|
-
self.defaults = context.pop("defaults", {})
|
|
14
|
-
self.coalesce_fields = context
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
class SearchError(ErrorMixin, LookupError):
|
|
18
|
-
def __str__(self):
|
|
19
|
-
return f"{self.model} with these keys not found: {self.coalesce_fields}."
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
class SyncDataError(ErrorMixin, SyncError):
|
|
23
|
-
def __str__(self):
|
|
24
|
-
return f"Sync failed for {self.model}: coalesce_fields={self.coalesce_fields} defaults={self.defaults}."
|
ipfabric_netbox-4.1.0b4/ipfabric_netbox/templates/ipfabric_netbox/partials/ingestion_status.html
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{% badge object.sync.get_status_display bg_color=object.sync.get_status_color %}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/migrations/0001_initial.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/templatetags/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/utilities/transform_map.py
RENAMED
|
File without changes
|