ipfabric_netbox 4.1.0b3__py3-none-any.whl → 4.1.0b5__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.
- ipfabric_netbox/__init__.py +1 -1
- ipfabric_netbox/api/nested_serializers.py +0 -1
- ipfabric_netbox/api/serializers.py +23 -1
- ipfabric_netbox/api/urls.py +2 -0
- ipfabric_netbox/api/views.py +7 -0
- ipfabric_netbox/exceptions.py +63 -6
- ipfabric_netbox/filtersets.py +30 -0
- ipfabric_netbox/migrations/0015_ipfabricingestionissue.py +48 -0
- ipfabric_netbox/models.py +25 -0
- ipfabric_netbox/tables.py +23 -0
- ipfabric_netbox/templates/ipfabric_netbox/ipfabricingestion.html +18 -47
- ipfabric_netbox/templates/ipfabric_netbox/partials/ingestion_all.html +6 -1
- ipfabric_netbox/templates/ipfabric_netbox/partials/ingestion_statistics.html +37 -0
- ipfabric_netbox/templates/ipfabric_netbox/partials/ingestion_status.html +1 -1
- ipfabric_netbox/templates/ipfabric_netbox/partials/object_tabs.html +12 -0
- ipfabric_netbox/utilities/ipfutils.py +158 -75
- ipfabric_netbox/views.py +55 -30
- {ipfabric_netbox-4.1.0b3.dist-info → ipfabric_netbox-4.1.0b5.dist-info}/METADATA +1 -1
- {ipfabric_netbox-4.1.0b3.dist-info → ipfabric_netbox-4.1.0b5.dist-info}/RECORD +20 -17
- {ipfabric_netbox-4.1.0b3.dist-info → ipfabric_netbox-4.1.0b5.dist-info}/WHEEL +0 -0
ipfabric_netbox/__init__.py
CHANGED
|
@@ -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()
|
ipfabric_netbox/api/urls.py
CHANGED
|
@@ -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
|
ipfabric_netbox/api/views.py
CHANGED
|
@@ -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
|
ipfabric_netbox/exceptions.py
CHANGED
|
@@ -1,24 +1,81 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING
|
|
2
|
+
|
|
1
3
|
from core.exceptions import SyncError
|
|
2
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
|
+
"""
|
|
3
14
|
|
|
4
|
-
|
|
15
|
+
# Store created issue object ID if it exists for this exception
|
|
16
|
+
issue_id = None
|
|
5
17
|
model: str = ""
|
|
6
18
|
defaults: dict[str, str] = {}
|
|
7
19
|
coalesce_fields: dict[str, str] = {}
|
|
8
20
|
|
|
9
|
-
def __init__(self, model: str,
|
|
21
|
+
def __init__(self, model: str, data: dict, context: dict = None, issue_id=None):
|
|
10
22
|
super().__init__()
|
|
11
23
|
self.model = model
|
|
12
|
-
self.data = data
|
|
24
|
+
self.data = data
|
|
25
|
+
context = context or {}
|
|
13
26
|
self.defaults = context.pop("defaults", {})
|
|
14
27
|
self.coalesce_fields = context
|
|
28
|
+
self.issue_id = issue_id
|
|
15
29
|
|
|
16
30
|
|
|
17
|
-
class SearchError(
|
|
31
|
+
class SearchError(IngestionIssue, LookupError):
|
|
32
|
+
def __init__(self, message: str, *args, **kwargs):
|
|
33
|
+
super().__init__(*args, **kwargs)
|
|
34
|
+
self.message = message
|
|
35
|
+
|
|
18
36
|
def __str__(self):
|
|
19
|
-
return
|
|
37
|
+
return self.message
|
|
20
38
|
|
|
21
39
|
|
|
22
|
-
class SyncDataError(
|
|
40
|
+
class SyncDataError(IngestionIssue, SyncError):
|
|
23
41
|
def __str__(self):
|
|
24
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
|
ipfabric_netbox/filtersets.py
CHANGED
|
@@ -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
|
+
]
|
ipfabric_netbox/models.py
CHANGED
|
@@ -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,
|
ipfabric_netbox/tables.py
CHANGED
|
@@ -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>
|
|
@@ -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>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{% badge object.sync.get_status_display bg_color=object.sync.get_status_color %}
|
|
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>
|
|
@@ -3,6 +3,7 @@ import logging
|
|
|
3
3
|
from importlib import metadata
|
|
4
4
|
from typing import Callable
|
|
5
5
|
from typing import TYPE_CHECKING
|
|
6
|
+
from typing import TypeVar
|
|
6
7
|
|
|
7
8
|
from core.choices import DataSourceStatusChoices
|
|
8
9
|
from core.exceptions import SyncError
|
|
@@ -21,24 +22,22 @@ from netbox.config import get_config
|
|
|
21
22
|
from netutils.utils import jinja2_convenience_function
|
|
22
23
|
|
|
23
24
|
from ..choices import IPFabricSourceTypeChoices
|
|
25
|
+
from ..exceptions import create_or_get_sync_issue
|
|
26
|
+
from ..exceptions import IPAddressDuplicateError
|
|
24
27
|
from ..exceptions import SearchError
|
|
25
28
|
from ..exceptions import SyncDataError
|
|
26
29
|
from .nbutils import device_serial_max_length
|
|
27
30
|
from .nbutils import order_devices
|
|
28
31
|
from .nbutils import order_members
|
|
29
32
|
|
|
30
|
-
FAILED_CREATE_INSTANCE_TEMPLATE = (
|
|
31
|
-
"Failed to create instance of '{model}': <br/>"
|
|
32
|
-
"message: '{err}'<br/>"
|
|
33
|
-
"raw data: '{data}'<br/>"
|
|
34
|
-
"context: '{context}'<br/>"
|
|
35
|
-
)
|
|
36
|
-
|
|
37
33
|
if TYPE_CHECKING:
|
|
38
34
|
from ..models import IPFabricIngestion
|
|
35
|
+
from ipam.models import IPAddress
|
|
39
36
|
|
|
40
37
|
logger = logging.getLogger("ipfabric_netbox.utilities.ipf_utils")
|
|
41
38
|
|
|
39
|
+
ModelTypeVar = TypeVar("ModelTypeVar", bound=Model)
|
|
40
|
+
|
|
42
41
|
|
|
43
42
|
def slugify_text(value):
|
|
44
43
|
return slugify(value)
|
|
@@ -98,7 +97,7 @@ class IPFabric(object):
|
|
|
98
97
|
formatted_snapshots[snapshot_ref] = (description, snapshot.snapshot_id)
|
|
99
98
|
return formatted_snapshots
|
|
100
99
|
|
|
101
|
-
def get_sites(self, snapshot=None) ->
|
|
100
|
+
def get_sites(self, snapshot=None) -> list:
|
|
102
101
|
if snapshot:
|
|
103
102
|
raw_sites = self.ipf.inventory.sites.all(snapshot_id=snapshot)
|
|
104
103
|
else:
|
|
@@ -159,6 +158,9 @@ class IPFabricSyncRunner(object):
|
|
|
159
158
|
except Exception as err:
|
|
160
159
|
# Log the error to logger outside of job - console/file
|
|
161
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
|
|
162
164
|
# Logging section for logs inside job - facing user
|
|
163
165
|
self = args[0]
|
|
164
166
|
if isinstance(err, SearchError):
|
|
@@ -172,6 +174,11 @@ class IPFabricSyncRunner(object):
|
|
|
172
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.",
|
|
173
175
|
obj=self.sync,
|
|
174
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
|
+
)
|
|
175
182
|
else:
|
|
176
183
|
self.logger.log_failure(
|
|
177
184
|
f"Syncing failed with: `{err}`. See above error for more details.",
|
|
@@ -203,13 +210,21 @@ class IPFabricSyncRunner(object):
|
|
|
203
210
|
except Exception as err:
|
|
204
211
|
message = f"Error getting context for `{model}`."
|
|
205
212
|
if isinstance(err, ObjectDoesNotExist):
|
|
206
|
-
message +=
|
|
213
|
+
message += (
|
|
214
|
+
" Could not find related object using template in transform maps."
|
|
215
|
+
)
|
|
207
216
|
elif isinstance(err, MultipleObjectsReturned):
|
|
208
217
|
message += " Multiple objects returned using on template in transform maps, the template is not strict enough."
|
|
209
|
-
|
|
210
|
-
|
|
218
|
+
_, issue = create_or_get_sync_issue(
|
|
219
|
+
exception=err,
|
|
220
|
+
ingestion=self.ingestion,
|
|
221
|
+
message=message,
|
|
222
|
+
model=model,
|
|
223
|
+
data=data,
|
|
211
224
|
)
|
|
212
|
-
raise
|
|
225
|
+
raise SearchError(
|
|
226
|
+
message=message, data=data, model=model, issue_id=issue.id
|
|
227
|
+
) from err
|
|
213
228
|
|
|
214
229
|
queryset = transform_map.target_model.model_class().objects
|
|
215
230
|
|
|
@@ -228,25 +243,50 @@ class IPFabricSyncRunner(object):
|
|
|
228
243
|
context.pop("defaults", None)
|
|
229
244
|
object = queryset.using(connection_name).get(**context)
|
|
230
245
|
except queryset.model.DoesNotExist as err:
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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,
|
|
234
254
|
)
|
|
235
|
-
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
|
|
236
262
|
except queryset.model.MultipleObjectsReturned as err:
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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,
|
|
240
271
|
)
|
|
241
|
-
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
|
|
242
279
|
except Exception as err:
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
280
|
+
_, issue = create_or_get_sync_issue(
|
|
281
|
+
exception=err,
|
|
282
|
+
ingestion=self.ingestion,
|
|
283
|
+
model=model,
|
|
284
|
+
context=context,
|
|
285
|
+
data=data,
|
|
248
286
|
)
|
|
249
|
-
raise SyncDataError(
|
|
287
|
+
raise SyncDataError(
|
|
288
|
+
model=model, context=context, data=data, issue_id=issue.id
|
|
289
|
+
) from err
|
|
250
290
|
|
|
251
291
|
return object
|
|
252
292
|
|
|
@@ -436,7 +476,6 @@ class IPFabricSyncRunner(object):
|
|
|
436
476
|
f"{len(data.get('ipaddress', []))} management IP's collected",
|
|
437
477
|
obj=self.sync.snapshot_data.source,
|
|
438
478
|
)
|
|
439
|
-
|
|
440
479
|
self.logger.log_info("Ordering devices", obj=self.sync)
|
|
441
480
|
|
|
442
481
|
members = order_members(data.get("virtualchassis", []))
|
|
@@ -526,11 +565,29 @@ class IPFabricSyncRunner(object):
|
|
|
526
565
|
)
|
|
527
566
|
|
|
528
567
|
@handle_errors
|
|
529
|
-
def sync_model(
|
|
568
|
+
def sync_model(
|
|
569
|
+
self,
|
|
570
|
+
app_label: str,
|
|
571
|
+
model: str,
|
|
572
|
+
data: dict | None,
|
|
573
|
+
stats: bool = True,
|
|
574
|
+
sync: bool = False,
|
|
575
|
+
) -> ModelTypeVar | None:
|
|
530
576
|
"""Sync a single item to NetBox."""
|
|
577
|
+
# The `sync` param is a workaround since we need to get some models (Device...) even when not syncing them.
|
|
578
|
+
if not sync:
|
|
579
|
+
return None
|
|
580
|
+
|
|
531
581
|
if not data:
|
|
532
582
|
return None
|
|
533
|
-
|
|
583
|
+
|
|
584
|
+
instance = self.get_model_or_update(app_label, model, data)
|
|
585
|
+
|
|
586
|
+
# Only log when we successfully synced the item and asked for it
|
|
587
|
+
if stats:
|
|
588
|
+
self.logger.increment_statistics(model=model)
|
|
589
|
+
|
|
590
|
+
return instance
|
|
534
591
|
|
|
535
592
|
def sync_items(
|
|
536
593
|
self,
|
|
@@ -548,11 +605,14 @@ class IPFabricSyncRunner(object):
|
|
|
548
605
|
return
|
|
549
606
|
|
|
550
607
|
for item in items:
|
|
551
|
-
synced_object = self.sync_model(
|
|
608
|
+
synced_object = self.sync_model(
|
|
609
|
+
app_label=app_label,
|
|
610
|
+
model=model,
|
|
611
|
+
data=item,
|
|
612
|
+
sync=self.settings.get(model),
|
|
613
|
+
)
|
|
552
614
|
if synced_object is None:
|
|
553
615
|
continue
|
|
554
|
-
# Only log when we successfully synced the item
|
|
555
|
-
self.logger.increment_statistics(model=model)
|
|
556
616
|
|
|
557
617
|
if cf:
|
|
558
618
|
synced_object.snapshot()
|
|
@@ -588,22 +648,40 @@ class IPFabricSyncRunner(object):
|
|
|
588
648
|
devices_total = len(devices)
|
|
589
649
|
|
|
590
650
|
for device in devices:
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
651
|
+
self.sync_model(
|
|
652
|
+
"dcim", "manufacturer", device, sync=self.settings.get("manufacturer")
|
|
653
|
+
)
|
|
654
|
+
self.sync_model(
|
|
655
|
+
"dcim", "devicetype", device, sync=self.settings.get("devicetype")
|
|
656
|
+
)
|
|
657
|
+
self.sync_model(
|
|
658
|
+
"dcim", "platform", device, sync=self.settings.get("platform")
|
|
659
|
+
)
|
|
660
|
+
self.sync_model(
|
|
661
|
+
"dcim", "devicerole", device, sync=self.settings.get("devicerole")
|
|
662
|
+
)
|
|
599
663
|
|
|
600
664
|
virtual_chassis = device.get("virtual_chassis", {})
|
|
601
|
-
self.sync_model(
|
|
665
|
+
self.sync_model(
|
|
666
|
+
"dcim",
|
|
667
|
+
"virtualchassis",
|
|
668
|
+
virtual_chassis,
|
|
669
|
+
False,
|
|
670
|
+
sync=self.settings.get("virtualchassis"),
|
|
671
|
+
)
|
|
602
672
|
|
|
603
|
-
|
|
604
|
-
|
|
673
|
+
# We need to get a Device instance even when not syncing it but syncing Interfaces, IPs or MACs
|
|
674
|
+
device_object: Device | None = self.sync_model(
|
|
675
|
+
"dcim",
|
|
676
|
+
"device",
|
|
677
|
+
device,
|
|
678
|
+
sync=self.settings.get("device")
|
|
679
|
+
or self.settings.get("interface")
|
|
680
|
+
or self.settings.get("ipaddress")
|
|
681
|
+
or self.settings.get("macaddress"),
|
|
682
|
+
)
|
|
605
683
|
|
|
606
|
-
if self.settings.get("device"):
|
|
684
|
+
if device_object and self.settings.get("device"):
|
|
607
685
|
device_object.snapshot()
|
|
608
686
|
device_object.custom_field_data[
|
|
609
687
|
"ipfabric_source"
|
|
@@ -612,23 +690,26 @@ class IPFabricSyncRunner(object):
|
|
|
612
690
|
device_object.custom_field_data["ipfabric_ingestion"] = ingestion.pk
|
|
613
691
|
device_object.save()
|
|
614
692
|
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
693
|
+
self.logger.increment_statistics(model="device")
|
|
694
|
+
logger.info(
|
|
695
|
+
f"Device {self.logger.log_data.get('statistics', {}).get('device', {}).get('current')} out of {devices_total}"
|
|
696
|
+
)
|
|
619
697
|
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
698
|
+
# The Device exists now, so we can update the master of the VC.
|
|
699
|
+
# The logic is handled in transform maps.
|
|
700
|
+
self.sync_model(
|
|
701
|
+
"dcim",
|
|
702
|
+
"virtualchassis",
|
|
703
|
+
virtual_chassis,
|
|
704
|
+
False,
|
|
705
|
+
sync=self.settings.get("virtualchassis"),
|
|
706
|
+
)
|
|
623
707
|
|
|
624
708
|
device_interfaces = interface_dict.get(device.get("sn"), [])
|
|
625
709
|
for device_interface in device_interfaces:
|
|
626
|
-
|
|
710
|
+
self.sync_interface(
|
|
627
711
|
device_interface, managed_ips, device_object, device
|
|
628
712
|
)
|
|
629
|
-
if interface_object is None:
|
|
630
|
-
continue
|
|
631
|
-
self.logger.increment_statistics(model="interface")
|
|
632
713
|
|
|
633
714
|
@handle_errors
|
|
634
715
|
def sync_ipaddress(
|
|
@@ -638,22 +719,20 @@ class IPFabricSyncRunner(object):
|
|
|
638
719
|
primary_ip: str | None,
|
|
639
720
|
login_ip: str | None,
|
|
640
721
|
):
|
|
641
|
-
|
|
642
|
-
return
|
|
643
|
-
ip_address_obj = self.get_model_or_update(
|
|
722
|
+
ip_address_obj: "IPAddress | None" = self.sync_model(
|
|
644
723
|
"ipam",
|
|
645
724
|
"ipaddress",
|
|
646
725
|
managed_ip,
|
|
726
|
+
sync=self.settings.get("ipaddress"),
|
|
647
727
|
)
|
|
648
728
|
if ip_address_obj is None:
|
|
649
|
-
return
|
|
650
|
-
self.logger.increment_statistics(model="ipaddress")
|
|
729
|
+
return None
|
|
651
730
|
|
|
652
731
|
connection_name = self.get_db_connection_name()
|
|
653
732
|
|
|
654
733
|
try:
|
|
655
|
-
# Removing
|
|
656
|
-
# But do it here too so the change is shown in StagedChange diff
|
|
734
|
+
# Removing another IP is done in .signals.clear_other_primary_ip
|
|
735
|
+
# But do it here too, so the change is shown in StagedChange diff
|
|
657
736
|
other_device = Device.objects.using(connection_name).get(
|
|
658
737
|
primary_ip4=ip_address_obj
|
|
659
738
|
)
|
|
@@ -669,7 +748,7 @@ class IPFabricSyncRunner(object):
|
|
|
669
748
|
device_object.snapshot()
|
|
670
749
|
device_object.primary_ip4 = ip_address_obj
|
|
671
750
|
device_object.save(using=connection_name)
|
|
672
|
-
except ValueError as err:
|
|
751
|
+
except (ValueError, AttributeError) as err:
|
|
673
752
|
self.logger.log_failure(
|
|
674
753
|
f"Error assigning primary IP to device: {err}", obj=self.sync
|
|
675
754
|
)
|
|
@@ -680,17 +759,17 @@ class IPFabricSyncRunner(object):
|
|
|
680
759
|
def sync_macaddress(
|
|
681
760
|
self, data: dict | None, interface_object: Interface
|
|
682
761
|
) -> MACAddress | None:
|
|
683
|
-
if not self.settings.get("macaddress") or not data:
|
|
684
|
-
return None
|
|
685
762
|
# Need to create MAC Address object before we can assign it to Interface
|
|
686
763
|
# TODO: Figure out how to do this using transform maps
|
|
687
764
|
macaddress_data = {
|
|
688
765
|
"mac": data,
|
|
689
|
-
"id": interface_object
|
|
766
|
+
"id": getattr(interface_object, "pk", None),
|
|
690
767
|
}
|
|
691
|
-
macaddress_object = self.
|
|
692
|
-
"dcim", "macaddress", macaddress_data
|
|
768
|
+
macaddress_object: MACAddress | None = self.sync_model(
|
|
769
|
+
"dcim", "macaddress", macaddress_data, sync=self.settings.get("macaddress")
|
|
693
770
|
)
|
|
771
|
+
if macaddress_object is None:
|
|
772
|
+
return None
|
|
694
773
|
try:
|
|
695
774
|
interface_object.snapshot()
|
|
696
775
|
interface_object.primary_mac_address = macaddress_object
|
|
@@ -711,15 +790,19 @@ class IPFabricSyncRunner(object):
|
|
|
711
790
|
device: dict,
|
|
712
791
|
):
|
|
713
792
|
device_interface["loginIp"] = device.get("loginIp")
|
|
714
|
-
|
|
715
|
-
|
|
793
|
+
# We need to get an Interface instance even when not syncing it but syncing IPs or MACs
|
|
794
|
+
interface_object: Interface | None = self.sync_model(
|
|
795
|
+
"dcim",
|
|
796
|
+
"interface",
|
|
797
|
+
device_interface,
|
|
798
|
+
sync=self.settings.get("interface")
|
|
799
|
+
or self.settings.get("ipaddress")
|
|
800
|
+
or self.settings.get("macaddress"),
|
|
716
801
|
)
|
|
717
|
-
if interface_object is None:
|
|
718
|
-
return None
|
|
719
802
|
|
|
720
|
-
for ipaddress in managed_ips.get(
|
|
721
|
-
|
|
722
|
-
):
|
|
803
|
+
for ipaddress in managed_ips.get(
|
|
804
|
+
getattr(device_object, "serial", None), {}
|
|
805
|
+
).get(getattr(interface_object, "name", None), []):
|
|
723
806
|
self.sync_ipaddress(
|
|
724
807
|
ipaddress,
|
|
725
808
|
device_object,
|
ipfabric_netbox/views.py
CHANGED
|
@@ -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,13 +1,13 @@
|
|
|
1
|
-
ipfabric_netbox/__init__.py,sha256=
|
|
1
|
+
ipfabric_netbox/__init__.py,sha256=GlnjXNerOuwwTdZ-lspaHy84E4hf8GkMQ9jafQrWn70,674
|
|
2
2
|
ipfabric_netbox/api/__init__.py,sha256=DOkvDAI4BoNgdCiNxfseeExEHyOrK8weG-LvjPRyK8A,101
|
|
3
|
-
ipfabric_netbox/api/nested_serializers.py,sha256=
|
|
4
|
-
ipfabric_netbox/api/serializers.py,sha256=
|
|
5
|
-
ipfabric_netbox/api/urls.py,sha256=
|
|
6
|
-
ipfabric_netbox/api/views.py,sha256=
|
|
3
|
+
ipfabric_netbox/api/nested_serializers.py,sha256=dG9YdK6wY6dDEvUWsjzsla-J_GnB5szzDjrcZhdWoEg,2124
|
|
4
|
+
ipfabric_netbox/api/serializers.py,sha256=KihGDzoYTM9M9lrU0uUQPFl3ru7t59MuIebBdQe_DhY,5130
|
|
5
|
+
ipfabric_netbox/api/urls.py,sha256=oZuDEJ6C7dpz8c9J3aJGi1CBSAij8Y4ClLa3Lu5AWk0,1236
|
|
6
|
+
ipfabric_netbox/api/views.py,sha256=j-5-t22519p-4iJxx6PwUNaJumdweIHCdUkYKijiTyI,5041
|
|
7
7
|
ipfabric_netbox/choices.py,sha256=tQ5RHwmuArQ7dCLcE3A_Bnw2ctGHgw67z--AawyyPPg,5261
|
|
8
8
|
ipfabric_netbox/data/transform_map.json,sha256=4PsucgMHcLW3SPoKEptQCd0gA5tCF4hjrR4bGQFCWy8,21744
|
|
9
|
-
ipfabric_netbox/exceptions.py,sha256=
|
|
10
|
-
ipfabric_netbox/filtersets.py,sha256=
|
|
9
|
+
ipfabric_netbox/exceptions.py,sha256=DT4dpbakvqoROtBR_F0LzvQCMNWpGhufFcUbZTx0OLY,2655
|
|
10
|
+
ipfabric_netbox/filtersets.py,sha256=unVb64gR_m5oMRo1nP4yWrcogMRvfnzVQRmna6-9fKw,6121
|
|
11
11
|
ipfabric_netbox/forms.py,sha256=07p_vngzsnHG0aev83JqBhb8fSBts0w-MTKu4BuoNLw,44494
|
|
12
12
|
ipfabric_netbox/jobs.py,sha256=KrTUeCuFUIU7vKCUS3RiBYCBG7g7GzhGagM_qFMGQJ4,3089
|
|
13
13
|
ipfabric_netbox/migrations/0001_initial.py,sha256=VphxkWL6QzWq2tcrdXlog718xQtiEGizKwS830z_fOs,13824
|
|
@@ -25,11 +25,12 @@ ipfabric_netbox/migrations/0011_update_part_number_DCIM_inventory_item_template.
|
|
|
25
25
|
ipfabric_netbox/migrations/0012_remove_status_field.py,sha256=UZTY2BOBuV4Y_7zuPRfsCFLo57HZ6DsNRIhFlF_VOTM,387
|
|
26
26
|
ipfabric_netbox/migrations/0013_switch_to_branching_plugin.py,sha256=JfdTNerjuFyjW3pDm4vueOp2lrDh2xt6lL1Et5i4crg,10754
|
|
27
27
|
ipfabric_netbox/migrations/0014_ipfabrictransformmapgroup_ipfabrictransformmap_group.py,sha256=RCfgKyqQhTkfSPhf0IukI3fjeAEBUs5pKWIpsLxgzp0,2272
|
|
28
|
+
ipfabric_netbox/migrations/0015_ipfabricingestionissue.py,sha256=AjAkyboa4BSXsN53BqzO1k_U6QHu4rlA2IhzhubocJw,1732
|
|
28
29
|
ipfabric_netbox/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
29
|
-
ipfabric_netbox/models.py,sha256=
|
|
30
|
+
ipfabric_netbox/models.py,sha256=CXRMO1nKlcVz_R9WbTjQ0FWsXe1C6ZHIEoRhavI8Jyo,36067
|
|
30
31
|
ipfabric_netbox/navigation.py,sha256=2dEJ_wKHb52Tl0FOV1TH3JbxRe8YZ56ewrTsBFGKpCg,2210
|
|
31
32
|
ipfabric_netbox/signals.py,sha256=cGa5PVD2i24pGXiVNfbu6ruIDqPVdwKQHTSWe9Ura84,1838
|
|
32
|
-
ipfabric_netbox/tables.py,sha256=
|
|
33
|
+
ipfabric_netbox/tables.py,sha256=pHKv6Bosjnc-7aApak3nLhzxqBiA30olPdaMO8F1PQg,8262
|
|
33
34
|
ipfabric_netbox/template_content.py,sha256=bP2nUf5MM-GUbtQxJ8QoYVNXhrDCrkb8wZnbeqa07EA,315
|
|
34
35
|
ipfabric_netbox/templates/ipfabric_netbox/inc/clone_form.html,sha256=K-2TTDaS1F4wUIR8FFFPqex4KJbySXtHiz5V-OEwelY,967
|
|
35
36
|
ipfabric_netbox/templates/ipfabric_netbox/inc/diff.html,sha256=xOiIrvRIBtqDD65u6xcLo2xdwDKNpAylDmzznaJRGCw,3281
|
|
@@ -42,7 +43,7 @@ ipfabric_netbox/templates/ipfabric_netbox/inc/snapshotdata.html,sha256=1ItOCPjjp
|
|
|
42
43
|
ipfabric_netbox/templates/ipfabric_netbox/inc/transform_map_field_map.html,sha256=mRU-rBweVFvaPFHbVYPw7vcYyXiVaXCOkeHm7xWdKPA,500
|
|
43
44
|
ipfabric_netbox/templates/ipfabric_netbox/inc/transform_map_relationship_map.html,sha256=qyuG_EXZMGUscA3sv_6WGSrf_bR7JTRlKyMYf6EYyo4,504
|
|
44
45
|
ipfabric_netbox/templates/ipfabric_netbox/ipfabric_table.html,sha256=d5qcBeKDgh57eSZ6J1oYc--qfGmHRC6PLhrffFBQiM8,1631
|
|
45
|
-
ipfabric_netbox/templates/ipfabric_netbox/ipfabricingestion.html,sha256=
|
|
46
|
+
ipfabric_netbox/templates/ipfabric_netbox/ipfabricingestion.html,sha256=fm_X2FLnoTS6s6AL3WmU6p3puDojROSkPG0jA4EBQeM,4435
|
|
46
47
|
ipfabric_netbox/templates/ipfabric_netbox/ipfabricsnapshot.html,sha256=hj8ORs_4mM_xTjmw3McHN-da5seC8nbbkzobn0f1TSc,3482
|
|
47
48
|
ipfabric_netbox/templates/ipfabric_netbox/ipfabricsource.html,sha256=koR_t6Mf2FhWlPZHchWsTOQDSLB7AWrqtY0TRnIzrrM,3864
|
|
48
49
|
ipfabric_netbox/templates/ipfabric_netbox/ipfabricsync.html,sha256=BicVS7mCP85fFZJEt46GUm5xppi1Jw3byw1el9BB2WE,4448
|
|
@@ -51,10 +52,12 @@ ipfabric_netbox/templates/ipfabric_netbox/ipfabrictransformmap.html,sha256=qFo_K
|
|
|
51
52
|
ipfabric_netbox/templates/ipfabric_netbox/ipfabrictransformmap_list.html,sha256=p8zqn0-B6mawSUM3zQrus6dsKUM5SRBTO0X94pLboX8,452
|
|
52
53
|
ipfabric_netbox/templates/ipfabric_netbox/ipfabrictransformmap_restore.html,sha256=cuvOBKnlMudMI-CX77W3vTpwhtc4CBcc6j6Ye-IBBMQ,2562
|
|
53
54
|
ipfabric_netbox/templates/ipfabric_netbox/ipfabrictransformmapgroup.html,sha256=H3uvjA4PdJq5uX2kizdHV1pAxwcPpyfc9IbJi1ZK5-k,975
|
|
54
|
-
ipfabric_netbox/templates/ipfabric_netbox/partials/ingestion_all.html,sha256=
|
|
55
|
+
ipfabric_netbox/templates/ipfabric_netbox/partials/ingestion_all.html,sha256=W9W31m8b7mGrsfu0f_hE1TcudDqSJ0G2Bp9JE4UTIIE,662
|
|
55
56
|
ipfabric_netbox/templates/ipfabric_netbox/partials/ingestion_progress.html,sha256=NellDFy1qzgVbCtkOZbiSi3ufau6FOLIQPUoNiU6Bg4,595
|
|
56
|
-
ipfabric_netbox/templates/ipfabric_netbox/partials/
|
|
57
|
+
ipfabric_netbox/templates/ipfabric_netbox/partials/ingestion_statistics.html,sha256=_KtbxZQlIa_141OQSIAvSsMMfZRkNj27kJqrYHuLt7Y,867
|
|
58
|
+
ipfabric_netbox/templates/ipfabric_netbox/partials/ingestion_status.html,sha256=qYXe-V-kQn1INqCHLSSpdrzXJ_IIt84AgCgAGwqpNbs,178
|
|
57
59
|
ipfabric_netbox/templates/ipfabric_netbox/partials/job_logs.html,sha256=PMZxxrRVzn_JI8k5XCNV5BQnoFj9zYePN6afP6C_J2E,1622
|
|
60
|
+
ipfabric_netbox/templates/ipfabric_netbox/partials/object_tabs.html,sha256=3xbGy38Y1RH13bVPlcyj5L6J_FM-mOW1y0fx54hj1xE,364
|
|
58
61
|
ipfabric_netbox/templates/ipfabric_netbox/partials/sync_last_ingestion.html,sha256=3v6tC88xE5cic9l9lQw7Msawoq-AW2oo2odd-osGv50,179
|
|
59
62
|
ipfabric_netbox/templates/static/ipfabric_netbox/css/rack.css,sha256=z1H-RmmsqF2pGdhn5J64TMFiccy62xZUHHlCRd8fJrQ,118
|
|
60
63
|
ipfabric_netbox/templatetags/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -63,11 +66,11 @@ ipfabric_netbox/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3h
|
|
|
63
66
|
ipfabric_netbox/tests/test_models.py,sha256=4SGMX0LZ_fThkc-F1AMZIQbpmJvQNVFnS6QYZpIZkNA,14998
|
|
64
67
|
ipfabric_netbox/urls.py,sha256=ok66LP09rYi01qJmwdGGlBzV9wrGWVwVAIngPcreJxg,5449
|
|
65
68
|
ipfabric_netbox/utilities/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
66
|
-
ipfabric_netbox/utilities/ipfutils.py,sha256=
|
|
69
|
+
ipfabric_netbox/utilities/ipfutils.py,sha256=u7fn4HaBwhs8CXuoUsYv1CfFfSLN3JYc2ByeN0kjF8g,31370
|
|
67
70
|
ipfabric_netbox/utilities/logging.py,sha256=GYknjocMN6LQ2873_az3y0RKm29TCXaWviUIIneH-x0,3445
|
|
68
71
|
ipfabric_netbox/utilities/nbutils.py,sha256=kFBEiJOGvr_49hJWCS2duXojx2-A9kVk0Xp_vj0ohfs,2641
|
|
69
72
|
ipfabric_netbox/utilities/transform_map.py,sha256=QotbGc2TksINJrb62STgAigpC5Nsgi5umYHu_0rZd8k,2204
|
|
70
|
-
ipfabric_netbox/views.py,sha256=
|
|
71
|
-
ipfabric_netbox-4.1.
|
|
72
|
-
ipfabric_netbox-4.1.
|
|
73
|
-
ipfabric_netbox-4.1.
|
|
73
|
+
ipfabric_netbox/views.py,sha256=nGWO-PlTyKviBzGEdmE3C_1I-xFjZMldqOWGx12wdbg,36855
|
|
74
|
+
ipfabric_netbox-4.1.0b5.dist-info/METADATA,sha256=MeNMpz-OvdEyzOrg39PNkuGqbyXaKTVdHUR2Y8v4psg,4548
|
|
75
|
+
ipfabric_netbox-4.1.0b5.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
|
76
|
+
ipfabric_netbox-4.1.0b5.dist-info/RECORD,,
|
|
File without changes
|