nautobot 2.3.1__py3-none-any.whl → 2.3.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of nautobot might be problematic. Click here for more details.
- nautobot/core/celery/schedulers.py +18 -0
- nautobot/core/settings.yaml +3 -3
- nautobot/core/tables.py +1 -1
- nautobot/core/templates/home.html +4 -3
- nautobot/core/templatetags/buttons.py +1 -1
- nautobot/core/views/utils.py +3 -3
- nautobot/dcim/factory.py +3 -3
- nautobot/dcim/tables/devices.py +7 -7
- nautobot/dcim/templates/dcim/device.html +12 -0
- nautobot/dcim/templates/dcim/softwareimagefile_retrieve.html +12 -0
- nautobot/dcim/utils.py +9 -6
- nautobot/extras/api/serializers.py +2 -0
- nautobot/extras/filters/__init__.py +14 -2
- nautobot/extras/forms/forms.py +6 -0
- nautobot/extras/forms/mixins.py +2 -2
- nautobot/extras/management/__init__.py +3 -0
- nautobot/extras/migrations/0115_scheduledjob_time_zone.py +23 -0
- nautobot/extras/models/jobs.py +24 -11
- nautobot/extras/tables.py +34 -4
- nautobot/extras/templates/extras/scheduledjob.html +13 -2
- nautobot/extras/tests/test_api.py +17 -18
- nautobot/extras/tests/test_filters.py +57 -1
- nautobot/extras/tests/test_models.py +299 -1
- nautobot/extras/tests/test_views.py +3 -2
- nautobot/extras/views.py +7 -0
- nautobot/ipam/api/views.py +9 -2
- nautobot/ipam/choices.py +17 -0
- nautobot/ipam/factory.py +6 -0
- nautobot/ipam/filters.py +1 -1
- nautobot/ipam/forms.py +5 -3
- nautobot/ipam/migrations/0048_vrf_status.py +23 -0
- nautobot/ipam/migrations/0049_vrf_data_migration.py +25 -0
- nautobot/ipam/models.py +2 -0
- nautobot/ipam/tables.py +3 -2
- nautobot/ipam/templates/ipam/vrf.html +4 -0
- nautobot/ipam/templates/ipam/vrf_edit.html +1 -0
- nautobot/ipam/tests/test_api.py +33 -3
- nautobot/ipam/tests/test_views.py +3 -0
- nautobot/project-static/css/base.css +6 -0
- nautobot/project-static/docs/release-notes/version-2.3.html +163 -33
- nautobot/project-static/docs/search/search_index.json +1 -1
- nautobot/project-static/docs/sitemap.xml +271 -271
- nautobot/project-static/docs/sitemap.xml.gz +0 -0
- nautobot/project-static/docs/user-guide/administration/configuration/optional-settings.html +3 -3
- nautobot/project-static/js/homepage_layout.js +3 -0
- {nautobot-2.3.1.dist-info → nautobot-2.3.2.dist-info}/METADATA +1 -1
- {nautobot-2.3.1.dist-info → nautobot-2.3.2.dist-info}/RECORD +51 -48
- {nautobot-2.3.1.dist-info → nautobot-2.3.2.dist-info}/LICENSE.txt +0 -0
- {nautobot-2.3.1.dist-info → nautobot-2.3.2.dist-info}/NOTICE +0 -0
- {nautobot-2.3.1.dist-info → nautobot-2.3.2.dist-info}/WHEEL +0 -0
- {nautobot-2.3.1.dist-info → nautobot-2.3.2.dist-info}/entry_points.txt +0 -0
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
{% load helpers %}
|
|
5
5
|
{% load perms %}
|
|
6
6
|
{% load plugins %}
|
|
7
|
+
{% load tz %}
|
|
7
8
|
|
|
8
9
|
{% block buttons %}
|
|
9
10
|
{% plugin_buttons object %}
|
|
@@ -94,13 +95,23 @@
|
|
|
94
95
|
<tr>
|
|
95
96
|
<td>Start Time</td>
|
|
96
97
|
<td>
|
|
97
|
-
{{ object.start_time }}
|
|
98
|
+
{{ object.start_time|timezone:object.time_zone|date:'Y-m-d H:i:s T' }}
|
|
99
|
+
{% if default_time_zone != object.time_zone %}
|
|
100
|
+
<br>{{ object.start_time|timezone:default_time_zone|date:'Y-m-d H:i:s T' }}
|
|
101
|
+
{% endif %}
|
|
98
102
|
</td>
|
|
99
103
|
</tr>
|
|
100
104
|
<tr>
|
|
101
105
|
<td>Last Run At</td>
|
|
102
106
|
<td>
|
|
103
|
-
{
|
|
107
|
+
{% if object.last_run_at %}
|
|
108
|
+
{{ object.last_run_at|timezone:object.time_zone|date:'Y-m-d H:i:s T' }}
|
|
109
|
+
{% if default_time_zone != object.time_zone %}
|
|
110
|
+
<br>{{ object.last_run_at|timezone:default_time_zone|date:'Y-m-d H:i:s T' }}
|
|
111
|
+
{% endif %}
|
|
112
|
+
{% else %}
|
|
113
|
+
{{ object.last_run_at|placeholder }}
|
|
114
|
+
{% endif %}
|
|
104
115
|
</td>
|
|
105
116
|
</tr>
|
|
106
117
|
<tr>
|
|
@@ -12,6 +12,11 @@ from django.urls import reverse
|
|
|
12
12
|
from django.utils.timezone import make_aware, now
|
|
13
13
|
from rest_framework import status
|
|
14
14
|
|
|
15
|
+
try:
|
|
16
|
+
from zoneinfo import ZoneInfo
|
|
17
|
+
except ImportError: # Python 3.8
|
|
18
|
+
from backports.zoneinfo import ZoneInfo
|
|
19
|
+
|
|
15
20
|
from nautobot.core.choices import ColorChoices
|
|
16
21
|
from nautobot.core.models.fields import slugify_dashes_to_underscores
|
|
17
22
|
from nautobot.core.testing import APITestCase, APIViewTestCases
|
|
@@ -1583,7 +1588,7 @@ class JobTest(
|
|
|
1583
1588
|
"schedule": {
|
|
1584
1589
|
"name": "test",
|
|
1585
1590
|
"interval": "future",
|
|
1586
|
-
"start_time": str(
|
|
1591
|
+
"start_time": str(now() + timedelta(minutes=1)),
|
|
1587
1592
|
},
|
|
1588
1593
|
}
|
|
1589
1594
|
|
|
@@ -1780,7 +1785,7 @@ class JobTest(
|
|
|
1780
1785
|
"var2": "Ground control to Major Tom",
|
|
1781
1786
|
"var23": "Commencing countdown, engines on",
|
|
1782
1787
|
"var1": test_file,
|
|
1783
|
-
"_schedule_start_time": str(
|
|
1788
|
+
"_schedule_start_time": str(now() + timedelta(minutes=1)),
|
|
1784
1789
|
"_schedule_interval": "future",
|
|
1785
1790
|
"_schedule_name": "test",
|
|
1786
1791
|
}
|
|
@@ -1799,7 +1804,7 @@ class JobTest(
|
|
|
1799
1804
|
data = {
|
|
1800
1805
|
"data": {"var1": "x", "var2": 1, "var3": False, "var4": d.pk},
|
|
1801
1806
|
"schedule": {
|
|
1802
|
-
"start_time": str(
|
|
1807
|
+
"start_time": str(now() + timedelta(minutes=1)),
|
|
1803
1808
|
"interval": "future",
|
|
1804
1809
|
"name": "test",
|
|
1805
1810
|
},
|
|
@@ -1838,7 +1843,7 @@ class JobTest(
|
|
|
1838
1843
|
data = {
|
|
1839
1844
|
"data": {},
|
|
1840
1845
|
"schedule": {
|
|
1841
|
-
"start_time": str(
|
|
1846
|
+
"start_time": str(now() + timedelta(minutes=1)),
|
|
1842
1847
|
"interval": "future",
|
|
1843
1848
|
"name": "test",
|
|
1844
1849
|
},
|
|
@@ -1914,7 +1919,7 @@ class JobTest(
|
|
|
1914
1919
|
data = {
|
|
1915
1920
|
"data": {"var1": "x", "var2": 1, "var3": False, "var4": d.pk},
|
|
1916
1921
|
"schedule": {
|
|
1917
|
-
"start_time": str(
|
|
1922
|
+
"start_time": str(now() - timedelta(minutes=1)),
|
|
1918
1923
|
"interval": "future",
|
|
1919
1924
|
"name": "test",
|
|
1920
1925
|
},
|
|
@@ -1933,7 +1938,7 @@ class JobTest(
|
|
|
1933
1938
|
data = {
|
|
1934
1939
|
"data": {"var1": "x", "var2": 1, "var3": False, "var4": d.pk},
|
|
1935
1940
|
"schedule": {
|
|
1936
|
-
"start_time": str(
|
|
1941
|
+
"start_time": str(now() + timedelta(minutes=1)),
|
|
1937
1942
|
"interval": "hourly",
|
|
1938
1943
|
"name": "test",
|
|
1939
1944
|
},
|
|
@@ -2438,30 +2443,24 @@ class ScheduledJobTest(
|
|
|
2438
2443
|
name="test2",
|
|
2439
2444
|
task="pass.TestPass",
|
|
2440
2445
|
job_model=job_model,
|
|
2441
|
-
interval=JobExecutionType.
|
|
2446
|
+
interval=JobExecutionType.TYPE_DAILY,
|
|
2442
2447
|
user=user,
|
|
2443
2448
|
approval_required=True,
|
|
2444
|
-
start_time=
|
|
2449
|
+
start_time=datetime(2020, 1, 23, 12, 34, 56, tzinfo=ZoneInfo("America/New_York")),
|
|
2450
|
+
time_zone=ZoneInfo("America/New_York"),
|
|
2445
2451
|
)
|
|
2446
2452
|
ScheduledJob.objects.create(
|
|
2447
2453
|
name="test3",
|
|
2448
2454
|
task="pass.TestPass",
|
|
2449
2455
|
job_model=job_model,
|
|
2450
|
-
interval=JobExecutionType.
|
|
2456
|
+
interval=JobExecutionType.TYPE_CUSTOM,
|
|
2457
|
+
crontab="34 12 * * *",
|
|
2458
|
+
enabled=False,
|
|
2451
2459
|
user=user,
|
|
2452
2460
|
approval_required=True,
|
|
2453
2461
|
start_time=now(),
|
|
2454
2462
|
)
|
|
2455
2463
|
|
|
2456
|
-
# TODO: Unskip after resolving #2908, #2909
|
|
2457
|
-
@skip("DRF's built-in OrderingFilter triggering natural key attribute error in our base")
|
|
2458
|
-
def test_list_objects_ascending_ordered(self):
|
|
2459
|
-
pass
|
|
2460
|
-
|
|
2461
|
-
@skip("DRF's built-in OrderingFilter triggering natural key attribute error in our base")
|
|
2462
|
-
def test_list_objects_descending_ordered(self):
|
|
2463
|
-
pass
|
|
2464
|
-
|
|
2465
2464
|
|
|
2466
2465
|
class JobApprovalTest(APITestCase):
|
|
2467
2466
|
@classmethod
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from datetime import datetime
|
|
1
2
|
import uuid
|
|
2
3
|
|
|
3
4
|
from django.contrib.auth import get_user_model
|
|
@@ -5,6 +6,12 @@ from django.contrib.contenttypes.models import ContentType
|
|
|
5
6
|
from django.core.files.uploadedfile import SimpleUploadedFile
|
|
6
7
|
from django.db.models import Q
|
|
7
8
|
from django.test import override_settings, RequestFactory
|
|
9
|
+
from django.utils.timezone import now
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
from zoneinfo import ZoneInfo
|
|
13
|
+
except ImportError: # Python 3.8
|
|
14
|
+
from backports.zoneinfo import ZoneInfo
|
|
8
15
|
|
|
9
16
|
from nautobot.core.choices import ColorChoices
|
|
10
17
|
from nautobot.core.testing import FilterTestCases
|
|
@@ -22,6 +29,7 @@ from nautobot.dcim.models import (
|
|
|
22
29
|
from nautobot.extras.choices import (
|
|
23
30
|
CustomFieldTypeChoices,
|
|
24
31
|
DynamicGroupTypeChoices,
|
|
32
|
+
JobExecutionType,
|
|
25
33
|
JobResultStatusChoices,
|
|
26
34
|
MetadataTypeDataTypeChoices,
|
|
27
35
|
ObjectChangeActionChoices,
|
|
@@ -93,6 +101,7 @@ from nautobot.extras.models import (
|
|
|
93
101
|
RelationshipAssociation,
|
|
94
102
|
Role,
|
|
95
103
|
SavedView,
|
|
104
|
+
ScheduledJob,
|
|
96
105
|
Secret,
|
|
97
106
|
SecretsGroup,
|
|
98
107
|
SecretsGroupAssociation,
|
|
@@ -1124,13 +1133,49 @@ class JobResultFilterSetTestCase(FilterTestCases.FilterTestCase):
|
|
|
1124
1133
|
def setUpTestData(cls):
|
|
1125
1134
|
jobs = Job.objects.all()[:3]
|
|
1126
1135
|
cls.jobs = jobs
|
|
1136
|
+
user = User.objects.create(username="user1", is_active=True)
|
|
1137
|
+
job_model = Job.objects.get_for_class_path("pass.TestPass")
|
|
1138
|
+
scheduled_jobs = [
|
|
1139
|
+
ScheduledJob.objects.create(
|
|
1140
|
+
name="test1",
|
|
1141
|
+
task="pass.TestPass",
|
|
1142
|
+
job_model=job_model,
|
|
1143
|
+
interval=JobExecutionType.TYPE_IMMEDIATELY,
|
|
1144
|
+
user=user,
|
|
1145
|
+
approval_required=True,
|
|
1146
|
+
start_time=now(),
|
|
1147
|
+
),
|
|
1148
|
+
ScheduledJob.objects.create(
|
|
1149
|
+
name="test2",
|
|
1150
|
+
task="pass.TestPass",
|
|
1151
|
+
job_model=job_model,
|
|
1152
|
+
interval=JobExecutionType.TYPE_DAILY,
|
|
1153
|
+
user=user,
|
|
1154
|
+
approval_required=True,
|
|
1155
|
+
start_time=datetime(2020, 1, 23, 12, 34, 56, tzinfo=ZoneInfo("America/New_York")),
|
|
1156
|
+
time_zone=ZoneInfo("America/New_York"),
|
|
1157
|
+
),
|
|
1158
|
+
ScheduledJob.objects.create(
|
|
1159
|
+
name="test3",
|
|
1160
|
+
task="pass.TestPass",
|
|
1161
|
+
job_model=job_model,
|
|
1162
|
+
interval=JobExecutionType.TYPE_CUSTOM,
|
|
1163
|
+
crontab="34 12 * * *",
|
|
1164
|
+
enabled=False,
|
|
1165
|
+
user=user,
|
|
1166
|
+
approval_required=True,
|
|
1167
|
+
start_time=now(),
|
|
1168
|
+
),
|
|
1169
|
+
]
|
|
1170
|
+
cls.scheduled_jobs = scheduled_jobs
|
|
1127
1171
|
user = UserFactory.create()
|
|
1128
|
-
for job in jobs:
|
|
1172
|
+
for idx, job in enumerate(jobs):
|
|
1129
1173
|
JobResult.objects.create(
|
|
1130
1174
|
job_model=job,
|
|
1131
1175
|
name=job.class_path,
|
|
1132
1176
|
user=user,
|
|
1133
1177
|
status=JobResultStatusChoices.STATUS_STARTED,
|
|
1178
|
+
scheduled_job=scheduled_jobs[idx],
|
|
1134
1179
|
)
|
|
1135
1180
|
|
|
1136
1181
|
def test_job_model(self):
|
|
@@ -1144,6 +1189,17 @@ class JobResultFilterSetTestCase(FilterTestCases.FilterTestCase):
|
|
|
1144
1189
|
self.filterset(params, self.queryset).qs, self.queryset.filter(job_model__in=jobs).distinct()
|
|
1145
1190
|
)
|
|
1146
1191
|
|
|
1192
|
+
def test_scheduled_job(self):
|
|
1193
|
+
scheduled_jobs = list(self.scheduled_jobs[:2])
|
|
1194
|
+
filter_params = [
|
|
1195
|
+
{"scheduled_job": [scheduled_jobs[0].pk, scheduled_jobs[1].name]},
|
|
1196
|
+
]
|
|
1197
|
+
for params in filter_params:
|
|
1198
|
+
self.assertQuerysetEqualAndNotEmpty(
|
|
1199
|
+
self.filterset(params, self.queryset).qs,
|
|
1200
|
+
self.queryset.filter(scheduled_job__in=scheduled_jobs).distinct(),
|
|
1201
|
+
)
|
|
1202
|
+
|
|
1147
1203
|
|
|
1148
1204
|
class JobHookFilterSetTestCase(FilterTestCases.NameOnlyFilterTestCase):
|
|
1149
1205
|
queryset = JobHook.objects.all()
|
|
@@ -14,8 +14,15 @@ from django.db.models import ProtectedError
|
|
|
14
14
|
from django.db.utils import IntegrityError
|
|
15
15
|
from django.test import override_settings
|
|
16
16
|
from django.test.utils import isolate_apps
|
|
17
|
-
from django.utils.timezone import now
|
|
17
|
+
from django.utils.timezone import get_default_timezone, now
|
|
18
|
+
from django_celery_beat.tzcrontab import TzAwareCrontab
|
|
18
19
|
from jinja2.exceptions import TemplateAssertionError, TemplateSyntaxError
|
|
20
|
+
import time_machine
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
from zoneinfo import ZoneInfo
|
|
24
|
+
except ImportError: # python 3.8
|
|
25
|
+
from backports.zoneinfo import ZoneInfo
|
|
19
26
|
|
|
20
27
|
from nautobot.circuits.models import CircuitType
|
|
21
28
|
from nautobot.core.choices import ColorChoices
|
|
@@ -30,6 +37,7 @@ from nautobot.dcim.models import (
|
|
|
30
37
|
Platform,
|
|
31
38
|
)
|
|
32
39
|
from nautobot.extras.choices import (
|
|
40
|
+
JobExecutionType,
|
|
33
41
|
JobResultStatusChoices,
|
|
34
42
|
LogLevelChoices,
|
|
35
43
|
MetadataTypeDataTypeChoices,
|
|
@@ -65,6 +73,7 @@ from nautobot.extras.models import (
|
|
|
65
73
|
ObjectMetadata,
|
|
66
74
|
Role,
|
|
67
75
|
SavedView,
|
|
76
|
+
ScheduledJob,
|
|
68
77
|
Secret,
|
|
69
78
|
SecretsGroup,
|
|
70
79
|
SecretsGroupAssociation,
|
|
@@ -1790,6 +1799,295 @@ class SavedViewTest(ModelTestCases.BaseModelTestCase):
|
|
|
1790
1799
|
self.assertEqual(self.ipaddress_global_sv.is_shared, True)
|
|
1791
1800
|
|
|
1792
1801
|
|
|
1802
|
+
@override_settings(TIME_ZONE="UTC")
|
|
1803
|
+
class ScheduledJobTest(ModelTestCases.BaseModelTestCase):
|
|
1804
|
+
"""Tests for the `ScheduledJob` model class."""
|
|
1805
|
+
|
|
1806
|
+
model = ScheduledJob
|
|
1807
|
+
|
|
1808
|
+
def setUp(self):
|
|
1809
|
+
self.user = User.objects.create_user(username="scheduledjobuser")
|
|
1810
|
+
self.job_model = JobModel.objects.get(name="TestPass")
|
|
1811
|
+
|
|
1812
|
+
self.daily_utc_job = ScheduledJob.objects.create(
|
|
1813
|
+
name="Daily UTC Job",
|
|
1814
|
+
task="pass.TestPass",
|
|
1815
|
+
job_model=self.job_model,
|
|
1816
|
+
interval=JobExecutionType.TYPE_DAILY,
|
|
1817
|
+
start_time=datetime(year=2050, month=1, day=22, hour=17, minute=0, tzinfo=get_default_timezone()),
|
|
1818
|
+
time_zone=get_default_timezone(),
|
|
1819
|
+
)
|
|
1820
|
+
self.daily_est_job = ScheduledJob.objects.create(
|
|
1821
|
+
name="Daily EST Job",
|
|
1822
|
+
task="pass.TestPass",
|
|
1823
|
+
job_model=self.job_model,
|
|
1824
|
+
interval=JobExecutionType.TYPE_DAILY,
|
|
1825
|
+
start_time=datetime(year=2050, month=1, day=22, hour=17, minute=0, tzinfo=ZoneInfo("America/New_York")),
|
|
1826
|
+
time_zone=ZoneInfo("America/New_York"),
|
|
1827
|
+
)
|
|
1828
|
+
self.crontab_utc_job = ScheduledJob.create_schedule(
|
|
1829
|
+
job_model=self.job_model,
|
|
1830
|
+
user=self.user,
|
|
1831
|
+
name="Crontab UTC Job",
|
|
1832
|
+
interval=JobExecutionType.TYPE_CUSTOM,
|
|
1833
|
+
crontab="0 17 * * *",
|
|
1834
|
+
)
|
|
1835
|
+
self.crontab_est_job = ScheduledJob.objects.create(
|
|
1836
|
+
name="Crontab EST Job",
|
|
1837
|
+
task="pass.TestPass",
|
|
1838
|
+
job_model=self.job_model,
|
|
1839
|
+
interval=JobExecutionType.TYPE_CUSTOM,
|
|
1840
|
+
start_time=datetime(year=2050, month=1, day=22, hour=17, minute=0, tzinfo=ZoneInfo("America/New_York")),
|
|
1841
|
+
time_zone=ZoneInfo("America/New_York"),
|
|
1842
|
+
crontab="0 17 * * *",
|
|
1843
|
+
)
|
|
1844
|
+
self.one_off_utc_job = ScheduledJob.objects.create(
|
|
1845
|
+
name="One-off UTC Job",
|
|
1846
|
+
task="pass.TestPass",
|
|
1847
|
+
job_model=self.job_model,
|
|
1848
|
+
interval=JobExecutionType.TYPE_FUTURE,
|
|
1849
|
+
start_time=datetime(year=2050, month=1, day=22, hour=0, minute=0, tzinfo=ZoneInfo("UTC")),
|
|
1850
|
+
time_zone=ZoneInfo("UTC"),
|
|
1851
|
+
)
|
|
1852
|
+
self.one_off_est_job = ScheduledJob.create_schedule(
|
|
1853
|
+
job_model=self.job_model,
|
|
1854
|
+
user=self.user,
|
|
1855
|
+
name="One-off EST Job",
|
|
1856
|
+
interval=JobExecutionType.TYPE_FUTURE,
|
|
1857
|
+
start_time=datetime(year=2050, month=1, day=22, hour=0, minute=0, tzinfo=ZoneInfo("America/New_York")),
|
|
1858
|
+
)
|
|
1859
|
+
|
|
1860
|
+
def test_schedule(self):
|
|
1861
|
+
"""Test the schedule property."""
|
|
1862
|
+
with self.subTest("Test TYPE_DAILY schedules"):
|
|
1863
|
+
daily_utc_schedule = self.daily_utc_job.schedule
|
|
1864
|
+
daily_est_schedule = self.daily_est_job.schedule
|
|
1865
|
+
self.assertIsInstance(daily_utc_schedule, TzAwareCrontab)
|
|
1866
|
+
self.assertIsInstance(daily_est_schedule, TzAwareCrontab)
|
|
1867
|
+
self.assertNotEqual(daily_utc_schedule, daily_est_schedule)
|
|
1868
|
+
# Crontabs are validated in test_to_cron()
|
|
1869
|
+
|
|
1870
|
+
with self.subTest("Test TYPE_CUSTOM schedules"):
|
|
1871
|
+
crontab_utc_schedule = self.crontab_utc_job.schedule
|
|
1872
|
+
crontab_est_schedule = self.crontab_est_job.schedule
|
|
1873
|
+
self.assertIsInstance(crontab_utc_schedule, TzAwareCrontab)
|
|
1874
|
+
self.assertIsInstance(crontab_est_schedule, TzAwareCrontab)
|
|
1875
|
+
self.assertNotEqual(crontab_utc_schedule, crontab_est_schedule)
|
|
1876
|
+
# Crontabs are validated in test_to_cron()
|
|
1877
|
+
|
|
1878
|
+
with self.subTest("Test TYPE_FUTURE schedules"):
|
|
1879
|
+
# TYPE_FUTURE schedules are one off, not cron tabs:
|
|
1880
|
+
self.assertEqual(self.one_off_utc_job.schedule.clocked_time, self.one_off_utc_job.start_time)
|
|
1881
|
+
self.assertEqual(self.one_off_est_job.schedule.clocked_time, self.one_off_est_job.start_time)
|
|
1882
|
+
self.assertEqual(
|
|
1883
|
+
self.one_off_est_job.schedule.clocked_time - self.one_off_utc_job.schedule.clocked_time,
|
|
1884
|
+
timedelta(hours=5),
|
|
1885
|
+
)
|
|
1886
|
+
|
|
1887
|
+
def test_to_cron(self):
|
|
1888
|
+
"""Test the to_cron() method and its interaction with time zone variants."""
|
|
1889
|
+
|
|
1890
|
+
with self.subTest("Test TYPE_DAILY schedule with UTC time zone and UTC schedule time zone"):
|
|
1891
|
+
self.daily_utc_job.refresh_from_db()
|
|
1892
|
+
daily_utc_schedule = self.daily_utc_job.to_cron()
|
|
1893
|
+
self.assertEqual(daily_utc_schedule.tz, ZoneInfo("UTC"))
|
|
1894
|
+
self.assertEqual(daily_utc_schedule.hour, {17})
|
|
1895
|
+
self.assertEqual(daily_utc_schedule.minute, {0})
|
|
1896
|
+
last_run = datetime(2050, 1, 21, 17, 0, tzinfo=ZoneInfo("UTC"))
|
|
1897
|
+
with time_machine.travel("2050-01-22 16:59 +0000"):
|
|
1898
|
+
is_due, _ = daily_utc_schedule.is_due(last_run_at=last_run)
|
|
1899
|
+
self.assertFalse(is_due)
|
|
1900
|
+
with time_machine.travel("2050-01-22 17:00 +0000"):
|
|
1901
|
+
is_due, _ = daily_utc_schedule.is_due(last_run_at=last_run)
|
|
1902
|
+
self.assertTrue(is_due)
|
|
1903
|
+
|
|
1904
|
+
with self.subTest("Test TYPE_DAILY schedule with UTC time zone and EST schedule time zone"):
|
|
1905
|
+
self.daily_est_job.refresh_from_db()
|
|
1906
|
+
daily_est_schedule = self.daily_est_job.to_cron()
|
|
1907
|
+
self.assertEqual(daily_est_schedule.tz, ZoneInfo("America/New_York"))
|
|
1908
|
+
self.assertEqual(daily_est_schedule.hour, {17})
|
|
1909
|
+
self.assertEqual(daily_est_schedule.minute, {0})
|
|
1910
|
+
last_run = datetime(2050, 1, 21, 22, 0, tzinfo=ZoneInfo("UTC"))
|
|
1911
|
+
with time_machine.travel("2050-01-22 21:59 +0000"):
|
|
1912
|
+
is_due, _ = daily_est_schedule.is_due(last_run_at=last_run)
|
|
1913
|
+
self.assertFalse(is_due)
|
|
1914
|
+
with time_machine.travel("2050-01-22 22:00 +0000"):
|
|
1915
|
+
is_due, _ = daily_est_schedule.is_due(last_run_at=last_run)
|
|
1916
|
+
self.assertTrue(is_due)
|
|
1917
|
+
|
|
1918
|
+
with self.subTest("Test TYPE_CUSTOM schedule with UTC time zone and UTC schedule time zone"):
|
|
1919
|
+
self.crontab_utc_job.refresh_from_db()
|
|
1920
|
+
crontab_utc_schedule = self.crontab_utc_job.to_cron()
|
|
1921
|
+
self.assertEqual(crontab_utc_schedule.tz, ZoneInfo("UTC"))
|
|
1922
|
+
self.assertEqual(crontab_utc_schedule.hour, {17})
|
|
1923
|
+
self.assertEqual(crontab_utc_schedule.minute, {0})
|
|
1924
|
+
|
|
1925
|
+
with self.subTest("Test TYPE_CUSTOM schedule with UTC time zone and EST schedule time zone"):
|
|
1926
|
+
self.crontab_est_job.refresh_from_db()
|
|
1927
|
+
crontab_est_schedule = self.crontab_est_job.to_cron()
|
|
1928
|
+
self.assertEqual(crontab_est_schedule.tz, ZoneInfo("America/New_York"))
|
|
1929
|
+
self.assertEqual(crontab_est_schedule.hour, {17})
|
|
1930
|
+
self.assertEqual(crontab_est_schedule.minute, {0})
|
|
1931
|
+
|
|
1932
|
+
with self.subTest("Test TYPE_FUTURE schedules do not map to cron"):
|
|
1933
|
+
with self.assertRaises(ValueError):
|
|
1934
|
+
self.one_off_utc_job.to_cron()
|
|
1935
|
+
with self.assertRaises(ValueError):
|
|
1936
|
+
self.one_off_est_job.to_cron()
|
|
1937
|
+
|
|
1938
|
+
with override_settings(TIME_ZONE="America/New_York"):
|
|
1939
|
+
with self.subTest("Test TYPE_DAILY schedule with EST time zone and UTC schedule time zone"):
|
|
1940
|
+
self.daily_utc_job.refresh_from_db()
|
|
1941
|
+
daily_utc_schedule = self.daily_utc_job.to_cron()
|
|
1942
|
+
self.assertEqual(daily_utc_schedule.tz, ZoneInfo("UTC"))
|
|
1943
|
+
self.assertEqual(daily_utc_schedule.hour, {17})
|
|
1944
|
+
self.assertEqual(daily_utc_schedule.minute, {0})
|
|
1945
|
+
last_run = datetime(2050, 1, 21, 12, 0, tzinfo=ZoneInfo("America/New_York"))
|
|
1946
|
+
with time_machine.travel("2050-01-22 11:59 -0500"):
|
|
1947
|
+
is_due, _ = daily_utc_schedule.is_due(last_run_at=last_run)
|
|
1948
|
+
self.assertFalse(is_due)
|
|
1949
|
+
with time_machine.travel("2050-01-22 12:00 -0500"):
|
|
1950
|
+
is_due, _ = daily_utc_schedule.is_due(last_run_at=last_run)
|
|
1951
|
+
self.assertTrue(is_due)
|
|
1952
|
+
|
|
1953
|
+
with self.subTest("Test TYPE_DAILY schedule with EST time zone and EST schedule time zone"):
|
|
1954
|
+
self.daily_est_job.refresh_from_db()
|
|
1955
|
+
daily_est_schedule = self.daily_est_job.to_cron()
|
|
1956
|
+
self.assertEqual(daily_est_schedule.tz, ZoneInfo("America/New_York"))
|
|
1957
|
+
self.assertEqual(daily_est_schedule.hour, {17})
|
|
1958
|
+
self.assertEqual(daily_est_schedule.minute, {0})
|
|
1959
|
+
last_run = datetime(2050, 1, 21, 22, 0, tzinfo=ZoneInfo("America/New_York"))
|
|
1960
|
+
with time_machine.travel("2050-01-22 16:59 -0500"):
|
|
1961
|
+
is_due, _ = daily_est_schedule.is_due(last_run_at=last_run)
|
|
1962
|
+
self.assertFalse(is_due)
|
|
1963
|
+
with time_machine.travel("2050-01-22 17:00 -0500"):
|
|
1964
|
+
is_due, _ = daily_est_schedule.is_due(last_run_at=last_run)
|
|
1965
|
+
self.assertTrue(is_due)
|
|
1966
|
+
|
|
1967
|
+
with self.subTest("Test TYPE_CUSTOM schedule with EST time zone and UTC schedule time zone"):
|
|
1968
|
+
self.crontab_utc_job.refresh_from_db()
|
|
1969
|
+
crontab_utc_schedule = self.crontab_utc_job.to_cron()
|
|
1970
|
+
self.assertEqual(crontab_utc_schedule.tz, ZoneInfo("UTC"))
|
|
1971
|
+
self.assertEqual(crontab_utc_schedule.hour, {17})
|
|
1972
|
+
self.assertEqual(crontab_utc_schedule.minute, {0})
|
|
1973
|
+
|
|
1974
|
+
with self.subTest("Test TYPE_CUSTOM schedule with EST time zone and EST schedule time zone"):
|
|
1975
|
+
self.crontab_est_job.refresh_from_db()
|
|
1976
|
+
crontab_est_schedule = self.crontab_est_job.to_cron()
|
|
1977
|
+
self.assertEqual(crontab_est_schedule.tz, ZoneInfo("America/New_York"))
|
|
1978
|
+
self.assertEqual(crontab_est_schedule.hour, {17})
|
|
1979
|
+
self.assertEqual(crontab_est_schedule.minute, {0})
|
|
1980
|
+
|
|
1981
|
+
def test_crontab_dst(self):
|
|
1982
|
+
"""Test that TYPE_CUSTOM behavior around DST is as expected."""
|
|
1983
|
+
cronjob = ScheduledJob.objects.create(
|
|
1984
|
+
name="DST Aware Cronjob",
|
|
1985
|
+
task="pass.TestPass",
|
|
1986
|
+
job_model=self.job_model,
|
|
1987
|
+
enabled=False,
|
|
1988
|
+
interval=JobExecutionType.TYPE_CUSTOM,
|
|
1989
|
+
start_time=datetime(year=2024, month=1, day=1, hour=17, minute=0, tzinfo=ZoneInfo("America/New_York")),
|
|
1990
|
+
crontab="0 17 * * *", # 5 PM local time
|
|
1991
|
+
time_zone=ZoneInfo("America/New_York"),
|
|
1992
|
+
)
|
|
1993
|
+
|
|
1994
|
+
# Before DST takes effect
|
|
1995
|
+
with self.subTest("Test UTC time zone with EST job"):
|
|
1996
|
+
cronjob.refresh_from_db()
|
|
1997
|
+
crontab = cronjob.to_cron()
|
|
1998
|
+
with time_machine.travel("2024-03-09 21:59 +0000"):
|
|
1999
|
+
is_due, _ = crontab.is_due(last_run_at=datetime(2024, 3, 8, 17, 0, tzinfo=ZoneInfo("America/New_York")))
|
|
2000
|
+
self.assertFalse(is_due)
|
|
2001
|
+
with time_machine.travel("2024-03-09 22:00 +0000"):
|
|
2002
|
+
is_due, _ = crontab.is_due(last_run_at=datetime(2024, 3, 8, 17, 0, tzinfo=ZoneInfo("America/New_York")))
|
|
2003
|
+
self.assertTrue(is_due)
|
|
2004
|
+
|
|
2005
|
+
with self.subTest("Test EST time zone with EST job"), override_settings(TIME_ZONE="America/New_York"):
|
|
2006
|
+
cronjob.refresh_from_db()
|
|
2007
|
+
crontab = cronjob.to_cron()
|
|
2008
|
+
with time_machine.travel("2024-03-09 16:59 -0500"):
|
|
2009
|
+
is_due, _ = crontab.is_due(last_run_at=datetime(2024, 3, 8, 17, 0, tzinfo=ZoneInfo("America/New_York")))
|
|
2010
|
+
self.assertFalse(is_due)
|
|
2011
|
+
with time_machine.travel("2024-03-09 17:00 -0500"):
|
|
2012
|
+
is_due, _ = crontab.is_due(last_run_at=datetime(2024, 3, 8, 17, 0, tzinfo=ZoneInfo("America/New_York")))
|
|
2013
|
+
self.assertTrue(is_due)
|
|
2014
|
+
|
|
2015
|
+
# Day that DST takes effect
|
|
2016
|
+
with self.subTest("Test UTC time zone with EDT job"):
|
|
2017
|
+
cronjob.refresh_from_db()
|
|
2018
|
+
crontab = cronjob.to_cron()
|
|
2019
|
+
with time_machine.travel("2024-03-10 20:59 +0000"):
|
|
2020
|
+
is_due, _ = crontab.is_due(last_run_at=datetime(2024, 3, 9, 17, 0, tzinfo=ZoneInfo("America/New_York")))
|
|
2021
|
+
self.assertFalse(is_due)
|
|
2022
|
+
with time_machine.travel("2024-03-10 21:00 +0000"):
|
|
2023
|
+
is_due, _ = crontab.is_due(last_run_at=datetime(2024, 3, 9, 17, 0, tzinfo=ZoneInfo("America/New_York")))
|
|
2024
|
+
self.assertTrue(is_due)
|
|
2025
|
+
|
|
2026
|
+
with self.subTest("Test EDT time zone with EDT job"), override_settings(TIME_ZONE="America/New_York"):
|
|
2027
|
+
cronjob.refresh_from_db()
|
|
2028
|
+
crontab = cronjob.to_cron()
|
|
2029
|
+
with time_machine.travel("2024-03-10 16:59 -0400"):
|
|
2030
|
+
is_due, _ = crontab.is_due(last_run_at=datetime(2024, 3, 9, 17, 0, tzinfo=ZoneInfo("America/New_York")))
|
|
2031
|
+
self.assertFalse(is_due)
|
|
2032
|
+
with time_machine.travel("2024-03-10 17:00 -0400"):
|
|
2033
|
+
is_due, _ = crontab.is_due(last_run_at=datetime(2024, 3, 9, 17, 0, tzinfo=ZoneInfo("America/New_York")))
|
|
2034
|
+
self.assertTrue(is_due)
|
|
2035
|
+
|
|
2036
|
+
def test_daily_dst(self):
|
|
2037
|
+
"""Test the interaction of TYPE_DAILY around DST."""
|
|
2038
|
+
daily = ScheduledJob.objects.create(
|
|
2039
|
+
name="Daily Job",
|
|
2040
|
+
task="pass.TestPass",
|
|
2041
|
+
job_model=self.job_model,
|
|
2042
|
+
enabled=False,
|
|
2043
|
+
interval=JobExecutionType.TYPE_DAILY,
|
|
2044
|
+
start_time=datetime(year=2024, month=1, day=1, hour=17, minute=0, tzinfo=ZoneInfo("America/New_York")),
|
|
2045
|
+
time_zone=ZoneInfo("America/New_York"),
|
|
2046
|
+
)
|
|
2047
|
+
|
|
2048
|
+
# Before DST takes effect
|
|
2049
|
+
with self.subTest("Test UTC time zone with EST job"):
|
|
2050
|
+
daily.refresh_from_db()
|
|
2051
|
+
crontab = daily.to_cron()
|
|
2052
|
+
with time_machine.travel("2024-03-09 21:59 +0000"):
|
|
2053
|
+
is_due, _ = crontab.is_due(last_run_at=datetime(2024, 3, 8, 17, 0, tzinfo=ZoneInfo("America/New_York")))
|
|
2054
|
+
self.assertFalse(is_due)
|
|
2055
|
+
with time_machine.travel("2024-03-09 22:00 +0000"):
|
|
2056
|
+
is_due, _ = crontab.is_due(last_run_at=datetime(2024, 3, 8, 17, 0, tzinfo=ZoneInfo("America/New_York")))
|
|
2057
|
+
self.assertTrue(is_due)
|
|
2058
|
+
|
|
2059
|
+
with self.subTest("Test EST time zone with EST job"), override_settings(TIME_ZONE="America/New_York"):
|
|
2060
|
+
daily.refresh_from_db()
|
|
2061
|
+
crontab = daily.to_cron()
|
|
2062
|
+
with time_machine.travel("2024-03-09 16:59 -0500"):
|
|
2063
|
+
is_due, _ = crontab.is_due(last_run_at=datetime(2024, 3, 8, 17, 0, tzinfo=ZoneInfo("America/New_York")))
|
|
2064
|
+
self.assertFalse(is_due)
|
|
2065
|
+
with time_machine.travel("2024-03-09 17:00 -0500"):
|
|
2066
|
+
is_due, _ = crontab.is_due(last_run_at=datetime(2024, 3, 8, 17, 0, tzinfo=ZoneInfo("America/New_York")))
|
|
2067
|
+
self.assertTrue(is_due)
|
|
2068
|
+
|
|
2069
|
+
# Day that DST takes effect
|
|
2070
|
+
with self.subTest("Test UTC time zone with EDT job"):
|
|
2071
|
+
daily.refresh_from_db()
|
|
2072
|
+
crontab = daily.to_cron()
|
|
2073
|
+
with time_machine.travel("2024-03-10 20:59 +0000"):
|
|
2074
|
+
is_due, _ = crontab.is_due(last_run_at=datetime(2024, 3, 9, 17, 0, tzinfo=ZoneInfo("America/New_York")))
|
|
2075
|
+
self.assertFalse(is_due)
|
|
2076
|
+
with time_machine.travel("2024-03-10 21:00 +0000"):
|
|
2077
|
+
is_due, _ = crontab.is_due(last_run_at=datetime(2024, 3, 9, 17, 0, tzinfo=ZoneInfo("America/New_York")))
|
|
2078
|
+
self.assertTrue(is_due)
|
|
2079
|
+
|
|
2080
|
+
with self.subTest("Test EDT time zone with EDT job"), override_settings(TIME_ZONE="America/New_York"):
|
|
2081
|
+
daily.refresh_from_db()
|
|
2082
|
+
crontab = daily.to_cron()
|
|
2083
|
+
with time_machine.travel("2024-03-10 16:59 -0400"):
|
|
2084
|
+
is_due, _ = crontab.is_due(last_run_at=datetime(2024, 3, 9, 17, 0, tzinfo=ZoneInfo("America/New_York")))
|
|
2085
|
+
self.assertFalse(is_due)
|
|
2086
|
+
with time_machine.travel("2024-03-10 17:00 -0400"):
|
|
2087
|
+
is_due, _ = crontab.is_due(last_run_at=datetime(2024, 3, 9, 17, 0, tzinfo=ZoneInfo("America/New_York")))
|
|
2088
|
+
self.assertTrue(is_due)
|
|
2089
|
+
|
|
2090
|
+
|
|
1793
2091
|
class SecretTest(ModelTestCases.BaseModelTestCase):
|
|
1794
2092
|
"""
|
|
1795
2093
|
Tests for the `Secret` model class.
|
|
@@ -1744,16 +1744,17 @@ class ScheduledJobTestCase(
|
|
|
1744
1744
|
ScheduledJob.objects.create(
|
|
1745
1745
|
name="test2",
|
|
1746
1746
|
task="pass.TestPass",
|
|
1747
|
-
interval=JobExecutionType.
|
|
1747
|
+
interval=JobExecutionType.TYPE_DAILY,
|
|
1748
1748
|
user=user,
|
|
1749
1749
|
start_time=timezone.now(),
|
|
1750
1750
|
)
|
|
1751
1751
|
ScheduledJob.objects.create(
|
|
1752
1752
|
name="test3",
|
|
1753
1753
|
task="pass.TestPass",
|
|
1754
|
-
interval=JobExecutionType.
|
|
1754
|
+
interval=JobExecutionType.TYPE_CUSTOM,
|
|
1755
1755
|
user=user,
|
|
1756
1756
|
start_time=timezone.now(),
|
|
1757
|
+
crontab="15 10 * * *",
|
|
1757
1758
|
)
|
|
1758
1759
|
|
|
1759
1760
|
def test_only_enabled_is_listed(self):
|
nautobot/extras/views.py
CHANGED
|
@@ -2,6 +2,7 @@ import logging
|
|
|
2
2
|
from urllib.parse import parse_qs
|
|
3
3
|
|
|
4
4
|
from celery import chain
|
|
5
|
+
from django.conf import settings
|
|
5
6
|
from django.contrib import messages
|
|
6
7
|
from django.contrib.auth.models import AnonymousUser
|
|
7
8
|
from django.contrib.contenttypes.models import ContentType
|
|
@@ -23,6 +24,11 @@ from django_tables2 import RequestConfig
|
|
|
23
24
|
from jsonschema.validators import Draft7Validator
|
|
24
25
|
from rest_framework.decorators import action
|
|
25
26
|
|
|
27
|
+
try:
|
|
28
|
+
from zoneinfo import ZoneInfo
|
|
29
|
+
except ImportError: # python 3.8
|
|
30
|
+
from backports.zoneinfo import ZoneInfo
|
|
31
|
+
|
|
26
32
|
from nautobot.core.forms import restrict_form_fields
|
|
27
33
|
from nautobot.core.models.querysets import count_related
|
|
28
34
|
from nautobot.core.models.utils import pretty_print_query
|
|
@@ -1916,6 +1922,7 @@ class ScheduledJobView(generic.ObjectView):
|
|
|
1916
1922
|
return {
|
|
1917
1923
|
"labels": labels,
|
|
1918
1924
|
"job_class_found": (job_class is not None),
|
|
1925
|
+
"default_time_zone": ZoneInfo(settings.TIME_ZONE),
|
|
1919
1926
|
**super().get_extra_context(request, instance),
|
|
1920
1927
|
}
|
|
1921
1928
|
|
nautobot/ipam/api/views.py
CHANGED
|
@@ -202,7 +202,7 @@ class PrefixViewSet(NautobotModelViewSet):
|
|
|
202
202
|
if requested_prefix["prefix_length"] >= available_prefix.prefixlen:
|
|
203
203
|
allocated_prefix = f"{available_prefix.network}/{requested_prefix['prefix_length']}"
|
|
204
204
|
requested_prefix["prefix"] = allocated_prefix
|
|
205
|
-
requested_prefix["namespace"] = prefix.namespace
|
|
205
|
+
requested_prefix["namespace"] = prefix.namespace
|
|
206
206
|
break
|
|
207
207
|
else:
|
|
208
208
|
return Response(
|
|
@@ -210,6 +210,10 @@ class PrefixViewSet(NautobotModelViewSet):
|
|
|
210
210
|
status=status.HTTP_204_NO_CONTENT,
|
|
211
211
|
)
|
|
212
212
|
|
|
213
|
+
# The serializer usage above has mapped "custom_fields" dict to "_custom_field_data".
|
|
214
|
+
# We need to convert it back to "custom_fields" as we're going to deserialize it a second time below
|
|
215
|
+
requested_prefix["custom_fields"] = requested_prefix.pop("_custom_field_data", {})
|
|
216
|
+
|
|
213
217
|
# Remove the allocated prefix from the list of available prefixes
|
|
214
218
|
available_prefixes.remove(allocated_prefix)
|
|
215
219
|
|
|
@@ -299,7 +303,10 @@ class PrefixViewSet(NautobotModelViewSet):
|
|
|
299
303
|
prefix_length = prefix.prefix.prefixlen
|
|
300
304
|
for requested_ip in requested_ips:
|
|
301
305
|
requested_ip["address"] = f"{next(available_ips)}/{prefix_length}"
|
|
302
|
-
requested_ip["namespace"] = prefix.namespace
|
|
306
|
+
requested_ip["namespace"] = prefix.namespace
|
|
307
|
+
# The serializer usage above has mapped "custom_fields" dict to "_custom_field_data".
|
|
308
|
+
# We need to convert it back to "custom_fields" as we're going to deserialize it a second time below
|
|
309
|
+
requested_ip["custom_fields"] = requested_ip.pop("_custom_field_data", {})
|
|
303
310
|
|
|
304
311
|
# Initialize the serializer with a list or a single object depending on what was requested
|
|
305
312
|
context = {"request": request, "depth": 0}
|
nautobot/ipam/choices.py
CHANGED
|
@@ -102,6 +102,23 @@ class IPAddressTypeChoices(ChoiceSet):
|
|
|
102
102
|
)
|
|
103
103
|
|
|
104
104
|
|
|
105
|
+
#
|
|
106
|
+
# VRFs
|
|
107
|
+
#
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class VRFStatusChoices(ChoiceSet):
|
|
111
|
+
STATUS_ACTIVE = "active"
|
|
112
|
+
STATUS_DOWN = "down"
|
|
113
|
+
STATUS_DEPRECATED = "deprecated"
|
|
114
|
+
|
|
115
|
+
CHOICES = (
|
|
116
|
+
(STATUS_ACTIVE, "Active"),
|
|
117
|
+
(STATUS_DOWN, "Down"),
|
|
118
|
+
(STATUS_DEPRECATED, "Deprecated"),
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
105
122
|
#
|
|
106
123
|
# VLANs
|
|
107
124
|
#
|