nautobot 2.4.12__py3-none-any.whl → 2.4.14__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 (366) hide show
  1. nautobot/core/graphql/generators.py +8 -0
  2. nautobot/core/graphql/schema.py +30 -30
  3. nautobot/core/management/commands/migrate.py +90 -1
  4. nautobot/core/settings.yaml +3 -3
  5. nautobot/core/tables.py +4 -4
  6. nautobot/core/templates/inc/footer.html +4 -4
  7. nautobot/core/testing/api.py +7 -0
  8. nautobot/core/views/utils.py +1 -1
  9. nautobot/dcim/choices.py +2 -0
  10. nautobot/dcim/constants.py +0 -16
  11. nautobot/dcim/factory.py +1 -1
  12. nautobot/dcim/migrations/0071_alter_consoleport_options_and_more.py +42 -0
  13. nautobot/dcim/models/device_components.py +10 -2
  14. nautobot/dcim/models/devices.py +18 -0
  15. nautobot/dcim/templates/dcim/device.html +1 -34
  16. nautobot/dcim/templates/dcim/rack.html +2 -318
  17. nautobot/dcim/templates/dcim/rack_edit.html +2 -47
  18. nautobot/dcim/templates/dcim/rack_elevation_list.html +4 -1
  19. nautobot/dcim/templates/dcim/rack_retrieve.html +318 -0
  20. nautobot/dcim/templates/dcim/rack_update.html +47 -0
  21. nautobot/dcim/tests/test_models.py +1 -0
  22. nautobot/dcim/urls.py +3 -28
  23. nautobot/dcim/utils.py +4 -30
  24. nautobot/dcim/views.py +73 -71
  25. nautobot/extras/choices.py +12 -4
  26. nautobot/extras/filters/mixins.py +8 -6
  27. nautobot/extras/forms/forms.py +9 -0
  28. nautobot/extras/forms/mixins.py +4 -2
  29. nautobot/extras/migrations/0062_collect_roles_from_related_apps_roles.py +30 -7
  30. nautobot/extras/migrations/0124_add_joblogentry_index.py +16 -0
  31. nautobot/extras/models/customfields.py +52 -3
  32. nautobot/extras/models/jobs.py +6 -0
  33. nautobot/extras/models/relationships.py +55 -6
  34. nautobot/extras/templates/extras/graphqlquery.html +2 -97
  35. nautobot/extras/templates/extras/graphqlquery_list.html +1 -0
  36. nautobot/extras/templates/extras/graphqlquery_retrieve.html +97 -0
  37. nautobot/extras/templates/extras/secretsgroup.html +2 -29
  38. nautobot/extras/templates/extras/secretsgroup_edit.html +2 -82
  39. nautobot/extras/templates/extras/secretsgroup_retrieve.html +29 -0
  40. nautobot/extras/templates/extras/secretsgroup_update.html +82 -0
  41. nautobot/extras/tests/test_customfields.py +115 -7
  42. nautobot/extras/tests/test_relationships.py +7 -1
  43. nautobot/extras/tests/test_views.py +113 -1
  44. nautobot/extras/urls.py +2 -51
  45. nautobot/extras/utils.py +4 -1
  46. nautobot/extras/views.py +42 -135
  47. nautobot/ipam/api/views.py +69 -6
  48. nautobot/ipam/migrations/0052_alter_ipaddress_index_together_and_more.py +28 -0
  49. nautobot/ipam/models.py +13 -1
  50. nautobot/ipam/tests/test_api.py +351 -3
  51. nautobot/ipam/utils/testing.py +76 -29
  52. nautobot/project-static/docs/404.html +11 -34
  53. nautobot/project-static/docs/apps/index.html +11 -34
  54. nautobot/project-static/docs/apps/nautobot-apps.html +11 -34
  55. nautobot/project-static/docs/assets/javascripts/{bundle.56ea9cef.min.js → bundle.50899def.min.js} +2 -2
  56. nautobot/project-static/docs/assets/javascripts/{bundle.56ea9cef.min.js.map → bundle.50899def.min.js.map} +2 -2
  57. nautobot/project-static/docs/assets/stylesheets/{main.342714a4.min.css → main.7e37652d.min.css} +1 -1
  58. nautobot/project-static/docs/assets/stylesheets/{main.342714a4.min.css.map → main.7e37652d.min.css.map} +1 -1
  59. nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +11 -34
  60. nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +11 -34
  61. nautobot/project-static/docs/code-reference/nautobot/apps/api.html +11 -34
  62. nautobot/project-static/docs/code-reference/nautobot/apps/change_logging.html +11 -34
  63. nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +11 -34
  64. nautobot/project-static/docs/code-reference/nautobot/apps/config.html +11 -34
  65. nautobot/project-static/docs/code-reference/nautobot/apps/constants.html +11 -34
  66. nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +11 -34
  67. nautobot/project-static/docs/code-reference/nautobot/apps/events.html +11 -34
  68. nautobot/project-static/docs/code-reference/nautobot/apps/exceptions.html +11 -34
  69. nautobot/project-static/docs/code-reference/nautobot/apps/factory.html +11 -34
  70. nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +11 -34
  71. nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +11 -34
  72. nautobot/project-static/docs/code-reference/nautobot/apps/graphql.html +11 -34
  73. nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +11 -34
  74. nautobot/project-static/docs/code-reference/nautobot/apps/models.html +11 -34
  75. nautobot/project-static/docs/code-reference/nautobot/apps/querysets.html +11 -34
  76. nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +11 -34
  77. nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +11 -34
  78. nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +11 -34
  79. nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +11 -34
  80. nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +11 -34
  81. nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +11 -34
  82. nautobot/project-static/docs/code-reference/nautobot/apps/views.html +13 -34
  83. nautobot/project-static/docs/development/apps/api/configuration-view.html +11 -34
  84. nautobot/project-static/docs/development/apps/api/database-backend-config.html +11 -34
  85. nautobot/project-static/docs/development/apps/api/models/django-admin.html +11 -34
  86. nautobot/project-static/docs/development/apps/api/models/global-search.html +11 -34
  87. nautobot/project-static/docs/development/apps/api/models/graphql.html +11 -34
  88. nautobot/project-static/docs/development/apps/api/models/index.html +11 -34
  89. nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +11 -34
  90. nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +11 -34
  91. nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +11 -34
  92. nautobot/project-static/docs/development/apps/api/platform-features/git-repository-content.html +11 -34
  93. nautobot/project-static/docs/development/apps/api/platform-features/index.html +11 -34
  94. nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +11 -34
  95. nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +11 -34
  96. nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +11 -34
  97. nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +11 -34
  98. nautobot/project-static/docs/development/apps/api/platform-features/table-extensions.html +11 -34
  99. nautobot/project-static/docs/development/apps/api/platform-features/uniquely-identify-objects.html +11 -34
  100. nautobot/project-static/docs/development/apps/api/prometheus.html +11 -34
  101. nautobot/project-static/docs/development/apps/api/setup.html +11 -34
  102. nautobot/project-static/docs/development/apps/api/testing.html +11 -34
  103. nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +11 -34
  104. nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +11 -34
  105. nautobot/project-static/docs/development/apps/api/ui-extensions/index.html +11 -34
  106. nautobot/project-static/docs/development/apps/api/ui-extensions/navigation.html +11 -34
  107. nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +11 -34
  108. nautobot/project-static/docs/development/apps/api/views/base-template.html +11 -34
  109. nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +11 -34
  110. nautobot/project-static/docs/development/apps/api/views/django-generic-views.html +11 -34
  111. nautobot/project-static/docs/development/apps/api/views/help-documentation.html +11 -34
  112. nautobot/project-static/docs/development/apps/api/views/index.html +11 -34
  113. nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +11 -34
  114. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +11 -34
  115. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +11 -34
  116. nautobot/project-static/docs/development/apps/api/views/notes.html +11 -34
  117. nautobot/project-static/docs/development/apps/api/views/rest-api.html +11 -34
  118. nautobot/project-static/docs/development/apps/api/views/urls.html +11 -34
  119. nautobot/project-static/docs/development/apps/index.html +11 -34
  120. nautobot/project-static/docs/development/apps/migration/code-updates.html +11 -34
  121. nautobot/project-static/docs/development/apps/migration/dependency-updates.html +11 -34
  122. nautobot/project-static/docs/development/apps/migration/from-v1.html +11 -34
  123. nautobot/project-static/docs/development/apps/migration/model-updates/dcim.html +11 -34
  124. nautobot/project-static/docs/development/apps/migration/model-updates/extras.html +11 -34
  125. nautobot/project-static/docs/development/apps/migration/model-updates/global.html +11 -34
  126. nautobot/project-static/docs/development/apps/migration/model-updates/ipam.html +11 -34
  127. nautobot/project-static/docs/development/apps/migration/ui-component-framework/best-practices.html +11 -34
  128. nautobot/project-static/docs/development/apps/migration/ui-component-framework/custom-content.html +11 -34
  129. nautobot/project-static/docs/development/apps/migration/ui-component-framework/index.html +11 -34
  130. nautobot/project-static/docs/development/apps/migration/ui-component-framework/migration-steps.html +11 -34
  131. nautobot/project-static/docs/development/apps/porting-from-netbox.html +11 -34
  132. nautobot/project-static/docs/development/core/application-registry.html +139 -133
  133. nautobot/project-static/docs/development/core/best-practices.html +11 -34
  134. nautobot/project-static/docs/development/core/bootstrap-ui.html +11 -34
  135. nautobot/project-static/docs/development/core/caching.html +11 -34
  136. nautobot/project-static/docs/development/core/controllers.html +11 -34
  137. nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +11 -34
  138. nautobot/project-static/docs/development/core/generic-views.html +11 -34
  139. nautobot/project-static/docs/development/core/getting-started.html +11 -34
  140. nautobot/project-static/docs/development/core/homepage.html +11 -34
  141. nautobot/project-static/docs/development/core/index.html +11 -34
  142. nautobot/project-static/docs/development/core/minikube-dev-environment-for-k8s-jobs.html +11 -34
  143. nautobot/project-static/docs/development/core/model-checklist.html +11 -34
  144. nautobot/project-static/docs/development/core/model-features.html +11 -34
  145. nautobot/project-static/docs/development/core/natural-keys.html +11 -34
  146. nautobot/project-static/docs/development/core/navigation-menu.html +11 -34
  147. nautobot/project-static/docs/development/core/release-checklist.html +11 -34
  148. nautobot/project-static/docs/development/core/role-internals.html +11 -34
  149. nautobot/project-static/docs/development/core/settings.html +11 -34
  150. nautobot/project-static/docs/development/core/style-guide.html +11 -34
  151. nautobot/project-static/docs/development/core/templates.html +11 -34
  152. nautobot/project-static/docs/development/core/testing.html +11 -34
  153. nautobot/project-static/docs/development/core/ui-component-framework.html +11 -34
  154. nautobot/project-static/docs/development/core/user-preferences.html +11 -34
  155. nautobot/project-static/docs/development/index.html +11 -34
  156. nautobot/project-static/docs/development/jobs/getting-started.html +11 -34
  157. nautobot/project-static/docs/development/jobs/index.html +11 -34
  158. nautobot/project-static/docs/development/jobs/installation.html +11 -34
  159. nautobot/project-static/docs/development/jobs/job-extensions.html +11 -34
  160. nautobot/project-static/docs/development/jobs/job-logging.html +11 -34
  161. nautobot/project-static/docs/development/jobs/job-patterns.html +11 -34
  162. nautobot/project-static/docs/development/jobs/job-structure.html +11 -34
  163. nautobot/project-static/docs/development/jobs/migration/from-v1.html +11 -34
  164. nautobot/project-static/docs/development/jobs/testing.html +11 -34
  165. nautobot/project-static/docs/index.html +11 -34
  166. nautobot/project-static/docs/overview/application_stack.html +11 -34
  167. nautobot/project-static/docs/overview/design_philosophy.html +11 -34
  168. nautobot/project-static/docs/release-notes/index.html +11 -34
  169. nautobot/project-static/docs/release-notes/version-1.0.html +11 -34
  170. nautobot/project-static/docs/release-notes/version-1.1.html +11 -34
  171. nautobot/project-static/docs/release-notes/version-1.2.html +11 -34
  172. nautobot/project-static/docs/release-notes/version-1.3.html +11 -34
  173. nautobot/project-static/docs/release-notes/version-1.4.html +11 -34
  174. nautobot/project-static/docs/release-notes/version-1.5.html +11 -34
  175. nautobot/project-static/docs/release-notes/version-1.6.html +11 -34
  176. nautobot/project-static/docs/release-notes/version-2.0.html +11 -34
  177. nautobot/project-static/docs/release-notes/version-2.1.html +11 -34
  178. nautobot/project-static/docs/release-notes/version-2.2.html +11 -34
  179. nautobot/project-static/docs/release-notes/version-2.3.html +11 -34
  180. nautobot/project-static/docs/release-notes/version-2.4.html +271 -34
  181. nautobot/project-static/docs/requirements.txt +1 -1
  182. nautobot/project-static/docs/search/search_index.json +1 -1
  183. nautobot/project-static/docs/sitemap.xml +299 -299
  184. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  185. nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +11 -34
  186. nautobot/project-static/docs/user-guide/administration/configuration/authentication/remote.html +11 -34
  187. nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +11 -34
  188. nautobot/project-static/docs/user-guide/administration/configuration/index.html +11 -34
  189. nautobot/project-static/docs/user-guide/administration/configuration/redis.html +11 -34
  190. nautobot/project-static/docs/user-guide/administration/configuration/settings.html +14 -37
  191. nautobot/project-static/docs/user-guide/administration/configuration/time-zones.html +11 -34
  192. nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +11 -34
  193. nautobot/project-static/docs/user-guide/administration/guides/docker.html +11 -34
  194. nautobot/project-static/docs/user-guide/administration/guides/health-checks.html +11 -34
  195. nautobot/project-static/docs/user-guide/administration/guides/permissions.html +11 -34
  196. nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +11 -34
  197. nautobot/project-static/docs/user-guide/administration/guides/replicating-nautobot.html +11 -34
  198. nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +11 -34
  199. nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +11 -34
  200. nautobot/project-static/docs/user-guide/administration/guides/selinux-troubleshooting.html +11 -34
  201. nautobot/project-static/docs/user-guide/administration/installation/app-install.html +11 -34
  202. nautobot/project-static/docs/user-guide/administration/installation/external-authentication.html +11 -34
  203. nautobot/project-static/docs/user-guide/administration/installation/http-server.html +11 -34
  204. nautobot/project-static/docs/user-guide/administration/installation/index.html +11 -34
  205. nautobot/project-static/docs/user-guide/administration/installation/install_system.html +11 -34
  206. nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +11 -34
  207. nautobot/project-static/docs/user-guide/administration/installation/services.html +11 -34
  208. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-netbox.html +11 -34
  209. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +11 -34
  210. nautobot/project-static/docs/user-guide/administration/security/index.html +11 -34
  211. nautobot/project-static/docs/user-guide/administration/security/notices.html +11 -34
  212. nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +19 -39
  213. nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +11 -34
  214. nautobot/project-static/docs/user-guide/administration/upgrading/database-backup.html +11 -34
  215. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/after-you-upgrade.html +11 -34
  216. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/before-you-upgrade.html +11 -34
  217. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/for-developers.html +11 -34
  218. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/index.html +11 -34
  219. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/whats-changed.html +11 -34
  220. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/region-and-site-data-migration-guide.html +11 -34
  221. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +11 -34
  222. nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +11 -34
  223. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuit.html +11 -34
  224. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittermination.html +11 -34
  225. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittype.html +11 -34
  226. nautobot/project-static/docs/user-guide/core-data-model/circuits/provider.html +11 -34
  227. nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +11 -34
  228. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloud.html +11 -34
  229. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudaccount.html +11 -34
  230. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetwork.html +11 -34
  231. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetworkprefixassignment.html +11 -34
  232. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudresourcetype.html +11 -34
  233. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservice.html +11 -34
  234. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservicenetworkassignment.html +11 -34
  235. nautobot/project-static/docs/user-guide/core-data-model/dcim/cable.html +11 -34
  236. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +11 -34
  237. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +11 -34
  238. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +11 -34
  239. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +11 -34
  240. nautobot/project-static/docs/user-guide/core-data-model/dcim/controller.html +11 -34
  241. nautobot/project-static/docs/user-guide/core-data-model/dcim/controllermanageddevicegroup.html +11 -34
  242. nautobot/project-static/docs/user-guide/core-data-model/dcim/device.html +11 -34
  243. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +11 -34
  244. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +11 -34
  245. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicefamily.html +11 -34
  246. nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +11 -34
  247. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +11 -34
  248. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +11 -34
  249. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +11 -34
  250. nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +14 -37
  251. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +11 -34
  252. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +11 -34
  253. nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +19 -52
  254. nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +11 -34
  255. nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +11 -34
  256. nautobot/project-static/docs/user-guide/core-data-model/dcim/manufacturer.html +11 -34
  257. nautobot/project-static/docs/user-guide/core-data-model/dcim/module.html +11 -34
  258. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebay.html +14 -37
  259. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebaytemplate.html +11 -34
  260. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulefamily.html +11 -34
  261. nautobot/project-static/docs/user-guide/core-data-model/dcim/moduletype.html +11 -34
  262. nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +11 -34
  263. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerfeed.html +11 -34
  264. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +11 -34
  265. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +11 -34
  266. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerpanel.html +11 -34
  267. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +11 -34
  268. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +11 -34
  269. nautobot/project-static/docs/user-guide/core-data-model/dcim/rack.html +11 -34
  270. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackgroup.html +11 -34
  271. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackreservation.html +11 -34
  272. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +11 -34
  273. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +11 -34
  274. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +11 -34
  275. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareversion.html +11 -34
  276. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualchassis.html +11 -34
  277. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualdevicecontext.html +11 -34
  278. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +11 -34
  279. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +11 -34
  280. nautobot/project-static/docs/user-guide/core-data-model/extras/contact.html +11 -34
  281. nautobot/project-static/docs/user-guide/core-data-model/extras/team.html +11 -34
  282. nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +11 -34
  283. nautobot/project-static/docs/user-guide/core-data-model/ipam/namespace.html +11 -34
  284. nautobot/project-static/docs/user-guide/core-data-model/ipam/prefix.html +11 -34
  285. nautobot/project-static/docs/user-guide/core-data-model/ipam/rir.html +11 -34
  286. nautobot/project-static/docs/user-guide/core-data-model/ipam/routetarget.html +11 -34
  287. nautobot/project-static/docs/user-guide/core-data-model/ipam/service.html +11 -34
  288. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +11 -34
  289. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlangroup.html +11 -34
  290. nautobot/project-static/docs/user-guide/core-data-model/ipam/vrf.html +11 -34
  291. nautobot/project-static/docs/user-guide/core-data-model/overview/introduction.html +11 -34
  292. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenant.html +11 -34
  293. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenantgroup.html +11 -34
  294. nautobot/project-static/docs/user-guide/core-data-model/virtualization/cluster.html +11 -34
  295. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustergroup.html +11 -34
  296. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustertype.html +11 -34
  297. nautobot/project-static/docs/user-guide/core-data-model/virtualization/virtualmachine.html +11 -34
  298. nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +11 -34
  299. nautobot/project-static/docs/user-guide/core-data-model/wireless/index.html +11 -34
  300. nautobot/project-static/docs/user-guide/core-data-model/wireless/radioprofile.html +11 -34
  301. nautobot/project-static/docs/user-guide/core-data-model/wireless/supporteddatarate.html +11 -34
  302. nautobot/project-static/docs/user-guide/core-data-model/wireless/wirelessnetwork.html +11 -34
  303. nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +11 -34
  304. nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +11 -34
  305. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +11 -34
  306. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +11 -34
  307. nautobot/project-static/docs/user-guide/feature-guides/getting-started/index.html +11 -34
  308. nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +11 -34
  309. nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +11 -34
  310. nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +11 -34
  311. nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +11 -34
  312. nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +11 -34
  313. nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +11 -34
  314. nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +11 -34
  315. nautobot/project-static/docs/user-guide/feature-guides/graphql.html +11 -34
  316. nautobot/project-static/docs/user-guide/feature-guides/ip-address-merge-tool.html +11 -34
  317. nautobot/project-static/docs/user-guide/feature-guides/relationships.html +11 -34
  318. nautobot/project-static/docs/user-guide/feature-guides/software-image-files-and-versions.html +11 -34
  319. nautobot/project-static/docs/user-guide/feature-guides/wireless-networks-and-controllers.html +11 -34
  320. nautobot/project-static/docs/user-guide/index.html +11 -34
  321. nautobot/project-static/docs/user-guide/platform-functionality/change-logging.html +11 -34
  322. nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +11 -34
  323. nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +11 -34
  324. nautobot/project-static/docs/user-guide/platform-functionality/customlink.html +11 -34
  325. nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +11 -34
  326. nautobot/project-static/docs/user-guide/platform-functionality/events.html +11 -34
  327. nautobot/project-static/docs/user-guide/platform-functionality/exporttemplate.html +11 -34
  328. nautobot/project-static/docs/user-guide/platform-functionality/externalintegration.html +11 -34
  329. nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +11 -34
  330. nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +11 -34
  331. nautobot/project-static/docs/user-guide/platform-functionality/graphqlquery.html +11 -34
  332. nautobot/project-static/docs/user-guide/platform-functionality/imageattachment.html +11 -34
  333. nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +11 -34
  334. nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +11 -34
  335. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +11 -34
  336. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +11 -34
  337. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobqueue.html +11 -34
  338. nautobot/project-static/docs/user-guide/platform-functionality/jobs/kubernetes-job-support.html +11 -34
  339. nautobot/project-static/docs/user-guide/platform-functionality/jobs/managing-jobs.html +11 -34
  340. nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +11 -34
  341. nautobot/project-static/docs/user-guide/platform-functionality/napalm.html +11 -34
  342. nautobot/project-static/docs/user-guide/platform-functionality/note.html +11 -34
  343. nautobot/project-static/docs/user-guide/platform-functionality/objectmetadata.html +11 -34
  344. nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +11 -34
  345. nautobot/project-static/docs/user-guide/platform-functionality/rendering-jinja-templates.html +11 -34
  346. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +11 -34
  347. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +11 -34
  348. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +11 -34
  349. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/ui-related-endpoints.html +11 -34
  350. nautobot/project-static/docs/user-guide/platform-functionality/role.html +11 -34
  351. nautobot/project-static/docs/user-guide/platform-functionality/savedview.html +11 -34
  352. nautobot/project-static/docs/user-guide/platform-functionality/secret.html +11 -34
  353. nautobot/project-static/docs/user-guide/platform-functionality/staticgroupassociation.html +11 -34
  354. nautobot/project-static/docs/user-guide/platform-functionality/status.html +11 -34
  355. nautobot/project-static/docs/user-guide/platform-functionality/tag.html +11 -34
  356. nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +11 -34
  357. nautobot/project-static/docs/user-guide/platform-functionality/users/objectpermission.html +11 -34
  358. nautobot/project-static/docs/user-guide/platform-functionality/users/token.html +11 -34
  359. nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +11 -34
  360. nautobot/tenancy/api/views.py +2 -1
  361. {nautobot-2.4.12.dist-info → nautobot-2.4.14.dist-info}/METADATA +5 -5
  362. {nautobot-2.4.12.dist-info → nautobot-2.4.14.dist-info}/RECORD +366 -357
  363. {nautobot-2.4.12.dist-info → nautobot-2.4.14.dist-info}/LICENSE.txt +0 -0
  364. {nautobot-2.4.12.dist-info → nautobot-2.4.14.dist-info}/NOTICE +0 -0
  365. {nautobot-2.4.12.dist-info → nautobot-2.4.14.dist-info}/WHEEL +0 -0
  366. {nautobot-2.4.12.dist-info → nautobot-2.4.14.dist-info}/entry_points.txt +0 -0
@@ -1850,8 +1850,18 @@ class SecretTestCase(
1850
1850
  }
1851
1851
 
1852
1852
 
1853
- class SecretsGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
1853
+ class SecretsGroupTestCase(
1854
+ ViewTestCases.OrganizationalObjectViewTestCase,
1855
+ ViewTestCases.BulkEditObjectsViewTestCase,
1856
+ ):
1854
1857
  model = SecretsGroup
1858
+ custom_test_permissions = [
1859
+ "extras.view_secret",
1860
+ "extras.add_secretsgroup",
1861
+ "extras.view_secretsgroup",
1862
+ "extras.add_secretsgroupassociation",
1863
+ "extras.change_secretsgroupassociation",
1864
+ ]
1855
1865
 
1856
1866
  @classmethod
1857
1867
  def setUpTestData(cls):
@@ -1895,6 +1905,108 @@ class SecretsGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
1895
1905
  "secrets_group_associations-MIN_NUM_FORMS": "0",
1896
1906
  "secrets_group_associations-MAX_NUM_FORMS": "1000",
1897
1907
  }
1908
+ cls.bulk_edit_data = {
1909
+ "description": "This is a very detailed new description",
1910
+ }
1911
+
1912
+ def test_create_group_with_valid_secret_association(self):
1913
+ """Test that a SecretsGroup with a valid Secret association saves correctly via the formset."""
1914
+ self.add_permissions(*self.custom_test_permissions)
1915
+ # Create a secret to associate
1916
+ secret = Secret.objects.create(
1917
+ name="AWS_Secret",
1918
+ provider="text-file",
1919
+ parameters={"path": "/tmp"}, # noqa: S108 # hardcoded-temp-file -- false positive
1920
+ )
1921
+
1922
+ form_data = {
1923
+ "name": "test",
1924
+ "description": "test bulk edits",
1925
+ "secrets_group_associations-TOTAL_FORMS": "1",
1926
+ "secrets_group_associations-INITIAL_FORMS": "0",
1927
+ "secrets_group_associations-MIN_NUM_FORMS": "0",
1928
+ "secrets_group_associations-MAX_NUM_FORMS": "1000",
1929
+ "secrets_group_associations-0-secret": secret.pk,
1930
+ "secrets_group_associations-0-access_type": SecretsGroupAccessTypeChoices.TYPE_HTTP,
1931
+ "secrets_group_associations-0-secret_type": SecretsGroupSecretTypeChoices.TYPE_PASSWORD,
1932
+ }
1933
+
1934
+ # Submit the form to the "add SecretsGroup" view
1935
+ response = self.client.post(reverse("extras:secretsgroup_add"), data=form_data, follow=True)
1936
+
1937
+ self.assertEqual(response.status_code, 200)
1938
+ self.assertTrue(SecretsGroup.objects.filter(name="test").exists())
1939
+
1940
+ # Checks that the association was created correctly
1941
+ group = SecretsGroup.objects.get(name="test")
1942
+ self.assertEqual(group.secrets_group_associations.count(), 1)
1943
+
1944
+ association = group.secrets_group_associations.first()
1945
+ self.assertEqual(association.secret, secret)
1946
+ self.assertEqual(association.access_type, SecretsGroupAccessTypeChoices.TYPE_HTTP)
1947
+ self.assertEqual(association.secret_type, SecretsGroupSecretTypeChoices.TYPE_PASSWORD)
1948
+
1949
+ def test_create_group_with_invalid_secret_association(self):
1950
+ """Test that invalid Secret association formset raises validation error and does not save."""
1951
+ self.add_permissions(*self.custom_test_permissions)
1952
+ url = reverse("extras:secretsgroup_add")
1953
+
1954
+ form_data = {
1955
+ "name": "Invalid Secrets Group",
1956
+ "description": "Missing required fields",
1957
+ "secrets_group_associations-TOTAL_FORMS": "1",
1958
+ "secrets_group_associations-INITIAL_FORMS": "0",
1959
+ "secrets_group_associations-MIN_NUM_FORMS": "0",
1960
+ "secrets_group_associations-MAX_NUM_FORMS": "1000",
1961
+ "secrets_group_associations-0-secret": "", # invalid
1962
+ "secrets_group_associations-0-access_type": SecretsGroupAccessTypeChoices.TYPE_HTTP,
1963
+ "secrets_group_associations-0-secret_type": "", # invalid
1964
+ }
1965
+
1966
+ response = self.client.post(url, data=form_data)
1967
+
1968
+ self.assertEqual(response.status_code, 200)
1969
+
1970
+ # Checks that no new SecretsGroup was created
1971
+ self.assertFalse(SecretsGroup.objects.filter(name="Invalid Secrets Group").exists())
1972
+
1973
+ # Checks that formset errors are raised in the context
1974
+ self.assertFormsetError(
1975
+ response.context["secrets"], form_index=0, field="secret", errors=["This field is required."]
1976
+ )
1977
+
1978
+ def test_create_group_with_deleted_secret_fails_cleanly(self):
1979
+ """
1980
+ Creating a SecretsGroup with a deleted Secret should fail with a formset error.
1981
+ """
1982
+ self.add_permissions(*self.custom_test_permissions)
1983
+
1984
+ secret = Secret.objects.create(name="TempSecret", provider="text-file", parameters={"path": "/tmp"}) # noqa: S108 # hardcoded-temp-file -- false positive
1985
+ secret_pk = secret.pk
1986
+ secret.delete()
1987
+
1988
+ form_data = {
1989
+ "name": "Test Group",
1990
+ "description": "This should not be created",
1991
+ "secrets_group_associations-TOTAL_FORMS": "1",
1992
+ "secrets_group_associations-INITIAL_FORMS": "0",
1993
+ "secrets_group_associations-MIN_NUM_FORMS": "0",
1994
+ "secrets_group_associations-MAX_NUM_FORMS": "1000",
1995
+ "secrets_group_associations-0-secret": secret_pk,
1996
+ "secrets_group_associations-0-access_type": SecretsGroupAccessTypeChoices.TYPE_HTTP,
1997
+ "secrets_group_associations-0-secret_type": SecretsGroupSecretTypeChoices.TYPE_PASSWORD,
1998
+ }
1999
+
2000
+ response = self.client.post(reverse("extras:secretsgroup_add"), data=form_data)
2001
+ self.assertEqual(response.status_code, 200)
2002
+
2003
+ self.assertFormsetError(
2004
+ response.context["secrets"],
2005
+ form_index=0,
2006
+ field="secret",
2007
+ errors=["Select a valid choice. That choice is not one of the available choices."],
2008
+ )
2009
+ self.assertFalse(SecretsGroup.objects.filter(name="Test Group").exists())
1898
2010
 
1899
2011
 
1900
2012
  class GraphQLQueriesTestCase(
nautobot/extras/urls.py CHANGED
@@ -6,11 +6,9 @@ from nautobot.extras.models import (
6
6
  CustomField,
7
7
  DynamicGroup,
8
8
  GitRepository,
9
- GraphQLQuery,
10
9
  Job,
11
10
  Note,
12
11
  Relationship,
13
- SecretsGroup,
14
12
  )
15
13
 
16
14
  app_name = "extras"
@@ -24,6 +22,7 @@ router.register("contact-associations", views.ContactAssociationUIViewSet)
24
22
  router.register("custom-links", views.CustomLinkUIViewSet)
25
23
  router.register("export-templates", views.ExportTemplateUIViewSet)
26
24
  router.register("external-integrations", views.ExternalIntegrationUIViewSet)
25
+ router.register("graphql-queries", views.GraphQLQueryUIViewSet)
27
26
  router.register("job-buttons", views.JobButtonUIViewSet)
28
27
  router.register("job-hooks", views.JobHookUIViewSet)
29
28
  router.register("job-queues", views.JobQueueUIViewSet)
@@ -34,6 +33,7 @@ router.register("relationships", views.RelationshipUIViewSet)
34
33
  router.register("roles", views.RoleUIViewSet)
35
34
  router.register("saved-views", views.SavedViewUIViewSet)
36
35
  router.register("secrets", views.SecretUIViewSet)
36
+ router.register("secrets-groups", views.SecretsGroupUIViewSet)
37
37
  router.register("static-group-associations", views.StaticGroupAssociationUIViewSet)
38
38
  router.register("statuses", views.StatusUIViewSet)
39
39
  router.register("tags", views.TagUIViewSet)
@@ -181,37 +181,6 @@ urlpatterns = [
181
181
  views.GitRepositoryDryRunView.as_view(),
182
182
  name="gitrepository_dryrun",
183
183
  ),
184
- # GraphQL Queries
185
- path("graphql-queries/", views.GraphQLQueryListView.as_view(), name="graphqlquery_list"),
186
- path("graphql-queries/add/", views.GraphQLQueryEditView.as_view(), name="graphqlquery_add"),
187
- path(
188
- "graphql-queries/delete/",
189
- views.GraphQLQueryBulkDeleteView.as_view(),
190
- name="GraphQLQuery_bulk_delete",
191
- ),
192
- path("graphql-queries/<uuid:pk>/", views.GraphQLQueryView.as_view(), name="graphqlquery"),
193
- path(
194
- "graphql-queries/<uuid:pk>/edit/",
195
- views.GraphQLQueryEditView.as_view(),
196
- name="graphqlquery_edit",
197
- ),
198
- path(
199
- "graphql-queries/<uuid:pk>/delete/",
200
- views.GraphQLQueryDeleteView.as_view(),
201
- name="graphqlquery_delete",
202
- ),
203
- path(
204
- "graphql-queries/<uuid:pk>/changelog/",
205
- views.ObjectChangeLogView.as_view(),
206
- name="graphqlquery_changelog",
207
- kwargs={"model": GraphQLQuery},
208
- ),
209
- path(
210
- "graphql-queries/<uuid:pk>/notes/",
211
- views.ObjectNotesView.as_view(),
212
- name="graphqlquery_notes",
213
- kwargs={"model": GraphQLQuery},
214
- ),
215
184
  # Image attachments
216
185
  path(
217
186
  "image-attachments/<uuid:pk>/edit/",
@@ -311,24 +280,6 @@ urlpatterns = [
311
280
  views.SecretProviderParametersFormView.as_view(),
312
281
  name="secret_provider_parameters_form",
313
282
  ),
314
- path("secrets-groups/", views.SecretsGroupListView.as_view(), name="secretsgroup_list"),
315
- path("secrets-groups/add/", views.SecretsGroupEditView.as_view(), name="secretsgroup_add"),
316
- path("secrets-groups/delete/", views.SecretsGroupBulkDeleteView.as_view(), name="secretsgroup_bulk_delete"),
317
- path("secrets-groups/<uuid:pk>/", views.SecretsGroupView.as_view(), name="secretsgroup"),
318
- path("secrets-groups/<uuid:pk>/edit/", views.SecretsGroupEditView.as_view(), name="secretsgroup_edit"),
319
- path("secrets-groups/<uuid:pk>/delete/", views.SecretsGroupDeleteView.as_view(), name="secretsgroup_delete"),
320
- path(
321
- "secrets-groups/<uuid:pk>/changelog/",
322
- views.ObjectChangeLogView.as_view(),
323
- name="secretsgroup_changelog",
324
- kwargs={"model": SecretsGroup},
325
- ),
326
- path(
327
- "secrets-groups/<uuid:pk>/notes/",
328
- views.ObjectNotesView.as_view(),
329
- name="secretsgroup_notes",
330
- kwargs={"model": SecretsGroup},
331
- ),
332
283
  ]
333
284
 
334
285
  urlpatterns += router.urls
nautobot/extras/utils.py CHANGED
@@ -275,11 +275,14 @@ def extras_features(*features):
275
275
  """
276
276
 
277
277
  def wrapper(model_class):
278
- # Initialize the model_features store if not already defined
278
+ # Initialize the model_features and feature_models stores if not already defined
279
279
  if "model_features" not in registry:
280
280
  registry["model_features"] = {f: collections.defaultdict(list) for f in EXTRAS_FEATURES}
281
+ if "feature_models" not in registry:
282
+ registry["feature_models"] = {f: [] for f in EXTRAS_FEATURES}
281
283
  for feature in features:
282
284
  if feature in EXTRAS_FEATURES:
285
+ registry["feature_models"][feature].append(model_class)
283
286
  app_label, model_name = model_class._meta.label_lower.split(".")
284
287
  registry["model_features"][feature][app_label].append(model_name)
285
288
  else:
nautobot/extras/views.py CHANGED
@@ -945,11 +945,13 @@ class DynamicGroupBulkDeleteView(generic.BulkDeleteView):
945
945
  filterset = filters.DynamicGroupFilterSet
946
946
 
947
947
 
948
- # 3.0 TODO: remove, deprecated since 2.3 (#5845)
949
948
  class ObjectDynamicGroupsView(generic.GenericView):
950
949
  """
951
950
  Present a list of dynamic groups associated to a particular object.
952
951
 
952
+ Note that this isn't currently widely used, as most object detail views currently render the table inline
953
+ rather than using this separate view. This may change in the future.
954
+
953
955
  base_template: Specify to explicitly identify the base object detail template to render.
954
956
  If not provided, "<app>/<model>.html", "<app>/<model>_retrieve.html", or "generic/object_retrieve.html"
955
957
  will be used, as per `get_base_template()`.
@@ -969,7 +971,6 @@ class ObjectDynamicGroupsView(generic.GenericView):
969
971
  data=obj.dynamic_groups.restrict(request.user, "view"), orderable=False
970
972
  )
971
973
  dynamicgroups_table.columns.hide("content_type")
972
- dynamicgroups_table.columns.hide("members")
973
974
 
974
975
  # Apply the request context
975
976
  paginate = {
@@ -1229,32 +1230,24 @@ class GitRepositoryResultView(generic.ObjectView):
1229
1230
  #
1230
1231
 
1231
1232
 
1232
- class GraphQLQueryListView(generic.ObjectListView):
1233
+ class GraphQLQueryUIViewSet(
1234
+ ObjectDetailViewMixin,
1235
+ ObjectListViewMixin,
1236
+ ObjectEditViewMixin,
1237
+ ObjectDestroyViewMixin,
1238
+ ObjectBulkDestroyViewMixin,
1239
+ ObjectChangeLogViewMixin,
1240
+ ObjectNotesViewMixin,
1241
+ ):
1242
+ filterset_form_class = forms.GraphQLQueryFilterForm
1233
1243
  queryset = GraphQLQuery.objects.all()
1234
- table = tables.GraphQLQueryTable
1235
- filterset = filters.GraphQLQueryFilterSet
1236
- filterset_form = forms.GraphQLQueryFilterForm
1244
+ form_class = forms.GraphQLQueryForm
1245
+ filterset_class = filters.GraphQLQueryFilterSet
1246
+ serializer_class = serializers.GraphQLQuerySerializer
1247
+ table_class = tables.GraphQLQueryTable
1237
1248
  action_buttons = ("add",)
1238
1249
 
1239
1250
 
1240
- class GraphQLQueryView(generic.ObjectView):
1241
- queryset = GraphQLQuery.objects.all()
1242
-
1243
-
1244
- class GraphQLQueryEditView(generic.ObjectEditView):
1245
- queryset = GraphQLQuery.objects.all()
1246
- model_form = forms.GraphQLQueryForm
1247
-
1248
-
1249
- class GraphQLQueryDeleteView(generic.ObjectDeleteView):
1250
- queryset = GraphQLQuery.objects.all()
1251
-
1252
-
1253
- class GraphQLQueryBulkDeleteView(generic.BulkDeleteView):
1254
- queryset = GraphQLQuery.objects.all()
1255
- table = tables.GraphQLQueryTable
1256
-
1257
-
1258
1251
  #
1259
1252
  # Image attachments
1260
1253
  #
@@ -2744,123 +2737,37 @@ class SecretProviderParametersFormView(generic.GenericView):
2744
2737
  )
2745
2738
 
2746
2739
 
2747
- class SecretsGroupListView(generic.ObjectListView):
2748
- queryset = SecretsGroup.objects.all()
2749
- filterset = filters.SecretsGroupFilterSet
2750
- filterset_form = forms.SecretsGroupFilterForm
2751
- table = tables.SecretsGroupTable
2752
- action_buttons = ("add",)
2753
-
2754
-
2755
- class SecretsGroupView(generic.ObjectView):
2756
- queryset = SecretsGroup.objects.all()
2757
-
2758
- def get_extra_context(self, request, instance):
2759
- return {"secrets_group_associations": SecretsGroupAssociation.objects.filter(secrets_group=instance)}
2760
-
2761
-
2762
- class SecretsGroupEditView(generic.ObjectEditView):
2740
+ class SecretsGroupUIViewSet(NautobotUIViewSet):
2741
+ bulk_update_form_class = forms.SecretsGroupBulkEditForm
2742
+ filterset_class = filters.SecretsGroupFilterSet
2743
+ filterset_form_class = forms.SecretsGroupFilterForm
2744
+ form_class = forms.SecretsGroupForm
2745
+ serializer_class = serializers.SecretsGroupSerializer
2746
+ table_class = tables.SecretsGroupTable
2747
+ template_name = "extras/secretsgroup_update.html"
2763
2748
  queryset = SecretsGroup.objects.all()
2764
- model_form = forms.SecretsGroupForm
2765
- template_name = "extras/secretsgroup_edit.html"
2766
-
2767
- def get_extra_context(self, request, instance):
2768
- ctx = super().get_extra_context(request, instance)
2769
-
2770
- if request.POST:
2771
- ctx["secrets"] = forms.SecretsGroupAssociationFormSet(data=request.POST, instance=instance)
2772
- else:
2773
- ctx["secrets"] = forms.SecretsGroupAssociationFormSet(instance=instance)
2774
-
2775
- return ctx
2776
-
2777
- def post(self, request, *args, **kwargs):
2778
- obj = self.alter_obj(self.get_object(kwargs), request, args, kwargs)
2779
- form = self.model_form(data=request.POST, files=request.FILES, instance=obj)
2780
- restrict_form_fields(form, request.user)
2781
-
2782
- if form.is_valid():
2783
- logger.debug("Form validation was successful")
2784
-
2785
- try:
2786
- with transaction.atomic():
2787
- object_created = not form.instance.present_in_database
2788
- obj = form.save()
2789
-
2790
- # Check that the new object conforms with any assigned object-level permissions
2791
- self.queryset.get(pk=obj.pk)
2792
-
2793
- # Process the formsets for secrets
2794
- ctx = self.get_extra_context(request, obj)
2795
- secrets = ctx["secrets"]
2796
- if secrets.is_valid():
2797
- secrets.save()
2798
- else:
2799
- raise RuntimeError(secrets.errors)
2800
- verb = "Created" if object_created else "Modified"
2801
- msg = f"{verb} {self.queryset.model._meta.verbose_name}"
2802
- logger.info(f"{msg} {obj} (PK: {obj.pk})")
2803
- try:
2804
- msg = format_html('{} <a href="{}">{}</a>', msg, obj.get_absolute_url(), obj)
2805
- except AttributeError:
2806
- msg = format_html("{} {}", msg, obj)
2807
- messages.success(request, msg)
2808
2749
 
2809
- if "_addanother" in request.POST:
2810
- # If the object has clone_fields, pre-populate a new instance of the form
2811
- if hasattr(obj, "clone_fields"):
2812
- url = f"{request.path}?{prepare_cloned_fields(obj)}"
2813
- return redirect(url)
2814
-
2815
- return redirect(request.get_full_path())
2816
-
2817
- return_url = form.cleaned_data.get("return_url")
2818
- if url_has_allowed_host_and_scheme(url=return_url, allowed_hosts=request.get_host()):
2819
- return redirect(iri_to_uri(return_url))
2820
- else:
2821
- return redirect(self.get_return_url(request, obj))
2750
+ def get_extra_context(self, request, instance=None):
2751
+ context = super().get_extra_context(request, instance)
2752
+ if self.action == "retrieve" and instance:
2753
+ context["secrets_group_associations"] = SecretsGroupAssociation.objects.filter(secrets_group=instance)
2754
+ if self.action in ("create", "update"):
2755
+ if request.method == "POST":
2756
+ context["secrets"] = forms.SecretsGroupAssociationFormSet(data=request.POST, instance=instance)
2757
+ else:
2758
+ context["secrets"] = forms.SecretsGroupAssociationFormSet(instance=instance)
2759
+ return context
2822
2760
 
2823
- except ObjectDoesNotExist:
2824
- msg = "Object save failed due to object-level permissions violation."
2825
- logger.debug(msg)
2826
- form.add_error(None, msg)
2827
- except RuntimeError:
2828
- msg = "Errors encountered when saving secrets group associations. See below."
2829
- logger.debug(msg)
2830
- form.add_error(None, msg)
2831
- except ProtectedError as err:
2832
- # e.g. Trying to delete a choice that is in use.
2833
- err_msg = err.args[0]
2834
- protected_obj = err.protected_objects[0]
2835
- msg = f"{protected_obj.value}: {err_msg} Please cancel this edit and start again."
2836
- logger.debug(msg)
2837
- form.add_error(None, msg)
2761
+ def form_save(self, form, **kwargs):
2762
+ obj = super().form_save(form, **kwargs)
2763
+ secrets = forms.SecretsGroupAssociationFormSet(data=self.request.POST, instance=form.instance)
2838
2764
 
2765
+ if secrets.is_valid():
2766
+ secrets.save()
2839
2767
  else:
2840
- logger.debug("Form validation failed")
2768
+ raise ValidationError(secrets.errors)
2841
2769
 
2842
- return render(
2843
- request,
2844
- self.template_name,
2845
- {
2846
- "obj": obj,
2847
- "obj_type": self.queryset.model._meta.verbose_name,
2848
- "form": form,
2849
- "return_url": self.get_return_url(request, obj),
2850
- "editing": obj.present_in_database,
2851
- **self.get_extra_context(request, obj),
2852
- },
2853
- )
2854
-
2855
-
2856
- class SecretsGroupDeleteView(generic.ObjectDeleteView):
2857
- queryset = SecretsGroup.objects.all()
2858
-
2859
-
2860
- class SecretsGroupBulkDeleteView(generic.BulkDeleteView):
2861
- queryset = SecretsGroup.objects.all()
2862
- filterset = filters.SecretsGroupFilterSet
2863
- table = tables.SecretsGroupTable
2770
+ return obj
2864
2771
 
2865
2772
 
2866
2773
  #
@@ -1,7 +1,8 @@
1
1
  from django.conf import settings
2
2
  from django.core.cache import cache
3
3
  from django.shortcuts import get_object_or_404
4
- from drf_spectacular.utils import extend_schema, extend_schema_view
4
+ from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiParameter
5
+ import netaddr
5
6
  from rest_framework import status
6
7
  from rest_framework.decorators import action
7
8
  from rest_framework.exceptions import APIException
@@ -123,6 +124,26 @@ class PrefixViewSet(NautobotModelViewSet):
123
124
  return serializers.PrefixLegacySerializer
124
125
  return super().get_serializer_class()
125
126
 
127
+ @staticmethod
128
+ def get_ipaddress_param(request, name, default):
129
+ """Extract IP address parameter from request.
130
+ :param request: django-rest request object
131
+ :param name: name of the query parameter which contains the IP address string
132
+ :param default: fallback IP address string in case no value is present in the query parameter
133
+ :return: tuple of mutually exclusive (Response|None, netaddr.IPAddress object|None).
134
+ Will return a Response in case the client sent incorrectly formatted IP Address in
135
+ the parameter. It is up to the caller to return the Response.
136
+ """
137
+ response, result = None, None
138
+ try:
139
+ result = netaddr.IPAddress(request.query_params.get(name, default))
140
+ except (netaddr.core.AddrFormatError, ValueError, TypeError) as e:
141
+ response = Response(
142
+ {"detail": (f"Incorrectly formatted address in parameter {name}: {e}")},
143
+ status=status.HTTP_400_BAD_REQUEST,
144
+ )
145
+ return response, result
146
+
126
147
  class LocationIncompatibleLegacyBehavior(APIException):
127
148
  status_code = 412
128
149
  default_detail = (
@@ -225,6 +246,33 @@ class PrefixViewSet(NautobotModelViewSet):
225
246
 
226
247
  return Response(serializer.data)
227
248
 
249
+ @extend_schema(
250
+ methods=["get", "post"],
251
+ parameters=[
252
+ OpenApiParameter(
253
+ name="range_start",
254
+ location="query",
255
+ description="IP from which enumeration/allocation should start.",
256
+ type={
257
+ "oneOf": [
258
+ {"type": "string", "format": "ipv6"},
259
+ {"type": "string", "format": "ipv4"},
260
+ ]
261
+ },
262
+ ),
263
+ OpenApiParameter(
264
+ name="range_end",
265
+ location="query",
266
+ description="IP from which enumeration/allocation should stop.",
267
+ type={
268
+ "oneOf": [
269
+ {"type": "string", "format": "ipv6"},
270
+ {"type": "string", "format": "ipv4"},
271
+ ]
272
+ },
273
+ ),
274
+ ],
275
+ )
228
276
  @extend_schema(methods=["get"], responses={200: serializers.AvailableIPSerializer(many=True)})
229
277
  @extend_schema(
230
278
  methods=["post"],
@@ -251,6 +299,21 @@ class PrefixViewSet(NautobotModelViewSet):
251
299
  """
252
300
  prefix = get_object_or_404(Prefix.objects.restrict(request.user), pk=pk)
253
301
 
302
+ default_first, default_last = netaddr.IPAddress(prefix.prefix.first), netaddr.IPAddress(prefix.prefix.last)
303
+ ((error_response_start, range_start), (error_response_end, range_end)) = (
304
+ self.get_ipaddress_param(request, "range_start", default_first),
305
+ self.get_ipaddress_param(request, "range_end", default_last),
306
+ )
307
+ if response := error_response_start or error_response_end:
308
+ return response
309
+
310
+ available_ips = prefix.get_available_ips()
311
+ # range_start and range_end are inclusive
312
+ if range_start > default_first:
313
+ available_ips.remove(netaddr.IPRange(default_first, range_start - 1))
314
+ if range_end < default_last:
315
+ available_ips.remove(netaddr.IPRange(range_end + 1, default_last))
316
+
254
317
  # Create the next available IP within the prefix
255
318
  if request.method == "POST":
256
319
  with cache.lock(
@@ -260,23 +323,23 @@ class PrefixViewSet(NautobotModelViewSet):
260
323
  requested_ips = request.data if isinstance(request.data, list) else [request.data]
261
324
 
262
325
  # Determine if the requested number of IPs is available
263
- available_ips = prefix.get_available_ips()
264
326
  if available_ips.size < len(requested_ips):
265
327
  return Response(
266
328
  {
267
329
  "detail": (
268
330
  f"An insufficient number of IP addresses are available within the prefix {prefix} "
269
- f"({len(requested_ips)} requested, {len(available_ips)} available)"
331
+ f"({len(requested_ips)} requested, {available_ips.size} available between "
332
+ f"{range_start} and {range_end})."
270
333
  )
271
334
  },
272
335
  status=status.HTTP_204_NO_CONTENT,
273
336
  )
274
337
 
275
338
  # Assign addresses from the list of available IPs and copy Namespace assignment from the parent Prefix
276
- available_ips = iter(available_ips)
277
339
  prefix_length = prefix.prefix.prefixlen
340
+ available_ips_iter = iter(available_ips)
278
341
  for requested_ip in requested_ips:
279
- requested_ip["address"] = f"{next(available_ips)}/{prefix_length}"
342
+ requested_ip["address"] = f"{next(available_ips_iter)}/{prefix_length}"
280
343
  requested_ip["namespace"] = prefix.namespace
281
344
 
282
345
  # Initialize the serializer with a list or a single object depending on what was requested
@@ -307,7 +370,7 @@ class PrefixViewSet(NautobotModelViewSet):
307
370
 
308
371
  # Calculate available IPs within the prefix
309
372
  ip_list = []
310
- for index, ip in enumerate(prefix.get_available_ips(), start=1):
373
+ for index, ip in enumerate(available_ips, start=1):
311
374
  ip_list.append(ip)
312
375
  if index == limit:
313
376
  break
@@ -0,0 +1,28 @@
1
+ # Generated by Django 4.2.20 on 2025-07-17 16:10
2
+
3
+ from django.db import migrations
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+ dependencies = [
8
+ ("ipam", "0051_added_optional_vrf_relationship_to_vdc"),
9
+ ]
10
+
11
+ operations = [
12
+ migrations.AlterIndexTogether(
13
+ name="ipaddress",
14
+ index_together={("ip_version", "host", "mask_length")},
15
+ ),
16
+ migrations.AlterIndexTogether(
17
+ name="prefix",
18
+ index_together={
19
+ ("namespace", "ip_version", "network", "prefix_length"),
20
+ ("namespace", "network", "broadcast", "prefix_length"),
21
+ ("network", "broadcast", "prefix_length"),
22
+ },
23
+ ),
24
+ migrations.AlterIndexTogether(
25
+ name="vrf",
26
+ index_together={("namespace", "name", "rd")},
27
+ ),
28
+ ]
nautobot/ipam/models.py CHANGED
@@ -170,6 +170,9 @@ class VRF(PrimaryModel):
170
170
  # where multiple different-RD VRFs with the same name may already exist.
171
171
  # ["namespace", "name"],
172
172
  ]
173
+ index_together = [
174
+ ["namespace", "name", "rd"],
175
+ ]
173
176
  verbose_name = "VRF"
174
177
  verbose_name_plural = "VRFs"
175
178
 
@@ -409,7 +412,12 @@ class VRFPrefixAssignment(BaseModel):
409
412
  super().clean()
410
413
 
411
414
  if self.prefix.namespace != self.vrf.namespace:
412
- raise ValidationError({"prefix": "Prefix must be in same namespace as VRF"})
415
+ raise ValidationError(
416
+ {
417
+ "prefix": f"Prefix (namespace {self.prefix.namespace}) must be in same namespace as "
418
+ "VRF (namespace {self.vrf.namespace})"
419
+ }
420
+ )
413
421
 
414
422
 
415
423
  @extras_features(
@@ -589,6 +597,7 @@ class Prefix(PrimaryModel):
589
597
  index_together = [
590
598
  ["network", "broadcast", "prefix_length"],
591
599
  ["namespace", "network", "broadcast", "prefix_length"],
600
+ ["namespace", "ip_version", "network", "prefix_length"],
592
601
  ]
593
602
  unique_together = ["namespace", "network", "prefix_length"]
594
603
  verbose_name_plural = "prefixes"
@@ -1250,6 +1259,9 @@ class IPAddress(PrimaryModel):
1250
1259
  verbose_name = "IP address"
1251
1260
  verbose_name_plural = "IP addresses"
1252
1261
  unique_together = ["parent", "host"]
1262
+ index_together = [
1263
+ ["ip_version", "host", "mask_length"],
1264
+ ]
1253
1265
 
1254
1266
  def __init__(self, *args, address=None, namespace=None, **kwargs):
1255
1267
  super().__init__(*args, **kwargs)