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.

Files changed (198) hide show
  1. nautobot/__init__.py +19 -3
  2. nautobot/apps/filters.py +2 -0
  3. nautobot/circuits/filters.py +1 -1
  4. nautobot/circuits/tests/test_models.py +5 -3
  5. nautobot/cloud/filters.py +3 -6
  6. nautobot/cloud/tests/test_filters.py +21 -0
  7. nautobot/core/admin.py +2 -0
  8. nautobot/core/celery/__init__.py +5 -3
  9. nautobot/core/jobs/__init__.py +5 -3
  10. nautobot/core/management/commands/generate_performance_test_endpoints.py +9 -6
  11. nautobot/core/models/utils.py +6 -1
  12. nautobot/core/templates/inc/javascript.html +1 -0
  13. nautobot/core/templatetags/ui_framework.py +20 -4
  14. nautobot/core/testing/__init__.py +2 -0
  15. nautobot/core/testing/forms.py +1 -1
  16. nautobot/core/testing/mixins.py +9 -0
  17. nautobot/core/tests/test_api.py +1 -1
  18. nautobot/core/tests/test_graphql.py +3 -3
  19. nautobot/core/tests/test_jobs.py +30 -28
  20. nautobot/core/ui/object_detail.py +1 -1
  21. nautobot/dcim/api/serializers.py +36 -0
  22. nautobot/dcim/api/views.py +1 -1
  23. nautobot/dcim/elevations.py +17 -4
  24. nautobot/dcim/factory.py +9 -1
  25. nautobot/dcim/filters/__init__.py +27 -1
  26. nautobot/dcim/forms.py +13 -1
  27. nautobot/dcim/models/devices.py +11 -5
  28. nautobot/dcim/signals.py +26 -0
  29. nautobot/dcim/templates/dcim/virtualdevicecontext_retrieve.html +0 -62
  30. nautobot/dcim/templates/dcim/virtualdevicecontext_update.html +6 -0
  31. nautobot/dcim/tests/test_api.py +176 -0
  32. nautobot/dcim/tests/test_filters.py +56 -3
  33. nautobot/dcim/tests/test_jobs.py +4 -6
  34. nautobot/dcim/tests/test_models.py +40 -0
  35. nautobot/dcim/views.py +24 -14
  36. nautobot/extras/api/mixins.py +1 -1
  37. nautobot/extras/api/views.py +2 -2
  38. nautobot/extras/choices.py +8 -3
  39. nautobot/extras/filters/__init__.py +4 -0
  40. nautobot/extras/jobs.py +181 -103
  41. nautobot/extras/management/utils.py +13 -2
  42. nautobot/extras/models/datasources.py +11 -4
  43. nautobot/extras/models/jobs.py +20 -17
  44. nautobot/extras/plugins/__init__.py +26 -1
  45. nautobot/extras/tables.py +25 -29
  46. nautobot/extras/templates/extras/inc/jobresult.html +12 -13
  47. nautobot/extras/templates/extras/objectchange.html +28 -12
  48. nautobot/extras/test_jobs/atomic_transaction.py +6 -6
  49. nautobot/extras/test_jobs/fail.py +75 -1
  50. nautobot/extras/tests/test_api.py +17 -16
  51. nautobot/extras/tests/test_datasources.py +64 -54
  52. nautobot/extras/tests/test_filters.py +2 -0
  53. nautobot/extras/tests/test_jobs.py +69 -62
  54. nautobot/extras/tests/test_models.py +1 -1
  55. nautobot/extras/tests/test_plugins.py +32 -1
  56. nautobot/extras/tests/test_relationships.py +5 -5
  57. nautobot/extras/tests/test_views.py +12 -2
  58. nautobot/extras/views.py +10 -1
  59. nautobot/ipam/api/serializers.py +7 -8
  60. nautobot/ipam/api/views.py +2 -2
  61. nautobot/ipam/factory.py +27 -8
  62. nautobot/ipam/filters.py +67 -29
  63. nautobot/ipam/formfields.py +51 -0
  64. nautobot/ipam/forms.py +28 -1
  65. nautobot/ipam/migrations/0051_added_optional_vrf_relationship_to_vdc.py +41 -0
  66. nautobot/ipam/models.py +63 -5
  67. nautobot/ipam/querysets.py +6 -0
  68. nautobot/ipam/tables.py +21 -7
  69. nautobot/ipam/templates/ipam/rir.html +1 -43
  70. nautobot/ipam/tests/test_api.py +107 -66
  71. nautobot/ipam/tests/test_filters.py +145 -5
  72. nautobot/ipam/tests/test_models.py +16 -0
  73. nautobot/ipam/tests/test_views.py +15 -2
  74. nautobot/ipam/urls.py +1 -21
  75. nautobot/ipam/views.py +24 -41
  76. nautobot/project-static/css/base.css +11 -0
  77. nautobot/project-static/css/dark.css +2 -1
  78. nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +62 -0
  79. nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +43 -5
  80. nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +35 -0
  81. nautobot/project-static/docs/development/apps/api/configuration-view.html +0 -3
  82. nautobot/project-static/docs/development/apps/api/models/graphql.html +0 -4
  83. nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +94 -1
  84. nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +0 -3
  85. nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +0 -3
  86. nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +0 -3
  87. nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +0 -3
  88. nautobot/project-static/docs/development/apps/api/prometheus.html +0 -3
  89. nautobot/project-static/docs/development/apps/api/testing.html +0 -6
  90. nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +0 -3
  91. nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +0 -3
  92. nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +0 -3
  93. nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +0 -3
  94. nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +1 -7
  95. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +0 -7
  96. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +0 -4
  97. nautobot/project-static/docs/development/apps/api/views/notes.html +0 -3
  98. nautobot/project-static/docs/development/apps/index.html +2 -35
  99. nautobot/project-static/docs/development/apps/migration/code-updates.html +1 -1
  100. nautobot/project-static/docs/development/core/application-registry.html +0 -6
  101. nautobot/project-static/docs/development/core/best-practices.html +0 -27
  102. nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +58 -4
  103. nautobot/project-static/docs/development/core/getting-started.html +12 -16
  104. nautobot/project-static/docs/development/core/homepage.html +0 -3
  105. nautobot/project-static/docs/development/core/style-guide.html +0 -5
  106. nautobot/project-static/docs/development/core/templates.html +0 -3
  107. nautobot/project-static/docs/development/core/testing.html +0 -9
  108. nautobot/project-static/docs/development/jobs/index.html +30 -43
  109. nautobot/project-static/docs/objects.inv +0 -0
  110. nautobot/project-static/docs/overview/application_stack.html +0 -18
  111. nautobot/project-static/docs/release-notes/version-2.4.html +374 -0
  112. nautobot/project-static/docs/requirements.txt +2 -2
  113. nautobot/project-static/docs/search/search_index.json +1 -1
  114. nautobot/project-static/docs/sitemap.xml +290 -290
  115. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  116. nautobot/project-static/docs/user-guide/administration/configuration/settings.html +0 -10
  117. nautobot/project-static/docs/user-guide/administration/guides/docker.html +0 -15
  118. nautobot/project-static/docs/user-guide/administration/installation/index.html +0 -16
  119. nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +1 -4
  120. nautobot/project-static/docs/user-guide/administration/installation/services.html +0 -11
  121. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +3 -3
  122. nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +5 -35
  123. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/tables/v2-code-location-changes.yaml +1 -1
  124. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +1 -1
  125. nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +0 -4
  126. nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +0 -3
  127. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +0 -4
  128. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +0 -4
  129. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +0 -4
  130. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +0 -4
  131. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +0 -4
  132. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +0 -4
  133. nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +0 -3
  134. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +0 -4
  135. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +0 -4
  136. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +0 -4
  137. nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +1 -17
  138. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +0 -3
  139. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +0 -4
  140. nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +0 -4
  141. nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +0 -3
  142. nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +1 -7
  143. nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +0 -4
  144. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +0 -4
  145. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +0 -4
  146. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +0 -4
  147. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +0 -4
  148. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +0 -4
  149. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +0 -4
  150. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +0 -6
  151. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +0 -3
  152. nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +0 -4
  153. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +0 -4
  154. nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +0 -8
  155. nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +3 -3
  156. nautobot/project-static/docs/user-guide/feature-guides/graphql.html +0 -6
  157. nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +0 -3
  158. nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +3 -15
  159. nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +0 -26
  160. nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +0 -8
  161. nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +0 -3
  162. nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +0 -8
  163. nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +0 -7
  164. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +0 -3
  165. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +0 -3
  166. nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +0 -14
  167. nautobot/project-static/docs/user-guide/platform-functionality/note.html +0 -3
  168. nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +1 -10
  169. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +0 -3
  170. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +0 -14
  171. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +0 -19
  172. nautobot/project-static/docs/user-guide/platform-functionality/secret.html +3 -9
  173. nautobot/project-static/docs/user-guide/platform-functionality/status.html +0 -8
  174. nautobot/project-static/docs/user-guide/platform-functionality/tag.html +0 -4
  175. nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +1 -13
  176. nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +0 -5
  177. nautobot/project-static/js/editor.js +292 -0
  178. nautobot/project-static/monaco-editor-0.52.2/README.md +81 -0
  179. nautobot/project-static/monaco-editor-0.52.2/vs/base/browser/ui/codicons/codicon/codicon.ttf +0 -0
  180. nautobot/project-static/monaco-editor-0.52.2/vs/base/worker/workerMain.js +31 -0
  181. nautobot/project-static/monaco-editor-0.52.2/vs/basic-languages/xml/xml.js +10 -0
  182. nautobot/project-static/monaco-editor-0.52.2/vs/basic-languages/yaml/yaml.js +10 -0
  183. nautobot/project-static/monaco-editor-0.52.2/vs/editor/editor.main.css +8 -0
  184. nautobot/project-static/monaco-editor-0.52.2/vs/editor/editor.main.js +798 -0
  185. nautobot/project-static/monaco-editor-0.52.2/vs/language/json/jsonMode.js +19 -0
  186. nautobot/project-static/monaco-editor-0.52.2/vs/language/json/jsonWorker.js +42 -0
  187. nautobot/project-static/monaco-editor-0.52.2/vs/loader.js +11 -0
  188. nautobot/tenancy/filters/__init__.py +3 -5
  189. nautobot/tenancy/tests/test_filters.py +10 -0
  190. nautobot/virtualization/views.py +0 -1
  191. nautobot/wireless/tables.py +9 -4
  192. nautobot/wireless/tests/test_api.py +0 -9
  193. {nautobot-2.4.3.dist-info → nautobot-2.4.5.dist-info}/METADATA +4 -4
  194. {nautobot-2.4.3.dist-info → nautobot-2.4.5.dist-info}/RECORD +198 -186
  195. {nautobot-2.4.3.dist-info → nautobot-2.4.5.dist-info}/LICENSE.txt +0 -0
  196. {nautobot-2.4.3.dist-info → nautobot-2.4.5.dist-info}/NOTICE +0 -0
  197. {nautobot-2.4.3.dist-info → nautobot-2.4.5.dist-info}/WHEEL +0 -0
  198. {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.assertEqual(job_result.status, JobResultStatusChoices.STATUS_FAILURE)
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.assertEqual(job_result.status, JobResultStatusChoices.STATUS_SUCCESS)
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.assertEqual(job_result.status, JobResultStatusChoices.STATUS_SUCCESS)
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.assertEqual(job_result.status, JobResultStatusChoices.STATUS_SUCCESS)
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.assertEqual(job_result.status, JobResultStatusChoices.STATUS_SUCCESS)
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 = "TestFailJob"
462
- job_result = create_job_result_and_run_job(module, name)
463
- self.assertEqual(job_result.status, JobResultStatusChoices.STATUS_FAILURE)
464
- logs = job_result.job_log_entries
465
- self.assertGreater(logs.count(), 0)
466
- try:
467
- logs.get(message="before_start() was called as expected")
468
- logs.get(message="I'm a test job that fails!")
469
- logs.get(message="on_failure() was called as expected")
470
- logs.get(message="after_return() was called as expected")
471
- except models.JobLogEntry.DoesNotExist:
472
- for log in logs.all():
473
- print(log.message)
474
- print(job_result.traceback)
475
- raise
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.assertEqual(job_result.status, JobResultStatusChoices.STATUS_FAILURE)
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.assertEqual(job_result.status, JobResultStatusChoices.STATUS_SUCCESS)
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.assertEqual(job_result.status, JobResultStatusChoices.STATUS_SUCCESS)
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, fail=True)
534
- self.assertEqual(job_result.status, JobResultStatusChoices.STATUS_FAILURE)
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, fail=True)
551
- self.assertEqual(job_result.status, JobResultStatusChoices.STATUS_FAILURE)
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.assertEqual(job_result.status, JobResultStatusChoices.STATUS_SUCCESS)
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.assertEqual(job_result.status, JobResultStatusChoices.STATUS_SUCCESS)
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.assertEqual(job_result.status, JobResultStatusChoices.STATUS_SUCCESS)
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.assertEqual(job_result.status, JobResultStatusChoices.STATUS_FAILURE)
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.assertEqual(job_result_1.status, JobResultStatusChoices.STATUS_SUCCESS)
699
+ self.assertJobResultStatus(job_result_1)
699
700
  job_result_2 = create_job_result_and_run_job(module, name)
700
- self.assertEqual(job_result_2.status, JobResultStatusChoices.STATUS_SUCCESS)
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.assertEqual(
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
- failed_job_result = create_job_result_and_run_job(module, name)
727
+ try:
728
+ failed_job_result = create_job_result_and_run_job(module, name)
731
729
 
732
- self.assertEqual(
733
- failed_job_result.status, JobResultStatusChoices.STATUS_FAILURE, msg="Duplicate singleton job didn't error."
734
- )
735
- self.assertIsNone(cache.get(job_class.singleton_cache_key, None))
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
- passed_job_result = create_job_result_and_run_job(
745
- module, name, celery_kwargs={"nautobot_job_ignore_singleton_lock": True}
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
- self.assertEqual(
749
- passed_job_result.status,
750
- JobResultStatusChoices.STATUS_SUCCESS,
751
- msg="Duplicate singleton job didn't succeed with nautobot_job_ignore_singleton_lock=True.",
752
- )
753
- self.assertIsNone(cache.get(job_class.singleton_cache_key, None))
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.assertEqual(job_result.status, JobResultStatusChoices.STATUS_SUCCESS)
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.assertEqual(job_result.status, JobResultStatusChoices.STATUS_SUCCESS, job_result.traceback)
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.assertEqual(job_result.status, JobResultStatusChoices.STATUS_SUCCESS, job_result.traceback)
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.assertEqual(job_result.status, JobResultStatusChoices.STATUS_FAILURE)
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.assertEqual(job_result.status, JobResultStatusChoices.STATUS_FAILURE)
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.assertEqual(job_result.status, JobResultStatusChoices.STATUS_SUCCESS)
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.assertEqual(job_result.status, JobResultStatusChoices.STATUS_FAILURE)
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.assertEqual(job_result.status, JobResultStatusChoices.STATUS_FAILURE)
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="ExampleLoggingJob")
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.assertEqual(job_result.status, JobResultStatusChoices.STATUS_FAILURE)
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.assertEqual(job_result.status, JobResultStatusChoices.STATUS_FAILURE)
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.assertEqual(job_result.status, JobResultStatusChoices.STATUS_FAILURE)
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.assertEqual(job_result.status, JobResultStatusChoices.STATUS_SUCCESS)
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.assertEqual(job_result.status, JobResultStatusChoices.STATUS_FAILURE)
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(get_permission_for_model(instance.content_type.model_class(), "view"))
826
+ self.add_permissions(
827
+ get_permission_for_model(instance.content_type.model_class(), "view"), "extras.view_dynamicgroup"
828
+ )
827
829
 
828
- response = super().test_get_object_with_permission()
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={"log_level__in": [LogLevelChoices.LOG_ERROR, LogLevelChoices.LOG_CRITICAL]},
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
  )
@@ -63,14 +63,13 @@ class VRFDeviceAssignmentSerializer(ValidatedModelSerializer):
63
63
  validators = []
64
64
 
65
65
  def validate(self, attrs):
66
- if attrs.get("device"):
67
- validator = UniqueTogetherValidator(queryset=VRFDeviceAssignment.objects.all(), fields=("device", "vrf"))
68
- validator(attrs, self)
69
- if attrs.get("virtual_machine"):
70
- validator = UniqueTogetherValidator(
71
- queryset=VRFDeviceAssignment.objects.all(), fields=("virtual_machine", "vrf")
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
 
@@ -55,13 +55,13 @@ class VRFViewSet(NautobotModelViewSet):
55
55
  filterset_class = filters.VRFFilterSet
56
56
 
57
57
 
58
- class VRFDeviceAssignmentViewSet(NautobotModelViewSet):
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(NautobotModelViewSet):
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.