ipfabric_netbox 4.1.0b3__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.

Files changed (78) hide show
  1. {ipfabric_netbox-4.1.0b3 → ipfabric_netbox-4.1.0b5}/PKG-INFO +1 -1
  2. {ipfabric_netbox-4.1.0b3 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/__init__.py +1 -1
  3. {ipfabric_netbox-4.1.0b3 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/api/nested_serializers.py +0 -1
  4. {ipfabric_netbox-4.1.0b3 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/api/serializers.py +23 -1
  5. {ipfabric_netbox-4.1.0b3 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/api/urls.py +2 -0
  6. {ipfabric_netbox-4.1.0b3 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/api/views.py +7 -0
  7. ipfabric_netbox-4.1.0b5/ipfabric_netbox/exceptions.py +81 -0
  8. {ipfabric_netbox-4.1.0b3 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/filtersets.py +30 -0
  9. ipfabric_netbox-4.1.0b5/ipfabric_netbox/migrations/0015_ipfabricingestionissue.py +48 -0
  10. {ipfabric_netbox-4.1.0b3 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/models.py +25 -0
  11. {ipfabric_netbox-4.1.0b3 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/tables.py +23 -0
  12. {ipfabric_netbox-4.1.0b3 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/templates/ipfabric_netbox/ipfabricingestion.html +18 -47
  13. {ipfabric_netbox-4.1.0b3 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/templates/ipfabric_netbox/partials/ingestion_all.html +6 -1
  14. ipfabric_netbox-4.1.0b5/ipfabric_netbox/templates/ipfabric_netbox/partials/ingestion_statistics.html +37 -0
  15. ipfabric_netbox-4.1.0b5/ipfabric_netbox/templates/ipfabric_netbox/partials/ingestion_status.html +1 -0
  16. ipfabric_netbox-4.1.0b5/ipfabric_netbox/templates/ipfabric_netbox/partials/object_tabs.html +12 -0
  17. {ipfabric_netbox-4.1.0b3 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/utilities/ipfutils.py +158 -75
  18. {ipfabric_netbox-4.1.0b3 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/views.py +55 -30
  19. {ipfabric_netbox-4.1.0b3 → ipfabric_netbox-4.1.0b5}/pyproject.toml +1 -1
  20. ipfabric_netbox-4.1.0b3/ipfabric_netbox/exceptions.py +0 -24
  21. ipfabric_netbox-4.1.0b3/ipfabric_netbox/templates/ipfabric_netbox/partials/ingestion_status.html +0 -1
  22. {ipfabric_netbox-4.1.0b3 → ipfabric_netbox-4.1.0b5}/README.md +0 -0
  23. {ipfabric_netbox-4.1.0b3 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/api/__init__.py +0 -0
  24. {ipfabric_netbox-4.1.0b3 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/choices.py +0 -0
  25. {ipfabric_netbox-4.1.0b3 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/data/transform_map.json +0 -0
  26. {ipfabric_netbox-4.1.0b3 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/forms.py +0 -0
  27. {ipfabric_netbox-4.1.0b3 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/jobs.py +0 -0
  28. {ipfabric_netbox-4.1.0b3 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/migrations/0001_initial.py +0 -0
  29. {ipfabric_netbox-4.1.0b3 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/migrations/0001_initial_squashed_0013_switch_to_branching_plugin.py +0 -0
  30. {ipfabric_netbox-4.1.0b3 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/migrations/0002_ipfabricsnapshot_status.py +0 -0
  31. {ipfabric_netbox-4.1.0b3 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/migrations/0003_ipfabricsource_type_and_more.py +0 -0
  32. {ipfabric_netbox-4.1.0b3 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/migrations/0004_ipfabricsync_auto_merge.py +0 -0
  33. {ipfabric_netbox-4.1.0b3 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/migrations/0005_alter_ipfabricrelationshipfield_source_model_and_more.py +0 -0
  34. {ipfabric_netbox-4.1.0b3 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/migrations/0006_alter_ipfabrictransformmap_target_model.py +0 -0
  35. {ipfabric_netbox-4.1.0b3 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/migrations/0007_prepare_custom_fields.py +0 -0
  36. {ipfabric_netbox-4.1.0b3 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/migrations/0008_prepare_transform_maps.py +0 -0
  37. {ipfabric_netbox-4.1.0b3 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/migrations/0009_transformmap_changes_for_netbox_v4_2.py +0 -0
  38. {ipfabric_netbox-4.1.0b3 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/migrations/0010_remove_uuid_from_get_or_create.py +0 -0
  39. {ipfabric_netbox-4.1.0b3 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/migrations/0011_update_part_number_DCIM_inventory_item_template.py +0 -0
  40. {ipfabric_netbox-4.1.0b3 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/migrations/0012_remove_status_field.py +0 -0
  41. {ipfabric_netbox-4.1.0b3 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/migrations/0013_switch_to_branching_plugin.py +0 -0
  42. {ipfabric_netbox-4.1.0b3 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/migrations/0014_ipfabrictransformmapgroup_ipfabrictransformmap_group.py +0 -0
  43. {ipfabric_netbox-4.1.0b3 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/migrations/__init__.py +0 -0
  44. {ipfabric_netbox-4.1.0b3 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/navigation.py +0 -0
  45. {ipfabric_netbox-4.1.0b3 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/signals.py +0 -0
  46. {ipfabric_netbox-4.1.0b3 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/template_content.py +0 -0
  47. {ipfabric_netbox-4.1.0b3 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/templates/ipfabric_netbox/inc/clone_form.html +0 -0
  48. {ipfabric_netbox-4.1.0b3 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/templates/ipfabric_netbox/inc/diff.html +0 -0
  49. {ipfabric_netbox-4.1.0b3 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/templates/ipfabric_netbox/inc/json.html +0 -0
  50. {ipfabric_netbox-4.1.0b3 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/templates/ipfabric_netbox/inc/logs_pending.html +0 -0
  51. {ipfabric_netbox-4.1.0b3 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/templates/ipfabric_netbox/inc/merge_form.html +0 -0
  52. {ipfabric_netbox-4.1.0b3 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/templates/ipfabric_netbox/inc/site_topology_button.html +0 -0
  53. {ipfabric_netbox-4.1.0b3 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/templates/ipfabric_netbox/inc/site_topology_modal.html +0 -0
  54. {ipfabric_netbox-4.1.0b3 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/templates/ipfabric_netbox/inc/snapshotdata.html +0 -0
  55. {ipfabric_netbox-4.1.0b3 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/templates/ipfabric_netbox/inc/transform_map_field_map.html +0 -0
  56. {ipfabric_netbox-4.1.0b3 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/templates/ipfabric_netbox/inc/transform_map_relationship_map.html +0 -0
  57. {ipfabric_netbox-4.1.0b3 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/templates/ipfabric_netbox/ipfabric_table.html +0 -0
  58. {ipfabric_netbox-4.1.0b3 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/templates/ipfabric_netbox/ipfabricsnapshot.html +0 -0
  59. {ipfabric_netbox-4.1.0b3 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/templates/ipfabric_netbox/ipfabricsource.html +0 -0
  60. {ipfabric_netbox-4.1.0b3 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/templates/ipfabric_netbox/ipfabricsync.html +0 -0
  61. {ipfabric_netbox-4.1.0b3 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/templates/ipfabric_netbox/ipfabricsync_list.html +0 -0
  62. {ipfabric_netbox-4.1.0b3 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/templates/ipfabric_netbox/ipfabrictransformmap.html +0 -0
  63. {ipfabric_netbox-4.1.0b3 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/templates/ipfabric_netbox/ipfabrictransformmap_list.html +0 -0
  64. {ipfabric_netbox-4.1.0b3 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/templates/ipfabric_netbox/ipfabrictransformmap_restore.html +0 -0
  65. {ipfabric_netbox-4.1.0b3 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/templates/ipfabric_netbox/ipfabrictransformmapgroup.html +0 -0
  66. {ipfabric_netbox-4.1.0b3 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/templates/ipfabric_netbox/partials/ingestion_progress.html +0 -0
  67. {ipfabric_netbox-4.1.0b3 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/templates/ipfabric_netbox/partials/job_logs.html +0 -0
  68. {ipfabric_netbox-4.1.0b3 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/templates/ipfabric_netbox/partials/sync_last_ingestion.html +0 -0
  69. {ipfabric_netbox-4.1.0b3 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/templates/static/ipfabric_netbox/css/rack.css +0 -0
  70. {ipfabric_netbox-4.1.0b3 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/templatetags/__init__.py +0 -0
  71. {ipfabric_netbox-4.1.0b3 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/templatetags/ipfabric_netbox_helpers.py +0 -0
  72. {ipfabric_netbox-4.1.0b3 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/tests/__init__.py +0 -0
  73. {ipfabric_netbox-4.1.0b3 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/tests/test_models.py +0 -0
  74. {ipfabric_netbox-4.1.0b3 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/urls.py +0 -0
  75. {ipfabric_netbox-4.1.0b3 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/utilities/__init__.py +0 -0
  76. {ipfabric_netbox-4.1.0b3 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/utilities/logging.py +0 -0
  77. {ipfabric_netbox-4.1.0b3 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/utilities/nbutils.py +0 -0
  78. {ipfabric_netbox-4.1.0b3 → ipfabric_netbox-4.1.0b5}/ipfabric_netbox/utilities/transform_map.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: ipfabric_netbox
3
- Version: 4.1.0b3
3
+ Version: 4.1.0b5
4
4
  Summary: NetBox plugin to sync IP Fabric data into NetBox
5
5
  License: MIT
6
6
  Keywords: netbox,ipfabric,plugin,sync
@@ -6,7 +6,7 @@ class NetboxIPFabricConfig(PluginConfig):
6
6
  name = "ipfabric_netbox"
7
7
  verbose_name = "NetBox IP Fabric SoT Plugin"
8
8
  description = "Sync IP Fabric into NetBox"
9
- version = "4.1.0b3"
9
+ version = "4.1.0b5"
10
10
  base_url = "ipfabric"
11
11
  min_version = "4.2.4"
12
12
 
@@ -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 = IPFabricSyncSerializer(read_only=True)
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 class="card">
92
- <h5 class="card-header">Statistics</h5>
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
- </div>
136
- <div class="row mb-3">
137
- <div class="col col-md-12" {% if not object.job.completed %} hx-get="{% url 'plugins:ipfabric_netbox:ipfabricingestion_logs' pk=object.pk %}?type=info"
138
- hx-trigger="every 5s" hx-target="#ingestion_logs" hx-select="#ingestion_logs" hx-select-oob="#ingestion_status:innerHTML"
139
- hx-swap="innerHTML" {% endif %}>
140
- <div id="ingestion_logs">{% include 'ipfabric_netbox/partials/job_logs.html' with job=object.job %}</div>
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
- {% block modals %}
145
- {% include 'inc/htmx_modal.html' with size='lg' %}
146
- {% endblock modals %}
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>
@@ -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>