nautobot 3.0.3__py3-none-any.whl → 3.0.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.
Files changed (405) hide show
  1. nautobot/core/authentication.py +0 -1
  2. nautobot/core/celery/schedulers.py +1 -3
  3. nautobot/core/cli/__init__.py +81 -39
  4. nautobot/core/settings.yaml +12 -4
  5. nautobot/core/tables.py +28 -17
  6. nautobot/core/templates/graphene/graphiql.html +3 -5
  7. nautobot/core/templates/inc/javascript.html +5 -10
  8. nautobot/core/templates/inc/media.html +5 -4
  9. nautobot/core/templates/inc/media_failure.html +73 -0
  10. nautobot/core/templates/media_failure.html +1 -0
  11. nautobot/core/tests/test_cli.py +120 -1
  12. nautobot/core/tests/test_templatetags_helpers.py +9 -9
  13. nautobot/core/ui/object_detail.py +1 -0
  14. nautobot/dcim/forms.py +1 -0
  15. nautobot/dcim/tables/devices.py +6 -5
  16. nautobot/dcim/tables/template_code.py +8 -4
  17. nautobot/dcim/templates/dcim/platform_create.html +3 -4
  18. nautobot/dcim/tests/test_tables.py +5 -6
  19. nautobot/dcim/views.py +6 -7
  20. nautobot/extras/models/jobs.py +7 -1
  21. nautobot/extras/signals.py +143 -113
  22. nautobot/extras/tables.py +3 -3
  23. nautobot/extras/templates/extras/inc/jobresult_js.html +1 -2
  24. nautobot/extras/templates/extras/scheduledjob.html +3 -1
  25. nautobot/extras/tests/test_customfields.py +75 -9
  26. nautobot/extras/tests/test_utils.py +116 -1
  27. nautobot/extras/utils.py +18 -16
  28. nautobot/extras/views.py +2 -14
  29. nautobot/ipam/apps.py +1 -0
  30. nautobot/ipam/filters.py +58 -3
  31. nautobot/ipam/tables.py +8 -4
  32. nautobot/ipam/tests/test_filters.py +55 -0
  33. nautobot/project-static/dist/css/nautobot.css +1 -1
  34. nautobot/project-static/dist/css/nautobot.css.map +1 -1
  35. nautobot/project-static/docs/404.html +64 -8
  36. nautobot/project-static/docs/apps/index.html +64 -8
  37. nautobot/project-static/docs/apps/nautobot-apps.html +64 -8
  38. nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +64 -8
  39. nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +64 -8
  40. nautobot/project-static/docs/code-reference/nautobot/apps/api.html +64 -8
  41. nautobot/project-static/docs/code-reference/nautobot/apps/change_logging.html +64 -8
  42. nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +64 -8
  43. nautobot/project-static/docs/code-reference/nautobot/apps/config.html +64 -8
  44. nautobot/project-static/docs/code-reference/nautobot/apps/constants.html +64 -8
  45. nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +64 -8
  46. nautobot/project-static/docs/code-reference/nautobot/apps/events.html +64 -8
  47. nautobot/project-static/docs/code-reference/nautobot/apps/exceptions.html +64 -8
  48. nautobot/project-static/docs/code-reference/nautobot/apps/factory.html +64 -8
  49. nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +64 -8
  50. nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +64 -8
  51. nautobot/project-static/docs/code-reference/nautobot/apps/graphql.html +64 -8
  52. nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +64 -8
  53. nautobot/project-static/docs/code-reference/nautobot/apps/models.html +64 -8
  54. nautobot/project-static/docs/code-reference/nautobot/apps/querysets.html +64 -8
  55. nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +64 -8
  56. nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +64 -8
  57. nautobot/project-static/docs/code-reference/nautobot/apps/templatetags.html +64 -8
  58. nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +64 -8
  59. nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +64 -8
  60. nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +64 -8
  61. nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +64 -8
  62. nautobot/project-static/docs/code-reference/nautobot/apps/views.html +64 -8
  63. nautobot/project-static/docs/development/apps/api/configuration-view.html +64 -8
  64. nautobot/project-static/docs/development/apps/api/database-backend-config.html +64 -8
  65. nautobot/project-static/docs/development/apps/api/models/django-admin.html +67 -11
  66. nautobot/project-static/docs/development/apps/api/models/global-search.html +64 -8
  67. nautobot/project-static/docs/development/apps/api/models/graphql.html +64 -8
  68. nautobot/project-static/docs/development/apps/api/models/index.html +64 -8
  69. nautobot/project-static/docs/development/apps/api/models/queryset.html +13440 -0
  70. nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +64 -8
  71. nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +64 -8
  72. nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +64 -8
  73. nautobot/project-static/docs/development/apps/api/platform-features/git-repository-content.html +64 -8
  74. nautobot/project-static/docs/development/apps/api/platform-features/index.html +64 -8
  75. nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +64 -8
  76. nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +64 -8
  77. nautobot/project-static/docs/development/apps/api/platform-features/prepopulating-data.html +64 -8
  78. nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +64 -8
  79. nautobot/project-static/docs/development/apps/api/platform-features/table-extensions.html +64 -8
  80. nautobot/project-static/docs/development/apps/api/platform-features/uniquely-identify-objects.html +64 -8
  81. nautobot/project-static/docs/development/apps/api/prometheus.html +64 -8
  82. nautobot/project-static/docs/development/apps/api/setup.html +64 -8
  83. nautobot/project-static/docs/development/apps/api/testing.html +64 -8
  84. nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +64 -8
  85. nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +64 -8
  86. nautobot/project-static/docs/development/apps/api/ui-extensions/index.html +64 -8
  87. nautobot/project-static/docs/development/apps/api/ui-extensions/navigation.html +64 -8
  88. nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +64 -8
  89. nautobot/project-static/docs/development/apps/api/views/base-template.html +64 -8
  90. nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +64 -8
  91. nautobot/project-static/docs/development/apps/api/views/django-generic-views.html +64 -8
  92. nautobot/project-static/docs/development/apps/api/views/help-documentation.html +64 -8
  93. nautobot/project-static/docs/development/apps/api/views/index.html +67 -11
  94. nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +64 -8
  95. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +64 -8
  96. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +64 -8
  97. nautobot/project-static/docs/development/apps/api/views/notes.html +64 -8
  98. nautobot/project-static/docs/development/apps/api/views/rest-api.html +64 -8
  99. nautobot/project-static/docs/development/apps/api/views/urls.html +64 -8
  100. nautobot/project-static/docs/development/apps/index.html +64 -8
  101. nautobot/project-static/docs/development/apps/migration/code-updates.html +64 -8
  102. nautobot/project-static/docs/development/apps/migration/dependency-updates.html +64 -8
  103. nautobot/project-static/docs/development/apps/migration/from-v1.html +64 -8
  104. nautobot/project-static/docs/development/apps/migration/from-v2/migrating-v2-to-v3.html +64 -8
  105. nautobot/project-static/docs/development/apps/migration/from-v2/new-nautobot-custom-ui-apis.html +64 -8
  106. nautobot/project-static/docs/development/apps/migration/from-v2/overview.html +68 -8
  107. nautobot/project-static/docs/development/apps/migration/from-v2/upgrading-from-bootstrap-v3-to-v5.html +64 -8
  108. nautobot/project-static/docs/development/apps/migration/model-updates/dcim.html +64 -8
  109. nautobot/project-static/docs/development/apps/migration/model-updates/extras.html +64 -8
  110. nautobot/project-static/docs/development/apps/migration/model-updates/global.html +64 -8
  111. nautobot/project-static/docs/development/apps/migration/model-updates/ipam.html +64 -8
  112. nautobot/project-static/docs/development/apps/migration/ui-component-framework/best-practices.html +64 -8
  113. nautobot/project-static/docs/development/apps/migration/ui-component-framework/breadcrumbs-titles.html +64 -8
  114. nautobot/project-static/docs/development/apps/migration/ui-component-framework/custom-content.html +64 -8
  115. nautobot/project-static/docs/development/apps/migration/ui-component-framework/index.html +64 -8
  116. nautobot/project-static/docs/development/apps/migration/ui-component-framework/migration-steps.html +64 -8
  117. nautobot/project-static/docs/development/apps/porting-from-netbox.html +64 -8
  118. nautobot/project-static/docs/development/core/application-registry.html +64 -8
  119. nautobot/project-static/docs/development/core/best-practices.html +64 -8
  120. nautobot/project-static/docs/development/core/caching.html +64 -8
  121. nautobot/project-static/docs/development/core/controllers.html +64 -8
  122. nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +64 -8
  123. nautobot/project-static/docs/development/core/docs-media-standards.html +64 -8
  124. nautobot/project-static/docs/development/core/generic-views.html +64 -8
  125. nautobot/project-static/docs/development/core/getting-started.html +64 -8
  126. nautobot/project-static/docs/development/core/homepage.html +64 -8
  127. nautobot/project-static/docs/development/core/index.html +64 -8
  128. nautobot/project-static/docs/development/core/minikube-dev-environment-for-k8s-jobs.html +64 -8
  129. nautobot/project-static/docs/development/core/model-checklist.html +64 -8
  130. nautobot/project-static/docs/development/core/model-features.html +64 -8
  131. nautobot/project-static/docs/development/core/natural-keys.html +64 -8
  132. nautobot/project-static/docs/development/core/navigation-menu.html +64 -8
  133. nautobot/project-static/docs/development/core/release-checklist.html +66 -8
  134. nautobot/project-static/docs/development/core/role-internals.html +64 -8
  135. nautobot/project-static/docs/development/core/settings.html +64 -8
  136. nautobot/project-static/docs/development/core/style-guide.html +64 -8
  137. nautobot/project-static/docs/development/core/templates.html +64 -8
  138. nautobot/project-static/docs/development/core/testing.html +64 -8
  139. nautobot/project-static/docs/development/core/ui-best-practices.html +64 -8
  140. nautobot/project-static/docs/development/core/ui-component-framework.html +64 -8
  141. nautobot/project-static/docs/development/core/user-preferences.html +64 -8
  142. nautobot/project-static/docs/development/index.html +64 -8
  143. nautobot/project-static/docs/development/jobs/getting-started.html +64 -8
  144. nautobot/project-static/docs/development/jobs/index.html +64 -8
  145. nautobot/project-static/docs/development/jobs/installation.html +64 -8
  146. nautobot/project-static/docs/development/jobs/job-extensions.html +64 -8
  147. nautobot/project-static/docs/development/jobs/job-logging.html +64 -8
  148. nautobot/project-static/docs/development/jobs/job-patterns.html +64 -8
  149. nautobot/project-static/docs/development/jobs/job-structure.html +64 -8
  150. nautobot/project-static/docs/development/jobs/migration/from-v1.html +64 -8
  151. nautobot/project-static/docs/development/jobs/testing.html +64 -8
  152. nautobot/project-static/docs/index.html +64 -8
  153. nautobot/project-static/docs/overview/application_stack.html +64 -8
  154. nautobot/project-static/docs/overview/design_philosophy.html +64 -8
  155. nautobot/project-static/docs/release-notes/index.html +64 -8
  156. nautobot/project-static/docs/release-notes/version-1.0.html +64 -8
  157. nautobot/project-static/docs/release-notes/version-1.1.html +64 -8
  158. nautobot/project-static/docs/release-notes/version-1.2.html +65 -9
  159. nautobot/project-static/docs/release-notes/version-1.3.html +64 -8
  160. nautobot/project-static/docs/release-notes/version-1.4.html +64 -8
  161. nautobot/project-static/docs/release-notes/version-1.5.html +64 -8
  162. nautobot/project-static/docs/release-notes/version-1.6.html +64 -8
  163. nautobot/project-static/docs/release-notes/version-2.0.html +64 -8
  164. nautobot/project-static/docs/release-notes/version-2.1.html +64 -8
  165. nautobot/project-static/docs/release-notes/version-2.2.html +64 -8
  166. nautobot/project-static/docs/release-notes/version-2.3.html +64 -8
  167. nautobot/project-static/docs/release-notes/version-2.4.html +584 -8
  168. nautobot/project-static/docs/release-notes/version-3.0.html +467 -8
  169. nautobot/project-static/docs/search/search_index.json +1 -1
  170. nautobot/project-static/docs/sitemap.xml +337 -329
  171. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  172. nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +64 -8
  173. nautobot/project-static/docs/user-guide/administration/configuration/authentication/remote.html +64 -8
  174. nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +64 -8
  175. nautobot/project-static/docs/user-guide/administration/configuration/index.html +64 -8
  176. nautobot/project-static/docs/user-guide/administration/configuration/redis.html +64 -8
  177. nautobot/project-static/docs/user-guide/administration/configuration/settings.html +75 -12
  178. nautobot/project-static/docs/user-guide/administration/configuration/time-zones.html +64 -8
  179. nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +64 -8
  180. nautobot/project-static/docs/user-guide/administration/guides/docker.html +64 -8
  181. nautobot/project-static/docs/user-guide/administration/guides/health-checks.html +64 -8
  182. nautobot/project-static/docs/user-guide/administration/guides/permissions.html +64 -8
  183. nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +64 -8
  184. nautobot/project-static/docs/user-guide/administration/guides/replicating-nautobot.html +64 -8
  185. nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +64 -8
  186. nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +64 -8
  187. nautobot/project-static/docs/user-guide/administration/guides/selinux-troubleshooting.html +64 -8
  188. nautobot/project-static/docs/user-guide/administration/installation/app-install.html +64 -8
  189. nautobot/project-static/docs/user-guide/administration/installation/external-authentication.html +64 -8
  190. nautobot/project-static/docs/user-guide/administration/installation/http-server.html +72 -9
  191. nautobot/project-static/docs/user-guide/administration/installation/index.html +64 -8
  192. nautobot/project-static/docs/user-guide/administration/installation/install_system.html +64 -8
  193. nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +64 -8
  194. nautobot/project-static/docs/user-guide/administration/installation/services.html +64 -8
  195. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-netbox.html +65 -9
  196. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +64 -8
  197. nautobot/project-static/docs/user-guide/administration/security/index.html +64 -8
  198. nautobot/project-static/docs/user-guide/administration/security/notices.html +64 -8
  199. nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +64 -8
  200. nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +64 -8
  201. nautobot/project-static/docs/user-guide/administration/upgrading/database-backup.html +67 -11
  202. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/after-you-upgrade.html +64 -8
  203. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/before-you-upgrade.html +64 -8
  204. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/for-developers.html +64 -8
  205. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/index.html +64 -8
  206. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/whats-changed.html +64 -8
  207. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/region-and-site-data-migration-guide.html +64 -8
  208. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +64 -8
  209. nautobot/project-static/docs/user-guide/administration/upgrading/from-v2/index.html +68 -8
  210. nautobot/project-static/docs/user-guide/administration/upgrading/postgresql.html +13391 -0
  211. nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +125 -15
  212. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuit.html +64 -8
  213. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittermination.html +64 -8
  214. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittype.html +64 -8
  215. nautobot/project-static/docs/user-guide/core-data-model/circuits/provider.html +64 -8
  216. nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +64 -8
  217. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloud.html +64 -8
  218. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudaccount.html +64 -8
  219. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetwork.html +64 -8
  220. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetworkprefixassignment.html +64 -8
  221. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudresourcetype.html +64 -8
  222. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservice.html +64 -8
  223. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservicenetworkassignment.html +64 -8
  224. nautobot/project-static/docs/user-guide/core-data-model/dcim/cable.html +64 -8
  225. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +64 -8
  226. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +64 -8
  227. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +64 -8
  228. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +64 -8
  229. nautobot/project-static/docs/user-guide/core-data-model/dcim/controller.html +64 -8
  230. nautobot/project-static/docs/user-guide/core-data-model/dcim/controllermanageddevicegroup.html +64 -8
  231. nautobot/project-static/docs/user-guide/core-data-model/dcim/device.html +64 -8
  232. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +64 -8
  233. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +64 -8
  234. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicefamily.html +64 -8
  235. nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +64 -8
  236. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +64 -8
  237. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +64 -8
  238. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +64 -8
  239. nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +64 -8
  240. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +64 -8
  241. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +64 -8
  242. nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +64 -8
  243. nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +64 -8
  244. nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +64 -8
  245. nautobot/project-static/docs/user-guide/core-data-model/dcim/manufacturer.html +64 -8
  246. nautobot/project-static/docs/user-guide/core-data-model/dcim/module.html +64 -8
  247. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebay.html +64 -8
  248. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebaytemplate.html +64 -8
  249. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulefamily.html +64 -8
  250. nautobot/project-static/docs/user-guide/core-data-model/dcim/moduletype.html +64 -8
  251. nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +64 -8
  252. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerfeed.html +64 -8
  253. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +64 -8
  254. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +64 -8
  255. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerpanel.html +64 -8
  256. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +64 -8
  257. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +64 -8
  258. nautobot/project-static/docs/user-guide/core-data-model/dcim/rack.html +64 -8
  259. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackgroup.html +64 -8
  260. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackreservation.html +64 -8
  261. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +64 -8
  262. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +64 -8
  263. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +64 -8
  264. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareversion.html +64 -8
  265. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualchassis.html +64 -8
  266. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualdevicecontext.html +64 -8
  267. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +64 -8
  268. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +64 -8
  269. nautobot/project-static/docs/user-guide/core-data-model/extras/contact.html +64 -8
  270. nautobot/project-static/docs/user-guide/core-data-model/extras/team.html +64 -8
  271. nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +64 -8
  272. nautobot/project-static/docs/user-guide/core-data-model/ipam/namespace.html +64 -8
  273. nautobot/project-static/docs/user-guide/core-data-model/ipam/prefix.html +64 -8
  274. nautobot/project-static/docs/user-guide/core-data-model/ipam/rir.html +64 -8
  275. nautobot/project-static/docs/user-guide/core-data-model/ipam/routetarget.html +64 -8
  276. nautobot/project-static/docs/user-guide/core-data-model/ipam/service.html +64 -8
  277. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +64 -8
  278. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlangroup.html +64 -8
  279. nautobot/project-static/docs/user-guide/core-data-model/ipam/vrf.html +64 -8
  280. nautobot/project-static/docs/user-guide/core-data-model/load-balancers/certificateprofile.html +64 -8
  281. nautobot/project-static/docs/user-guide/core-data-model/load-balancers/healthcheckmonitor.html +64 -8
  282. nautobot/project-static/docs/user-guide/core-data-model/load-balancers/index.html +64 -8
  283. nautobot/project-static/docs/user-guide/core-data-model/load-balancers/loadbalancerpool.html +64 -8
  284. nautobot/project-static/docs/user-guide/core-data-model/load-balancers/loadbalancerpoolmember.html +64 -8
  285. nautobot/project-static/docs/user-guide/core-data-model/load-balancers/virtualserver.html +64 -8
  286. nautobot/project-static/docs/user-guide/core-data-model/overview/introduction.html +64 -8
  287. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenant.html +64 -8
  288. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenantgroup.html +64 -8
  289. nautobot/project-static/docs/user-guide/core-data-model/virtualization/cluster.html +64 -8
  290. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustergroup.html +64 -8
  291. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustertype.html +64 -8
  292. nautobot/project-static/docs/user-guide/core-data-model/virtualization/virtualmachine.html +64 -8
  293. nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +64 -8
  294. nautobot/project-static/docs/user-guide/core-data-model/vpn/index.html +64 -8
  295. nautobot/project-static/docs/user-guide/core-data-model/vpn/vpn.html +64 -8
  296. nautobot/project-static/docs/user-guide/core-data-model/vpn/vpnphase1policy.html +64 -8
  297. nautobot/project-static/docs/user-guide/core-data-model/vpn/vpnphase2policy.html +64 -8
  298. nautobot/project-static/docs/user-guide/core-data-model/vpn/vpnprofile.html +64 -8
  299. nautobot/project-static/docs/user-guide/core-data-model/vpn/vpntunnel.html +64 -8
  300. nautobot/project-static/docs/user-guide/core-data-model/vpn/vpntunnelendpoint.html +64 -8
  301. nautobot/project-static/docs/user-guide/core-data-model/wireless/index.html +64 -8
  302. nautobot/project-static/docs/user-guide/core-data-model/wireless/radioprofile.html +64 -8
  303. nautobot/project-static/docs/user-guide/core-data-model/wireless/supporteddatarate.html +64 -8
  304. nautobot/project-static/docs/user-guide/core-data-model/wireless/wirelessnetwork.html +64 -8
  305. nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +64 -8
  306. nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +64 -8
  307. nautobot/project-static/docs/user-guide/feature-guides/data-compliance.html +64 -8
  308. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +64 -8
  309. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +64 -8
  310. nautobot/project-static/docs/user-guide/feature-guides/getting-started/index.html +64 -8
  311. nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +64 -8
  312. nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +64 -8
  313. nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +64 -8
  314. nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +64 -8
  315. nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +75 -12
  316. nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +85 -17
  317. nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +64 -8
  318. nautobot/project-static/docs/user-guide/feature-guides/graphql.html +64 -8
  319. nautobot/project-static/docs/user-guide/feature-guides/images/getting-started-nautobot-ui/12-add-tenant-dark.png +0 -0
  320. nautobot/project-static/docs/user-guide/feature-guides/images/getting-started-nautobot-ui/12-add-tenant-light.png +0 -0
  321. nautobot/project-static/docs/user-guide/feature-guides/images/getting-started-nautobot-ui/13-assign-tenant-to-device-dark.png +0 -0
  322. nautobot/project-static/docs/user-guide/feature-guides/images/getting-started-nautobot-ui/13-assign-tenant-to-device-light.png +0 -0
  323. nautobot/project-static/docs/user-guide/feature-guides/images/getting-started-nautobot-ui/14-assign-tenant-to-device-2-dark.png +0 -0
  324. nautobot/project-static/docs/user-guide/feature-guides/images/getting-started-nautobot-ui/14-assign-tenant-to-device-2-light.png +0 -0
  325. nautobot/project-static/docs/user-guide/feature-guides/images/getting-started-nautobot-ui/22-create-vlans-dark.png +0 -0
  326. nautobot/project-static/docs/user-guide/feature-guides/images/getting-started-nautobot-ui/22-create-vlans-light.png +0 -0
  327. nautobot/project-static/docs/user-guide/feature-guides/images/getting-started-nautobot-ui/23-create-vlans-2-dark.png +0 -0
  328. nautobot/project-static/docs/user-guide/feature-guides/images/getting-started-nautobot-ui/23-create-vlans-2-light.png +0 -0
  329. nautobot/project-static/docs/user-guide/feature-guides/images/getting-started-nautobot-ui/24-vlan-main-page-dark.png +0 -0
  330. nautobot/project-static/docs/user-guide/feature-guides/images/getting-started-nautobot-ui/24-vlan-main-page-light.png +0 -0
  331. nautobot/project-static/docs/user-guide/feature-guides/images/getting-started-nautobot-ui/25-add-vlan-to-interface-dark.png +0 -0
  332. nautobot/project-static/docs/user-guide/feature-guides/images/getting-started-nautobot-ui/25-add-vlan-to-interface-light.png +0 -0
  333. nautobot/project-static/docs/user-guide/feature-guides/images/getting-started-nautobot-ui/26-add-vlan-to-interface-2-dark.png +0 -0
  334. nautobot/project-static/docs/user-guide/feature-guides/images/getting-started-nautobot-ui/26-add-vlan-to-interface-2-light.png +0 -0
  335. nautobot/project-static/docs/user-guide/feature-guides/ip-address-merge-tool.html +64 -8
  336. nautobot/project-static/docs/user-guide/feature-guides/load-balancers.html +64 -8
  337. nautobot/project-static/docs/user-guide/feature-guides/relationships.html +64 -8
  338. nautobot/project-static/docs/user-guide/feature-guides/software-image-files-and-versions.html +64 -8
  339. nautobot/project-static/docs/user-guide/feature-guides/wireless-networks-and-controllers.html +64 -8
  340. nautobot/project-static/docs/user-guide/index.html +64 -8
  341. nautobot/project-static/docs/user-guide/platform-functionality/approval-workflow.html +64 -8
  342. nautobot/project-static/docs/user-guide/platform-functionality/change-logging.html +64 -8
  343. nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +64 -8
  344. nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +64 -8
  345. nautobot/project-static/docs/user-guide/platform-functionality/customlink.html +64 -8
  346. nautobot/project-static/docs/user-guide/platform-functionality/data-validation.html +64 -8
  347. nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +64 -8
  348. nautobot/project-static/docs/user-guide/platform-functionality/echarts.html +64 -8
  349. nautobot/project-static/docs/user-guide/platform-functionality/events.html +64 -8
  350. nautobot/project-static/docs/user-guide/platform-functionality/exporttemplate.html +64 -8
  351. nautobot/project-static/docs/user-guide/platform-functionality/externalintegration.html +64 -8
  352. nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +64 -8
  353. nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +64 -8
  354. nautobot/project-static/docs/user-guide/platform-functionality/graphqlquery.html +64 -8
  355. nautobot/project-static/docs/user-guide/platform-functionality/imageattachment.html +64 -8
  356. nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +64 -8
  357. nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +64 -8
  358. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +64 -8
  359. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +64 -8
  360. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobqueue.html +64 -8
  361. nautobot/project-static/docs/user-guide/platform-functionality/jobs/kubernetes-job-support.html +64 -8
  362. nautobot/project-static/docs/user-guide/platform-functionality/jobs/managing-jobs.html +64 -8
  363. nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +64 -8
  364. nautobot/project-static/docs/user-guide/platform-functionality/napalm.html +64 -8
  365. nautobot/project-static/docs/user-guide/platform-functionality/note.html +64 -8
  366. nautobot/project-static/docs/user-guide/platform-functionality/objectmetadata.html +64 -8
  367. nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +64 -8
  368. nautobot/project-static/docs/user-guide/platform-functionality/rendering-jinja-templates.html +64 -8
  369. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +64 -8
  370. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +116 -34
  371. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +64 -8
  372. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/ui-related-endpoints.html +64 -8
  373. nautobot/project-static/docs/user-guide/platform-functionality/role.html +64 -8
  374. nautobot/project-static/docs/user-guide/platform-functionality/secret.html +64 -8
  375. nautobot/project-static/docs/user-guide/platform-functionality/staticgroupassociation.html +64 -8
  376. nautobot/project-static/docs/user-guide/platform-functionality/status.html +64 -8
  377. nautobot/project-static/docs/user-guide/platform-functionality/tag.html +64 -8
  378. nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +64 -8
  379. nautobot/project-static/docs/user-guide/platform-functionality/user-interface/configurablecolumns.html +64 -8
  380. nautobot/project-static/docs/user-guide/platform-functionality/user-interface/savedview.html +64 -8
  381. nautobot/project-static/docs/user-guide/platform-functionality/user-interface/search.html +64 -8
  382. nautobot/project-static/docs/user-guide/platform-functionality/users/groups.html +64 -8
  383. nautobot/project-static/docs/user-guide/platform-functionality/users/objectpermission.html +64 -8
  384. nautobot/project-static/docs/user-guide/platform-functionality/users/token.html +64 -8
  385. nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +64 -8
  386. nautobot/tenancy/tables.py +1 -1
  387. nautobot/ui/package-lock.json +36 -36
  388. nautobot/ui/package.json +3 -3
  389. nautobot/ui/src/scss/nautobot.scss +2 -1
  390. nautobot/users/models.py +33 -0
  391. nautobot/users/tests/test_models.py +83 -0
  392. {nautobot-3.0.3.dist-info → nautobot-3.0.5.dist-info}/METADATA +4 -4
  393. {nautobot-3.0.3.dist-info → nautobot-3.0.5.dist-info}/RECORD +397 -386
  394. nautobot/project-static/docs/user-guide/feature-guides/images/getting-started-nautobot-ui/12-add-tenant.png +0 -0
  395. nautobot/project-static/docs/user-guide/feature-guides/images/getting-started-nautobot-ui/13-assign-tenant-to-device.png +0 -0
  396. nautobot/project-static/docs/user-guide/feature-guides/images/getting-started-nautobot-ui/14-assign-tenant-to-device-2.png +0 -0
  397. nautobot/project-static/docs/user-guide/feature-guides/images/getting-started-nautobot-ui/22-create-vlans.png +0 -0
  398. nautobot/project-static/docs/user-guide/feature-guides/images/getting-started-nautobot-ui/23-create-vlans-2.png +0 -0
  399. nautobot/project-static/docs/user-guide/feature-guides/images/getting-started-nautobot-ui/24-vlan-main-page.png +0 -0
  400. nautobot/project-static/docs/user-guide/feature-guides/images/getting-started-nautobot-ui/25-add-vlan-to-interface.png +0 -0
  401. nautobot/project-static/docs/user-guide/feature-guides/images/getting-started-nautobot-ui/26-add-vlan-to-interface-2.png +0 -0
  402. {nautobot-3.0.3.dist-info → nautobot-3.0.5.dist-info}/LICENSE.txt +0 -0
  403. {nautobot-3.0.3.dist-info → nautobot-3.0.5.dist-info}/NOTICE +0 -0
  404. {nautobot-3.0.3.dist-info → nautobot-3.0.5.dist-info}/WHEEL +0 -0
  405. {nautobot-3.0.3.dist-info → nautobot-3.0.5.dist-info}/entry_points.txt +0 -0
nautobot/extras/tables.py CHANGED
@@ -374,11 +374,11 @@ class ApprovalWorkflowStageTable(BaseTable):
374
374
  actions_needed = tables.TemplateColumn(
375
375
  template_code="""
376
376
  {% if record.remaining_approvals == 1 %}
377
- {{ record.remaining_approvals }} more approval needed
377
+ {{ record.remaining_approvals }} more approval needed
378
378
  {% elif record.remaining_approvals == 0 %}
379
- <span class="text-secondary">&mdash;</span>
379
+ <span class="text-secondary">&mdash;</span>
380
380
  {% else %}
381
- {{ record.remaining_approvals }} more approvals needed
381
+ {{ record.remaining_approvals }} more approvals needed
382
382
  {% endif %}
383
383
  """,
384
384
  orderable=False,
@@ -10,5 +10,4 @@
10
10
  var job_result_id = "{{ result.pk }}";
11
11
 
12
12
  </script>
13
- <script src="{% versioned_static 'js/job_result.js' %}"
14
- onerror="window.location='{% url 'media_failure' %}?filename=js/job_result.js'"></script>
13
+ <script src="{% versioned_static 'js/job_result.js' %}" onerror="nb.media.handleFailure(this)"></script>
@@ -143,7 +143,9 @@
143
143
  <td>{% if value is None %}–{% else %}<code>{{ value }}</code>{% endif %}</td>
144
144
  </tr>
145
145
  {% empty %}
146
- <tr><td>—</td></tr>
146
+ <tr>
147
+ <td><span class="text-secondary">&mdash;</span></td>
148
+ </tr>
147
149
  {% endfor %}
148
150
  </table>
149
151
  </div>
@@ -2338,6 +2338,22 @@ class CustomFieldTableTest(TestCase):
2338
2338
  cf_multi_select.default = ["Foo", "Bar"]
2339
2339
  cf_multi_select.validated_save()
2340
2340
 
2341
+ # JSON custom field
2342
+ cf_json = CustomField(
2343
+ type=CustomFieldTypeChoices.TYPE_JSON,
2344
+ label="JSON Field",
2345
+ )
2346
+ cf_json.validated_save()
2347
+ cf_json.content_types.set([content_type])
2348
+
2349
+ # Markdown custom field
2350
+ cf_markdown = CustomField(
2351
+ type=CustomFieldTypeChoices.TYPE_MARKDOWN,
2352
+ label="Markdown Field",
2353
+ )
2354
+ cf_markdown.validated_save()
2355
+ cf_markdown.content_types.set([content_type])
2356
+
2341
2357
  statuses = Status.objects.get_for_model(Location)
2342
2358
 
2343
2359
  # Create a location
@@ -2346,7 +2362,7 @@ class CustomFieldTableTest(TestCase):
2346
2362
  name="Location Custom", status=statuses.first(), location_type=location_type
2347
2363
  )
2348
2364
 
2349
- # Assign custom field values for location 2
2365
+ # Assign custom field values for location
2350
2366
  self.location._custom_field_data = {
2351
2367
  cf_text.key: "bar",
2352
2368
  cf_integer.key: 456,
@@ -2355,16 +2371,39 @@ class CustomFieldTableTest(TestCase):
2355
2371
  cf_url.key: "http://example.com/2",
2356
2372
  cf_select.key: "Bar",
2357
2373
  cf_multi_select.key: ["Bar", "Baz"],
2374
+ cf_json.key: {"hello": "world"},
2375
+ cf_markdown.key: "## Heading",
2358
2376
  }
2359
2377
  self.location.validated_save()
2360
2378
 
2379
+ # Create a second location
2380
+ self.location_2 = Location.objects.create(
2381
+ name="Location Custom 2", status=statuses.first(), location_type=location_type
2382
+ )
2383
+
2384
+ # Assign custom field values for location 2
2385
+ self.location_2._custom_field_data = {
2386
+ cf_text.key: "<script></script>",
2387
+ cf_integer.key: 0,
2388
+ cf_boolean.key: False,
2389
+ cf_date.key: None,
2390
+ cf_url.key: "",
2391
+ cf_select.key: None,
2392
+ cf_multi_select.key: [],
2393
+ cf_json.key: {},
2394
+ cf_markdown.key: "",
2395
+ }
2396
+ self.location_2.validated_save()
2397
+
2398
+ self.maxDiff = None
2399
+
2361
2400
  def test_custom_field_table_render(self):
2362
- queryset = Location.objects.filter(name=self.location.name)
2401
+ queryset = Location.objects.filter(name__in=[self.location.name, self.location_2.name])
2363
2402
  location_table = LocationTable(queryset)
2364
2403
 
2365
2404
  custom_column_expected = {
2366
2405
  "text_field": "bar",
2367
- "number_field": "456",
2406
+ "number_field": 456,
2368
2407
  "boolean_field": '<span class="text-success"><i class="mdi mdi-check-bold" title="Yes"></i></span>',
2369
2408
  "date_field": "2020-01-02",
2370
2409
  "url_field": '<a href="http://example.com/2">http://example.com/2</a>',
@@ -2372,18 +2411,45 @@ class CustomFieldTableTest(TestCase):
2372
2411
  "multi_choice_field": (
2373
2412
  '<span class="badge bg-secondary">Bar</span> <span class="badge bg-secondary">Baz</span>'
2374
2413
  ),
2414
+ "json_field": '<pre><code class="language-json">{\n&quot;hello&quot;: &quot;world&quot;\n}</code></pre>',
2415
+ "markdown_field": "<h2>Heading</h2>",
2375
2416
  }
2376
2417
 
2377
2418
  bound_row = location_table.rows[0]
2378
2419
 
2379
2420
  for col_name, col_expected_value in custom_column_expected.items():
2380
- internal_col_name = "cf_" + col_name
2381
- custom_column = location_table.base_columns.get(internal_col_name)
2382
- self.assertIsNotNone(custom_column, internal_col_name)
2383
- self.assertIsInstance(custom_column, CustomFieldColumn)
2421
+ with self.subTest(col_name=col_name, col_expected_value=col_expected_value):
2422
+ internal_col_name = "cf_" + col_name
2423
+ custom_column = location_table.base_columns.get(internal_col_name)
2424
+ self.assertIsNotNone(custom_column, internal_col_name)
2425
+ self.assertIsInstance(custom_column, CustomFieldColumn)
2426
+
2427
+ rendered_value = bound_row.get_cell(internal_col_name) # pylint: disable=no-member
2428
+ self.assertHTMLEqual(str(rendered_value), str(col_expected_value))
2429
+
2430
+ custom_column_expected_2 = {
2431
+ "text_field": "<script></script>",
2432
+ "number_field": 0,
2433
+ "boolean_field": '<span class="text-danger"><i class="mdi mdi-close-thick" title="No"></i></span>',
2434
+ "date_field": '<span class="text-secondary">&mdash;</span>',
2435
+ "url_field": '<span class="text-secondary">&mdash;</span>',
2436
+ "choice_field": '<span class="text-secondary">&mdash;</span>',
2437
+ "multi_choice_field": '<span class="text-secondary">&mdash;</span>',
2438
+ "json_field": '<pre><code class="language-json">{}</code></pre>',
2439
+ "markdown_field": '<span class="text-secondary">&mdash;</span>',
2440
+ }
2441
+
2442
+ bound_row = location_table.rows[1]
2443
+
2444
+ for col_name, col_expected_value in custom_column_expected_2.items():
2445
+ with self.subTest(col_name=col_name, col_expected_value=col_expected_value):
2446
+ internal_col_name = "cf_" + col_name
2447
+ custom_column = location_table.base_columns.get(internal_col_name)
2448
+ self.assertIsNotNone(custom_column, internal_col_name)
2449
+ self.assertIsInstance(custom_column, CustomFieldColumn)
2384
2450
 
2385
- rendered_value = bound_row.get_cell(internal_col_name) # pylint: disable=no-member
2386
- self.assertEqual(rendered_value, col_expected_value)
2451
+ rendered_value = bound_row.get_cell(internal_col_name) # pylint: disable=no-member
2452
+ self.assertHTMLEqual(str(rendered_value), str(col_expected_value))
2387
2453
 
2388
2454
 
2389
2455
  class CustomFieldFilterFormTest(TestCase):
@@ -2,22 +2,26 @@ from unittest import mock
2
2
  import uuid
3
3
 
4
4
  from django.core.cache import cache
5
+ from django.test import override_settings
5
6
 
6
7
  from nautobot.core.testing import TestCase
7
8
  from nautobot.dcim.models import Cable, Device, PowerPort
8
9
  from nautobot.extras.choices import JobQueueTypeChoices
9
- from nautobot.extras.models import JobQueue
10
+ from nautobot.extras.models import JobQueue, JobResult
10
11
  from nautobot.extras.registry import registry
11
12
  from nautobot.extras.utils import (
12
13
  get_base_template,
13
14
  get_celery_queues,
14
15
  get_worker_count,
15
16
  populate_model_features_registry,
17
+ run_kubernetes_job_and_return_job_result,
16
18
  )
17
19
  from nautobot.users.models import Token
18
20
 
19
21
 
20
22
  class UtilsTestCase(TestCase):
23
+ databases = ("default", "job_logs")
24
+
21
25
  def test_get_base_template(self):
22
26
  with self.subTest("explicitly specified base_template always wins"):
23
27
  self.assertEqual(get_base_template("dcim/device/base.html", Device), "dcim/device/base.html")
@@ -125,3 +129,114 @@ class UtilsTestCase(TestCase):
125
129
  original_custom_fields_registry,
126
130
  "Registry should be restored to original state",
127
131
  )
132
+
133
+ @override_settings(
134
+ KUBERNETES_JOB_POD_NAME="test-pod",
135
+ KUBERNETES_JOB_POD_NAMESPACE="test-namespace",
136
+ KUBERNETES_JOB_MANIFEST={
137
+ "metadata": {"name": "test-job"},
138
+ "spec": {
139
+ "template": {
140
+ "spec": {
141
+ "containers": [
142
+ {
143
+ "command": [],
144
+ }
145
+ ]
146
+ }
147
+ }
148
+ },
149
+ },
150
+ KUBERNETES_SSL_CA_CERT_PATH="/path/to/ca.crt",
151
+ KUBERNETES_TOKEN_PATH="/path/to/token", # noqa: S106
152
+ KUBERNETES_DEFAULT_SERVICE_ADDRESS="https://kubernetes.default.svc",
153
+ )
154
+ @mock.patch("nautobot.extras.utils.transaction.on_commit")
155
+ @mock.patch("builtins.open", new_callable=mock.mock_open, read_data="test-token\n")
156
+ @mock.patch("nautobot.extras.utils.kubernetes.client.BatchV1Api")
157
+ @mock.patch("nautobot.extras.utils.kubernetes.client.ApiClient")
158
+ @mock.patch("nautobot.extras.utils.kubernetes.client.Configuration")
159
+ def test_run_kubernetes_job_and_return_job_result(
160
+ self,
161
+ mock_configuration,
162
+ mock_api_client,
163
+ mock_batch_api,
164
+ mock_open,
165
+ mock_on_commit,
166
+ ):
167
+ """Test run_kubernetes_job_and_return_job_result function."""
168
+ # Setup test data
169
+ job_result = JobResult.objects.create(
170
+ name="Test Job",
171
+ user=self.user,
172
+ )
173
+ # Mock the log method to avoid database writes during test
174
+ job_result.log = mock.Mock()
175
+ job_kwargs = '{"key": "value"}'
176
+
177
+ # Setup kubernetes client mocks
178
+ mock_config_instance = mock.MagicMock()
179
+ mock_config_instance.api_key_prefix = {}
180
+ mock_config_instance.api_key = {}
181
+ mock_configuration.return_value = mock_config_instance
182
+
183
+ mock_api_client_instance = mock.Mock()
184
+ mock_api_client.return_value.__enter__.return_value = mock_api_client_instance
185
+ mock_api_client.return_value.__exit__.return_value = None
186
+
187
+ mock_api_instance = mock.Mock()
188
+ mock_batch_api.return_value = mock_api_instance
189
+
190
+ # Capture the callback passed to transaction.on_commit
191
+ commit_callback = None
192
+
193
+ def capture_callback(callback):
194
+ nonlocal commit_callback
195
+ commit_callback = callback
196
+ # Execute immediately for testing
197
+ callback()
198
+
199
+ mock_on_commit.side_effect = capture_callback
200
+
201
+ # Execute the function
202
+ result = run_kubernetes_job_and_return_job_result(job_result, job_kwargs)
203
+
204
+ # Verify job_result was updated and saved
205
+ job_result.refresh_from_db()
206
+ self.assertEqual(job_result.task_kwargs, job_kwargs)
207
+ self.assertEqual(result, job_result)
208
+
209
+ # Verify transaction.on_commit was called
210
+ mock_on_commit.assert_called_once()
211
+ self.assertIsNotNone(commit_callback)
212
+
213
+ # Verify kubernetes configuration was set up correctly
214
+ mock_configuration.assert_called_once()
215
+ self.assertEqual(mock_config_instance.host, "https://kubernetes.default.svc")
216
+ self.assertEqual(mock_config_instance.ssl_ca_cert, "/path/to/ca.crt")
217
+ self.assertEqual(mock_config_instance.api_key_prefix["authorization"], "Bearer")
218
+ self.assertEqual(mock_config_instance.api_key["authorization"], "test-token")
219
+
220
+ # Verify ApiClient was used as context manager
221
+ mock_api_client.assert_called_once_with(mock_config_instance)
222
+
223
+ # Verify BatchV1Api was created with the api_client_instance
224
+ mock_batch_api.assert_called_once_with(mock_api_client_instance)
225
+
226
+ # Verify the pod manifest was modified correctly
227
+ mock_api_instance.create_namespaced_job.assert_called_once()
228
+ create_call = mock_api_instance.create_namespaced_job.call_args
229
+ body = create_call[1]["body"]
230
+ self.assertEqual(body["metadata"]["name"], f"nautobot-job-{job_result.pk}")
231
+ self.assertEqual(
232
+ body["spec"]["template"]["spec"]["containers"][0]["command"],
233
+ ["nautobot-server", "runjob_with_job_result", str(job_result.pk)],
234
+ )
235
+ self.assertEqual(create_call[1]["namespace"], "test-namespace")
236
+
237
+ # Verify token file was opened
238
+ mock_open.assert_called_once_with("/path/to/token", "r", encoding="utf-8")
239
+
240
+ # Verify job_result.log was called (checking for log messages)
241
+ self.assertEqual(job_result.log.call_count, 1)
242
+ self.assertIn("Creating job pod", str(job_result.log.call_args_list[0]))
nautobot/extras/utils.py CHANGED
@@ -668,7 +668,7 @@ def refresh_job_model_from_job_class(job_model_class, job_class, job_queue_class
668
668
  return (job_model, created)
669
669
 
670
670
 
671
- def run_kubernetes_job_and_return_job_result(job_queue, job_result, job_kwargs):
671
+ def run_kubernetes_job_and_return_job_result(job_result, job_kwargs):
672
672
  """
673
673
  Pass the job to a kubernetes pod and execute it there.
674
674
  """
@@ -678,17 +678,6 @@ def run_kubernetes_job_and_return_job_result(job_queue, job_result, job_kwargs):
678
678
  pod_ssl_ca_cert = settings.KUBERNETES_SSL_CA_CERT_PATH
679
679
  pod_token = settings.KUBERNETES_TOKEN_PATH
680
680
 
681
- configuration = kubernetes.client.Configuration()
682
- configuration.host = settings.KUBERNETES_DEFAULT_SERVICE_ADDRESS
683
- configuration.ssl_ca_cert = pod_ssl_ca_cert
684
- with open(pod_token, "r") as token_file:
685
- token = token_file.read().strip()
686
- # configure API Key authorization: BearerToken
687
- configuration.api_key_prefix["authorization"] = "Bearer"
688
- configuration.api_key["authorization"] = token
689
- with kubernetes.client.ApiClient(configuration) as api_client:
690
- api_instance = kubernetes.client.BatchV1Api(api_client)
691
-
692
681
  job_result.task_kwargs = job_kwargs
693
682
  job_result.save()
694
683
  pod_manifest["metadata"]["name"] = "nautobot-job-" + str(job_result.pk)
@@ -697,10 +686,23 @@ def run_kubernetes_job_and_return_job_result(job_queue, job_result, job_kwargs):
697
686
  "runjob_with_job_result",
698
687
  f"{job_result.pk}",
699
688
  ]
700
- job_result.log(f"Creating job pod {pod_name} in namespace {pod_namespace}")
701
- api_instance.create_namespaced_job(body=pod_manifest, namespace=pod_namespace)
702
- job_result.log(f"Reading job pod {pod_name} in namespace {pod_namespace}")
703
- api_instance.read_namespaced_job(name="nautobot-job-" + str(job_result.pk), namespace=pod_namespace)
689
+
690
+ def create_kubernetes_job():
691
+ """Create and read the Kubernetes job after the transaction commits."""
692
+ configuration = kubernetes.client.Configuration()
693
+ configuration.host = settings.KUBERNETES_DEFAULT_SERVICE_ADDRESS
694
+ configuration.ssl_ca_cert = pod_ssl_ca_cert
695
+ with open(pod_token, "r", encoding="utf-8") as token_file:
696
+ token = token_file.read().strip()
697
+ # configure API Key authorization: BearerToken
698
+ configuration.api_key_prefix["authorization"] = "Bearer"
699
+ configuration.api_key["authorization"] = token
700
+ with kubernetes.client.ApiClient(configuration) as api_client:
701
+ api_instance = kubernetes.client.BatchV1Api(api_client)
702
+ job_result.log(f"Creating job pod {pod_name} in namespace {pod_namespace}")
703
+ api_instance.create_namespaced_job(body=pod_manifest, namespace=pod_namespace)
704
+
705
+ transaction.on_commit(create_kubernetes_job)
704
706
  return job_result
705
707
 
706
708
 
nautobot/extras/views.py CHANGED
@@ -820,7 +820,6 @@ class ComputedFieldUIViewSet(NautobotUIViewSet):
820
820
  serializer_class = serializers.ComputedFieldSerializer
821
821
  table_class = tables.ComputedFieldTable
822
822
  queryset = ComputedField.objects.all()
823
- action_buttons = ("add",)
824
823
  object_detail_content = object_detail.ObjectDetailContent(
825
824
  panels=(
826
825
  object_detail.ObjectFieldsPanel(
@@ -1246,7 +1245,6 @@ class CustomFieldUIViewSet(NautobotUIViewSet):
1246
1245
  form_class = forms.CustomFieldForm
1247
1246
  table_class = tables.CustomFieldTable
1248
1247
  template_name = "extras/customfield_update.html"
1249
- action_buttons = ("add",)
1250
1248
 
1251
1249
  class CustomFieldObjectFieldsPanel(object_detail.ObjectFieldsPanel):
1252
1250
  def render_value(self, key, value, context):
@@ -1392,7 +1390,6 @@ class DynamicGroupUIViewSet(NautobotUIViewSet):
1392
1390
  queryset = DynamicGroup.objects.all()
1393
1391
  serializer_class = serializers.DynamicGroupSerializer
1394
1392
  table_class = tables.DynamicGroupTable
1395
- action_buttons = ("add",)
1396
1393
 
1397
1394
  def get_extra_context(self, request, instance):
1398
1395
  context = super().get_extra_context(request, instance)
@@ -1896,23 +1893,14 @@ class GitRepositoryUIViewSet(NautobotUIViewSet):
1896
1893
  #
1897
1894
 
1898
1895
 
1899
- class GraphQLQueryUIViewSet(
1900
- ObjectDetailViewMixin,
1901
- ObjectListViewMixin,
1902
- ObjectEditViewMixin,
1903
- ObjectDestroyViewMixin,
1904
- ObjectBulkDestroyViewMixin,
1905
- ObjectChangeLogViewMixin,
1906
- ObjectDataComplianceViewMixin,
1907
- ObjectNotesViewMixin,
1908
- ):
1896
+ class GraphQLQueryUIViewSet(NautobotUIViewSet):
1909
1897
  filterset_form_class = forms.GraphQLQueryFilterForm
1910
1898
  queryset = GraphQLQuery.objects.all()
1911
1899
  form_class = forms.GraphQLQueryForm
1912
1900
  filterset_class = filters.GraphQLQueryFilterSet
1913
1901
  serializer_class = serializers.GraphQLQuerySerializer
1914
1902
  table_class = tables.GraphQLQueryTable
1915
- action_buttons = ("add",)
1903
+ action_buttons = ("add", "export", "import")
1916
1904
 
1917
1905
  object_detail_content = object_detail.ObjectDetailContent(
1918
1906
  panels=(
nautobot/ipam/apps.py CHANGED
@@ -9,6 +9,7 @@ class IPAMConfig(NautobotConfig):
9
9
  "ipaddress",
10
10
  "namespace",
11
11
  "prefix",
12
+ "service",
12
13
  "vlan",
13
14
  "vrf",
14
15
  ]
nautobot/ipam/filters.py CHANGED
@@ -1,4 +1,5 @@
1
1
  import contextlib
2
+ import ipaddress
2
3
  import uuid
3
4
 
4
5
  from django.core.exceptions import ValidationError
@@ -221,6 +222,10 @@ class PrefixFilterSet(
221
222
  method="filter_prefix",
222
223
  label="Prefix",
223
224
  )
225
+ prefix_exact = MultiValueCharFilter(
226
+ method="filter_prefix_exact",
227
+ label="Prefix (exact, strict)",
228
+ )
224
229
  within = MultiValueCharFilter(
225
230
  method="search_within",
226
231
  label="Within prefix",
@@ -327,6 +332,20 @@ class PrefixFilterSet(
327
332
  return queryset.net_equals(*prefixes)
328
333
  return queryset.none()
329
334
 
335
+ def filter_prefix_exact(self, queryset, name, value):
336
+ """
337
+ Strict version of `prefix` filter.
338
+ Rejects prefixes with host bits set (e.g. 10.32.0.34/28 vs 10.32.0.32/28).
339
+ """
340
+ prefixes = self._strip_values(value)
341
+
342
+ for prefix in prefixes:
343
+ try:
344
+ ipaddress.ip_network(prefix, strict=True)
345
+ except ValueError:
346
+ raise ValidationError(f"Invalid prefix_exact value as it is not a subnet boundary: {prefix}.")
347
+ return self.filter_prefix(queryset, name, value)
348
+
330
349
  def search_within(self, queryset, name, value):
331
350
  prefixes = self._strip_values(value)
332
351
  with contextlib.suppress(netaddr.AddrFormatError, ValueError):
@@ -417,6 +436,10 @@ class IPAddressFilterSet(
417
436
  method="search_by_prefix",
418
437
  label="Contained in prefix",
419
438
  )
439
+ prefix_exact = MultiValueCharFilter(
440
+ method="search_by_prefix_exact",
441
+ label="Prefix (exact, strict)",
442
+ )
420
443
  address = MultiValueCharFilter(
421
444
  method="filter_address",
422
445
  label="Address",
@@ -512,17 +535,49 @@ class IPAddressFilterSet(
512
535
  params = self.generate_query__has_interface_assignments(value)
513
536
  return queryset.filter(params)
514
537
 
515
- def search_by_prefix(self, queryset, name, value):
538
+ def _strip_prefix_values(self, values):
539
+ """Normalize inputs: strip whitespace + resolve UUIDs to Prefix.prefix."""
516
540
  prefixes = []
517
- for prefix in value:
541
+ for prefix in values:
518
542
  prefix = prefix.strip()
543
+ if not prefix:
544
+ continue
519
545
  if is_uuid(prefix):
520
546
  prefixes.append(Prefix.objects.get(pk=prefix).prefix)
521
- elif prefix:
547
+ else:
522
548
  prefixes.append(prefix)
549
+ return prefixes
523
550
 
551
+ def search_by_prefix(self, queryset, name, value):
552
+ prefixes = self._strip_prefix_values(value)
524
553
  return queryset.net_host_contained(*prefixes)
525
554
 
555
+ def search_by_prefix_exact(self, queryset, name, value):
556
+ """
557
+ Strict version of `prefix` filter.
558
+ Rejects prefixes with host bits set (e.g. 10.32.0.34/28 vs 10.32.0.32/28).
559
+ """
560
+ prefixes = self._strip_prefix_values(value)
561
+
562
+ # Validate network is on CIDR boundary
563
+ for prefix in prefixes:
564
+ if "/" not in str(prefix):
565
+ # If someone passes a host-only string here, treat it as invalid for "prefix_exact".
566
+ raise ValidationError(f"Invalid prefix_exact value (missing mask): {prefix}")
567
+
568
+ with contextlib.suppress(netaddr.AddrFormatError, ValueError):
569
+ ip_network = netaddr.IPNetwork(str(prefix)).cidr
570
+ # cidr will always a proper network subnet; compare against original input
571
+ if str(ip_network) != str(prefix):
572
+ raise ValidationError(
573
+ f"Invalid prefix_exact value as it is not a subnet boundary: {prefix}, did you mean {ip_network}?"
574
+ )
575
+ continue
576
+
577
+ # Defensive programming in case there is logic missed above
578
+ raise ValidationError(f"Invalid prefix_exact value as it is not a subnet boundary: {prefix}.")
579
+ return self.search_by_prefix(queryset, name, prefixes)
580
+
526
581
  def filter_address(self, queryset, name, value):
527
582
  try:
528
583
  return queryset.net_in(value)
nautobot/ipam/tables.py CHANGED
@@ -40,7 +40,11 @@ AVAILABLE_LABEL = mark_safe('<span class="badge bg-success">Available</span>')
40
40
 
41
41
  UTILIZATION_GRAPH = """
42
42
  {% load helpers %}
43
- {% if record.present_in_database %}{% utilization_graph record.get_utilization %}{% else %}&mdash;{% endif %}
43
+ {% if record.present_in_database %}
44
+ {% utilization_graph record.get_utilization %}
45
+ {% else %}
46
+ <span class="text-secondary">&mdash;</span>
47
+ {% endif %}
44
48
  """
45
49
 
46
50
 
@@ -73,7 +77,7 @@ PREFIX_ROLE_LINK = """
73
77
  {% if record.role %}
74
78
  <a href="{% url 'ipam:prefix_list' %}?role={{ record.role.name }}">{{ record.role }}</a>
75
79
  {% else %}
76
- &mdash;
80
+ <span class="text-secondary">&mdash;</span>
77
81
  {% endif %}
78
82
  """
79
83
 
@@ -157,7 +161,7 @@ VRF_TARGETS = """
157
161
  {% for rt in value.all %}
158
162
  <a href="{{ rt.get_absolute_url }}">{{ rt }}</a>{% if not forloop.last %}<br />{% endif %}
159
163
  {% empty %}
160
- &mdash;
164
+ <span class="text-secondary">&mdash;</span>
161
165
  {% endfor %}
162
166
  """
163
167
 
@@ -179,7 +183,7 @@ VLAN_PREFIXES = """
179
183
  {% for prefix in record.prefixes.all %}
180
184
  <a href="{% url 'ipam:prefix' pk=prefix.pk %}">{{ prefix }}</a>{% if not forloop.last %}<br />{% endif %}
181
185
  {% empty %}
182
- &mdash;
186
+ <span class="text-secondary">&mdash;</span>
183
187
  {% endfor %}
184
188
  """
185
189
 
@@ -1,4 +1,5 @@
1
1
  from django.contrib.contenttypes.models import ContentType
2
+ from django.core.exceptions import ValidationError
2
3
  from django.db.models import Q
3
4
 
4
5
  from nautobot.core.testing import FilterTestCases, TestCase
@@ -242,6 +243,32 @@ class PrefixTestCase(FilterTestCases.FilterTestCase, FilterTestCases.TenancyFilt
242
243
  ancestors = [ancestor.id for ancestor in prefixes[2].ancestors()]
243
244
  self.assertQuerysetEqualAndNotEmpty(filterset.qs, self.queryset.filter(id__in=ancestors))
244
245
 
246
+ def test_prefix(self):
247
+ Prefix.objects.create(
248
+ prefix="192.0.2.0/29",
249
+ type=PrefixTypeChoices.TYPE_POOL,
250
+ namespace=Namespace.objects.first(),
251
+ status=Status.objects.get_for_model(Prefix).first(),
252
+ )
253
+ params = {"prefix": "192.0.2.0/29"}
254
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
255
+ params = {"prefix": "192.0.2.1/29"}
256
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
257
+
258
+ def test_prefix_exact(self):
259
+ Prefix.objects.create(
260
+ prefix="192.0.2.0/29",
261
+ type=PrefixTypeChoices.TYPE_POOL,
262
+ namespace=Namespace.objects.first(),
263
+ status=Status.objects.get_for_model(Prefix).first(),
264
+ )
265
+ params = {"prefix_exact": "192.0.2.0/29"}
266
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
267
+ params = {"prefix_exact": "192.0.2.1/29"}
268
+ with self.assertRaises(ValidationError) as exc:
269
+ self.filterset(params, self.queryset).qs # pylint: disable=expression-not-assigned
270
+ self.assertTrue("Invalid prefix_exact value" in str(exc.exception))
271
+
245
272
 
246
273
  class PrefixLocationAssignmentTestCase(FilterTestCases.FilterTestCase):
247
274
  queryset = PrefixLocationAssignment.objects.all()
@@ -807,6 +834,34 @@ class IPAddressTestCase(FilterTestCases.FilterTestCase, FilterTestCases.TenancyF
807
834
  self.filterset(params, self.queryset).qs, self.queryset.net_host_contained(ipv4_parent, ipv6_parent)
808
835
  )
809
836
 
837
+ def test_prefix_exact(self):
838
+ ipv4_parent = self.queryset.filter(ip_version=4).first().address.supernet()[-1]
839
+ ipv6_parent = self.queryset.filter(ip_version=6).first().address.supernet()[-1]
840
+
841
+ params = {"prefix_exact": [str(ipv4_parent), str(ipv6_parent)]}
842
+ self.assertQuerysetEqualAndNotEmpty(
843
+ self.filterset(params, self.queryset).qs, self.queryset.net_host_contained(ipv4_parent, ipv6_parent)
844
+ )
845
+
846
+ # Get the first usable IP address in the subnet (not the network address)
847
+ ipv4_parent = self.queryset.filter(ip_version=4).first().address
848
+ ipv6_parent = self.queryset.filter(ip_version=6).first().address
849
+
850
+ params = {"prefix_exact": [str(ipv4_parent)]}
851
+ with self.assertRaises(ValidationError) as exc:
852
+ self.filterset(params, self.queryset).qs # pylint: disable=expression-not-assigned
853
+ self.assertTrue("Invalid prefix_exact value" in str(exc.exception))
854
+
855
+ params = {"prefix_exact": [str(ipv6_parent)]}
856
+ with self.assertRaises(ValidationError) as exc:
857
+ self.filterset(params, self.queryset).qs # pylint: disable=expression-not-assigned
858
+ self.assertTrue("Invalid prefix_exact value" in str(exc.exception))
859
+
860
+ params = {"prefix_exact": ["10.1.1.1"]}
861
+ with self.assertRaises(ValidationError) as exc:
862
+ self.filterset(params, self.queryset).qs # pylint: disable=expression-not-assigned
863
+ self.assertTrue("Invalid prefix_exact value (missing mask)" in str(exc.exception))
864
+
810
865
  def test_filter_address(self):
811
866
  """Check IPv4 and IPv6, with and without a mask"""
812
867
  ipv4_addresses = self.queryset.filter(ip_version=4)[:2]