nautobot 2.4.3__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/apps/filters.py +2 -0
- nautobot/circuits/filters.py +1 -1
- nautobot/circuits/tests/test_models.py +5 -3
- nautobot/cloud/filters.py +3 -6
- nautobot/cloud/tests/test_filters.py +21 -0
- nautobot/core/admin.py +2 -0
- nautobot/core/celery/__init__.py +5 -3
- nautobot/core/jobs/__init__.py +5 -3
- nautobot/core/management/commands/generate_performance_test_endpoints.py +9 -6
- nautobot/core/models/utils.py +6 -1
- nautobot/core/templates/inc/javascript.html +1 -0
- nautobot/core/templatetags/ui_framework.py +20 -4
- nautobot/core/testing/__init__.py +2 -0
- nautobot/core/testing/forms.py +1 -1
- nautobot/core/testing/mixins.py +9 -0
- nautobot/core/tests/test_api.py +1 -1
- nautobot/core/tests/test_graphql.py +3 -3
- nautobot/core/tests/test_jobs.py +30 -28
- nautobot/core/ui/object_detail.py +1 -1
- nautobot/dcim/api/serializers.py +36 -0
- nautobot/dcim/api/views.py +1 -1
- nautobot/dcim/elevations.py +17 -4
- nautobot/dcim/factory.py +9 -1
- nautobot/dcim/filters/__init__.py +27 -1
- nautobot/dcim/forms.py +13 -1
- nautobot/dcim/models/devices.py +11 -5
- nautobot/dcim/signals.py +26 -0
- nautobot/dcim/templates/dcim/virtualdevicecontext_retrieve.html +0 -62
- nautobot/dcim/templates/dcim/virtualdevicecontext_update.html +6 -0
- nautobot/dcim/tests/test_api.py +176 -0
- nautobot/dcim/tests/test_filters.py +56 -3
- nautobot/dcim/tests/test_jobs.py +4 -6
- nautobot/dcim/tests/test_models.py +40 -0
- nautobot/dcim/views.py +24 -14
- nautobot/extras/api/mixins.py +1 -1
- nautobot/extras/api/views.py +2 -2
- nautobot/extras/choices.py +8 -3
- nautobot/extras/filters/__init__.py +4 -0
- nautobot/extras/jobs.py +181 -103
- nautobot/extras/management/utils.py +13 -2
- nautobot/extras/models/datasources.py +11 -4
- nautobot/extras/models/jobs.py +20 -17
- nautobot/extras/plugins/__init__.py +26 -1
- nautobot/extras/tables.py +25 -29
- nautobot/extras/templates/extras/inc/jobresult.html +12 -13
- nautobot/extras/templates/extras/objectchange.html +28 -12
- nautobot/extras/test_jobs/atomic_transaction.py +6 -6
- nautobot/extras/test_jobs/fail.py +75 -1
- nautobot/extras/tests/test_api.py +17 -16
- nautobot/extras/tests/test_datasources.py +64 -54
- nautobot/extras/tests/test_filters.py +2 -0
- nautobot/extras/tests/test_jobs.py +69 -62
- nautobot/extras/tests/test_models.py +1 -1
- nautobot/extras/tests/test_plugins.py +32 -1
- nautobot/extras/tests/test_relationships.py +5 -5
- nautobot/extras/tests/test_views.py +12 -2
- nautobot/extras/views.py +10 -1
- nautobot/ipam/api/serializers.py +7 -8
- nautobot/ipam/api/views.py +2 -2
- nautobot/ipam/factory.py +27 -8
- nautobot/ipam/filters.py +67 -29
- nautobot/ipam/formfields.py +51 -0
- nautobot/ipam/forms.py +28 -1
- nautobot/ipam/migrations/0051_added_optional_vrf_relationship_to_vdc.py +41 -0
- nautobot/ipam/models.py +63 -5
- nautobot/ipam/querysets.py +6 -0
- nautobot/ipam/tables.py +21 -7
- nautobot/ipam/templates/ipam/rir.html +1 -43
- nautobot/ipam/tests/test_api.py +107 -66
- nautobot/ipam/tests/test_filters.py +145 -5
- nautobot/ipam/tests/test_models.py +16 -0
- nautobot/ipam/tests/test_views.py +15 -2
- nautobot/ipam/urls.py +1 -21
- nautobot/ipam/views.py +24 -41
- nautobot/project-static/css/base.css +11 -0
- nautobot/project-static/css/dark.css +2 -1
- nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +62 -0
- 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/apps/api/configuration-view.html +0 -3
- nautobot/project-static/docs/development/apps/api/models/graphql.html +0 -4
- nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +94 -1
- nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +0 -3
- nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +0 -3
- nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +0 -3
- nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +0 -3
- nautobot/project-static/docs/development/apps/api/prometheus.html +0 -3
- nautobot/project-static/docs/development/apps/api/testing.html +0 -6
- nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +0 -3
- nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +0 -3
- nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +0 -3
- nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +0 -3
- nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +1 -7
- nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +0 -7
- nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +0 -4
- nautobot/project-static/docs/development/apps/api/views/notes.html +0 -3
- nautobot/project-static/docs/development/apps/index.html +2 -35
- nautobot/project-static/docs/development/apps/migration/code-updates.html +1 -1
- nautobot/project-static/docs/development/core/application-registry.html +0 -6
- nautobot/project-static/docs/development/core/best-practices.html +0 -27
- nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +58 -4
- nautobot/project-static/docs/development/core/getting-started.html +12 -16
- nautobot/project-static/docs/development/core/homepage.html +0 -3
- nautobot/project-static/docs/development/core/style-guide.html +0 -5
- nautobot/project-static/docs/development/core/templates.html +0 -3
- nautobot/project-static/docs/development/core/testing.html +0 -9
- nautobot/project-static/docs/development/jobs/index.html +30 -43
- nautobot/project-static/docs/objects.inv +0 -0
- nautobot/project-static/docs/overview/application_stack.html +0 -18
- nautobot/project-static/docs/release-notes/version-2.4.html +374 -0
- nautobot/project-static/docs/requirements.txt +2 -2
- 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/project-static/docs/user-guide/administration/configuration/settings.html +0 -10
- nautobot/project-static/docs/user-guide/administration/guides/docker.html +0 -15
- nautobot/project-static/docs/user-guide/administration/installation/index.html +0 -16
- nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +1 -4
- nautobot/project-static/docs/user-guide/administration/installation/services.html +0 -11
- nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +3 -3
- nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +5 -35
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/tables/v2-code-location-changes.yaml +1 -1
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +1 -1
- nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +0 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +0 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +1 -17
- nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +0 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +0 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +1 -7
- nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +0 -6
- nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +0 -3
- nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +0 -8
- nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +3 -3
- nautobot/project-static/docs/user-guide/feature-guides/graphql.html +0 -6
- nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +0 -3
- nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +3 -15
- nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +0 -26
- nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +0 -8
- nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +0 -3
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +0 -8
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +0 -7
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +0 -3
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +0 -3
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +0 -14
- nautobot/project-static/docs/user-guide/platform-functionality/note.html +0 -3
- nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +1 -10
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +0 -3
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +0 -14
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +0 -19
- nautobot/project-static/docs/user-guide/platform-functionality/secret.html +3 -9
- nautobot/project-static/docs/user-guide/platform-functionality/status.html +0 -8
- nautobot/project-static/docs/user-guide/platform-functionality/tag.html +0 -4
- nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +1 -13
- nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +0 -5
- nautobot/project-static/js/editor.js +292 -0
- nautobot/project-static/monaco-editor-0.52.2/README.md +81 -0
- nautobot/project-static/monaco-editor-0.52.2/vs/base/browser/ui/codicons/codicon/codicon.ttf +0 -0
- nautobot/project-static/monaco-editor-0.52.2/vs/base/worker/workerMain.js +31 -0
- nautobot/project-static/monaco-editor-0.52.2/vs/basic-languages/xml/xml.js +10 -0
- nautobot/project-static/monaco-editor-0.52.2/vs/basic-languages/yaml/yaml.js +10 -0
- nautobot/project-static/monaco-editor-0.52.2/vs/editor/editor.main.css +8 -0
- nautobot/project-static/monaco-editor-0.52.2/vs/editor/editor.main.js +798 -0
- nautobot/project-static/monaco-editor-0.52.2/vs/language/json/jsonMode.js +19 -0
- nautobot/project-static/monaco-editor-0.52.2/vs/language/json/jsonWorker.js +42 -0
- nautobot/project-static/monaco-editor-0.52.2/vs/loader.js +11 -0
- nautobot/tenancy/filters/__init__.py +3 -5
- nautobot/tenancy/tests/test_filters.py +10 -0
- nautobot/virtualization/views.py +0 -1
- nautobot/wireless/tables.py +9 -4
- nautobot/wireless/tests/test_api.py +0 -9
- {nautobot-2.4.3.dist-info → nautobot-2.4.5.dist-info}/METADATA +4 -4
- {nautobot-2.4.3.dist-info → nautobot-2.4.5.dist-info}/RECORD +198 -186
- {nautobot-2.4.3.dist-info → nautobot-2.4.5.dist-info}/LICENSE.txt +0 -0
- {nautobot-2.4.3.dist-info → nautobot-2.4.5.dist-info}/NOTICE +0 -0
- {nautobot-2.4.3.dist-info → nautobot-2.4.5.dist-info}/WHEEL +0 -0
- {nautobot-2.4.3.dist-info → nautobot-2.4.5.dist-info}/entry_points.txt +0 -0
|
@@ -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):
|
|
@@ -17,11 +17,12 @@ from nautobot.dcim.models import Device, DeviceType, Location, LocationType, Man
|
|
|
17
17
|
from nautobot.dcim.tests.test_views import create_test_device
|
|
18
18
|
from nautobot.extras import plugins
|
|
19
19
|
from nautobot.extras.choices import CustomFieldTypeChoices, RelationshipTypeChoices
|
|
20
|
+
from nautobot.extras.context_managers import web_request_context
|
|
20
21
|
from nautobot.extras.jobs import get_job
|
|
21
22
|
from nautobot.extras.models import CustomField, Relationship, RelationshipAssociation, Role, Secret, Status
|
|
22
23
|
from nautobot.extras.plugins.exceptions import PluginImproperlyConfigured
|
|
23
24
|
from nautobot.extras.plugins.utils import load_plugin
|
|
24
|
-
from nautobot.extras.plugins.validators import wrap_model_clean_methods
|
|
25
|
+
from nautobot.extras.plugins.validators import CustomValidator, wrap_model_clean_methods
|
|
25
26
|
from nautobot.extras.plugins.views import extract_app_data
|
|
26
27
|
from nautobot.extras.registry import DatasourceContent, registry
|
|
27
28
|
from nautobot.ipam.models import IPAddress, Namespace, Prefix
|
|
@@ -478,6 +479,16 @@ class AppAPITest(APIViewTestCases.APIViewTestCase):
|
|
|
478
479
|
pass
|
|
479
480
|
|
|
480
481
|
|
|
482
|
+
class TestUserContextCustomValidator(CustomValidator):
|
|
483
|
+
model = "dcim.locationtype"
|
|
484
|
+
|
|
485
|
+
def clean(self):
|
|
486
|
+
"""
|
|
487
|
+
Used to validate that the correct user context is available in the custom validator.
|
|
488
|
+
"""
|
|
489
|
+
self.validation_error(self.context["user"])
|
|
490
|
+
|
|
491
|
+
|
|
481
492
|
class AppCustomValidationTest(TestCase):
|
|
482
493
|
def setUp(self):
|
|
483
494
|
# When creating a fresh test DB, wrapping model clean methods fails, which is normal.
|
|
@@ -485,6 +496,7 @@ class AppCustomValidationTest(TestCase):
|
|
|
485
496
|
# must manually call the method again to actually perform the action, now that the
|
|
486
497
|
# ContentType table has been created.
|
|
487
498
|
wrap_model_clean_methods()
|
|
499
|
+
super().setUp()
|
|
488
500
|
|
|
489
501
|
def test_custom_validator_raises_exception(self):
|
|
490
502
|
location_type = LocationType.objects.get(name="Campus")
|
|
@@ -513,6 +525,25 @@ class AppCustomValidationTest(TestCase):
|
|
|
513
525
|
with self.assertRaises(ValidationError):
|
|
514
526
|
relationship_assoc.clean()
|
|
515
527
|
|
|
528
|
+
def test_custom_validator_non_web_request_uses_anonymous_user(self):
|
|
529
|
+
location_type = LocationType.objects.get(name="Campus")
|
|
530
|
+
registry["plugin_custom_validators"]["dcim.locationtype"] = [TestUserContextCustomValidator]
|
|
531
|
+
|
|
532
|
+
from django.contrib.auth.models import AnonymousUser
|
|
533
|
+
|
|
534
|
+
with self.assertRaises(ValidationError) as context:
|
|
535
|
+
location_type.clean()
|
|
536
|
+
self.assertEqual(context.exception.message, AnonymousUser())
|
|
537
|
+
|
|
538
|
+
def test_custom_validator_web_request_uses_real_user(self):
|
|
539
|
+
location_type = LocationType.objects.get(name="Campus")
|
|
540
|
+
registry["plugin_custom_validators"]["dcim.locationtype"] = [TestUserContextCustomValidator]
|
|
541
|
+
|
|
542
|
+
with self.assertRaises(ValidationError) as context:
|
|
543
|
+
with web_request_context(user=self.user):
|
|
544
|
+
location_type.clean()
|
|
545
|
+
self.assertEqual(context.exception.message, self.user)
|
|
546
|
+
|
|
516
547
|
|
|
517
548
|
class ExampleModelCustomActionViewTest(TestCase):
|
|
518
549
|
"""Test for custom action view `all_names` added to Example App"""
|
|
@@ -1784,7 +1784,7 @@ class RelationshipJobTestCase(RequiredRelationshipTestMixin, TransactionTestCase
|
|
|
1784
1784
|
|
|
1785
1785
|
pk_list = [str(vlan.id) for vlan in vlans]
|
|
1786
1786
|
job_result = self.create_job(pk_list)
|
|
1787
|
-
self.
|
|
1787
|
+
self.assertJobResultStatus(job_result, JobResultStatusChoices.STATUS_FAILURE)
|
|
1788
1788
|
error_log = JobLogEntry.objects.get(job_result=job_result, log_level=LogLevelChoices.LOG_ERROR)
|
|
1789
1789
|
self.assertIn("VLANs require at least one device, but no devices exist yet.", error_log.message)
|
|
1790
1790
|
|
|
@@ -1793,7 +1793,7 @@ class RelationshipJobTestCase(RequiredRelationshipTestMixin, TransactionTestCase
|
|
|
1793
1793
|
|
|
1794
1794
|
# Try editing all 6 VLANs without adding the required device(fails):
|
|
1795
1795
|
job_result = self.create_job(pk_list)
|
|
1796
|
-
self.
|
|
1796
|
+
self.assertJobResultStatus(job_result, JobResultStatusChoices.STATUS_FAILURE)
|
|
1797
1797
|
error_log = JobLogEntry.objects.get(job_result=job_result, log_level=LogLevelChoices.LOG_ERROR)
|
|
1798
1798
|
self.assertIn(
|
|
1799
1799
|
'6 VLANs require a device for the required relationship \\"VLANs require at least one Device',
|
|
@@ -1802,7 +1802,7 @@ class RelationshipJobTestCase(RequiredRelationshipTestMixin, TransactionTestCase
|
|
|
1802
1802
|
|
|
1803
1803
|
# Try editing 3 VLANs without adding the required device(fails):
|
|
1804
1804
|
job_result = self.create_job(pk_list[:3])
|
|
1805
|
-
self.
|
|
1805
|
+
self.assertJobResultStatus(job_result, JobResultStatusChoices.STATUS_FAILURE)
|
|
1806
1806
|
error_log = JobLogEntry.objects.get(job_result=job_result, log_level=LogLevelChoices.LOG_ERROR)
|
|
1807
1807
|
self.assertIn(
|
|
1808
1808
|
'These VLANs require a device for the required relationship \\"VLANs require at least one Device',
|
|
@@ -1813,11 +1813,11 @@ class RelationshipJobTestCase(RequiredRelationshipTestMixin, TransactionTestCase
|
|
|
1813
1813
|
|
|
1814
1814
|
# Try editing 6 VLANs and adding the required device (succeeds):
|
|
1815
1815
|
job_result = self.create_job(pk_list, add_cr_vlans_devices_m2m__source=[str(device_for_association.id)])
|
|
1816
|
-
self.
|
|
1816
|
+
self.assertJobResultStatus(job_result)
|
|
1817
1817
|
|
|
1818
1818
|
# Try editing 6 VLANs and removing the required device (fails):
|
|
1819
1819
|
job_result = self.create_job(pk_list, remove_cr_vlans_devices_m2m__source=[str(device_for_association.id)])
|
|
1820
|
-
self.
|
|
1820
|
+
self.assertJobResultStatus(job_result, JobResultStatusChoices.STATUS_FAILURE)
|
|
1821
1821
|
error_log = JobLogEntry.objects.get(job_result=job_result, log_level=LogLevelChoices.LOG_ERROR)
|
|
1822
1822
|
self.assertIn(
|
|
1823
1823
|
'6 VLANs require a device for the required relationship \\"VLANs require at least one Device',
|
|
@@ -823,9 +823,12 @@ class DynamicGroupTestCase(
|
|
|
823
823
|
location_ct = ContentType.objects.get_for_model(Location)
|
|
824
824
|
instance = self._get_queryset().exclude(content_type=location_ct).first()
|
|
825
825
|
# Add view permissions for the group's members:
|
|
826
|
-
self.add_permissions(
|
|
826
|
+
self.add_permissions(
|
|
827
|
+
get_permission_for_model(instance.content_type.model_class(), "view"), "extras.view_dynamicgroup"
|
|
828
|
+
)
|
|
827
829
|
|
|
828
|
-
response =
|
|
830
|
+
response = self.client.get(instance.get_absolute_url())
|
|
831
|
+
self.assertHttpStatus(response, 200)
|
|
829
832
|
|
|
830
833
|
response_body = extract_page_body(response.content.decode(response.charset))
|
|
831
834
|
# Check that the "members" table in the detail view includes all appropriate member objects
|
|
@@ -1177,6 +1180,13 @@ class GitRepositoryTestCase(
|
|
|
1177
1180
|
self.form_data = form_data
|
|
1178
1181
|
super().test_edit_object_with_constrained_permission()
|
|
1179
1182
|
|
|
1183
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
1184
|
+
def test_view_when_no_sync_job_result_exists(self):
|
|
1185
|
+
instance = self._get_queryset().first()
|
|
1186
|
+
response = self.client.get(reverse("extras:gitrepository_result", kwargs={"pk": instance.pk}))
|
|
1187
|
+
self.assertEqual(response.status_code, 200)
|
|
1188
|
+
self.assertEqual(response.context["result"], {})
|
|
1189
|
+
|
|
1180
1190
|
def test_post_sync_repo_anonymous(self):
|
|
1181
1191
|
self.client.logout()
|
|
1182
1192
|
url = reverse("extras:gitrepository_sync", kwargs={"pk": self._get_queryset().first().pk})
|
nautobot/extras/views.py
CHANGED
|
@@ -1165,6 +1165,9 @@ class GitRepositoryResultView(generic.ObjectView):
|
|
|
1165
1165
|
def get_extra_context(self, request, instance):
|
|
1166
1166
|
job_result = instance.get_latest_sync()
|
|
1167
1167
|
|
|
1168
|
+
if job_result is None:
|
|
1169
|
+
job_result = {}
|
|
1170
|
+
|
|
1168
1171
|
return {
|
|
1169
1172
|
"result": job_result,
|
|
1170
1173
|
"base_template": "extras/gitrepository.html",
|
|
@@ -2059,7 +2062,13 @@ def get_annotated_jobresult_queryset():
|
|
|
2059
2062
|
error_log_count=count_related(
|
|
2060
2063
|
JobLogEntry,
|
|
2061
2064
|
"job_result",
|
|
2062
|
-
filter_dict={
|
|
2065
|
+
filter_dict={
|
|
2066
|
+
"log_level__in": [
|
|
2067
|
+
LogLevelChoices.LOG_FAILURE,
|
|
2068
|
+
LogLevelChoices.LOG_ERROR,
|
|
2069
|
+
LogLevelChoices.LOG_CRITICAL,
|
|
2070
|
+
],
|
|
2071
|
+
},
|
|
2063
2072
|
),
|
|
2064
2073
|
)
|
|
2065
2074
|
)
|
nautobot/ipam/api/serializers.py
CHANGED
|
@@ -63,14 +63,13 @@ class VRFDeviceAssignmentSerializer(ValidatedModelSerializer):
|
|
|
63
63
|
validators = []
|
|
64
64
|
|
|
65
65
|
def validate(self, attrs):
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
validator(attrs, self)
|
|
66
|
+
foreign_key_fields = ["device", "virtual_machine", "virtual_device_context"]
|
|
67
|
+
for foreign_key in foreign_key_fields:
|
|
68
|
+
if attrs.get(foreign_key):
|
|
69
|
+
validator = UniqueTogetherValidator(
|
|
70
|
+
queryset=VRFDeviceAssignment.objects.all(), fields=(foreign_key, "vrf")
|
|
71
|
+
)
|
|
72
|
+
validator(attrs, self)
|
|
74
73
|
return super().validate(attrs)
|
|
75
74
|
|
|
76
75
|
|
nautobot/ipam/api/views.py
CHANGED
|
@@ -55,13 +55,13 @@ class VRFViewSet(NautobotModelViewSet):
|
|
|
55
55
|
filterset_class = filters.VRFFilterSet
|
|
56
56
|
|
|
57
57
|
|
|
58
|
-
class VRFDeviceAssignmentViewSet(
|
|
58
|
+
class VRFDeviceAssignmentViewSet(ModelViewSet):
|
|
59
59
|
queryset = VRFDeviceAssignment.objects.all()
|
|
60
60
|
serializer_class = serializers.VRFDeviceAssignmentSerializer
|
|
61
61
|
filterset_class = filters.VRFDeviceAssignmentFilterSet
|
|
62
62
|
|
|
63
63
|
|
|
64
|
-
class VRFPrefixAssignmentViewSet(
|
|
64
|
+
class VRFPrefixAssignmentViewSet(ModelViewSet):
|
|
65
65
|
queryset = VRFPrefixAssignment.objects.all()
|
|
66
66
|
serializer_class = serializers.VRFPrefixAssignmentSerializer
|
|
67
67
|
filterset_class = filters.VRFPrefixAssignmentFilterSet
|
nautobot/ipam/factory.py
CHANGED
|
@@ -15,7 +15,7 @@ from nautobot.core.factory import (
|
|
|
15
15
|
random_instance,
|
|
16
16
|
UniqueFaker,
|
|
17
17
|
)
|
|
18
|
-
from nautobot.dcim.models import Location
|
|
18
|
+
from nautobot.dcim.models import Location, VirtualDeviceContext
|
|
19
19
|
from nautobot.extras.models import Role, Status
|
|
20
20
|
from nautobot.ipam.choices import PrefixTypeChoices
|
|
21
21
|
from nautobot.ipam.models import IPAddress, Namespace, Prefix, RIR, RouteTarget, VLAN, VLANGroup, VRF
|
|
@@ -127,6 +127,24 @@ class VRFFactory(PrimaryModelFactory):
|
|
|
127
127
|
else:
|
|
128
128
|
self.export_targets.set(get_random_instances(RouteTarget))
|
|
129
129
|
|
|
130
|
+
@factory.post_generation
|
|
131
|
+
def prefixes(self, create, extracted, **kwargs):
|
|
132
|
+
if create:
|
|
133
|
+
if extracted:
|
|
134
|
+
self.prefixes.set(extracted)
|
|
135
|
+
else:
|
|
136
|
+
self.prefixes.set(
|
|
137
|
+
get_random_instances(lambda: Prefix.objects.filter(namespace=self.namespace), minimum=0)
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
@factory.post_generation
|
|
141
|
+
def virtual_device_contexts(self, create, extracted, **kwargs):
|
|
142
|
+
if create:
|
|
143
|
+
if extracted:
|
|
144
|
+
self.virtual_device_contexts.set(extracted)
|
|
145
|
+
else:
|
|
146
|
+
self.virtual_device_contexts.set(get_random_instances(VirtualDeviceContext))
|
|
147
|
+
|
|
130
148
|
|
|
131
149
|
class VLANGroupFactory(OrganizationalModelFactory):
|
|
132
150
|
class Meta:
|
|
@@ -295,7 +313,6 @@ class PrefixFactory(PrimaryModelFactory):
|
|
|
295
313
|
has_role = NautobotBoolIterator()
|
|
296
314
|
has_tenant = NautobotBoolIterator()
|
|
297
315
|
has_vlan = NautobotBoolIterator()
|
|
298
|
-
# has_vrf = NautobotBoolIterator()
|
|
299
316
|
is_ipv6 = NautobotBoolIterator()
|
|
300
317
|
|
|
301
318
|
prefix = factory.Maybe(
|
|
@@ -321,12 +338,6 @@ class PrefixFactory(PrimaryModelFactory):
|
|
|
321
338
|
None,
|
|
322
339
|
)
|
|
323
340
|
namespace = random_instance(Namespace, allow_null=False)
|
|
324
|
-
# TODO: Update for M2M tests
|
|
325
|
-
# vrf = factory.Maybe(
|
|
326
|
-
# "has_vrf",
|
|
327
|
-
# factory.SubFactory(VRFGetOrCreateFactory, tenant=factory.SelfAttribute("..tenant")),
|
|
328
|
-
# None,
|
|
329
|
-
# )
|
|
330
341
|
rir = factory.Maybe("has_rir", random_instance(RIR, allow_null=False), None)
|
|
331
342
|
date_allocated = factory.Maybe("has_date_allocated", factory.Faker("date_time", tzinfo=datetime.timezone.utc), None)
|
|
332
343
|
|
|
@@ -343,6 +354,14 @@ class PrefixFactory(PrimaryModelFactory):
|
|
|
343
354
|
)
|
|
344
355
|
)
|
|
345
356
|
|
|
357
|
+
@factory.post_generation
|
|
358
|
+
def vrfs(self, create, extracted, **kwargs):
|
|
359
|
+
if create:
|
|
360
|
+
if extracted:
|
|
361
|
+
self.vrfs.set(extracted)
|
|
362
|
+
else:
|
|
363
|
+
self.vrfs.set(get_random_instances(lambda: VRF.objects.filter(namespace=self.namespace), minimum=0))
|
|
364
|
+
|
|
346
365
|
@factory.post_generation
|
|
347
366
|
def children(self, create, extracted, **kwargs):
|
|
348
367
|
"""Creates child prefixes and ip addresses within the prefix IP space.
|