nautobot 2.2.2__py3-none-any.whl → 2.2.3__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.
- nautobot/apps/jobs.py +2 -0
- nautobot/core/api/utils.py +12 -9
- nautobot/core/apps/__init__.py +2 -2
- nautobot/core/celery/__init__.py +79 -68
- nautobot/core/celery/backends.py +9 -1
- nautobot/core/celery/control.py +4 -7
- nautobot/core/celery/schedulers.py +4 -2
- nautobot/core/celery/task.py +78 -5
- nautobot/core/graphql/schema.py +2 -1
- nautobot/core/jobs/__init__.py +2 -1
- nautobot/core/templates/generic/object_list.html +3 -3
- nautobot/core/templatetags/helpers.py +66 -9
- nautobot/core/testing/__init__.py +6 -1
- nautobot/core/testing/api.py +12 -13
- nautobot/core/testing/mixins.py +2 -2
- nautobot/core/testing/views.py +50 -51
- nautobot/core/tests/test_api.py +23 -2
- nautobot/core/tests/test_templatetags_helpers.py +32 -0
- nautobot/core/tests/test_views.py +19 -0
- nautobot/core/tests/test_views_utils.py +22 -1
- nautobot/core/utils/module_loading.py +89 -0
- nautobot/core/views/utils.py +3 -2
- nautobot/dcim/choices.py +14 -0
- nautobot/dcim/forms.py +51 -1
- nautobot/dcim/models/device_components.py +9 -5
- nautobot/dcim/templates/dcim/location.html +32 -13
- nautobot/dcim/templates/dcim/location_migrate_data_to_contact.html +102 -0
- nautobot/dcim/tests/test_views.py +137 -0
- nautobot/dcim/urls.py +5 -0
- nautobot/dcim/views.py +149 -1
- nautobot/extras/api/views.py +21 -10
- nautobot/extras/constants.py +3 -3
- nautobot/extras/datasources/git.py +47 -58
- nautobot/extras/forms/forms.py +3 -1
- nautobot/extras/jobs.py +79 -146
- nautobot/extras/models/datasources.py +0 -2
- nautobot/extras/models/jobs.py +36 -18
- nautobot/extras/plugins/__init__.py +1 -20
- nautobot/extras/signals.py +6 -9
- nautobot/extras/test_jobs/__init__.py +8 -0
- nautobot/extras/test_jobs/dry_run.py +3 -2
- nautobot/extras/test_jobs/fail.py +43 -0
- nautobot/extras/test_jobs/ipaddress_vars.py +40 -1
- nautobot/extras/test_jobs/jobs_module/__init__.py +5 -0
- nautobot/extras/test_jobs/jobs_module/jobs_submodule/__init__.py +1 -0
- nautobot/extras/test_jobs/jobs_module/jobs_submodule/jobs.py +6 -0
- nautobot/extras/test_jobs/pass.py +40 -0
- nautobot/extras/test_jobs/relative_import.py +11 -0
- nautobot/extras/tests/test_api.py +3 -0
- nautobot/extras/tests/test_datasources.py +125 -118
- nautobot/extras/tests/test_job_variables.py +57 -15
- nautobot/extras/tests/test_jobs.py +135 -1
- nautobot/extras/tests/test_models.py +26 -19
- nautobot/extras/tests/test_plugins.py +1 -3
- nautobot/extras/tests/test_views.py +2 -4
- nautobot/extras/views.py +47 -95
- nautobot/ipam/api/views.py +8 -1
- nautobot/ipam/graphql/types.py +11 -0
- nautobot/ipam/mixins.py +32 -0
- nautobot/ipam/models.py +2 -1
- nautobot/ipam/querysets.py +6 -1
- nautobot/ipam/tests/test_models.py +82 -0
- nautobot/project-static/docs/assets/extra.css +4 -0
- nautobot/project-static/docs/code-reference/nautobot/apps/api.html +1 -1
- nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +180 -211
- nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +1 -1
- nautobot/project-static/docs/development/core/application-registry.html +126 -84
- nautobot/project-static/docs/development/core/model-checklist.html +49 -1
- nautobot/project-static/docs/development/core/model-features.html +1 -1
- nautobot/project-static/docs/development/jobs/index.html +334 -58
- nautobot/project-static/docs/development/jobs/migration/from-v1.html +1 -1
- nautobot/project-static/docs/objects.inv +0 -0
- nautobot/project-static/docs/release-notes/version-2.2.html +237 -55
- nautobot/project-static/docs/search/search_index.json +1 -1
- nautobot/project-static/docs/sitemap.xml +254 -254
- nautobot/project-static/docs/sitemap.xml.gz +0 -0
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +7 -4
- nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +111 -0
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +15 -28
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +4 -4
- nautobot/project-static/js/forms.js +18 -11
- {nautobot-2.2.2.dist-info → nautobot-2.2.3.dist-info}/METADATA +3 -3
- {nautobot-2.2.2.dist-info → nautobot-2.2.3.dist-info}/RECORD +87 -81
- nautobot/extras/test_jobs/job_variables.py +0 -93
- {nautobot-2.2.2.dist-info → nautobot-2.2.3.dist-info}/LICENSE.txt +0 -0
- {nautobot-2.2.2.dist-info → nautobot-2.2.3.dist-info}/NOTICE +0 -0
- {nautobot-2.2.2.dist-info → nautobot-2.2.3.dist-info}/WHEEL +0 -0
- {nautobot-2.2.2.dist-info → nautobot-2.2.3.dist-info}/entry_points.txt +0 -0
|
@@ -3,26 +3,32 @@ from django.test import TestCase
|
|
|
3
3
|
from netaddr import IPAddress, IPNetwork
|
|
4
4
|
|
|
5
5
|
from nautobot.dcim.models import Device
|
|
6
|
-
from nautobot.extras.
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
6
|
+
from nautobot.extras.jobs import (
|
|
7
|
+
BooleanVar,
|
|
8
|
+
ChoiceVar,
|
|
9
|
+
FileVar,
|
|
10
|
+
IntegerVar,
|
|
11
|
+
IPAddressVar,
|
|
12
|
+
IPAddressWithMaskVar,
|
|
13
|
+
IPNetworkVar,
|
|
14
|
+
Job,
|
|
15
|
+
JSONVar,
|
|
16
|
+
MultiChoiceVar,
|
|
17
|
+
MultiObjectVar,
|
|
18
|
+
ObjectVar,
|
|
19
|
+
StringVar,
|
|
20
|
+
TextVar,
|
|
21
21
|
)
|
|
22
|
+
from nautobot.extras.models import Role
|
|
23
|
+
|
|
24
|
+
CHOICES = (("ff0000", "Red"), ("00ff00", "Green"), ("0000ff", "Blue"))
|
|
22
25
|
|
|
23
26
|
|
|
24
27
|
class JobVariablesTest(TestCase):
|
|
25
28
|
def test_stringvar(self):
|
|
29
|
+
class StringVarJob(Job):
|
|
30
|
+
var1 = StringVar(min_length=3, max_length=3, regex=r"[a-z]+")
|
|
31
|
+
|
|
26
32
|
# Validate min_length enforcement
|
|
27
33
|
data = {"var1": "xx"}
|
|
28
34
|
form = StringVarJob().as_form(data)
|
|
@@ -48,6 +54,9 @@ class JobVariablesTest(TestCase):
|
|
|
48
54
|
self.assertEqual(form.cleaned_data["var1"], data["var1"])
|
|
49
55
|
|
|
50
56
|
def test_textvar(self):
|
|
57
|
+
class TextVarJob(Job):
|
|
58
|
+
var1 = TextVar()
|
|
59
|
+
|
|
51
60
|
# Validate valid data
|
|
52
61
|
data = {"var1": "This is a test string"}
|
|
53
62
|
form = TextVarJob().as_form(data)
|
|
@@ -55,6 +64,9 @@ class JobVariablesTest(TestCase):
|
|
|
55
64
|
self.assertEqual(form.cleaned_data["var1"], data["var1"])
|
|
56
65
|
|
|
57
66
|
def test_integervar(self):
|
|
67
|
+
class IntegerVarJob(Job):
|
|
68
|
+
var1 = IntegerVar(min_value=5, max_value=10)
|
|
69
|
+
|
|
58
70
|
# Validate min_value enforcement
|
|
59
71
|
data = {"var1": 4}
|
|
60
72
|
form = IntegerVarJob().as_form(data)
|
|
@@ -74,6 +86,9 @@ class JobVariablesTest(TestCase):
|
|
|
74
86
|
self.assertEqual(form.cleaned_data["var1"], data["var1"])
|
|
75
87
|
|
|
76
88
|
def test_booleanvar(self):
|
|
89
|
+
class BooleanVarJob(Job):
|
|
90
|
+
var1 = BooleanVar()
|
|
91
|
+
|
|
77
92
|
# Validate True
|
|
78
93
|
data = {"var1": True}
|
|
79
94
|
form = BooleanVarJob().as_form(data)
|
|
@@ -87,6 +102,9 @@ class JobVariablesTest(TestCase):
|
|
|
87
102
|
self.assertEqual(form.cleaned_data["var1"], False)
|
|
88
103
|
|
|
89
104
|
def test_choicevar(self):
|
|
105
|
+
class ChoiceVarJob(Job):
|
|
106
|
+
var1 = ChoiceVar(choices=CHOICES)
|
|
107
|
+
|
|
90
108
|
# Validate valid choice
|
|
91
109
|
data = {"var1": "ff0000"}
|
|
92
110
|
form = ChoiceVarJob().as_form(data)
|
|
@@ -100,6 +118,9 @@ class JobVariablesTest(TestCase):
|
|
|
100
118
|
self.assertIn("var1", form.errors)
|
|
101
119
|
|
|
102
120
|
def test_multichoicevar(self):
|
|
121
|
+
class MultiChoiceVarJob(Job):
|
|
122
|
+
var1 = MultiChoiceVar(choices=CHOICES)
|
|
123
|
+
|
|
103
124
|
# Validate single choice
|
|
104
125
|
data = {"var1": ["ff0000"]}
|
|
105
126
|
form = MultiChoiceVarJob().as_form(data)
|
|
@@ -119,6 +140,9 @@ class JobVariablesTest(TestCase):
|
|
|
119
140
|
self.assertIn("var1", form.errors)
|
|
120
141
|
|
|
121
142
|
def test_objectvar(self):
|
|
143
|
+
class ObjectVarJob(Job):
|
|
144
|
+
var1 = ObjectVar(model=Role)
|
|
145
|
+
|
|
122
146
|
# Validate valid data
|
|
123
147
|
data = {"var1": Role.objects.get_for_model(Device).first().pk}
|
|
124
148
|
form = ObjectVarJob().as_form(data)
|
|
@@ -126,6 +150,9 @@ class JobVariablesTest(TestCase):
|
|
|
126
150
|
self.assertEqual(form.cleaned_data["var1"].pk, data["var1"])
|
|
127
151
|
|
|
128
152
|
def test_multiobjectvar(self):
|
|
153
|
+
class MultiObjectVarJob(Job):
|
|
154
|
+
var1 = MultiObjectVar(model=Role)
|
|
155
|
+
|
|
129
156
|
# Validate valid data
|
|
130
157
|
data = {"var1": [role.pk for role in Role.objects.all()[:3]]}
|
|
131
158
|
form = MultiObjectVarJob().as_form(data)
|
|
@@ -135,6 +162,9 @@ class JobVariablesTest(TestCase):
|
|
|
135
162
|
self.assertEqual(form.cleaned_data["var1"][2].pk, data["var1"][2])
|
|
136
163
|
|
|
137
164
|
def test_filevar(self):
|
|
165
|
+
class FileVarJob(Job):
|
|
166
|
+
var1 = FileVar()
|
|
167
|
+
|
|
138
168
|
# Test file
|
|
139
169
|
testfile = SimpleUploadedFile(name="test_file.txt", content=b"This is an test file for testing")
|
|
140
170
|
|
|
@@ -145,6 +175,9 @@ class JobVariablesTest(TestCase):
|
|
|
145
175
|
self.assertEqual(form.cleaned_data["var1"], testfile)
|
|
146
176
|
|
|
147
177
|
def test_ipaddressvar(self):
|
|
178
|
+
class IPAddressVarJob(Job):
|
|
179
|
+
var1 = IPAddressVar()
|
|
180
|
+
|
|
148
181
|
# Validate IP network enforcement
|
|
149
182
|
data = {"var1": "1.2.3"}
|
|
150
183
|
form = IPAddressVarJob().as_form(data)
|
|
@@ -164,6 +197,9 @@ class JobVariablesTest(TestCase):
|
|
|
164
197
|
self.assertEqual(form.cleaned_data["var1"], IPAddress(data["var1"]))
|
|
165
198
|
|
|
166
199
|
def test_ipaddresswithmaskvar(self):
|
|
200
|
+
class IPAddressWithMaskVarJob(Job):
|
|
201
|
+
var1 = IPAddressWithMaskVar()
|
|
202
|
+
|
|
167
203
|
# Validate IP network enforcement
|
|
168
204
|
data = {"var1": "1.2.3"}
|
|
169
205
|
form = IPAddressWithMaskVarJob().as_form(data)
|
|
@@ -183,6 +219,9 @@ class JobVariablesTest(TestCase):
|
|
|
183
219
|
self.assertEqual(form.cleaned_data["var1"], IPNetwork(data["var1"]))
|
|
184
220
|
|
|
185
221
|
def test_ipnetworkvar(self):
|
|
222
|
+
class IPNetworkVarJob(Job):
|
|
223
|
+
var1 = IPNetworkVar()
|
|
224
|
+
|
|
186
225
|
# Validate IP network enforcement
|
|
187
226
|
data = {"var1": "1.2.3"}
|
|
188
227
|
form = IPNetworkVarJob().as_form(data)
|
|
@@ -202,6 +241,9 @@ class JobVariablesTest(TestCase):
|
|
|
202
241
|
self.assertEqual(form.cleaned_data["var1"], IPNetwork(data["var1"]))
|
|
203
242
|
|
|
204
243
|
def test_jsonvar(self):
|
|
244
|
+
class JSONVarJob(Job):
|
|
245
|
+
var1 = JSONVar()
|
|
246
|
+
|
|
205
247
|
# Valid JSON value as dictionary
|
|
206
248
|
data = {"var1": {"key1": "value1"}}
|
|
207
249
|
form = JSONVarJob().as_form(data)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import datetime
|
|
2
2
|
from io import StringIO
|
|
3
3
|
import json
|
|
4
|
+
import os
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
import re
|
|
6
7
|
import tempfile
|
|
@@ -34,7 +35,7 @@ from nautobot.extras.choices import (
|
|
|
34
35
|
ObjectChangeEventContextChoices,
|
|
35
36
|
)
|
|
36
37
|
from nautobot.extras.context_managers import change_logging, JobHookChangeContext, web_request_context
|
|
37
|
-
from nautobot.extras.jobs import get_job
|
|
38
|
+
from nautobot.extras.jobs import get_job, get_jobs
|
|
38
39
|
|
|
39
40
|
|
|
40
41
|
class JobTest(TestCase):
|
|
@@ -174,6 +175,111 @@ class JobTest(TestCase):
|
|
|
174
175
|
self.assertFalse(job_class.supports_dryrun)
|
|
175
176
|
self.assertFalse(job_model.supports_dryrun)
|
|
176
177
|
|
|
178
|
+
def test_submodule_in_jobs_root(self):
|
|
179
|
+
"""
|
|
180
|
+
Test that a subdirectory/submodule in JOBS_ROOT can contain Jobs.
|
|
181
|
+
"""
|
|
182
|
+
job_class, job_model = get_job_class_and_model("jobs_module.jobs_submodule.jobs", "ChildJob")
|
|
183
|
+
self.assertIsNotNone(job_class)
|
|
184
|
+
self.assertIsNotNone(job_model)
|
|
185
|
+
|
|
186
|
+
def test_relative_import_among_files_in_jobs_root(self):
|
|
187
|
+
"""
|
|
188
|
+
Test that a module in JOBS_ROOT can import from other modules in JOBS_ROOT.
|
|
189
|
+
"""
|
|
190
|
+
job_class, job_model = get_job_class_and_model("relative_import", "TestReallyPass")
|
|
191
|
+
self.assertIsNotNone(job_class)
|
|
192
|
+
self.assertIsNotNone(job_model)
|
|
193
|
+
|
|
194
|
+
def test_get_jobs_from_jobs_root(self):
|
|
195
|
+
"""
|
|
196
|
+
Test that get_jobs() correctly loads jobs from JOBS_ROOT as its contents change.
|
|
197
|
+
"""
|
|
198
|
+
try:
|
|
199
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
200
|
+
with override_settings(JOBS_ROOT=temp_dir):
|
|
201
|
+
# Create a new Job and make sure it's discovered correctly
|
|
202
|
+
with open(os.path.join(temp_dir, "my_jobs.py"), "w") as fd:
|
|
203
|
+
fd.write("""\
|
|
204
|
+
from nautobot.apps.jobs import Job, register_jobs
|
|
205
|
+
class MyJob(Job):
|
|
206
|
+
def run(self):
|
|
207
|
+
pass
|
|
208
|
+
register_jobs(MyJob)
|
|
209
|
+
""")
|
|
210
|
+
jobs_data = get_jobs(reload=True)
|
|
211
|
+
self.assertIn("my_jobs.MyJob", jobs_data.keys())
|
|
212
|
+
self.assertIsNotNone(get_job("my_jobs.MyJob"))
|
|
213
|
+
# Also make sure some representative previous JOBS_ROOT jobs aren't still around:
|
|
214
|
+
self.assertNotIn("dry_run.TestDryRun", jobs_data.keys())
|
|
215
|
+
self.assertNotIn("pass.TestPass", jobs_data.keys())
|
|
216
|
+
|
|
217
|
+
# Create a second Job in the same module
|
|
218
|
+
with open(os.path.join(temp_dir, "my_jobs.py"), "a") as fd:
|
|
219
|
+
fd.write("""
|
|
220
|
+
class MyOtherJob(MyJob):
|
|
221
|
+
pass
|
|
222
|
+
register_jobs(MyOtherJob)
|
|
223
|
+
""")
|
|
224
|
+
jobs_data = get_jobs(reload=True)
|
|
225
|
+
self.assertIn("my_jobs.MyJob", jobs_data.keys())
|
|
226
|
+
self.assertIsNotNone(get_job("my_jobs.MyJob"))
|
|
227
|
+
self.assertIn("my_jobs.MyOtherJob", jobs_data.keys())
|
|
228
|
+
self.assertIsNotNone(get_job("my_jobs.MyOtherJob"))
|
|
229
|
+
|
|
230
|
+
# Create a third Job in another module
|
|
231
|
+
with open(os.path.join(temp_dir, "their_jobs.py"), "w") as fd:
|
|
232
|
+
fd.write("""
|
|
233
|
+
from nautobot.apps.jobs import Job, register_jobs
|
|
234
|
+
|
|
235
|
+
class MyJob(Job):
|
|
236
|
+
def run(self):
|
|
237
|
+
pass
|
|
238
|
+
register_jobs(MyJob)
|
|
239
|
+
""")
|
|
240
|
+
jobs_data = get_jobs(reload=True)
|
|
241
|
+
self.assertIn("my_jobs.MyJob", jobs_data.keys())
|
|
242
|
+
self.assertIsNotNone(get_job("my_jobs.MyJob"))
|
|
243
|
+
self.assertIn("my_jobs.MyOtherJob", jobs_data.keys())
|
|
244
|
+
self.assertIsNotNone(get_job("my_jobs.MyOtherJob"))
|
|
245
|
+
self.assertIn("their_jobs.MyJob", jobs_data.keys())
|
|
246
|
+
self.assertIsNotNone(get_job("their_jobs.MyJob"))
|
|
247
|
+
self.assertNotEqual(get_job("my_jobs.MyJob"), get_job("their_jobs.MyJob"))
|
|
248
|
+
|
|
249
|
+
# Delete a module
|
|
250
|
+
os.remove(os.path.join(temp_dir, "their_jobs.py"))
|
|
251
|
+
jobs_data = get_jobs(reload=True)
|
|
252
|
+
self.assertIn("my_jobs.MyJob", jobs_data.keys())
|
|
253
|
+
self.assertIsNotNone(get_job("my_jobs.MyJob"))
|
|
254
|
+
self.assertIn("my_jobs.MyOtherJob", jobs_data.keys())
|
|
255
|
+
self.assertIsNotNone(get_job("my_jobs.MyOtherJob"))
|
|
256
|
+
self.assertNotIn("their_jobs", jobs_data.keys())
|
|
257
|
+
self.assertIsNone(get_job("their_jobs.MyJob"))
|
|
258
|
+
|
|
259
|
+
# Create a module with an inauspicious name
|
|
260
|
+
with open(os.path.join(temp_dir, "traceback.py"), "w") as fd:
|
|
261
|
+
fd.write("""
|
|
262
|
+
from nautobot.apps.jobs import Job, register_jobs
|
|
263
|
+
|
|
264
|
+
class BadJob(Job):
|
|
265
|
+
def run(self):
|
|
266
|
+
raise RuntimeError("You ran a bad job!")
|
|
267
|
+
register_jobs(BadJob)
|
|
268
|
+
""")
|
|
269
|
+
jobs_data = get_jobs(reload=True)
|
|
270
|
+
self.assertIn("my_jobs.MyJob", jobs_data.keys())
|
|
271
|
+
self.assertIsNotNone(get_job("my_jobs.MyJob"))
|
|
272
|
+
self.assertIn("my_jobs.MyOtherJob", jobs_data.keys())
|
|
273
|
+
self.assertIsNotNone(get_job("my_jobs.MyOtherJob"))
|
|
274
|
+
# Since `traceback` conflicts with a system module, it should not get loaded
|
|
275
|
+
self.assertNotIn("traceback.BadJob", jobs_data.keys())
|
|
276
|
+
self.assertIsNone(get_job("traceback.BadJob"))
|
|
277
|
+
|
|
278
|
+
# TODO: testing with subdirectories/submodules under JOBS_ROOT...
|
|
279
|
+
finally:
|
|
280
|
+
# Clean up back to normal behavior
|
|
281
|
+
get_jobs(reload=True)
|
|
282
|
+
|
|
177
283
|
|
|
178
284
|
class JobTransactionTest(TransactionTestCase):
|
|
179
285
|
"""
|
|
@@ -216,6 +322,19 @@ class JobTransactionTest(TransactionTestCase):
|
|
|
216
322
|
name = "TestPass"
|
|
217
323
|
job_result = create_job_result_and_run_job(module, name)
|
|
218
324
|
self.assertEqual(job_result.status, JobResultStatusChoices.STATUS_SUCCESS)
|
|
325
|
+
self.assertEqual(job_result.result, True)
|
|
326
|
+
logs = job_result.job_log_entries
|
|
327
|
+
self.assertGreater(logs.count(), 0)
|
|
328
|
+
try:
|
|
329
|
+
logs.get(message="before_start() was called as expected")
|
|
330
|
+
logs.get(message="Success")
|
|
331
|
+
logs.get(message="on_success() was called as expected")
|
|
332
|
+
logs.get(message="after_return() was called as expected")
|
|
333
|
+
except models.JobLogEntry.DoesNotExist:
|
|
334
|
+
for log in logs.all():
|
|
335
|
+
print(log.message)
|
|
336
|
+
print(job_result.traceback)
|
|
337
|
+
raise
|
|
219
338
|
|
|
220
339
|
def test_job_result_manager_censor_sensitive_variables(self):
|
|
221
340
|
"""
|
|
@@ -237,6 +356,18 @@ class JobTransactionTest(TransactionTestCase):
|
|
|
237
356
|
name = "TestFail"
|
|
238
357
|
job_result = create_job_result_and_run_job(module, name)
|
|
239
358
|
self.assertEqual(job_result.status, JobResultStatusChoices.STATUS_FAILURE)
|
|
359
|
+
logs = job_result.job_log_entries
|
|
360
|
+
self.assertGreater(logs.count(), 0)
|
|
361
|
+
try:
|
|
362
|
+
logs.get(message="before_start() was called as expected")
|
|
363
|
+
logs.get(message="I'm a test job that fails!")
|
|
364
|
+
logs.get(message="on_failure() was called as expected")
|
|
365
|
+
logs.get(message="after_return() was called as expected")
|
|
366
|
+
except models.JobLogEntry.DoesNotExist:
|
|
367
|
+
for log in logs.all():
|
|
368
|
+
print(log.message)
|
|
369
|
+
print(job_result.traceback)
|
|
370
|
+
raise
|
|
240
371
|
|
|
241
372
|
def test_job_fail_with_sanitization(self):
|
|
242
373
|
"""
|
|
@@ -876,6 +1007,7 @@ class JobHookReceiverTransactionTest(TransactionTestCase):
|
|
|
876
1007
|
test_location = Location.objects.get(name="test_jhr")
|
|
877
1008
|
oc = get_changes_for_model(test_location).first()
|
|
878
1009
|
self.assertEqual(oc.change_context, ObjectChangeEventContextChoices.CONTEXT_JOB_HOOK)
|
|
1010
|
+
self.assertIsNotNone(job_result.user)
|
|
879
1011
|
self.assertEqual(oc.user_id, job_result.user.pk)
|
|
880
1012
|
|
|
881
1013
|
def test_missing_receive_job_hook_method(self):
|
|
@@ -933,6 +1065,8 @@ class JobHookTransactionTest(TransactionTestCase): # TODO: BaseModelTestCase mi
|
|
|
933
1065
|
module = "job_hook_receiver"
|
|
934
1066
|
name = "TestJobHookReceiverLog"
|
|
935
1067
|
self.job_class, self.job_model = get_job_class_and_model(module, name)
|
|
1068
|
+
self.assertIsNotNone(self.job_class)
|
|
1069
|
+
self.assertIsNotNone(self.job_model)
|
|
936
1070
|
job_hook = models.JobHook(
|
|
937
1071
|
name="JobHookTest",
|
|
938
1072
|
type_create=True,
|
|
@@ -65,6 +65,7 @@ from nautobot.extras.models import (
|
|
|
65
65
|
Webhook,
|
|
66
66
|
)
|
|
67
67
|
from nautobot.extras.models.statuses import StatusModel
|
|
68
|
+
from nautobot.extras.registry import registry
|
|
68
69
|
from nautobot.extras.secrets.exceptions import SecretParametersError, SecretProviderError, SecretValueNotFoundError
|
|
69
70
|
from nautobot.ipam.models import IPAddress
|
|
70
71
|
from nautobot.tenancy.models import Tenant
|
|
@@ -1079,25 +1080,31 @@ class JobModelTest(ModelTestCases.BaseModelTestCase):
|
|
|
1079
1080
|
def test_defaults(self):
|
|
1080
1081
|
"""Verify that defaults for discovered JobModel instances are as expected."""
|
|
1081
1082
|
for job_model in JobModel.objects.all():
|
|
1082
|
-
self.
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1083
|
+
with self.subTest(class_path=job_model.class_path):
|
|
1084
|
+
try:
|
|
1085
|
+
self.assertTrue(job_model.installed)
|
|
1086
|
+
# System jobs should be enabled by default, all others are disabled by default
|
|
1087
|
+
if job_model.module_name.startswith("nautobot."):
|
|
1088
|
+
self.assertTrue(job_model.enabled)
|
|
1089
|
+
else:
|
|
1090
|
+
self.assertFalse(job_model.enabled)
|
|
1091
|
+
for field_name in JOB_OVERRIDABLE_FIELDS:
|
|
1092
|
+
if field_name == "name" and "duplicate_name" in job_model.job_class.__module__:
|
|
1093
|
+
pass # name field for test_duplicate_name jobs tested in test_duplicate_job_name below
|
|
1094
|
+
else:
|
|
1095
|
+
self.assertFalse(
|
|
1096
|
+
getattr(job_model, f"{field_name}_override"),
|
|
1097
|
+
(field_name, getattr(job_model, field_name), getattr(job_model.job_class, field_name)),
|
|
1098
|
+
)
|
|
1099
|
+
self.assertEqual(
|
|
1100
|
+
getattr(job_model, field_name),
|
|
1101
|
+
getattr(job_model.job_class, field_name),
|
|
1102
|
+
field_name,
|
|
1103
|
+
)
|
|
1104
|
+
except AssertionError:
|
|
1105
|
+
print(list(JobModel.objects.all()))
|
|
1106
|
+
print(registry["jobs"])
|
|
1107
|
+
raise
|
|
1101
1108
|
|
|
1102
1109
|
def test_duplicate_job_name(self):
|
|
1103
1110
|
self.assertTrue(JobModel.objects.filter(name="TestDuplicateNameNoMeta").exists())
|
|
@@ -9,7 +9,6 @@ from django.urls import NoReverseMatch, reverse
|
|
|
9
9
|
import netaddr
|
|
10
10
|
|
|
11
11
|
from nautobot.circuits.models import Circuit, CircuitType, Provider
|
|
12
|
-
from nautobot.core.celery import app
|
|
13
12
|
from nautobot.core.testing import APIViewTestCases, disable_warnings, extract_page_body, TestCase, ViewTestCases
|
|
14
13
|
from nautobot.dcim.models import Device, DeviceType, Location, LocationType, Manufacturer
|
|
15
14
|
from nautobot.dcim.tests.test_views import create_test_device
|
|
@@ -114,9 +113,8 @@ class AppTest(TestCase):
|
|
|
114
113
|
"""
|
|
115
114
|
from example_app.jobs import ExampleJob
|
|
116
115
|
|
|
117
|
-
self.assertIn(ExampleJob, registry.get("
|
|
116
|
+
self.assertIn(ExampleJob.class_path, registry.get("jobs", {}))
|
|
118
117
|
self.assertEqual(ExampleJob, get_job("example_app.jobs.ExampleJob"))
|
|
119
|
-
self.assertIn("example_app.jobs.ExampleJob", app.tasks)
|
|
120
118
|
|
|
121
119
|
def test_git_datasource_contents_registration(self):
|
|
122
120
|
"""
|
|
@@ -1765,10 +1765,8 @@ class JobTestCase(
|
|
|
1765
1765
|
model = Job
|
|
1766
1766
|
|
|
1767
1767
|
def _get_queryset(self):
|
|
1768
|
-
"""Don't include hidden Jobs
|
|
1769
|
-
return self.model.objects.filter(
|
|
1770
|
-
installed=True, hidden=False, is_job_hook_receiver=False, is_job_button_receiver=False
|
|
1771
|
-
)
|
|
1768
|
+
"""Don't include hidden Jobs or non-installed Jobs, as they won't appear in the UI by default."""
|
|
1769
|
+
return self.model.objects.filter(installed=True, hidden=False)
|
|
1772
1770
|
|
|
1773
1771
|
@classmethod
|
|
1774
1772
|
def setUpTestData(cls):
|