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
nautobot/__init__.py
CHANGED
|
@@ -14,18 +14,34 @@ __initialized = False
|
|
|
14
14
|
|
|
15
15
|
def add_success_logger():
|
|
16
16
|
"""Add a custom log level for success messages."""
|
|
17
|
-
SUCCESS = 25
|
|
17
|
+
SUCCESS = 25 # between INFO and WARNING
|
|
18
18
|
logging.addLevelName(SUCCESS, "SUCCESS")
|
|
19
19
|
|
|
20
|
-
def success(self, message, *args, **
|
|
20
|
+
def success(self, message, *args, **kwargs):
|
|
21
|
+
kwargs["stacklevel"] = kwargs.get("stacklevel", 1) + 1 # so that funcName is the caller function, not "success"
|
|
21
22
|
if self.isEnabledFor(SUCCESS):
|
|
22
|
-
self._log(SUCCESS, message, args, **
|
|
23
|
+
self._log(SUCCESS, message, args, **kwargs)
|
|
23
24
|
|
|
24
25
|
logging.Logger.success = success
|
|
25
26
|
return success
|
|
26
27
|
|
|
27
28
|
|
|
29
|
+
def add_failure_logger():
|
|
30
|
+
"""Add a custom log level for failure messages less severe than an ERROR."""
|
|
31
|
+
FAILURE = 35 # between WARNING and ERROR
|
|
32
|
+
logging.addLevelName(FAILURE, "FAILURE")
|
|
33
|
+
|
|
34
|
+
def failure(self, message, *args, **kwargs):
|
|
35
|
+
kwargs["stacklevel"] = kwargs.get("stacklevel", 1) + 1 # so that funcName is the caller function, not "failure"
|
|
36
|
+
if self.isEnabledFor(FAILURE):
|
|
37
|
+
self._log(FAILURE, message, args, **kwargs)
|
|
38
|
+
|
|
39
|
+
logging.Logger.failure = failure
|
|
40
|
+
return failure
|
|
41
|
+
|
|
42
|
+
|
|
28
43
|
add_success_logger()
|
|
44
|
+
add_failure_logger()
|
|
29
45
|
logger = logging.getLogger(__name__)
|
|
30
46
|
|
|
31
47
|
|
nautobot/core/celery/__init__.py
CHANGED
|
@@ -14,7 +14,7 @@ from django.utils.module_loading import import_string
|
|
|
14
14
|
from kombu.serialization import register
|
|
15
15
|
from prometheus_client import CollectorRegistry, multiprocess, start_http_server
|
|
16
16
|
|
|
17
|
-
from nautobot import add_success_logger
|
|
17
|
+
from nautobot import add_failure_logger, add_success_logger
|
|
18
18
|
from nautobot.core.celery.control import discard_git_repository, refresh_git_repository # noqa: F401 # unused-import
|
|
19
19
|
from nautobot.core.celery.encoders import NautobotKombuJSONEncoder
|
|
20
20
|
from nautobot.core.celery.log import NautobotDatabaseHandler
|
|
@@ -138,14 +138,16 @@ def add_nautobot_log_handler(logger_instance, log_format=None):
|
|
|
138
138
|
|
|
139
139
|
@signals.after_setup_logger.connect
|
|
140
140
|
def setup_nautobot_global_logging(logger, **kwargs): # pylint: disable=redefined-outer-name
|
|
141
|
-
"""Add SUCCESS
|
|
141
|
+
"""Add SUCCESS and FAILURE logs to celery global logger."""
|
|
142
142
|
logger.success = add_success_logger()
|
|
143
|
+
logger.failure = add_failure_logger()
|
|
143
144
|
|
|
144
145
|
|
|
145
146
|
@signals.after_setup_task_logger.connect
|
|
146
147
|
def setup_nautobot_task_logging(logger, **kwargs): # pylint: disable=redefined-outer-name
|
|
147
|
-
"""Add SUCCESS
|
|
148
|
+
"""Add SUCCESS and FAILURE logs to celery task logger."""
|
|
148
149
|
logger.success = add_success_logger()
|
|
150
|
+
logger.failure = add_failure_logger()
|
|
149
151
|
|
|
150
152
|
|
|
151
153
|
@signals.celeryd_after_setup.connect
|
nautobot/core/jobs/__init__.py
CHANGED
|
@@ -295,8 +295,9 @@ class ImportObjects(Job):
|
|
|
295
295
|
validation_failed = True
|
|
296
296
|
else:
|
|
297
297
|
validation_failed = True
|
|
298
|
-
for field,
|
|
299
|
-
|
|
298
|
+
for field, errs in serializer.errors.items():
|
|
299
|
+
for err in errs:
|
|
300
|
+
self.logger.error("Row %d: `%s`: `%s`", row, field, err)
|
|
300
301
|
return new_objs, validation_failed
|
|
301
302
|
|
|
302
303
|
def run(self, *, content_type, csv_data=None, csv_file=None, roll_back_if_error=False): # pylint:disable=arguments-differ
|
|
@@ -68,6 +68,8 @@ def run_job_for_testing(job, username="test-user", profile=False, **kwargs):
|
|
|
68
68
|
username=username, defaults={"is_superuser": True, "password": "password"}
|
|
69
69
|
)
|
|
70
70
|
# Run the job synchronously in the current thread as if it were being executed by a worker
|
|
71
|
+
# TODO: in Nautobot core testing, we set `CELERY_TASK_ALWAYS_EAGER = True`, so we *could* use enqueue_job() instead,
|
|
72
|
+
# but switching now would be a potentially breaking change for apps...
|
|
71
73
|
job_result = JobResult.execute_job(
|
|
72
74
|
job_model=job,
|
|
73
75
|
user=user_instance,
|
nautobot/core/testing/mixins.py
CHANGED
|
@@ -18,6 +18,7 @@ from nautobot.core.models import fields as core_fields
|
|
|
18
18
|
from nautobot.core.testing import utils
|
|
19
19
|
from nautobot.core.utils import permissions
|
|
20
20
|
from nautobot.extras import management, models as extras_models
|
|
21
|
+
from nautobot.extras.choices import JobResultStatusChoices
|
|
21
22
|
from nautobot.users import models as users_models
|
|
22
23
|
|
|
23
24
|
# Use the proper swappable User model
|
|
@@ -188,6 +189,14 @@ class NautobotTestCaseMixin:
|
|
|
188
189
|
err_message = f"{msg}\n{err_message}"
|
|
189
190
|
self.assertIn(response.status_code, expected_status, err_message)
|
|
190
191
|
|
|
192
|
+
def assertJobResultStatus(self, job_result, expected_status=JobResultStatusChoices.STATUS_SUCCESS):
|
|
193
|
+
"""Assert that the given job_result has the expected_status, or print the job logs to aid in debugging."""
|
|
194
|
+
self.assertEqual(
|
|
195
|
+
job_result.status,
|
|
196
|
+
expected_status,
|
|
197
|
+
(job_result.traceback, list(job_result.job_log_entries.values_list("message", flat=True))),
|
|
198
|
+
)
|
|
199
|
+
|
|
191
200
|
def assertInstanceEqual(self, instance, data, exclude=None, api=False):
|
|
192
201
|
"""
|
|
193
202
|
Compare a model instance to a dictionary, checking that its attribute values match those specified
|
nautobot/core/tests/test_jobs.py
CHANGED
|
@@ -48,7 +48,7 @@ class ExportObjectListTest(TransactionTestCase):
|
|
|
48
48
|
username=self.user.username, # otherwise run_job_for_testing defaults to a superuser account
|
|
49
49
|
content_type=ContentType.objects.get_for_model(Status).pk,
|
|
50
50
|
)
|
|
51
|
-
self.
|
|
51
|
+
self.assertJobResultStatus(job_result, JobResultStatusChoices.STATUS_FAILURE)
|
|
52
52
|
log_error = JobLogEntry.objects.get(job_result=job_result, log_level=LogLevelChoices.LOG_ERROR)
|
|
53
53
|
self.assertEqual(log_error.message, f'User "{self.user}" does not have permission to view status objects')
|
|
54
54
|
self.assertFalse(job_result.files.exists())
|
|
@@ -70,7 +70,7 @@ class ExportObjectListTest(TransactionTestCase):
|
|
|
70
70
|
username=self.user.username, # otherwise run_job_for_testing defaults to a superuser account
|
|
71
71
|
content_type=ContentType.objects.get_for_model(Status).pk,
|
|
72
72
|
)
|
|
73
|
-
self.
|
|
73
|
+
self.assertJobResultStatus(job_result)
|
|
74
74
|
self.assertTrue(job_result.files.exists())
|
|
75
75
|
self.assertEqual(Path(job_result.files.first().file.name).name, "nautobot_statuses.csv")
|
|
76
76
|
csv_bytes = job_result.files.first().file.read()
|
|
@@ -86,7 +86,7 @@ class ExportObjectListTest(TransactionTestCase):
|
|
|
86
86
|
"ExportObjectList",
|
|
87
87
|
content_type=ContentType.objects.get_for_model(Status).pk,
|
|
88
88
|
)
|
|
89
|
-
self.
|
|
89
|
+
self.assertJobResultStatus(job_result)
|
|
90
90
|
self.assertTrue(job_result.files.exists())
|
|
91
91
|
self.assertEqual(Path(job_result.files.first().file.name).name, "nautobot_statuses.csv")
|
|
92
92
|
csv_data = job_result.files.first().file.read().decode("utf-8")
|
|
@@ -107,7 +107,7 @@ class ExportObjectListTest(TransactionTestCase):
|
|
|
107
107
|
content_type=ContentType.objects.get_for_model(Status).pk,
|
|
108
108
|
export_template=et.pk,
|
|
109
109
|
)
|
|
110
|
-
self.
|
|
110
|
+
self.assertJobResultStatus(job_result)
|
|
111
111
|
self.assertTrue(job_result.files.exists())
|
|
112
112
|
self.assertEqual(Path(job_result.files.first().file.name).name, "nautobot_statuses.txt")
|
|
113
113
|
text_data = job_result.files.first().file.read().decode("utf-8")
|
|
@@ -129,7 +129,7 @@ class ExportObjectListTest(TransactionTestCase):
|
|
|
129
129
|
content_type=ContentType.objects.get_for_model(DeviceType).pk,
|
|
130
130
|
export_format="yaml",
|
|
131
131
|
)
|
|
132
|
-
self.
|
|
132
|
+
self.assertJobResultStatus(job_result)
|
|
133
133
|
self.assertTrue(job_result.files.exists())
|
|
134
134
|
self.assertEqual(Path(job_result.files.first().file.name).name, "nautobot_device_types.yaml")
|
|
135
135
|
yaml_data = job_result.files.first().file.read().decode("utf-8")
|
|
@@ -159,7 +159,7 @@ class ImportObjectsTestCase(TransactionTestCase):
|
|
|
159
159
|
content_type=ContentType.objects.get_for_model(Status).pk,
|
|
160
160
|
csv_data=self.csv_data,
|
|
161
161
|
)
|
|
162
|
-
self.
|
|
162
|
+
self.assertJobResultStatus(job_result, JobResultStatusChoices.STATUS_FAILURE)
|
|
163
163
|
log_error = JobLogEntry.objects.get(job_result=job_result, log_level=LogLevelChoices.LOG_ERROR)
|
|
164
164
|
self.assertEqual(log_error.message, f'User "{self.user}" does not have permission to create status objects')
|
|
165
165
|
self.assertFalse(Status.objects.filter(name__startswith="test_status").exists())
|
|
@@ -172,7 +172,7 @@ class ImportObjectsTestCase(TransactionTestCase):
|
|
|
172
172
|
username=self.user.username, # otherwise run_job_for_testing defaults to a superuser account
|
|
173
173
|
content_type=ContentType.objects.get_for_model(Status).pk,
|
|
174
174
|
)
|
|
175
|
-
self.
|
|
175
|
+
self.assertJobResultStatus(job_result, JobResultStatusChoices.STATUS_FAILURE)
|
|
176
176
|
|
|
177
177
|
def test_csv_import_with_constrained_permission(self):
|
|
178
178
|
"""Job should only allow the user to import objects they have permission to add."""
|
|
@@ -191,7 +191,7 @@ class ImportObjectsTestCase(TransactionTestCase):
|
|
|
191
191
|
content_type=ContentType.objects.get_for_model(Status).pk,
|
|
192
192
|
csv_data=self.csv_data,
|
|
193
193
|
)
|
|
194
|
-
self.
|
|
194
|
+
self.assertJobResultStatus(job_result, JobResultStatusChoices.STATUS_FAILURE)
|
|
195
195
|
log_successes = JobLogEntry.objects.filter(
|
|
196
196
|
job_result=job_result, log_level=LogLevelChoices.LOG_INFO, message__icontains="created"
|
|
197
197
|
)
|
|
@@ -220,7 +220,7 @@ class ImportObjectsTestCase(TransactionTestCase):
|
|
|
220
220
|
content_type=ContentType.objects.get_for_model(Status).pk,
|
|
221
221
|
csv_data=self.csv_data,
|
|
222
222
|
)
|
|
223
|
-
self.
|
|
223
|
+
self.assertJobResultStatus(job_result)
|
|
224
224
|
self.assertFalse(
|
|
225
225
|
JobLogEntry.objects.filter(job_result=job_result, log_level=LogLevelChoices.LOG_WARNING).exists()
|
|
226
226
|
)
|
|
@@ -246,7 +246,7 @@ class ImportObjectsTestCase(TransactionTestCase):
|
|
|
246
246
|
content_type=ContentType.objects.get_for_model(Prefix).pk,
|
|
247
247
|
csv_file=csv_file.id,
|
|
248
248
|
)
|
|
249
|
-
self.
|
|
249
|
+
self.assertJobResultStatus(job_result)
|
|
250
250
|
self.assertFalse(
|
|
251
251
|
JobLogEntry.objects.filter(job_result=job_result, log_level=LogLevelChoices.LOG_WARNING).exists()
|
|
252
252
|
)
|
|
@@ -287,7 +287,7 @@ class ImportObjectsTestCase(TransactionTestCase):
|
|
|
287
287
|
content_type=ContentType.objects.get_for_model(Device).pk,
|
|
288
288
|
csv_file=csv_file.id,
|
|
289
289
|
)
|
|
290
|
-
self.
|
|
290
|
+
self.assertJobResultStatus(job_result)
|
|
291
291
|
self.assertFalse(
|
|
292
292
|
JobLogEntry.objects.filter(job_result=job_result, log_level=LogLevelChoices.LOG_WARNING).exists()
|
|
293
293
|
)
|
|
@@ -313,7 +313,7 @@ class ImportObjectsTestCase(TransactionTestCase):
|
|
|
313
313
|
csv_data=csv_data,
|
|
314
314
|
roll_back_if_error=True,
|
|
315
315
|
)
|
|
316
|
-
self.
|
|
316
|
+
self.assertJobResultStatus(job_result, JobResultStatusChoices.STATUS_FAILURE)
|
|
317
317
|
log_info = JobLogEntry.objects.filter(
|
|
318
318
|
job_result=job_result, log_level=LogLevelChoices.LOG_INFO, message__icontains="created"
|
|
319
319
|
)
|
|
@@ -335,7 +335,7 @@ class ImportObjectsTestCase(TransactionTestCase):
|
|
|
335
335
|
content_type=ContentType.objects.get_for_model(Status).pk,
|
|
336
336
|
csv_data=csv_data,
|
|
337
337
|
)
|
|
338
|
-
self.
|
|
338
|
+
self.assertJobResultStatus(job_result, JobResultStatusChoices.STATUS_FAILURE)
|
|
339
339
|
log_errors = JobLogEntry.objects.filter(job_result=job_result, log_level=LogLevelChoices.LOG_ERROR)
|
|
340
340
|
self.assertEqual(log_errors[0].message, "Row 1: `color`: `Enter a valid hexadecimal RGB color code.`")
|
|
341
341
|
self.assertFalse(Status.objects.filter(name="test_status0").exists())
|
|
@@ -375,7 +375,7 @@ class ImportObjectsTestCase(TransactionTestCase):
|
|
|
375
375
|
content_type=ContentType.objects.get_for_model(LocationType).pk,
|
|
376
376
|
csv_data=location_types_csv,
|
|
377
377
|
)
|
|
378
|
-
self.
|
|
378
|
+
self.assertJobResultStatus(location_types_job_result)
|
|
379
379
|
|
|
380
380
|
location_type_count = LocationType.objects.filter(name="ContactAssignmentImportTestLocationType").count()
|
|
381
381
|
self.assertEqual(location_type_count, 1, f"Unexpected count of LocationTypes {location_type_count}")
|
|
@@ -386,7 +386,7 @@ class ImportObjectsTestCase(TransactionTestCase):
|
|
|
386
386
|
content_type=ContentType.objects.get_for_model(Location).pk,
|
|
387
387
|
csv_data=locations_csv,
|
|
388
388
|
)
|
|
389
|
-
self.
|
|
389
|
+
self.assertJobResultStatus(locations_job_result)
|
|
390
390
|
|
|
391
391
|
location_count = Location.objects.filter(location_type__name="ContactAssignmentImportTestLocationType").count()
|
|
392
392
|
self.assertEqual(location_count, 2, f"Unexpected count of Locations {location_count}")
|
|
@@ -397,7 +397,7 @@ class ImportObjectsTestCase(TransactionTestCase):
|
|
|
397
397
|
content_type=ContentType.objects.get_for_model(Contact).pk,
|
|
398
398
|
csv_data=contacts_csv,
|
|
399
399
|
)
|
|
400
|
-
self.
|
|
400
|
+
self.assertJobResultStatus(contacts_job_result)
|
|
401
401
|
|
|
402
402
|
contact_count = Contact.objects.filter(name="Bob-ContactAssignmentImportTestLocation").count()
|
|
403
403
|
self.assertEqual(contact_count, 1, f"Unexpected number of contacts {contact_count}")
|
|
@@ -408,7 +408,7 @@ class ImportObjectsTestCase(TransactionTestCase):
|
|
|
408
408
|
content_type=ContentType.objects.get_for_model(Role).pk,
|
|
409
409
|
csv_data=roles_csv,
|
|
410
410
|
)
|
|
411
|
-
self.
|
|
411
|
+
self.assertJobResultStatus(roles_job_result)
|
|
412
412
|
|
|
413
413
|
role_count = Role.objects.filter(name="ContactAssignmentImportTestLocation-On Site").count()
|
|
414
414
|
self.assertEqual(role_count, 1, f"Unexpected number of role values {role_count}")
|
|
@@ -426,8 +426,7 @@ class ImportObjectsTestCase(TransactionTestCase):
|
|
|
426
426
|
content_type=ContentType.objects.get_for_model(ContactAssociation).pk,
|
|
427
427
|
csv_data=associations_csv,
|
|
428
428
|
)
|
|
429
|
-
|
|
430
|
-
self.assertEqual(associations_job_result.status, JobResultStatusChoices.STATUS_SUCCESS)
|
|
429
|
+
self.assertJobResultStatus(associations_job_result)
|
|
431
430
|
|
|
432
431
|
|
|
433
432
|
class LogsCleanupTestCase(TransactionTestCase):
|
|
@@ -469,7 +468,7 @@ class LogsCleanupTestCase(TransactionTestCase):
|
|
|
469
468
|
cleanup_types=[CleanupTypes.JOB_RESULT],
|
|
470
469
|
max_age=0,
|
|
471
470
|
)
|
|
472
|
-
self.
|
|
471
|
+
self.assertJobResultStatus(job_result, JobResultStatusChoices.STATUS_FAILURE)
|
|
473
472
|
log_error = JobLogEntry.objects.get(job_result=job_result, log_level=LogLevelChoices.LOG_ERROR)
|
|
474
473
|
self.assertEqual(log_error.message, f'User "{self.user}" does not have permission to delete JobResult records')
|
|
475
474
|
self.assertEqual(JobResult.objects.count(), job_result_count + 1)
|
|
@@ -484,7 +483,7 @@ class LogsCleanupTestCase(TransactionTestCase):
|
|
|
484
483
|
cleanup_types=[CleanupTypes.OBJECT_CHANGE],
|
|
485
484
|
max_age=0,
|
|
486
485
|
)
|
|
487
|
-
self.
|
|
486
|
+
self.assertJobResultStatus(job_result, JobResultStatusChoices.STATUS_FAILURE)
|
|
488
487
|
log_error = JobLogEntry.objects.get(job_result=job_result, log_level=LogLevelChoices.LOG_ERROR)
|
|
489
488
|
self.assertEqual(
|
|
490
489
|
log_error.message, f'User "{self.user}" does not have permission to delete ObjectChange records'
|
|
@@ -532,7 +531,7 @@ class LogsCleanupTestCase(TransactionTestCase):
|
|
|
532
531
|
cleanup_types=[CleanupTypes.JOB_RESULT, CleanupTypes.OBJECT_CHANGE],
|
|
533
532
|
max_age=0,
|
|
534
533
|
)
|
|
535
|
-
self.
|
|
534
|
+
self.assertJobResultStatus(job_result)
|
|
536
535
|
self.assertEqual(job_result.result["extras.JobResult"], 1)
|
|
537
536
|
self.assertEqual(job_result.result["extras.ObjectChange"], 1)
|
|
538
537
|
with self.assertRaises(JobResult.DoesNotExist):
|
|
@@ -629,7 +628,7 @@ class BulkEditTestCase(TransactionTestCase):
|
|
|
629
628
|
tag.content_types.add(self.namespace_ct)
|
|
630
629
|
|
|
631
630
|
def _common_no_error_test_assertion(self, model, job_result, expected_count, **filter_params):
|
|
632
|
-
self.
|
|
631
|
+
self.assertJobResultStatus(job_result)
|
|
633
632
|
self.assertEqual(model.objects.filter(**filter_params).count(), expected_count)
|
|
634
633
|
self.assertFalse(
|
|
635
634
|
JobLogEntry.objects.filter(job_result=job_result, log_level=LogLevelChoices.LOG_WARNING).exists()
|
|
@@ -650,7 +649,7 @@ class BulkEditTestCase(TransactionTestCase):
|
|
|
650
649
|
form_data={"pk": pk_list, "color": "aa1409"},
|
|
651
650
|
username=self.user.username,
|
|
652
651
|
)
|
|
653
|
-
self.
|
|
652
|
+
self.assertJobResultStatus(job_result, JobResultStatusChoices.STATUS_FAILURE)
|
|
654
653
|
job_log = JobLogEntry.objects.get(job_result=job_result, log_level=LogLevelChoices.LOG_ERROR)
|
|
655
654
|
self.assertEqual(job_log.message, f'User "{self.user}" does not have permission to update status objects')
|
|
656
655
|
|
|
@@ -961,7 +960,7 @@ class BulkDeleteTestCase(TransactionTestCase):
|
|
|
961
960
|
)
|
|
962
961
|
|
|
963
962
|
def _common_no_error_test_assertion(self, model, job_result, **filter_params):
|
|
964
|
-
self.
|
|
963
|
+
self.assertJobResultStatus(job_result)
|
|
965
964
|
self.assertEqual(model.objects.filter(**filter_params).count(), 0)
|
|
966
965
|
self.assertFalse(
|
|
967
966
|
JobLogEntry.objects.filter(job_result=job_result, log_level=LogLevelChoices.LOG_WARNING).exists()
|
|
@@ -979,7 +978,7 @@ class BulkDeleteTestCase(TransactionTestCase):
|
|
|
979
978
|
pk_list=statuses_to_delete,
|
|
980
979
|
username=self.user.username,
|
|
981
980
|
)
|
|
982
|
-
self.
|
|
981
|
+
self.assertJobResultStatus(job_result, JobResultStatusChoices.STATUS_FAILURE)
|
|
983
982
|
job_log = JobLogEntry.objects.get(job_result=job_result, log_level=LogLevelChoices.LOG_ERROR)
|
|
984
983
|
self.assertEqual(job_log.message, f'User "{self.user}" does not have permission to delete status objects')
|
|
985
984
|
self.assertEqual(Status.objects.filter(pk__in=statuses_to_delete).count(), len(statuses_to_delete))
|
|
@@ -1002,7 +1001,7 @@ class BulkDeleteTestCase(TransactionTestCase):
|
|
|
1002
1001
|
pk_list=statuses_to_delete,
|
|
1003
1002
|
username=self.user.username,
|
|
1004
1003
|
)
|
|
1005
|
-
self.
|
|
1004
|
+
self.assertJobResultStatus(job_result, JobResultStatusChoices.STATUS_FAILURE)
|
|
1006
1005
|
error_log = JobLogEntry.objects.get(job_result=job_result, log_level=LogLevelChoices.LOG_ERROR)
|
|
1007
1006
|
self.assertEqual(
|
|
1008
1007
|
error_log.message, "You do not have permissions to delete some of the objects provided in `pk_list`."
|
nautobot/dcim/tests/test_jobs.py
CHANGED
|
@@ -71,7 +71,7 @@ def create_common_data_for_software_related_test_cases():
|
|
|
71
71
|
class TestSoftwareImageFileTestCase(TransactionTestCase):
|
|
72
72
|
def test_correct_handling_for_model_protected_error(self):
|
|
73
73
|
create_common_data_for_software_related_test_cases()
|
|
74
|
-
software_image_file = SoftwareImageFile.objects.
|
|
74
|
+
software_image_file = SoftwareImageFile.objects.get(image_file_name="software_image_file_qs_test_1.bin")
|
|
75
75
|
|
|
76
76
|
self.add_permissions("dcim.delete_softwareimagefile")
|
|
77
77
|
pk_list = [str(software_image_file.pk)]
|
|
@@ -86,9 +86,7 @@ class TestSoftwareImageFileTestCase(TransactionTestCase):
|
|
|
86
86
|
pk_list=pk_list,
|
|
87
87
|
username=self.user.username,
|
|
88
88
|
)
|
|
89
|
-
|
|
90
|
-
print([(log.message, log.log_level) for log in logs])
|
|
91
|
-
self.assertEqual(job_result.status, JobResultStatusChoices.STATUS_FAILURE)
|
|
89
|
+
self.assertJobResultStatus(job_result, JobResultStatusChoices.STATUS_FAILURE)
|
|
92
90
|
error_log = JobLogEntry.objects.get(job_result=job_result, log_level=LogLevelChoices.LOG_ERROR)
|
|
93
91
|
self.assertIn("Caught ProtectedError while attempting to delete objects", error_log.message)
|
|
94
92
|
self.assertEqual(initial_count, SoftwareImageFile.objects.all().count())
|
|
@@ -97,7 +95,7 @@ class TestSoftwareImageFileTestCase(TransactionTestCase):
|
|
|
97
95
|
class TestSoftwareVersionTestCase(TransactionTestCase):
|
|
98
96
|
def test_correct_handling_for_model_protected_error(self):
|
|
99
97
|
create_common_data_for_software_related_test_cases()
|
|
100
|
-
software_version = SoftwareVersion.objects.
|
|
98
|
+
software_version = SoftwareVersion.objects.get(version="Test version 1.0.0")
|
|
101
99
|
|
|
102
100
|
initial_count = SoftwareVersion.objects.all().count()
|
|
103
101
|
self.add_permissions("dcim.delete_softwareversion")
|
|
@@ -112,7 +110,7 @@ class TestSoftwareVersionTestCase(TransactionTestCase):
|
|
|
112
110
|
pk_list=pk_list,
|
|
113
111
|
username=self.user.username,
|
|
114
112
|
)
|
|
115
|
-
self.
|
|
113
|
+
self.assertJobResultStatus(job_result, JobResultStatusChoices.STATUS_FAILURE)
|
|
116
114
|
error_log = JobLogEntry.objects.get(job_result=job_result, log_level=LogLevelChoices.LOG_ERROR)
|
|
117
115
|
self.assertIn("Caught ProtectedError while attempting to delete objects", error_log.message)
|
|
118
116
|
self.assertEqual(initial_count, SoftwareVersion.objects.all().count())
|
nautobot/extras/choices.py
CHANGED
|
@@ -251,8 +251,10 @@ class JobResultStatusChoices(ChoiceSet):
|
|
|
251
251
|
"""
|
|
252
252
|
|
|
253
253
|
STATUS_FAILURE = states.FAILURE
|
|
254
|
+
STATUS_IGNORED = states.IGNORED
|
|
254
255
|
STATUS_PENDING = states.PENDING
|
|
255
256
|
STATUS_RECEIVED = states.RECEIVED
|
|
257
|
+
STATUS_REJECTED = states.REJECTED
|
|
256
258
|
STATUS_RETRY = states.RETRY
|
|
257
259
|
STATUS_REVOKED = states.REVOKED
|
|
258
260
|
STATUS_STARTED = states.STARTED
|
|
@@ -302,27 +304,30 @@ class JobResultStatusChoices(ChoiceSet):
|
|
|
302
304
|
class LogLevelChoices(ChoiceSet):
|
|
303
305
|
LOG_DEBUG = "debug"
|
|
304
306
|
LOG_INFO = "info"
|
|
307
|
+
LOG_SUCCESS = "success"
|
|
305
308
|
LOG_WARNING = "warning"
|
|
309
|
+
LOG_FAILURE = "failure"
|
|
306
310
|
LOG_ERROR = "error"
|
|
307
311
|
LOG_CRITICAL = "critical"
|
|
308
|
-
LOG_SUCCESS = "success"
|
|
309
312
|
|
|
310
313
|
CHOICES = (
|
|
311
314
|
(LOG_DEBUG, "Debug"),
|
|
312
315
|
(LOG_INFO, "Info"),
|
|
316
|
+
(LOG_SUCCESS, "Success"),
|
|
313
317
|
(LOG_WARNING, "Warning"),
|
|
318
|
+
(LOG_FAILURE, "Failure"),
|
|
314
319
|
(LOG_ERROR, "Error"),
|
|
315
320
|
(LOG_CRITICAL, "Critical"),
|
|
316
|
-
(LOG_SUCCESS, "Success"),
|
|
317
321
|
)
|
|
318
322
|
|
|
319
323
|
CSS_CLASSES = {
|
|
320
324
|
LOG_DEBUG: "debug",
|
|
321
325
|
LOG_INFO: "info",
|
|
326
|
+
LOG_SUCCESS: "success",
|
|
322
327
|
LOG_WARNING: "warning",
|
|
328
|
+
LOG_FAILURE: "failure",
|
|
323
329
|
LOG_ERROR: "error",
|
|
324
330
|
LOG_CRITICAL: "critical",
|
|
325
|
-
LOG_SUCCESS: "success",
|
|
326
331
|
}
|
|
327
332
|
|
|
328
333
|
|