ipfabric_netbox 4.3.0b6__tar.gz → 4.3.0b8__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.3.0b6 → ipfabric_netbox-4.3.0b8}/PKG-INFO +1 -1
- {ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/ipfabric_netbox/__init__.py +1 -1
- {ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/ipfabric_netbox/api/serializers.py +5 -0
- {ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/ipfabric_netbox/api/views.py +47 -0
- {ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/ipfabric_netbox/forms.py +1 -5
- {ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/ipfabric_netbox/jobs.py +35 -13
- ipfabric_netbox-4.3.0b8/ipfabric_netbox/migrations/0020_clean_scheduled_jobs.py +70 -0
- {ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/ipfabric_netbox/models.py +51 -21
- {ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/ipfabric_netbox/templates/ipfabric_netbox/ipfabricsync.html +1 -1
- {ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/ipfabric_netbox/tests/api/test_api.py +149 -22
- {ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/ipfabric_netbox/tests/test_views.py +5 -2
- {ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/ipfabric_netbox/views.py +10 -2
- {ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/pyproject.toml +1 -1
- {ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/README.md +0 -0
- {ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/ipfabric_netbox/api/__init__.py +0 -0
- {ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/ipfabric_netbox/api/urls.py +0 -0
- {ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/ipfabric_netbox/choices.py +0 -0
- {ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/ipfabric_netbox/data/transform_map.json +0 -0
- {ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/ipfabric_netbox/exceptions.py +0 -0
- {ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/ipfabric_netbox/filtersets.py +0 -0
- {ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/ipfabric_netbox/graphql/__init__.py +0 -0
- {ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/ipfabric_netbox/graphql/enums.py +0 -0
- {ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/ipfabric_netbox/graphql/filters.py +0 -0
- {ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/ipfabric_netbox/graphql/schema.py +0 -0
- {ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/ipfabric_netbox/graphql/types.py +0 -0
- {ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/ipfabric_netbox/migrations/0001_initial.py +0 -0
- {ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/ipfabric_netbox/migrations/0001_initial_squashed_0013_switch_to_branching_plugin.py +0 -0
- {ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/ipfabric_netbox/migrations/0002_ipfabricsnapshot_status.py +0 -0
- {ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/ipfabric_netbox/migrations/0003_ipfabricsource_type_and_more.py +0 -0
- {ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/ipfabric_netbox/migrations/0004_ipfabricsync_auto_merge.py +0 -0
- {ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/ipfabric_netbox/migrations/0005_alter_ipfabricrelationshipfield_source_model_and_more.py +0 -0
- {ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/ipfabric_netbox/migrations/0006_alter_ipfabrictransformmap_target_model.py +0 -0
- {ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/ipfabric_netbox/migrations/0007_prepare_custom_fields.py +0 -0
- {ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/ipfabric_netbox/migrations/0008_prepare_transform_maps.py +0 -0
- {ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/ipfabric_netbox/migrations/0009_transformmap_changes_for_netbox_v4_2.py +0 -0
- {ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/ipfabric_netbox/migrations/0010_remove_uuid_from_get_or_create.py +0 -0
- {ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/ipfabric_netbox/migrations/0011_update_part_number_DCIM_inventory_item_template.py +0 -0
- {ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/ipfabric_netbox/migrations/0012_remove_status_field.py +0 -0
- {ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/ipfabric_netbox/migrations/0013_switch_to_branching_plugin.py +0 -0
- {ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/ipfabric_netbox/migrations/0014_ipfabrictransformmapgroup_ipfabrictransformmap_group.py +0 -0
- {ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/ipfabric_netbox/migrations/0015_ipfabricingestionissue.py +0 -0
- {ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/ipfabric_netbox/migrations/0016_tags_and_changelog_for_snapshots.py +0 -0
- {ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/ipfabric_netbox/migrations/0017_ipfabricsync_update_custom_fields.py +0 -0
- {ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/ipfabric_netbox/migrations/0018_remove_type_field.py +0 -0
- {ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/ipfabric_netbox/migrations/0019_alter_ipfabrictransformmap_options_and_more.py +0 -0
- {ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/ipfabric_netbox/migrations/__init__.py +0 -0
- {ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/ipfabric_netbox/navigation.py +0 -0
- {ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/ipfabric_netbox/signals.py +0 -0
- {ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/ipfabric_netbox/tables.py +0 -0
- {ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/ipfabric_netbox/template_content.py +0 -0
- {ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/ipfabric_netbox/templates/ipfabric_netbox/inc/clone_form.html +0 -0
- {ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/ipfabric_netbox/templates/ipfabric_netbox/inc/diff.html +0 -0
- {ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/ipfabric_netbox/templates/ipfabric_netbox/inc/json.html +0 -0
- {ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/ipfabric_netbox/templates/ipfabric_netbox/inc/logs_pending.html +0 -0
- {ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/ipfabric_netbox/templates/ipfabric_netbox/inc/merge_form.html +0 -0
- {ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/ipfabric_netbox/templates/ipfabric_netbox/inc/site_topology_button.html +0 -0
- {ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/ipfabric_netbox/templates/ipfabric_netbox/inc/site_topology_modal.html +0 -0
- {ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/ipfabric_netbox/templates/ipfabric_netbox/inc/snapshotdata.html +0 -0
- {ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/ipfabric_netbox/templates/ipfabric_netbox/inc/transform_map_field_map.html +0 -0
- {ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/ipfabric_netbox/templates/ipfabric_netbox/inc/transform_map_relationship_map.html +0 -0
- {ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/ipfabric_netbox/templates/ipfabric_netbox/ipfabric_table.html +0 -0
- {ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/ipfabric_netbox/templates/ipfabric_netbox/ipfabricingestion.html +0 -0
- {ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/ipfabric_netbox/templates/ipfabric_netbox/ipfabricsnapshot.html +0 -0
- {ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/ipfabric_netbox/templates/ipfabric_netbox/ipfabricsource.html +0 -0
- {ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/ipfabric_netbox/templates/ipfabric_netbox/ipfabrictransformmap.html +0 -0
- {ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/ipfabric_netbox/templates/ipfabric_netbox/ipfabrictransformmap_list.html +0 -0
- {ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/ipfabric_netbox/templates/ipfabric_netbox/ipfabrictransformmap_restore.html +0 -0
- {ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/ipfabric_netbox/templates/ipfabric_netbox/ipfabrictransformmapgroup.html +0 -0
- {ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/ipfabric_netbox/templates/ipfabric_netbox/partials/ingestion_all.html +0 -0
- {ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/ipfabric_netbox/templates/ipfabric_netbox/partials/ingestion_progress.html +0 -0
- {ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/ipfabric_netbox/templates/ipfabric_netbox/partials/ingestion_statistics.html +0 -0
- {ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/ipfabric_netbox/templates/ipfabric_netbox/partials/ingestion_status.html +0 -0
- {ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/ipfabric_netbox/templates/ipfabric_netbox/partials/job_logs.html +0 -0
- {ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/ipfabric_netbox/templates/ipfabric_netbox/partials/object_tabs.html +0 -0
- {ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/ipfabric_netbox/templates/ipfabric_netbox/partials/sync_last_ingestion.html +0 -0
- {ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/ipfabric_netbox/templates/static/ipfabric_netbox/css/rack.css +0 -0
- {ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/ipfabric_netbox/templatetags/__init__.py +0 -0
- {ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/ipfabric_netbox/templatetags/ipfabric_netbox_helpers.py +0 -0
- {ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/ipfabric_netbox/tests/__init__.py +0 -0
- {ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/ipfabric_netbox/tests/api/__init__.py +0 -0
- {ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/ipfabric_netbox/tests/test_forms.py +0 -0
- {ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/ipfabric_netbox/tests/test_models.py +0 -0
- {ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/ipfabric_netbox/urls.py +0 -0
- {ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/ipfabric_netbox/utilities/__init__.py +0 -0
- {ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/ipfabric_netbox/utilities/ipfutils.py +0 -0
- {ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/ipfabric_netbox/utilities/logging.py +0 -0
- {ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/ipfabric_netbox/utilities/nbutils.py +0 -0
- {ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/ipfabric_netbox/utilities/transform_map.py +0 -0
|
@@ -176,6 +176,7 @@ class IPFabricSnapshotSerializer(NestedGroupModelSerializer):
|
|
|
176
176
|
|
|
177
177
|
class IPFabricSyncSerializer(NestedGroupModelSerializer):
|
|
178
178
|
snapshot_data = IPFabricSnapshotSerializer(nested=True)
|
|
179
|
+
parameters = serializers.JSONField()
|
|
179
180
|
|
|
180
181
|
class Meta:
|
|
181
182
|
model = IPFabricSync
|
|
@@ -244,3 +245,7 @@ class IPFabricIngestionIssueSerializer(NestedGroupModelSerializer):
|
|
|
244
245
|
"message",
|
|
245
246
|
"model",
|
|
246
247
|
)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
class EmptySerializer(serializers.Serializer):
|
|
251
|
+
pass
|
|
@@ -1,10 +1,15 @@
|
|
|
1
|
+
from core.api.serializers_.jobs import JobSerializer
|
|
2
|
+
from django.core.exceptions import PermissionDenied
|
|
1
3
|
from django.db import transaction
|
|
4
|
+
from django.http import HttpResponseBadRequest
|
|
5
|
+
from drf_spectacular.utils import extend_schema
|
|
2
6
|
from netbox.api.viewsets import NetBoxModelViewSet
|
|
3
7
|
from netbox.api.viewsets import NetBoxReadOnlyModelViewSet
|
|
4
8
|
from rest_framework.decorators import action
|
|
5
9
|
from rest_framework.response import Response
|
|
6
10
|
from utilities.query import count_related
|
|
7
11
|
|
|
12
|
+
from .serializers import EmptySerializer
|
|
8
13
|
from .serializers import IPFabricIngestionIssueSerializer
|
|
9
14
|
from .serializers import IPFabricIngestionSerializer
|
|
10
15
|
from .serializers import IPFabricRelationshipFieldSerializer
|
|
@@ -56,6 +61,27 @@ class IPFabricSyncViewSet(NetBoxModelViewSet):
|
|
|
56
61
|
queryset = IPFabricSync.objects.all()
|
|
57
62
|
serializer_class = IPFabricSyncSerializer
|
|
58
63
|
|
|
64
|
+
@extend_schema(
|
|
65
|
+
methods=["post"],
|
|
66
|
+
request=EmptySerializer(),
|
|
67
|
+
responses={201: JobSerializer()},
|
|
68
|
+
)
|
|
69
|
+
@action(detail=True, methods=["post"])
|
|
70
|
+
def sync(self, request, pk):
|
|
71
|
+
if not request.user.has_perm("ipfabric_netbox.sync_ipfabricsync"):
|
|
72
|
+
raise PermissionDenied(
|
|
73
|
+
"This user does not have permission to sync IPFabricSync."
|
|
74
|
+
)
|
|
75
|
+
sync = self.get_object()
|
|
76
|
+
if not sync.ready_for_sync:
|
|
77
|
+
return HttpResponseBadRequest(
|
|
78
|
+
f"Sync '{sync.name}' is not ready to be synced."
|
|
79
|
+
)
|
|
80
|
+
job = sync.enqueue_sync_job(user=request.user, adhoc=True)
|
|
81
|
+
return Response(
|
|
82
|
+
JobSerializer(job, context={"request": request}).data, status=201
|
|
83
|
+
)
|
|
84
|
+
|
|
59
85
|
|
|
60
86
|
class IPFabricIngestionViewSet(NetBoxReadOnlyModelViewSet):
|
|
61
87
|
queryset = IPFabricIngestion.objects.all()
|
|
@@ -122,3 +148,24 @@ class IPFabricSourceViewSet(NetBoxModelViewSet):
|
|
|
122
148
|
)
|
|
123
149
|
serializer_class = IPFabricSourceSerializer
|
|
124
150
|
filterset_class = IPFabricSourceFilterSet
|
|
151
|
+
|
|
152
|
+
@extend_schema(
|
|
153
|
+
methods=["post"],
|
|
154
|
+
request=EmptySerializer(),
|
|
155
|
+
responses={201: JobSerializer()},
|
|
156
|
+
)
|
|
157
|
+
@action(detail=True, methods=["post"])
|
|
158
|
+
def sync(self, request, pk):
|
|
159
|
+
if not request.user.has_perm("ipfabric_netbox.sync_ipfabricsource"):
|
|
160
|
+
raise PermissionDenied(
|
|
161
|
+
"This user does not have permission to sync IPFabricSource."
|
|
162
|
+
)
|
|
163
|
+
source = self.get_object()
|
|
164
|
+
if not source.ready_for_sync:
|
|
165
|
+
return HttpResponseBadRequest(
|
|
166
|
+
f"Source '{source.name}' is not ready to be synced."
|
|
167
|
+
)
|
|
168
|
+
job = source.enqueue_sync_job(request=request)
|
|
169
|
+
return Response(
|
|
170
|
+
JobSerializer(job, context={"request": request}).data, status=201
|
|
171
|
+
)
|
|
@@ -802,11 +802,7 @@ class IPFabricSyncForm(NetBoxModelForm):
|
|
|
802
802
|
]
|
|
803
803
|
self.instance.parameters = parameters
|
|
804
804
|
self.instance.status = DataSourceStatusChoices.NEW
|
|
805
|
-
|
|
806
|
-
object = super().save(*args, **kwargs)
|
|
807
|
-
if object.scheduled:
|
|
808
|
-
object.enqueue_sync_job()
|
|
809
|
-
return object
|
|
805
|
+
return super().save(*args, **kwargs)
|
|
810
806
|
|
|
811
807
|
|
|
812
808
|
class IPFabricSyncBulkEditForm(NetBoxModelBulkEditForm):
|
|
@@ -4,7 +4,6 @@ from datetime import timedelta
|
|
|
4
4
|
from core.choices import DataSourceStatusChoices
|
|
5
5
|
from core.choices import JobStatusChoices
|
|
6
6
|
from core.exceptions import SyncError
|
|
7
|
-
from core.models import Job
|
|
8
7
|
from netbox.context_managers import event_tracking
|
|
9
8
|
from rq.timeouts import JobTimeoutException
|
|
10
9
|
from utilities.datetime import local_now
|
|
@@ -36,15 +35,15 @@ def sync_ipfabricsource(job, *args, **kwargs):
|
|
|
36
35
|
|
|
37
36
|
|
|
38
37
|
def sync_ipfabric(job, *args, **kwargs):
|
|
39
|
-
|
|
38
|
+
sync = IPFabricSync.objects.get(pk=job.object_id)
|
|
40
39
|
|
|
41
40
|
try:
|
|
42
41
|
job.start()
|
|
43
|
-
|
|
42
|
+
sync.sync(job=job)
|
|
44
43
|
job.terminate()
|
|
45
44
|
except Exception as e:
|
|
46
45
|
job.terminate(status=JobStatusChoices.STATUS_ERRORED)
|
|
47
|
-
IPFabricSync.objects.filter(pk=
|
|
46
|
+
IPFabricSync.objects.filter(pk=sync.pk).update(
|
|
48
47
|
status=DataSourceStatusChoices.FAILED
|
|
49
48
|
)
|
|
50
49
|
if type(e) in (SyncError, JobTimeoutException):
|
|
@@ -52,15 +51,38 @@ def sync_ipfabric(job, *args, **kwargs):
|
|
|
52
51
|
else:
|
|
53
52
|
raise e
|
|
54
53
|
finally:
|
|
55
|
-
if
|
|
56
|
-
new_scheduled_time = local_now() + timedelta(minutes=
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
54
|
+
if sync.interval and not kwargs.get("adhoc"):
|
|
55
|
+
new_scheduled_time = local_now() + timedelta(minutes=sync.interval)
|
|
56
|
+
# We want to create new Job only if scheduled time was before this Job started
|
|
57
|
+
# The current sync might have been changed while this job was running
|
|
58
|
+
sync.refresh_from_db()
|
|
59
|
+
if not sync.scheduled or (sync.scheduled and sync.scheduled > job.started):
|
|
60
|
+
logger.info(
|
|
61
|
+
f"Not scheduling a new job for IPFabricSync {sync.pk} as the scheduled time was changed while the job was running."
|
|
62
|
+
)
|
|
63
|
+
return
|
|
64
|
+
# Update the sync object with the new scheduled time
|
|
65
|
+
# This also triggers the creation of a new Job
|
|
66
|
+
# Running in fake request context to ensure user is set for changelog
|
|
67
|
+
request = NetBoxFakeRequest(
|
|
68
|
+
{
|
|
69
|
+
"META": {},
|
|
70
|
+
"POST": sync.parameters,
|
|
71
|
+
"GET": {},
|
|
72
|
+
"FILES": {},
|
|
73
|
+
"user": sync.user,
|
|
74
|
+
"path": "",
|
|
75
|
+
"id": job.job_id,
|
|
76
|
+
}
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
with event_tracking(request):
|
|
80
|
+
sync.scheduled = new_scheduled_time
|
|
81
|
+
sync.status = DataSourceStatusChoices.QUEUED
|
|
82
|
+
sync.full_clean()
|
|
83
|
+
sync.save()
|
|
84
|
+
logger.info(
|
|
85
|
+
f"Scheduled next sync for IPFabricSync {sync.pk} at {new_scheduled_time}."
|
|
64
86
|
)
|
|
65
87
|
|
|
66
88
|
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from datetime import timedelta
|
|
2
|
+
from typing import TYPE_CHECKING
|
|
3
|
+
|
|
4
|
+
from django.db import migrations
|
|
5
|
+
from utilities.datetime import local_now
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from django.apps import apps as apps_type
|
|
9
|
+
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def clean_scheduled_jobs(
|
|
13
|
+
apps: "apps_type", schema_editor: "BaseDatabaseSchemaEditor"
|
|
14
|
+
) -> None:
|
|
15
|
+
Job = apps.get_model("core", "Job")
|
|
16
|
+
IPFabricSync = apps.get_model("ipfabric_netbox", "IPFabricSync")
|
|
17
|
+
ObjectType = apps.get_model("core", "ObjectType")
|
|
18
|
+
|
|
19
|
+
for sync in IPFabricSync.objects.all():
|
|
20
|
+
try:
|
|
21
|
+
scheduled_jobs = Job.objects.filter(
|
|
22
|
+
object_id=sync.id,
|
|
23
|
+
object_type=ObjectType.objects.get_for_model(sync),
|
|
24
|
+
scheduled__isnull=False,
|
|
25
|
+
).order_by("scheduled")
|
|
26
|
+
if not scheduled_jobs.exists():
|
|
27
|
+
continue
|
|
28
|
+
if not sync.scheduled:
|
|
29
|
+
# Delete all scheduled jobs if the sync is not scheduled
|
|
30
|
+
scheduled_jobs.delete()
|
|
31
|
+
continue
|
|
32
|
+
if scheduled_jobs.count() == 1:
|
|
33
|
+
# Only one scheduled job exists, let's update scheduled time on the sync object
|
|
34
|
+
# This does not create a new job since sync is a Faked object in migration
|
|
35
|
+
sync.scheduled = scheduled_jobs.first().scheduled
|
|
36
|
+
sync.full_clean()
|
|
37
|
+
sync.save()
|
|
38
|
+
continue
|
|
39
|
+
# More than one scheduled job exists
|
|
40
|
+
# Find the one that is closest to scheduled + N * interval
|
|
41
|
+
interval = timedelta(minutes=sync.interval)
|
|
42
|
+
elapsed = local_now() - sync.scheduled
|
|
43
|
+
intervals_passed = (elapsed // interval) + 1
|
|
44
|
+
closest_future_scheduled = sync.scheduled + intervals_passed * interval
|
|
45
|
+
closest_job = min(
|
|
46
|
+
scheduled_jobs,
|
|
47
|
+
key=lambda job: abs(job.scheduled - closest_future_scheduled),
|
|
48
|
+
)
|
|
49
|
+
for job in scheduled_jobs:
|
|
50
|
+
if job != closest_job:
|
|
51
|
+
job.delete()
|
|
52
|
+
sync.scheduled = closest_job.scheduled
|
|
53
|
+
sync.full_clean()
|
|
54
|
+
sync.save()
|
|
55
|
+
except Exception as err:
|
|
56
|
+
# Always be safe inside a migration
|
|
57
|
+
print(f"Error cleaning scheduled jobs for IPFabricSync {sync.id}: {err}")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class Migration(migrations.Migration):
|
|
61
|
+
dependencies = [
|
|
62
|
+
(
|
|
63
|
+
"ipfabric_netbox",
|
|
64
|
+
"0019_alter_ipfabrictransformmap_options_and_more",
|
|
65
|
+
),
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
operations = [
|
|
69
|
+
migrations.RunPython(clean_scheduled_jobs, migrations.RunPython.noop),
|
|
70
|
+
]
|
|
@@ -8,6 +8,7 @@ from uuid import uuid4
|
|
|
8
8
|
|
|
9
9
|
import httpx
|
|
10
10
|
from core.choices import DataSourceStatusChoices
|
|
11
|
+
from core.choices import JobStatusChoices
|
|
11
12
|
from core.exceptions import SyncError
|
|
12
13
|
from core.models import Job
|
|
13
14
|
from core.models import ObjectType
|
|
@@ -669,6 +670,11 @@ class IPFabricSync(IPFabricClient, JobsMixin, TagsMixin, ChangeLoggedModel):
|
|
|
669
670
|
def get_status_color(self):
|
|
670
671
|
return DataSourceStatusChoices.colors.get(self.status)
|
|
671
672
|
|
|
673
|
+
def save(self, *args, **kwargs):
|
|
674
|
+
super().save(*args, **kwargs)
|
|
675
|
+
if self.scheduled:
|
|
676
|
+
self.enqueue_sync_job()
|
|
677
|
+
|
|
672
678
|
@property
|
|
673
679
|
def ready_for_sync(self):
|
|
674
680
|
if self.status not in (DataSourceStatusChoices.SYNCING,):
|
|
@@ -705,32 +711,56 @@ class IPFabricSync(IPFabricClient, JobsMixin, TagsMixin, ChangeLoggedModel):
|
|
|
705
711
|
pk__in=[tm.pk for tm in maps_by_target.values()]
|
|
706
712
|
)
|
|
707
713
|
|
|
708
|
-
def
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
714
|
+
def delete_scheduled_jobs(self) -> None:
|
|
715
|
+
Job.objects.filter(
|
|
716
|
+
object_type=ObjectType.objects.get_for_model(self),
|
|
717
|
+
object_id=self.pk,
|
|
718
|
+
status__in=[
|
|
719
|
+
JobStatusChoices.STATUS_PENDING,
|
|
720
|
+
JobStatusChoices.STATUS_SCHEDULED,
|
|
721
|
+
],
|
|
722
|
+
).delete()
|
|
723
|
+
|
|
724
|
+
def enqueue_sync_job(self, adhoc=False, user=None) -> Job | None:
|
|
725
|
+
def set_syncing_status():
|
|
726
|
+
self.status = DataSourceStatusChoices.QUEUED
|
|
727
|
+
IPFabricSync.objects.filter(pk=self.pk).update(status=self.status)
|
|
728
|
+
|
|
729
|
+
def sync_snapshots():
|
|
730
|
+
Job.enqueue(
|
|
731
|
+
import_string("ipfabric_netbox.jobs.sync_ipfabricsource"),
|
|
732
|
+
name=f"{self.name} Snapshot Sync (Pre Ingestion)",
|
|
733
|
+
instance=self.snapshot_data.source,
|
|
734
|
+
user=self.user,
|
|
735
|
+
)
|
|
719
736
|
|
|
720
737
|
# Enqueue a sync job
|
|
721
738
|
if not user:
|
|
722
739
|
user = self.user
|
|
723
740
|
|
|
724
|
-
if not adhoc
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
741
|
+
if not adhoc:
|
|
742
|
+
if self.scheduled:
|
|
743
|
+
# We want to schedule a recurring Job
|
|
744
|
+
# We need to replace the old scheduled Job to make sure it has current context
|
|
745
|
+
self.delete_scheduled_jobs()
|
|
746
|
+
set_syncing_status()
|
|
747
|
+
sync_snapshots()
|
|
748
|
+
job = Job.enqueue(
|
|
749
|
+
import_string("ipfabric_netbox.jobs.sync_ipfabric"),
|
|
750
|
+
name=f"{self.name} - (scheduled)",
|
|
751
|
+
instance=self,
|
|
752
|
+
user=self.user,
|
|
753
|
+
schedule_at=self.scheduled,
|
|
754
|
+
interval=self.interval,
|
|
755
|
+
)
|
|
756
|
+
else:
|
|
757
|
+
# There should be no scheduled Job anymore, clean it up
|
|
758
|
+
self.delete_scheduled_jobs()
|
|
759
|
+
job = None
|
|
760
|
+
else:
|
|
761
|
+
# Start adhoc job immediately
|
|
762
|
+
set_syncing_status()
|
|
763
|
+
sync_snapshots()
|
|
734
764
|
job = Job.enqueue(
|
|
735
765
|
import_string("ipfabric_netbox.jobs.sync_ipfabric"),
|
|
736
766
|
instance=self,
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
</tr>
|
|
59
59
|
<tr>
|
|
60
60
|
<th scope="row">Schedule</th>
|
|
61
|
-
<td><div>{{ object.scheduled }}</div></td>
|
|
61
|
+
<td><div>{% if scheduled_job %}<a href="{% url 'core:job' pk=scheduled_job.pk %}">{{ object.scheduled }}</a>{% else %} {{ object.scheduled | placeholder }} {% endif %}</div></td>
|
|
62
62
|
</tr>
|
|
63
63
|
<tr>
|
|
64
64
|
<th scope="row">Interval</th>
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from core.choices import DataSourceStatusChoices
|
|
1
2
|
from django.contrib.contenttypes.models import ContentType
|
|
2
3
|
from django.utils import timezone
|
|
3
4
|
from rest_framework import status
|
|
@@ -15,7 +16,7 @@ from ipfabric_netbox.models import IPFabricTransformMap
|
|
|
15
16
|
from ipfabric_netbox.models import IPFabricTransformMapGroup
|
|
16
17
|
|
|
17
18
|
|
|
18
|
-
BASE = "/api/plugins/ipfabric
|
|
19
|
+
BASE = "/api/plugins/ipfabric"
|
|
19
20
|
|
|
20
21
|
|
|
21
22
|
class IPFabricTransformMapGroupTest(APIViewTestCases.APIViewTestCase):
|
|
@@ -36,10 +37,10 @@ class IPFabricTransformMapGroupTest(APIViewTestCases.APIViewTestCase):
|
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
def _get_list_url(self):
|
|
39
|
-
return f"{BASE}transform-map-group/"
|
|
40
|
+
return f"{BASE}/transform-map-group/"
|
|
40
41
|
|
|
41
42
|
def _get_detail_url(self, instance):
|
|
42
|
-
return f"{BASE}transform-map-group/{instance.pk}/"
|
|
43
|
+
return f"{BASE}/transform-map-group/{instance.pk}/"
|
|
43
44
|
|
|
44
45
|
@classmethod
|
|
45
46
|
def setUpTestData(cls):
|
|
@@ -62,10 +63,10 @@ class IPFabricTransformMapTest(APIViewTestCases.APIViewTestCase):
|
|
|
62
63
|
]
|
|
63
64
|
|
|
64
65
|
def _get_list_url(self):
|
|
65
|
-
return f"{BASE}transform-map/"
|
|
66
|
+
return f"{BASE}/transform-map/"
|
|
66
67
|
|
|
67
68
|
def _get_detail_url(self, instance):
|
|
68
|
-
return f"{BASE}transform-map/{instance.pk}/"
|
|
69
|
+
return f"{BASE}/transform-map/{instance.pk}/"
|
|
69
70
|
|
|
70
71
|
@classmethod
|
|
71
72
|
def setUpTestData(cls):
|
|
@@ -132,10 +133,10 @@ class IPFabricTransformFieldTest(APIViewTestCases.APIViewTestCase):
|
|
|
132
133
|
]
|
|
133
134
|
|
|
134
135
|
def _get_list_url(self):
|
|
135
|
-
return f"{BASE}transform-field/"
|
|
136
|
+
return f"{BASE}/transform-field/"
|
|
136
137
|
|
|
137
138
|
def _get_detail_url(self, instance):
|
|
138
|
-
return f"{BASE}transform-field/{instance.pk}/"
|
|
139
|
+
return f"{BASE}/transform-field/{instance.pk}/"
|
|
139
140
|
|
|
140
141
|
@classmethod
|
|
141
142
|
def setUpTestData(cls):
|
|
@@ -235,10 +236,10 @@ class IPFabricRelationshipFieldTest(APIViewTestCases.APIViewTestCase):
|
|
|
235
236
|
]
|
|
236
237
|
|
|
237
238
|
def _get_list_url(self):
|
|
238
|
-
return f"{BASE}relationship-field/"
|
|
239
|
+
return f"{BASE}/relationship-field/"
|
|
239
240
|
|
|
240
241
|
def _get_detail_url(self, instance):
|
|
241
|
-
return f"{BASE}relationship-field/{instance.pk}/"
|
|
242
|
+
return f"{BASE}/relationship-field/{instance.pk}/"
|
|
242
243
|
|
|
243
244
|
@classmethod
|
|
244
245
|
def setUpTestData(cls):
|
|
@@ -340,10 +341,10 @@ class IPFabricSourceTest(APIViewTestCases.APIViewTestCase):
|
|
|
340
341
|
graphql_base_name = "ipfabric_source"
|
|
341
342
|
|
|
342
343
|
def _get_list_url(self):
|
|
343
|
-
return f"{BASE}source/"
|
|
344
|
+
return f"{BASE}/source/"
|
|
344
345
|
|
|
345
346
|
def _get_detail_url(self, instance):
|
|
346
|
-
return f"{BASE}source/{instance.pk}/"
|
|
347
|
+
return f"{BASE}/source/{instance.pk}/"
|
|
347
348
|
|
|
348
349
|
@classmethod
|
|
349
350
|
def setUpTestData(cls):
|
|
@@ -387,6 +388,66 @@ class IPFabricSourceTest(APIViewTestCases.APIViewTestCase):
|
|
|
387
388
|
},
|
|
388
389
|
]
|
|
389
390
|
|
|
391
|
+
def test_sync_action_success(self):
|
|
392
|
+
"""Test successful sync action with proper permissions and ready source."""
|
|
393
|
+
self.add_permissions(
|
|
394
|
+
"ipfabric_netbox.add_ipfabricsource",
|
|
395
|
+
"ipfabric_netbox.sync_ipfabricsource",
|
|
396
|
+
)
|
|
397
|
+
# Get the first source from setUpTestData
|
|
398
|
+
source = IPFabricSource.objects.first()
|
|
399
|
+
# Set status to make ready_for_sync return True
|
|
400
|
+
source.status = DataSourceStatusChoices.COMPLETED
|
|
401
|
+
source.save()
|
|
402
|
+
|
|
403
|
+
with self.settings(CELERY_TASK_ALWAYS_EAGER=True):
|
|
404
|
+
# Create a mock job object to simulate enqueue_sync_job response
|
|
405
|
+
from unittest.mock import Mock, patch
|
|
406
|
+
|
|
407
|
+
mock_job = Mock()
|
|
408
|
+
mock_job.id = "test-job-123"
|
|
409
|
+
mock_job.status = "queued"
|
|
410
|
+
|
|
411
|
+
with patch.object(source, "enqueue_sync_job", return_value=mock_job):
|
|
412
|
+
url = f"{BASE}/source/{source.pk}/sync/"
|
|
413
|
+
response = self.client.post(url, **self.header)
|
|
414
|
+
|
|
415
|
+
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
|
416
|
+
self.assertIn("id", response.data)
|
|
417
|
+
|
|
418
|
+
def test_sync_action_permission_denied(self):
|
|
419
|
+
"""Test sync action without proper permissions."""
|
|
420
|
+
# Note: Not adding sync_ipfabricsource permission
|
|
421
|
+
self.add_permissions(
|
|
422
|
+
"ipfabric_netbox.add_ipfabricsource",
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
source = IPFabricSource.objects.first()
|
|
426
|
+
url = f"{BASE}/source/{source.pk}/sync/"
|
|
427
|
+
response = self.client.post(url, **self.header)
|
|
428
|
+
|
|
429
|
+
self.assertHttpStatus(response, status.HTTP_403_FORBIDDEN)
|
|
430
|
+
|
|
431
|
+
def test_sync_action_not_ready(self):
|
|
432
|
+
"""Test sync action when source is not ready for sync."""
|
|
433
|
+
self.add_permissions(
|
|
434
|
+
"ipfabric_netbox.add_ipfabricsource",
|
|
435
|
+
"ipfabric_netbox.sync_ipfabricsource",
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
source = IPFabricSource.objects.first()
|
|
439
|
+
# Set status to make ready_for_sync return False
|
|
440
|
+
source.status = DataSourceStatusChoices.SYNCING
|
|
441
|
+
source.save()
|
|
442
|
+
|
|
443
|
+
url = f"{BASE}/source/{source.pk}/sync/"
|
|
444
|
+
response = self.client.post(url, **self.header)
|
|
445
|
+
|
|
446
|
+
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
|
|
447
|
+
self.assertIn(
|
|
448
|
+
f"Source '{source.name}' is not ready to be synced.", str(response.content)
|
|
449
|
+
)
|
|
450
|
+
|
|
390
451
|
|
|
391
452
|
class IPFabricSnapshotTest(
|
|
392
453
|
APIViewTestCases.GetObjectViewTestCase,
|
|
@@ -407,10 +468,10 @@ class IPFabricSnapshotTest(
|
|
|
407
468
|
]
|
|
408
469
|
|
|
409
470
|
def _get_list_url(self):
|
|
410
|
-
return f"{BASE}snapshot/"
|
|
471
|
+
return f"{BASE}/snapshot/"
|
|
411
472
|
|
|
412
473
|
def _get_detail_url(self, instance):
|
|
413
|
-
return f"{BASE}snapshot/{instance.pk}/"
|
|
474
|
+
return f"{BASE}/snapshot/{instance.pk}/"
|
|
414
475
|
|
|
415
476
|
@classmethod
|
|
416
477
|
def setUpTestData(cls):
|
|
@@ -468,7 +529,7 @@ class IPFabricSnapshotTest(
|
|
|
468
529
|
def test_sites_action_lists_all_and_filters(self):
|
|
469
530
|
self.add_permissions("ipfabric_netbox.view_ipfabricsnapshot")
|
|
470
531
|
# list all
|
|
471
|
-
url = f"{BASE}snapshot/{self.snapshots[0].pk}/sites/"
|
|
532
|
+
url = f"{BASE}/snapshot/{self.snapshots[0].pk}/sites/"
|
|
472
533
|
resp = self.client.get(url, **self.header)
|
|
473
534
|
self.assertHttpStatus(resp, status.HTTP_200_OK)
|
|
474
535
|
body = resp.json()
|
|
@@ -477,7 +538,7 @@ class IPFabricSnapshotTest(
|
|
|
477
538
|
labels = [i["name"] for i in body["results"]]
|
|
478
539
|
self.assertEqual(labels, self.snapshots[0].data["sites"])
|
|
479
540
|
# filter
|
|
480
|
-
url = f"{BASE}snapshot/{self.snapshots[0].pk}/sites/?q=site"
|
|
541
|
+
url = f"{BASE}/snapshot/{self.snapshots[0].pk}/sites/?q=site"
|
|
481
542
|
resp = self.client.get(url, **self.header)
|
|
482
543
|
self.assertHttpStatus(resp, status.HTTP_200_OK)
|
|
483
544
|
body = resp.json()
|
|
@@ -496,7 +557,7 @@ class IPFabricSnapshotTest(
|
|
|
496
557
|
IPFabricData.objects.filter(snapshot_data=self.snapshots[0]).count(), 0
|
|
497
558
|
)
|
|
498
559
|
# PATCH raw
|
|
499
|
-
url = f"{BASE}snapshot/{self.snapshots[0].pk}/raw/"
|
|
560
|
+
url = f"{BASE}/snapshot/{self.snapshots[0].pk}/raw/"
|
|
500
561
|
payload = {
|
|
501
562
|
"data": [
|
|
502
563
|
{"data": {"example": 1}, "type": "device"},
|
|
@@ -550,10 +611,10 @@ class IPFabricSyncTest(APIViewTestCases.APIViewTestCase):
|
|
|
550
611
|
}
|
|
551
612
|
|
|
552
613
|
def _get_list_url(self):
|
|
553
|
-
return f"{BASE}sync/"
|
|
614
|
+
return f"{BASE}/sync/"
|
|
554
615
|
|
|
555
616
|
def _get_detail_url(self, instance):
|
|
556
|
-
return f"{BASE}sync/{instance.pk}/"
|
|
617
|
+
return f"{BASE}/sync/{instance.pk}/"
|
|
557
618
|
|
|
558
619
|
@classmethod
|
|
559
620
|
def setUpTestData(cls):
|
|
@@ -637,6 +698,72 @@ class IPFabricSyncTest(APIViewTestCases.APIViewTestCase):
|
|
|
637
698
|
cls.create_data[1]["parameters"] = {"ipaddress": True, "prefix": True}
|
|
638
699
|
cls.create_data[2]["parameters"] = {"device": True, "interface": True}
|
|
639
700
|
|
|
701
|
+
def test_sync_action_success(self):
|
|
702
|
+
"""Test successful sync action with proper permissions and ready sync."""
|
|
703
|
+
self.add_permissions(
|
|
704
|
+
"ipfabric_netbox.add_ipfabricsync",
|
|
705
|
+
"ipfabric_netbox.sync_ipfabricsync",
|
|
706
|
+
)
|
|
707
|
+
# Get the first sync from setUpTestData
|
|
708
|
+
sync = IPFabricSync.objects.first()
|
|
709
|
+
# Set status and ensure snapshot has data to make ready_for_sync return True
|
|
710
|
+
sync.status = DataSourceStatusChoices.COMPLETED
|
|
711
|
+
sync.save()
|
|
712
|
+
|
|
713
|
+
# Ensure the snapshot has data
|
|
714
|
+
sync.snapshot_data.source.type = (
|
|
715
|
+
"local" # For local type, ready_for_sync checks are simpler
|
|
716
|
+
)
|
|
717
|
+
sync.snapshot_data.source.save()
|
|
718
|
+
|
|
719
|
+
with self.settings(CELERY_TASK_ALWAYS_EAGER=True):
|
|
720
|
+
# Create a mock job object to simulate enqueue_sync_job response
|
|
721
|
+
from unittest.mock import Mock, patch
|
|
722
|
+
|
|
723
|
+
mock_job = Mock()
|
|
724
|
+
mock_job.id = "test-sync-job-456"
|
|
725
|
+
mock_job.status = "queued"
|
|
726
|
+
|
|
727
|
+
with patch.object(sync, "enqueue_sync_job", return_value=mock_job):
|
|
728
|
+
url = f"{BASE}/sync/{sync.pk}/sync/"
|
|
729
|
+
response = self.client.post(url, **self.header)
|
|
730
|
+
|
|
731
|
+
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
|
732
|
+
self.assertIn("id", response.data)
|
|
733
|
+
|
|
734
|
+
def test_sync_action_permission_denied(self):
|
|
735
|
+
"""Test sync action without proper permissions."""
|
|
736
|
+
# Note: Not adding sync_ipfabricsource permission
|
|
737
|
+
self.add_permissions(
|
|
738
|
+
"ipfabric_netbox.add_ipfabricsync",
|
|
739
|
+
)
|
|
740
|
+
|
|
741
|
+
sync = IPFabricSync.objects.first()
|
|
742
|
+
url = f"{BASE}/sync/{sync.pk}/sync/"
|
|
743
|
+
response = self.client.post(url, **self.header)
|
|
744
|
+
|
|
745
|
+
self.assertHttpStatus(response, status.HTTP_403_FORBIDDEN)
|
|
746
|
+
|
|
747
|
+
def test_sync_action_not_ready(self):
|
|
748
|
+
"""Test sync action when sync is not ready for sync."""
|
|
749
|
+
self.add_permissions(
|
|
750
|
+
"ipfabric_netbox.add_ipfabricsync",
|
|
751
|
+
"ipfabric_netbox.sync_ipfabricsync",
|
|
752
|
+
)
|
|
753
|
+
|
|
754
|
+
sync = IPFabricSync.objects.first()
|
|
755
|
+
# Set status to make ready_for_sync return False
|
|
756
|
+
sync.status = DataSourceStatusChoices.SYNCING
|
|
757
|
+
sync.save()
|
|
758
|
+
|
|
759
|
+
url = f"{BASE}/sync/{sync.pk}/sync/"
|
|
760
|
+
response = self.client.post(url, **self.header)
|
|
761
|
+
|
|
762
|
+
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
|
|
763
|
+
self.assertIn(
|
|
764
|
+
f"Sync '{sync.name}' is not ready to be synced.", str(response.content)
|
|
765
|
+
)
|
|
766
|
+
|
|
640
767
|
|
|
641
768
|
class IPFabricIngestionTest(
|
|
642
769
|
APIViewTestCases.GetObjectViewTestCase,
|
|
@@ -653,10 +780,10 @@ class IPFabricIngestionTest(
|
|
|
653
780
|
]
|
|
654
781
|
|
|
655
782
|
def _get_list_url(self):
|
|
656
|
-
return f"{BASE}ingestion/"
|
|
783
|
+
return f"{BASE}/ingestion/"
|
|
657
784
|
|
|
658
785
|
def _get_detail_url(self, instance):
|
|
659
|
-
return f"{BASE}ingestion/{instance.pk}/"
|
|
786
|
+
return f"{BASE}/ingestion/{instance.pk}/"
|
|
660
787
|
|
|
661
788
|
@classmethod
|
|
662
789
|
def setUpTestData(cls):
|
|
@@ -754,10 +881,10 @@ class IPFabricIngestionIssueTest(
|
|
|
754
881
|
]
|
|
755
882
|
|
|
756
883
|
def _get_list_url(self):
|
|
757
|
-
return f"{BASE}ingestion-issues/"
|
|
884
|
+
return f"{BASE}/ingestion-issues/"
|
|
758
885
|
|
|
759
886
|
def _get_detail_url(self, instance):
|
|
760
|
-
return f"{BASE}ingestion-issues/{instance.pk}/"
|
|
887
|
+
return f"{BASE}/ingestion-issues/{instance.pk}/"
|
|
761
888
|
|
|
762
889
|
@classmethod
|
|
763
890
|
def setUpTestData(cls):
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import random
|
|
2
|
+
from datetime import timedelta
|
|
2
3
|
from unittest.mock import patch
|
|
3
4
|
from uuid import uuid4
|
|
4
5
|
|
|
@@ -611,7 +612,7 @@ class IPFabricSyncTestCase(
|
|
|
611
612
|
# ViewTestCases.BulkImportObjectsViewTestCase,
|
|
612
613
|
):
|
|
613
614
|
model = IPFabricSync
|
|
614
|
-
user_permissions = ("ipfabric_netbox.
|
|
615
|
+
user_permissions = ("ipfabric_netbox.sync_ipfabricsync",)
|
|
615
616
|
|
|
616
617
|
@classmethod
|
|
617
618
|
def setUpTestData(cls):
|
|
@@ -656,6 +657,8 @@ class IPFabricSyncTestCase(
|
|
|
656
657
|
status=DataSourceStatusChoices.NEW,
|
|
657
658
|
parameters=get_parameters(),
|
|
658
659
|
last_synced=timezone.now(),
|
|
660
|
+
scheduled=timezone.now() + timedelta(hours=6),
|
|
661
|
+
interval=123456,
|
|
659
662
|
),
|
|
660
663
|
IPFabricSync(
|
|
661
664
|
name="Sync Job 2",
|
|
@@ -716,7 +719,7 @@ class IPFabricSyncTestCase(
|
|
|
716
719
|
|
|
717
720
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
718
721
|
def test_get_htmx_request(self):
|
|
719
|
-
instance = self._get_queryset().
|
|
722
|
+
instance = self._get_queryset().last()
|
|
720
723
|
# Try GET with HTMX
|
|
721
724
|
response = self.client.get(
|
|
722
725
|
instance.get_absolute_url(), **{"HTTP_HX-Request": "true"}
|
|
@@ -782,7 +782,15 @@ class IPFabricSyncView(generic.ObjectView):
|
|
|
782
782
|
|
|
783
783
|
last_ingestion = instance.ipfabricingestion_set.last()
|
|
784
784
|
|
|
785
|
-
|
|
785
|
+
scheduled_job = None
|
|
786
|
+
if instance.scheduled:
|
|
787
|
+
scheduled_job = instance.jobs.filter(scheduled=instance.scheduled).last()
|
|
788
|
+
|
|
789
|
+
return {
|
|
790
|
+
"format": format,
|
|
791
|
+
"last_ingestion": last_ingestion,
|
|
792
|
+
"scheduled_job": scheduled_job,
|
|
793
|
+
}
|
|
786
794
|
|
|
787
795
|
|
|
788
796
|
@register_model_view(IPFabricSync, "sync")
|
|
@@ -790,7 +798,7 @@ class IPFabricStartSyncView(BaseObjectView):
|
|
|
790
798
|
queryset = IPFabricSync.objects.all()
|
|
791
799
|
|
|
792
800
|
def get_required_permission(self):
|
|
793
|
-
return "ipfabric_netbox.
|
|
801
|
+
return "ipfabric_netbox.sync_ipfabricsync"
|
|
794
802
|
|
|
795
803
|
def get(self, request, pk):
|
|
796
804
|
ipfabric = get_object_or_404(self.queryset, pk=pk)
|
|
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.3.0b6 → ipfabric_netbox-4.3.0b8}/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
|
|
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.3.0b6 → ipfabric_netbox-4.3.0b8}/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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ipfabric_netbox-4.3.0b6 → ipfabric_netbox-4.3.0b8}/ipfabric_netbox/utilities/transform_map.py
RENAMED
|
File without changes
|