nautobot 2.4.4__py3-none-any.whl → 2.4.5__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/__init__.py +19 -3
- nautobot/core/celery/__init__.py +5 -3
- nautobot/core/jobs/__init__.py +3 -2
- nautobot/core/testing/__init__.py +2 -0
- nautobot/core/testing/mixins.py +9 -0
- nautobot/core/tests/test_jobs.py +26 -27
- nautobot/dcim/tests/test_jobs.py +4 -6
- nautobot/extras/choices.py +8 -3
- nautobot/extras/jobs.py +181 -103
- nautobot/extras/management/utils.py +13 -2
- nautobot/extras/models/datasources.py +4 -1
- nautobot/extras/models/jobs.py +20 -17
- nautobot/extras/tables.py +25 -29
- nautobot/extras/test_jobs/atomic_transaction.py +6 -6
- nautobot/extras/test_jobs/fail.py +75 -1
- nautobot/extras/tests/test_api.py +1 -1
- nautobot/extras/tests/test_datasources.py +64 -54
- nautobot/extras/tests/test_jobs.py +69 -62
- nautobot/extras/tests/test_models.py +1 -1
- nautobot/extras/tests/test_relationships.py +5 -5
- nautobot/extras/views.py +7 -1
- nautobot/ipam/forms.py +15 -0
- nautobot/ipam/querysets.py +6 -0
- nautobot/ipam/templates/ipam/rir.html +1 -43
- nautobot/ipam/tests/test_models.py +16 -0
- nautobot/ipam/urls.py +1 -21
- nautobot/ipam/views.py +24 -41
- nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +43 -5
- nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +35 -0
- nautobot/project-static/docs/development/jobs/index.html +27 -14
- nautobot/project-static/docs/objects.inv +0 -0
- nautobot/project-static/docs/release-notes/version-2.4.html +183 -0
- nautobot/project-static/docs/requirements.txt +1 -1
- nautobot/project-static/docs/search/search_index.json +1 -1
- nautobot/project-static/docs/sitemap.xml +290 -290
- nautobot/project-static/docs/sitemap.xml.gz +0 -0
- {nautobot-2.4.4.dist-info → nautobot-2.4.5.dist-info}/METADATA +3 -3
- {nautobot-2.4.4.dist-info → nautobot-2.4.5.dist-info}/RECORD +42 -42
- {nautobot-2.4.4.dist-info → nautobot-2.4.5.dist-info}/LICENSE.txt +0 -0
- {nautobot-2.4.4.dist-info → nautobot-2.4.5.dist-info}/NOTICE +0 -0
- {nautobot-2.4.4.dist-info → nautobot-2.4.5.dist-info}/WHEEL +0 -0
- {nautobot-2.4.4.dist-info → nautobot-2.4.5.dist-info}/entry_points.txt +0 -0
|
@@ -62,6 +62,20 @@ class TestFailJob(Job):
|
|
|
62
62
|
logger.info("after_return() was called as expected")
|
|
63
63
|
|
|
64
64
|
|
|
65
|
+
class TestFailInBeforeStart(TestFailJob):
|
|
66
|
+
"""
|
|
67
|
+
Job that raises an exception in before_start().
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
def before_start(self, task_id, args, kwargs):
|
|
71
|
+
super().before_start(task_id, args, kwargs)
|
|
72
|
+
logger.info("I'm a test job that fails!")
|
|
73
|
+
raise RunJobTaskFailed("Setup failure")
|
|
74
|
+
|
|
75
|
+
def run(self):
|
|
76
|
+
raise RuntimeError("run() was unexpectedly called after a failure in before_start()")
|
|
77
|
+
|
|
78
|
+
|
|
65
79
|
class TestFailWithSanitization(Job):
|
|
66
80
|
"""
|
|
67
81
|
Job with fail result that should be sanitized.
|
|
@@ -91,4 +105,64 @@ class TestFailWithSanitization(Job):
|
|
|
91
105
|
raise exc
|
|
92
106
|
|
|
93
107
|
|
|
94
|
-
|
|
108
|
+
class TestFailCleanly(TestFailJob):
|
|
109
|
+
"""
|
|
110
|
+
Job that fails "cleanly" through self.fail() instead of raising an exception.
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
def run(self): # pylint: disable=arguments-differ
|
|
114
|
+
logger.info("I'm a test job that fails!")
|
|
115
|
+
self.fail("Failure")
|
|
116
|
+
return "We failed"
|
|
117
|
+
|
|
118
|
+
def on_failure(self, exc, task_id, args, kwargs, einfo):
|
|
119
|
+
if exc != "We failed":
|
|
120
|
+
raise RuntimeError(f"Expected exc to be the message returned from run(), but it was {exc!r}")
|
|
121
|
+
if task_id != self.request.id: # pylint: disable=no-member
|
|
122
|
+
raise RuntimeError(f"Expected task_id {task_id} to equal self.request.id {self.request.id}") # pylint: disable=no-member
|
|
123
|
+
if args:
|
|
124
|
+
raise RuntimeError(f"Expected args to be empty, but it was {args!r}")
|
|
125
|
+
if kwargs:
|
|
126
|
+
raise RuntimeError(f"Expected kwargs to be empty, but it was {kwargs!r}")
|
|
127
|
+
if einfo is not None:
|
|
128
|
+
raise RuntimeError(f"Expected einfo to be None, but it was {einfo!r}")
|
|
129
|
+
logger.info("on_failure() was called as expected")
|
|
130
|
+
|
|
131
|
+
def after_return(self, status, retval, task_id, args, kwargs, einfo):
|
|
132
|
+
if status is not JobResultStatusChoices.STATUS_FAILURE:
|
|
133
|
+
raise RuntimeError(f"Expected status to be {JobResultStatusChoices.STATUS_FAILURE}, but it was {status!r}")
|
|
134
|
+
if retval != "We failed":
|
|
135
|
+
raise RuntimeError(f"Expected retval to be the message returned from run(), but it was {retval!r}")
|
|
136
|
+
if task_id != self.request.id: # pylint: disable=no-member
|
|
137
|
+
raise RuntimeError(f"Expected task_id {task_id} to equal self.request.id {self.request.id}") # pylint: disable=no-member
|
|
138
|
+
if args:
|
|
139
|
+
raise RuntimeError(f"Expected args to be empty, but it was {args!r}")
|
|
140
|
+
if kwargs:
|
|
141
|
+
raise RuntimeError(f"Expected kwargs to be empty, but it was {kwargs!r}")
|
|
142
|
+
if einfo is not None:
|
|
143
|
+
raise RuntimeError(f"Expected einfo to be None, but it was {einfo!r}")
|
|
144
|
+
logger.info("after_return() was called as expected")
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class TestFailCleanlyInBeforeStart(TestFailCleanly):
|
|
148
|
+
"""
|
|
149
|
+
Job that fails "cleanly" during before_start() through self.fail() instead of raising an exception.
|
|
150
|
+
"""
|
|
151
|
+
|
|
152
|
+
def before_start(self, task_id, args, kwargs):
|
|
153
|
+
super().before_start(task_id, args, kwargs)
|
|
154
|
+
logger.info("I'm a test job that fails!")
|
|
155
|
+
self.fail("We failed")
|
|
156
|
+
return "We failed"
|
|
157
|
+
|
|
158
|
+
def run(self):
|
|
159
|
+
raise RuntimeError("run() was unexpectedly called after a failure in before_start()")
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
register_jobs(
|
|
163
|
+
TestFailJob,
|
|
164
|
+
TestFailInBeforeStart,
|
|
165
|
+
TestFailWithSanitization,
|
|
166
|
+
TestFailCleanly,
|
|
167
|
+
TestFailCleanlyInBeforeStart,
|
|
168
|
+
)
|
|
@@ -1917,7 +1917,7 @@ class JobTest(
|
|
|
1917
1917
|
mock_get_worker_count.return_value = 1
|
|
1918
1918
|
self.add_permissions("extras.run_job")
|
|
1919
1919
|
|
|
1920
|
-
job_model = Job.objects.get(job_class_name="
|
|
1920
|
+
job_model = Job.objects.get(job_class_name="TestHasSensitiveVariables")
|
|
1921
1921
|
job_model.enabled = True
|
|
1922
1922
|
job_model.validated_save()
|
|
1923
1923
|
|
|
@@ -238,11 +238,7 @@ class GitTest(TransactionTestCase):
|
|
|
238
238
|
repository=self.repo.pk,
|
|
239
239
|
)
|
|
240
240
|
|
|
241
|
-
self.
|
|
242
|
-
job_result.status,
|
|
243
|
-
JobResultStatusChoices.STATUS_FAILURE,
|
|
244
|
-
(job_result.result, list(job_result.job_log_entries.values_list("message", "log_object"))),
|
|
245
|
-
)
|
|
241
|
+
self.assertJobResultStatus(job_result, JobResultStatusChoices.STATUS_FAILURE)
|
|
246
242
|
self.repo.refresh_from_db()
|
|
247
243
|
|
|
248
244
|
log_entries = JobLogEntry.objects.filter(job_result=job_result)
|
|
@@ -308,11 +304,7 @@ class GitTest(TransactionTestCase):
|
|
|
308
304
|
repository=self.repo.pk,
|
|
309
305
|
)
|
|
310
306
|
|
|
311
|
-
self.
|
|
312
|
-
job_result.status,
|
|
313
|
-
JobResultStatusChoices.STATUS_SUCCESS,
|
|
314
|
-
(job_result.traceback, list(job_result.job_log_entries.values_list("message", flat=True))),
|
|
315
|
-
)
|
|
307
|
+
self.assertJobResultStatus(job_result)
|
|
316
308
|
self.repo.refresh_from_db()
|
|
317
309
|
MockGitRepo.assert_called_with(
|
|
318
310
|
os.path.join(tempdir, self.repo.slug),
|
|
@@ -331,11 +323,7 @@ class GitTest(TransactionTestCase):
|
|
|
331
323
|
job_model = GitRepositorySync().job_model
|
|
332
324
|
job_result = run_job_for_testing(job=job_model, repository=self.repo.pk)
|
|
333
325
|
job_result.refresh_from_db()
|
|
334
|
-
self.
|
|
335
|
-
job_result.status,
|
|
336
|
-
JobResultStatusChoices.STATUS_SUCCESS,
|
|
337
|
-
(job_result.traceback, list(job_result.job_log_entries.values_list("message", flat=True))),
|
|
338
|
-
)
|
|
326
|
+
self.assertJobResultStatus(job_result)
|
|
339
327
|
|
|
340
328
|
# Make sure explicit ConfigContext was successfully loaded from file
|
|
341
329
|
self.assert_explicit_config_context_exists("Frobozz 1000 NTP servers")
|
|
@@ -383,11 +371,7 @@ class GitTest(TransactionTestCase):
|
|
|
383
371
|
# Run the Git operation and refresh the object from the DB
|
|
384
372
|
job_result = run_job_for_testing(job=job_model, repository=self.repo.pk)
|
|
385
373
|
job_result.refresh_from_db()
|
|
386
|
-
self.
|
|
387
|
-
job_result.status,
|
|
388
|
-
JobResultStatusChoices.STATUS_SUCCESS,
|
|
389
|
-
(job_result.traceback, list(job_result.job_log_entries.values_list("message", flat=True))),
|
|
390
|
-
)
|
|
374
|
+
self.assertJobResultStatus(job_result)
|
|
391
375
|
|
|
392
376
|
# Verify that objects have been removed from the database
|
|
393
377
|
self.assertEqual(
|
|
@@ -442,11 +426,7 @@ class GitTest(TransactionTestCase):
|
|
|
442
426
|
)
|
|
443
427
|
job_result.refresh_from_db()
|
|
444
428
|
|
|
445
|
-
self.
|
|
446
|
-
job_result.status,
|
|
447
|
-
JobResultStatusChoices.STATUS_FAILURE,
|
|
448
|
-
job_result.result,
|
|
449
|
-
)
|
|
429
|
+
self.assertJobResultStatus(job_result, JobResultStatusChoices.STATUS_FAILURE)
|
|
450
430
|
|
|
451
431
|
# Due to transaction rollback on failure, the database should still/again match the pre-sync state, of
|
|
452
432
|
# no records owned by the repository.
|
|
@@ -547,11 +527,7 @@ class GitTest(TransactionTestCase):
|
|
|
547
527
|
)
|
|
548
528
|
job_result.refresh_from_db()
|
|
549
529
|
|
|
550
|
-
self.
|
|
551
|
-
job_result.status,
|
|
552
|
-
JobResultStatusChoices.STATUS_SUCCESS,
|
|
553
|
-
(job_result.traceback, list(job_result.job_log_entries.values_list("message", flat=True))),
|
|
554
|
-
)
|
|
530
|
+
self.assertJobResultStatus(job_result)
|
|
555
531
|
|
|
556
532
|
# Make sure ConfigContext was successfully loaded from file
|
|
557
533
|
config_context = ConfigContext.objects.get(
|
|
@@ -591,15 +567,7 @@ class GitTest(TransactionTestCase):
|
|
|
591
567
|
delete_job_result = JobResult.objects.filter(name=repo_name).first()
|
|
592
568
|
# Make sure we didn't get the wrong JobResult
|
|
593
569
|
self.assertNotEqual(job_result, delete_job_result)
|
|
594
|
-
self.
|
|
595
|
-
delete_job_result.status,
|
|
596
|
-
JobResultStatusChoices.STATUS_SUCCESS,
|
|
597
|
-
(
|
|
598
|
-
delete_job_result,
|
|
599
|
-
delete_job_result.traceback,
|
|
600
|
-
list(delete_job_result.job_log_entries.values_list("message", flat=True)),
|
|
601
|
-
),
|
|
602
|
-
)
|
|
570
|
+
self.assertJobResultStatus(delete_job_result)
|
|
603
571
|
|
|
604
572
|
with self.assertRaises(ConfigContext.DoesNotExist):
|
|
605
573
|
ConfigContext.objects.get(
|
|
@@ -637,11 +605,7 @@ class GitTest(TransactionTestCase):
|
|
|
637
605
|
job_model = GitRepositorySync().job_model
|
|
638
606
|
job_result = run_job_for_testing(job=job_model, repository=self.repo.pk)
|
|
639
607
|
job_result.refresh_from_db()
|
|
640
|
-
self.
|
|
641
|
-
job_result.status,
|
|
642
|
-
JobResultStatusChoices.STATUS_SUCCESS,
|
|
643
|
-
(job_result.traceback, list(job_result.job_log_entries.values_list("message", flat=True))),
|
|
644
|
-
)
|
|
608
|
+
self.assertJobResultStatus(job_result)
|
|
645
609
|
|
|
646
610
|
self.assert_explicit_config_context_exists("Frobozz 1000 NTP servers")
|
|
647
611
|
self.assert_implicit_config_context_exists("Location context")
|
|
@@ -673,11 +637,7 @@ class GitTest(TransactionTestCase):
|
|
|
673
637
|
# Resync, attempting and failing to update to the new commit
|
|
674
638
|
job_result = run_job_for_testing(job=job_model, repository=self.repo.pk)
|
|
675
639
|
job_result.refresh_from_db()
|
|
676
|
-
self.
|
|
677
|
-
job_result.status,
|
|
678
|
-
JobResultStatusChoices.STATUS_FAILURE,
|
|
679
|
-
job_result.result,
|
|
680
|
-
)
|
|
640
|
+
self.assertJobResultStatus(job_result, JobResultStatusChoices.STATUS_FAILURE)
|
|
681
641
|
log_entries = JobLogEntry.objects.filter(job_result=job_result)
|
|
682
642
|
|
|
683
643
|
# Assert database changes were rolled back
|
|
@@ -718,11 +678,7 @@ class GitTest(TransactionTestCase):
|
|
|
718
678
|
)
|
|
719
679
|
job_result.refresh_from_db()
|
|
720
680
|
|
|
721
|
-
self.
|
|
722
|
-
job_result.status,
|
|
723
|
-
JobResultStatusChoices.STATUS_SUCCESS,
|
|
724
|
-
(job_result.traceback, list(job_result.job_log_entries.values_list("message", flat=True))),
|
|
725
|
-
)
|
|
681
|
+
self.assertJobResultStatus(job_result)
|
|
726
682
|
|
|
727
683
|
log_entries = JobLogEntry.objects.filter(job_result=job_result)
|
|
728
684
|
|
|
@@ -798,3 +754,57 @@ class GitTest(TransactionTestCase):
|
|
|
798
754
|
"provides contents overlapping with this repository.",
|
|
799
755
|
str(cm.exception),
|
|
800
756
|
)
|
|
757
|
+
|
|
758
|
+
@mock.patch("nautobot.extras.models.datasources.GitRepo")
|
|
759
|
+
def test_clone_to_directory_with_secrets(self, MockGitRepo):
|
|
760
|
+
"""
|
|
761
|
+
The clone_to_directory method should correctly make use of secrets.
|
|
762
|
+
"""
|
|
763
|
+
with tempfile.TemporaryDirectory() as tempdir:
|
|
764
|
+
# Prepare secrets values
|
|
765
|
+
with open(os.path.join(tempdir, "username.txt"), "wt") as handle:
|
|
766
|
+
handle.write("núñez")
|
|
767
|
+
|
|
768
|
+
with open(os.path.join(tempdir, "token.txt"), "wt") as handle:
|
|
769
|
+
handle.write("1:3@/?=ab@")
|
|
770
|
+
|
|
771
|
+
# Create secrets and assign
|
|
772
|
+
username_secret = Secret.objects.create(
|
|
773
|
+
name="Git Username",
|
|
774
|
+
provider="text-file",
|
|
775
|
+
parameters={"path": os.path.join(tempdir, "username.txt")},
|
|
776
|
+
)
|
|
777
|
+
token_secret = Secret.objects.create(
|
|
778
|
+
name="Git Token",
|
|
779
|
+
provider="text-file",
|
|
780
|
+
parameters={"path": os.path.join(tempdir, "token.txt")},
|
|
781
|
+
)
|
|
782
|
+
secrets_group = SecretsGroup.objects.create(name="Git Credentials")
|
|
783
|
+
SecretsGroupAssociation.objects.create(
|
|
784
|
+
secret=username_secret,
|
|
785
|
+
secrets_group=secrets_group,
|
|
786
|
+
access_type=SecretsGroupAccessTypeChoices.TYPE_HTTP,
|
|
787
|
+
secret_type=SecretsGroupSecretTypeChoices.TYPE_USERNAME,
|
|
788
|
+
)
|
|
789
|
+
SecretsGroupAssociation.objects.create(
|
|
790
|
+
secret=token_secret,
|
|
791
|
+
secrets_group=secrets_group,
|
|
792
|
+
access_type=SecretsGroupAccessTypeChoices.TYPE_HTTP,
|
|
793
|
+
secret_type=SecretsGroupSecretTypeChoices.TYPE_TOKEN,
|
|
794
|
+
)
|
|
795
|
+
|
|
796
|
+
# Configure GitRepository model
|
|
797
|
+
self.repo.secrets_group = secrets_group
|
|
798
|
+
self.repo.remote_url = "http://localhost/git.git"
|
|
799
|
+
self.repo.save()
|
|
800
|
+
|
|
801
|
+
# Try to clone it
|
|
802
|
+
self.repo.clone_to_directory(tempdir, "main")
|
|
803
|
+
|
|
804
|
+
# Assert that GitRepo was called with proper args
|
|
805
|
+
args, kwargs = MockGitRepo.call_args
|
|
806
|
+
path, from_url = args
|
|
807
|
+
self.assertTrue(path.startswith(os.path.join(tempdir, self.repo.slug)))
|
|
808
|
+
self.assertEqual(from_url, "http://n%C3%BA%C3%B1ez:1%3A3%40%2F%3F%3Dab%40@localhost/git.git")
|
|
809
|
+
self.assertEqual(kwargs["depth"], 0)
|
|
810
|
+
self.assertEqual(kwargs["branch"], "main")
|
|
@@ -317,7 +317,7 @@ class JobTransactionTest(TransactionTestCase):
|
|
|
317
317
|
pk_list=pk_list,
|
|
318
318
|
username=self.user.username,
|
|
319
319
|
)
|
|
320
|
-
self.
|
|
320
|
+
self.assertJobResultStatus(job_result, JobResultStatusChoices.STATUS_FAILURE)
|
|
321
321
|
error_log = JobLogEntry.objects.get(job_result=job_result, log_level=LogLevelChoices.LOG_ERROR)
|
|
322
322
|
self.assertIn(
|
|
323
323
|
f"Unable to delete Job {system_job_queryset.first()}. System Job cannot be deleted", error_log.message
|
|
@@ -346,7 +346,7 @@ class JobTransactionTest(TransactionTestCase):
|
|
|
346
346
|
},
|
|
347
347
|
username=self.user.username,
|
|
348
348
|
)
|
|
349
|
-
self.
|
|
349
|
+
self.assertJobResultStatus(job_result)
|
|
350
350
|
self.assertEqual(Job.objects.filter(description=job_description).count(), queryset.count())
|
|
351
351
|
self.assertFalse(
|
|
352
352
|
JobLogEntry.objects.filter(job_result=job_result, log_level=LogLevelChoices.LOG_WARNING).exists()
|
|
@@ -389,7 +389,7 @@ class JobTransactionTest(TransactionTestCase):
|
|
|
389
389
|
},
|
|
390
390
|
username=self.user.username,
|
|
391
391
|
)
|
|
392
|
-
self.
|
|
392
|
+
self.assertJobResultStatus(job_result)
|
|
393
393
|
self.assertEqual(Job.objects.filter(description=job_description).count(), queryset.count())
|
|
394
394
|
self.assertFalse(
|
|
395
395
|
JobLogEntry.objects.filter(job_result=job_result, log_level=LogLevelChoices.LOG_WARNING).exists()
|
|
@@ -426,7 +426,7 @@ class JobTransactionTest(TransactionTestCase):
|
|
|
426
426
|
module = "pass"
|
|
427
427
|
name = "TestPassJob"
|
|
428
428
|
job_result = create_job_result_and_run_job(module, name)
|
|
429
|
-
self.
|
|
429
|
+
self.assertJobResultStatus(job_result)
|
|
430
430
|
self.assertEqual(job_result.result, True)
|
|
431
431
|
logs = job_result.job_log_entries
|
|
432
432
|
self.assertGreater(logs.count(), 0)
|
|
@@ -449,7 +449,7 @@ class JobTransactionTest(TransactionTestCase):
|
|
|
449
449
|
name = "TestHasSensitiveVariables"
|
|
450
450
|
# This function create_job_result_and_run_job and the subsequent functions' arguments are very messy
|
|
451
451
|
job_result = create_job_result_and_run_job(module, name, "local", 1, 2, "3", kwarg_1=1, kwarg_2="2")
|
|
452
|
-
self.
|
|
452
|
+
self.assertJobResultStatus(job_result)
|
|
453
453
|
self.assertEqual(job_result.task_args, [])
|
|
454
454
|
self.assertEqual(job_result.task_kwargs, {})
|
|
455
455
|
|
|
@@ -458,21 +458,22 @@ class JobTransactionTest(TransactionTestCase):
|
|
|
458
458
|
Job test with fail result.
|
|
459
459
|
"""
|
|
460
460
|
module = "fail"
|
|
461
|
-
name
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
461
|
+
for name in ["TestFailJob", "TestFailInBeforeStart", "TestFailCleanly", "TestFailCleanlyInBeforeStart"]:
|
|
462
|
+
with self.subTest(job=name):
|
|
463
|
+
job_result = create_job_result_and_run_job(module, name)
|
|
464
|
+
self.assertJobResultStatus(job_result, JobResultStatusChoices.STATUS_FAILURE)
|
|
465
|
+
logs = job_result.job_log_entries
|
|
466
|
+
self.assertGreater(logs.count(), 0)
|
|
467
|
+
try:
|
|
468
|
+
logs.get(message="before_start() was called as expected")
|
|
469
|
+
logs.get(message="I'm a test job that fails!")
|
|
470
|
+
logs.get(message="on_failure() was called as expected")
|
|
471
|
+
logs.get(message="after_return() was called as expected")
|
|
472
|
+
except models.JobLogEntry.DoesNotExist:
|
|
473
|
+
for log in logs.all():
|
|
474
|
+
print(log.message)
|
|
475
|
+
print(job_result.traceback)
|
|
476
|
+
raise
|
|
476
477
|
|
|
477
478
|
def test_job_fail_with_sanitization(self):
|
|
478
479
|
"""
|
|
@@ -482,7 +483,7 @@ class JobTransactionTest(TransactionTestCase):
|
|
|
482
483
|
name = "TestFailWithSanitization"
|
|
483
484
|
job_result = create_job_result_and_run_job(module, name)
|
|
484
485
|
json_result = json.dumps(job_result.result)
|
|
485
|
-
self.
|
|
486
|
+
self.assertJobResultStatus(job_result, JobResultStatusChoices.STATUS_FAILURE)
|
|
486
487
|
self.assertIn("(redacted)@github.com", json_result)
|
|
487
488
|
self.assertNotIn("abc123@github.com", json_result)
|
|
488
489
|
self.assertIn("(redacted)@github.com", job_result.traceback)
|
|
@@ -495,7 +496,7 @@ class JobTransactionTest(TransactionTestCase):
|
|
|
495
496
|
module = "atomic_transaction"
|
|
496
497
|
name = "TestAtomicDecorator"
|
|
497
498
|
job_result = create_job_result_and_run_job(module, name)
|
|
498
|
-
self.
|
|
499
|
+
self.assertJobResultStatus(job_result)
|
|
499
500
|
# Ensure DB transaction was not aborted
|
|
500
501
|
self.assertTrue(models.Status.objects.filter(name="Test database atomic rollback 1").exists())
|
|
501
502
|
# Ensure the correct job log messages were saved
|
|
@@ -513,7 +514,7 @@ class JobTransactionTest(TransactionTestCase):
|
|
|
513
514
|
module = "atomic_transaction"
|
|
514
515
|
name = "TestAtomicContextManager"
|
|
515
516
|
job_result = create_job_result_and_run_job(module, name)
|
|
516
|
-
self.
|
|
517
|
+
self.assertJobResultStatus(job_result)
|
|
517
518
|
# Ensure DB transaction was not aborted
|
|
518
519
|
self.assertTrue(models.Status.objects.filter(name="Test database atomic rollback 2").exists())
|
|
519
520
|
# Ensure the correct job log messages were saved
|
|
@@ -530,8 +531,8 @@ class JobTransactionTest(TransactionTestCase):
|
|
|
530
531
|
"""
|
|
531
532
|
module = "atomic_transaction"
|
|
532
533
|
name = "TestAtomicDecorator"
|
|
533
|
-
job_result = create_job_result_and_run_job(module, name,
|
|
534
|
-
self.
|
|
534
|
+
job_result = create_job_result_and_run_job(module, name, should_fail=True)
|
|
535
|
+
self.assertJobResultStatus(job_result, JobResultStatusChoices.STATUS_FAILURE)
|
|
535
536
|
# Ensure DB transaction was aborted
|
|
536
537
|
self.assertFalse(models.Status.objects.filter(name="Test database atomic rollback 1").exists())
|
|
537
538
|
# Ensure the correct job log messages were saved
|
|
@@ -547,8 +548,8 @@ class JobTransactionTest(TransactionTestCase):
|
|
|
547
548
|
"""
|
|
548
549
|
module = "atomic_transaction"
|
|
549
550
|
name = "TestAtomicContextManager"
|
|
550
|
-
job_result = create_job_result_and_run_job(module, name,
|
|
551
|
-
self.
|
|
551
|
+
job_result = create_job_result_and_run_job(module, name, should_fail=True)
|
|
552
|
+
self.assertJobResultStatus(job_result, JobResultStatusChoices.STATUS_FAILURE)
|
|
552
553
|
# Ensure DB transaction was aborted
|
|
553
554
|
self.assertFalse(models.Status.objects.filter(name="Test database atomic rollback 2").exists())
|
|
554
555
|
# Ensure the correct job log messages were saved
|
|
@@ -596,7 +597,7 @@ class JobTransactionTest(TransactionTestCase):
|
|
|
596
597
|
job_result_data = json.loads(log_info.log_object) if log_info.log_object else None
|
|
597
598
|
|
|
598
599
|
# Assert stuff
|
|
599
|
-
self.
|
|
600
|
+
self.assertJobResultStatus(job_result)
|
|
600
601
|
self.assertEqual(form_data, job_result_data)
|
|
601
602
|
|
|
602
603
|
@override_settings(
|
|
@@ -651,7 +652,7 @@ class JobTransactionTest(TransactionTestCase):
|
|
|
651
652
|
).first()
|
|
652
653
|
|
|
653
654
|
# Assert stuff
|
|
654
|
-
self.
|
|
655
|
+
self.assertJobResultStatus(job_result)
|
|
655
656
|
self.assertEqual(info_log.log_object, "")
|
|
656
657
|
self.assertEqual(info_log.message, f"Role: {role.name}")
|
|
657
658
|
self.assertEqual(job_result.result, "Nice Roles!")
|
|
@@ -670,7 +671,7 @@ class JobTransactionTest(TransactionTestCase):
|
|
|
670
671
|
).first()
|
|
671
672
|
|
|
672
673
|
# Assert stuff
|
|
673
|
-
self.
|
|
674
|
+
self.assertJobResultStatus(job_result)
|
|
674
675
|
self.assertEqual(info_log.log_object, "")
|
|
675
676
|
self.assertEqual(info_log.message, "The Location if any that the user provided.")
|
|
676
677
|
self.assertEqual(job_result.result, "Nice Location (or not)!")
|
|
@@ -685,7 +686,7 @@ class JobTransactionTest(TransactionTestCase):
|
|
|
685
686
|
job_result = create_job_result_and_run_job(module, name, **data)
|
|
686
687
|
|
|
687
688
|
# Assert stuff
|
|
688
|
-
self.
|
|
689
|
+
self.assertJobResultStatus(job_result, JobResultStatusChoices.STATUS_FAILURE)
|
|
689
690
|
self.assertIn("location is a required field", job_result.traceback)
|
|
690
691
|
|
|
691
692
|
def test_job_latest_result_property(self):
|
|
@@ -695,9 +696,9 @@ class JobTransactionTest(TransactionTestCase):
|
|
|
695
696
|
module = "pass"
|
|
696
697
|
name = "TestPassJob"
|
|
697
698
|
job_result_1 = create_job_result_and_run_job(module, name)
|
|
698
|
-
self.
|
|
699
|
+
self.assertJobResultStatus(job_result_1)
|
|
699
700
|
job_result_2 = create_job_result_and_run_job(module, name)
|
|
700
|
-
self.
|
|
701
|
+
self.assertJobResultStatus(job_result_2)
|
|
701
702
|
_job_class, job_model = get_job_class_and_model(module, name)
|
|
702
703
|
self.assertGreaterEqual(job_model.job_results.count(), 2)
|
|
703
704
|
latest_job_result = job_model.latest_result
|
|
@@ -710,11 +711,7 @@ class JobTransactionTest(TransactionTestCase):
|
|
|
710
711
|
# The job itself contains the 'assert' by loading the resulting profiling file from the workers filesystem
|
|
711
712
|
job_result = create_job_result_and_run_job(module, name, profile=True)
|
|
712
713
|
|
|
713
|
-
self.
|
|
714
|
-
job_result.status,
|
|
715
|
-
JobResultStatusChoices.STATUS_SUCCESS,
|
|
716
|
-
msg="Profiling test job errored, this indicates that either no profiling file was created or it is malformed.",
|
|
717
|
-
)
|
|
714
|
+
self.assertJobResultStatus(job_result)
|
|
718
715
|
|
|
719
716
|
profiling_result = Path(f"{tempfile.gettempdir()}/nautobot-jobresult-{job_result.id}.pstats")
|
|
720
717
|
self.assertTrue(profiling_result.exists())
|
|
@@ -727,12 +724,17 @@ class JobTransactionTest(TransactionTestCase):
|
|
|
727
724
|
job_class, _ = get_job_class_and_model(module, name, "local")
|
|
728
725
|
self.assertTrue(job_class.is_singleton)
|
|
729
726
|
cache.set(job_class.singleton_cache_key, 1)
|
|
730
|
-
|
|
727
|
+
try:
|
|
728
|
+
failed_job_result = create_job_result_and_run_job(module, name)
|
|
731
729
|
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
730
|
+
self.assertEqual(
|
|
731
|
+
failed_job_result.status,
|
|
732
|
+
JobResultStatusChoices.STATUS_FAILURE,
|
|
733
|
+
msg="Duplicate singleton job didn't error.",
|
|
734
|
+
)
|
|
735
|
+
finally:
|
|
736
|
+
# Clean up after ourselves
|
|
737
|
+
cache.delete(job_class.singleton_cache_key)
|
|
736
738
|
|
|
737
739
|
def test_job_ignore_singleton(self):
|
|
738
740
|
module = "singleton"
|
|
@@ -741,16 +743,21 @@ class JobTransactionTest(TransactionTestCase):
|
|
|
741
743
|
job_class, _ = get_job_class_and_model(module, name, "local")
|
|
742
744
|
self.assertTrue(job_class.is_singleton)
|
|
743
745
|
cache.set(job_class.singleton_cache_key, 1)
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
746
|
+
try:
|
|
747
|
+
passed_job_result = create_job_result_and_run_job(
|
|
748
|
+
module, name, celery_kwargs={"nautobot_job_ignore_singleton_lock": True}
|
|
749
|
+
)
|
|
747
750
|
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
751
|
+
self.assertEqual(
|
|
752
|
+
passed_job_result.status,
|
|
753
|
+
JobResultStatusChoices.STATUS_SUCCESS,
|
|
754
|
+
msg="Duplicate singleton job didn't succeed with nautobot_job_ignore_singleton_lock=True.",
|
|
755
|
+
)
|
|
756
|
+
# Singleton cache key should be cleared when the job completes
|
|
757
|
+
self.assertIsNone(cache.get(job_class.singleton_cache_key, None))
|
|
758
|
+
finally:
|
|
759
|
+
# Clean up after ourselves, just in case
|
|
760
|
+
cache.delete(job_class.singleton_cache_key)
|
|
754
761
|
|
|
755
762
|
@mock.patch("nautobot.extras.context_managers.enqueue_webhooks")
|
|
756
763
|
def test_job_fires_webhooks(self, mock_enqueue_webhooks):
|
|
@@ -762,7 +769,7 @@ class JobTransactionTest(TransactionTestCase):
|
|
|
762
769
|
webhook.content_types.set([status_ct])
|
|
763
770
|
|
|
764
771
|
job_result = create_job_result_and_run_job(module, name)
|
|
765
|
-
self.
|
|
772
|
+
self.assertJobResultStatus(job_result)
|
|
766
773
|
|
|
767
774
|
mock_enqueue_webhooks.assert_called_once()
|
|
768
775
|
|
|
@@ -865,7 +872,7 @@ class JobFileOutputTest(TransactionTestCase):
|
|
|
865
872
|
data = {"lines": 3}
|
|
866
873
|
job_result = create_job_result_and_run_job(module, name, **data)
|
|
867
874
|
|
|
868
|
-
self.
|
|
875
|
+
self.assertJobResultStatus(job_result)
|
|
869
876
|
# JobResult should have one attached file
|
|
870
877
|
self.assertEqual(1, job_result.files.count())
|
|
871
878
|
self.assertEqual(job_result.files.first().name, "output.txt")
|
|
@@ -896,7 +903,7 @@ class JobFileOutputTest(TransactionTestCase):
|
|
|
896
903
|
# Exactly JOB_CREATE_FILE_MAX_SIZE bytes should be okay:
|
|
897
904
|
with override_config(JOB_CREATE_FILE_MAX_SIZE=len("Hello world!\n")):
|
|
898
905
|
job_result = create_job_result_and_run_job(module, name, **data)
|
|
899
|
-
self.
|
|
906
|
+
self.assertJobResultStatus(job_result)
|
|
900
907
|
self.assertEqual(1, job_result.files.count())
|
|
901
908
|
self.assertEqual(job_result.files.first().name, "output.txt")
|
|
902
909
|
self.assertEqual(job_result.files.first().file.read().decode("utf-8"), "Hello World!\n")
|
|
@@ -904,7 +911,7 @@ class JobFileOutputTest(TransactionTestCase):
|
|
|
904
911
|
# Even one byte over is too much:
|
|
905
912
|
with override_config(JOB_CREATE_FILE_MAX_SIZE=len("Hello world!\n") - 1):
|
|
906
913
|
job_result = create_job_result_and_run_job(module, name, **data)
|
|
907
|
-
self.
|
|
914
|
+
self.assertJobResultStatus(job_result, JobResultStatusChoices.STATUS_FAILURE)
|
|
908
915
|
self.assertIn("ValueError", job_result.traceback)
|
|
909
916
|
self.assertEqual(0, job_result.files.count())
|
|
910
917
|
|
|
@@ -912,7 +919,7 @@ class JobFileOutputTest(TransactionTestCase):
|
|
|
912
919
|
with override_config(JOB_CREATE_FILE_MAX_SIZE=10 << 20):
|
|
913
920
|
with override_settings(JOB_CREATE_FILE_MAX_SIZE=len("Hello world!\n") - 1):
|
|
914
921
|
job_result = create_job_result_and_run_job(module, name, **data)
|
|
915
|
-
self.
|
|
922
|
+
self.assertJobResultStatus(job_result, JobResultStatusChoices.STATUS_FAILURE)
|
|
916
923
|
self.assertIn("ValueError", job_result.traceback)
|
|
917
924
|
self.assertEqual(0, job_result.files.count())
|
|
918
925
|
|
|
@@ -945,7 +952,7 @@ class RunJobManagementCommandTest(TransactionTestCase):
|
|
|
945
952
|
|
|
946
953
|
out, err = self.run_command("--local", "--no-color", "--username", self.user.username, job_model.class_path)
|
|
947
954
|
self.assertIn(f"Running {job_model.class_path}...", out)
|
|
948
|
-
self.assertIn("run: 0 debug, 1 info, 0 warning, 0 error, 0 critical", out)
|
|
955
|
+
self.assertIn("run: 0 debug, 1 info, 0 success, 0 warning, 0 failure, 0 error, 0 critical", out)
|
|
949
956
|
self.assertIn("info: Success", out)
|
|
950
957
|
self.assertIn(f"{job_model.class_path}: SUCCESS", out)
|
|
951
958
|
self.assertEqual("", err)
|
|
@@ -967,7 +974,7 @@ class RunJobManagementCommandTest(TransactionTestCase):
|
|
|
967
974
|
out, err = self.run_command("--local", "--no-color", "--username", self.user.username, job_model.class_path)
|
|
968
975
|
self.assertIn(f"Running {job_model.class_path}...", out)
|
|
969
976
|
# Changed job to actually log data. Can't display empty results if no logs were created.
|
|
970
|
-
self.assertIn("run: 0 debug, 1 info, 0 warning, 0 error, 0 critical", out)
|
|
977
|
+
self.assertIn("run: 0 debug, 1 info, 0 success, 0 warning, 0 failure, 0 error, 0 critical", out)
|
|
971
978
|
self.assertIn(f"{job_model.class_path}: SUCCESS", out)
|
|
972
979
|
self.assertEqual("", err)
|
|
973
980
|
|
|
@@ -999,7 +1006,7 @@ class JobLocationCustomFieldTest(TransactionTestCase):
|
|
|
999
1006
|
job_result = create_job_result_and_run_job(module, name)
|
|
1000
1007
|
job_result.refresh_from_db()
|
|
1001
1008
|
|
|
1002
|
-
self.
|
|
1009
|
+
self.assertJobResultStatus(job_result)
|
|
1003
1010
|
|
|
1004
1011
|
# Test location with a value for custom_field
|
|
1005
1012
|
location_1 = Location.objects.filter(name="Test Location One")
|
|
@@ -1077,7 +1084,7 @@ class JobButtonReceiverTransactionTest(TransactionTestCase):
|
|
|
1077
1084
|
module = "job_button_receiver"
|
|
1078
1085
|
name = "TestJobButtonReceiverFail"
|
|
1079
1086
|
job_result = create_job_result_and_run_job(module, name, **self.data)
|
|
1080
|
-
self.
|
|
1087
|
+
self.assertJobResultStatus(job_result, JobResultStatusChoices.STATUS_FAILURE)
|
|
1081
1088
|
|
|
1082
1089
|
|
|
1083
1090
|
class JobHookReceiverTest(TestCase):
|
|
@@ -1149,7 +1156,7 @@ class JobHookReceiverTransactionTest(TransactionTestCase):
|
|
|
1149
1156
|
module = "job_hook_receiver"
|
|
1150
1157
|
name = "TestJobHookReceiverFail"
|
|
1151
1158
|
job_result = create_job_result_and_run_job(module, name, **self.data)
|
|
1152
|
-
self.
|
|
1159
|
+
self.assertJobResultStatus(job_result, JobResultStatusChoices.STATUS_FAILURE)
|
|
1153
1160
|
|
|
1154
1161
|
|
|
1155
1162
|
class JobHookTest(TestCase):
|
|
@@ -1367,7 +1367,7 @@ class JobModelTest(ModelTestCases.BaseModelTestCase):
|
|
|
1367
1367
|
def setUpTestData(cls):
|
|
1368
1368
|
# JobModel instances are automatically instantiated at startup, so we just need to look them up.
|
|
1369
1369
|
cls.local_job = JobModel.objects.get(job_class_name="TestPassJob")
|
|
1370
|
-
cls.job_containing_sensitive_variables = JobModel.objects.get(job_class_name="
|
|
1370
|
+
cls.job_containing_sensitive_variables = JobModel.objects.get(job_class_name="TestHasSensitiveVariables")
|
|
1371
1371
|
cls.app_job = JobModel.objects.get(job_class_name="ExampleJob")
|
|
1372
1372
|
|
|
1373
1373
|
def test_job_class(self):
|