nautobot 2.3.4__py3-none-any.whl → 2.3.6__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 (327) hide show
  1. nautobot/core/api/utils.py +12 -2
  2. nautobot/core/forms/fields.py +5 -2
  3. nautobot/core/forms/utils.py +31 -6
  4. nautobot/core/models/fields.py +56 -0
  5. nautobot/core/tests/runner.py +13 -6
  6. nautobot/core/tests/test_utils.py +83 -0
  7. nautobot/core/tests/test_views.py +40 -1
  8. nautobot/core/views/generic.py +15 -15
  9. nautobot/core/views/mixins.py +12 -1
  10. nautobot/core/views/renderers.py +3 -1
  11. nautobot/core/views/utils.py +1 -1
  12. nautobot/dcim/api/serializers.py +1 -0
  13. nautobot/dcim/api/views.py +2 -0
  14. nautobot/dcim/forms.py +1 -1
  15. nautobot/dcim/templates/dcim/devicefamily_retrieve.html +1 -1
  16. nautobot/dcim/tests/test_api.py +61 -4
  17. nautobot/dcim/tests/test_models.py +0 -2
  18. nautobot/dcim/views.py +5 -2
  19. nautobot/extras/api/views.py +9 -0
  20. nautobot/extras/factory.py +2 -1
  21. nautobot/extras/models/jobs.py +9 -1
  22. nautobot/extras/models/models.py +2 -0
  23. nautobot/extras/querysets.py +10 -1
  24. nautobot/extras/tables.py +3 -0
  25. nautobot/extras/tests/test_api.py +39 -3
  26. nautobot/extras/tests/test_forms.py +2 -0
  27. nautobot/extras/tests/test_views.py +78 -3
  28. nautobot/extras/views.py +10 -7
  29. nautobot/ipam/api/serializers.py +30 -1
  30. nautobot/ipam/api/views.py +165 -3
  31. nautobot/ipam/filters.py +1 -1
  32. nautobot/ipam/forms.py +8 -1
  33. nautobot/ipam/migrations/0050_vlangroup_range.py +24 -0
  34. nautobot/ipam/models.py +56 -19
  35. nautobot/ipam/navigation.py +8 -1
  36. nautobot/ipam/tables.py +4 -4
  37. nautobot/ipam/templates/ipam/prefix.html +1 -1
  38. nautobot/ipam/templates/ipam/vlangroup.html +4 -0
  39. nautobot/ipam/tests/test_api.py +174 -0
  40. nautobot/ipam/tests/test_filters.py +1 -1
  41. nautobot/ipam/tests/test_models.py +35 -1
  42. nautobot/ipam/tests/test_utils.py +61 -0
  43. nautobot/ipam/tests/test_views.py +43 -41
  44. nautobot/ipam/utils/__init__.py +10 -17
  45. nautobot/ipam/views.py +2 -2
  46. nautobot/project-static/docs/404.html +2 -2
  47. nautobot/project-static/docs/apps/index.html +2 -2
  48. nautobot/project-static/docs/apps/nautobot-apps.html +2 -2
  49. nautobot/project-static/docs/assets/javascripts/workers/{search.07f07601.min.js → search.6ce7567c.min.js} +3 -3
  50. nautobot/project-static/docs/assets/javascripts/workers/{search.07f07601.min.js.map → search.6ce7567c.min.js.map} +2 -2
  51. nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +2 -2
  52. nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +2 -2
  53. nautobot/project-static/docs/code-reference/nautobot/apps/api.html +2 -2
  54. nautobot/project-static/docs/code-reference/nautobot/apps/change_logging.html +2 -2
  55. nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +2 -2
  56. nautobot/project-static/docs/code-reference/nautobot/apps/config.html +2 -2
  57. nautobot/project-static/docs/code-reference/nautobot/apps/constants.html +2 -2
  58. nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +2 -2
  59. nautobot/project-static/docs/code-reference/nautobot/apps/exceptions.html +2 -2
  60. nautobot/project-static/docs/code-reference/nautobot/apps/factory.html +2 -2
  61. nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +2 -2
  62. nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +4 -4
  63. nautobot/project-static/docs/code-reference/nautobot/apps/graphql.html +2 -2
  64. nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +2 -2
  65. nautobot/project-static/docs/code-reference/nautobot/apps/models.html +2 -2
  66. nautobot/project-static/docs/code-reference/nautobot/apps/querysets.html +2 -2
  67. nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +2 -2
  68. nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +2 -2
  69. nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +2 -2
  70. nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +2 -2
  71. nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +2 -2
  72. nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +2 -2
  73. nautobot/project-static/docs/code-reference/nautobot/apps/views.html +2 -2
  74. nautobot/project-static/docs/development/apps/api/configuration-view.html +2 -2
  75. nautobot/project-static/docs/development/apps/api/database-backend-config.html +2 -2
  76. nautobot/project-static/docs/development/apps/api/models/django-admin.html +2 -2
  77. nautobot/project-static/docs/development/apps/api/models/global-search.html +2 -2
  78. nautobot/project-static/docs/development/apps/api/models/graphql.html +2 -2
  79. nautobot/project-static/docs/development/apps/api/models/index.html +4 -4
  80. nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +2 -2
  81. nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +2 -2
  82. nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +2 -2
  83. nautobot/project-static/docs/development/apps/api/platform-features/git-repository-content.html +2 -2
  84. nautobot/project-static/docs/development/apps/api/platform-features/index.html +2 -2
  85. nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +2 -2
  86. nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +2 -2
  87. nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +2 -2
  88. nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +2 -2
  89. nautobot/project-static/docs/development/apps/api/platform-features/uniquely-identify-objects.html +2 -2
  90. nautobot/project-static/docs/development/apps/api/prometheus.html +2 -2
  91. nautobot/project-static/docs/development/apps/api/setup.html +2 -2
  92. nautobot/project-static/docs/development/apps/api/testing.html +2 -2
  93. nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +2 -2
  94. nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +2 -2
  95. nautobot/project-static/docs/development/apps/api/ui-extensions/index.html +2 -2
  96. nautobot/project-static/docs/development/apps/api/ui-extensions/navigation.html +2 -2
  97. nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +2 -2
  98. nautobot/project-static/docs/development/apps/api/views/base-template.html +2 -2
  99. nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +2 -2
  100. nautobot/project-static/docs/development/apps/api/views/django-generic-views.html +2 -2
  101. nautobot/project-static/docs/development/apps/api/views/help-documentation.html +2 -2
  102. nautobot/project-static/docs/development/apps/api/views/index.html +2 -2
  103. nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +2 -2
  104. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +2 -2
  105. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +2 -2
  106. nautobot/project-static/docs/development/apps/api/views/notes.html +2 -2
  107. nautobot/project-static/docs/development/apps/api/views/rest-api.html +2 -2
  108. nautobot/project-static/docs/development/apps/api/views/urls.html +2 -2
  109. nautobot/project-static/docs/development/apps/index.html +2 -2
  110. nautobot/project-static/docs/development/apps/migration/code-updates.html +2 -2
  111. nautobot/project-static/docs/development/apps/migration/dependency-updates.html +2 -2
  112. nautobot/project-static/docs/development/apps/migration/from-v1.html +2 -2
  113. nautobot/project-static/docs/development/apps/migration/model-updates/dcim.html +2 -2
  114. nautobot/project-static/docs/development/apps/migration/model-updates/extras.html +2 -2
  115. nautobot/project-static/docs/development/apps/migration/model-updates/global.html +2 -2
  116. nautobot/project-static/docs/development/apps/migration/model-updates/ipam.html +2 -2
  117. nautobot/project-static/docs/development/apps/porting-from-netbox.html +2 -2
  118. nautobot/project-static/docs/development/core/application-registry.html +2 -2
  119. nautobot/project-static/docs/development/core/best-practices.html +2 -2
  120. nautobot/project-static/docs/development/core/bootstrap-ui.html +2 -2
  121. nautobot/project-static/docs/development/core/caching.html +2 -2
  122. nautobot/project-static/docs/development/core/controllers.html +2 -2
  123. nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +2 -2
  124. nautobot/project-static/docs/development/core/generic-views.html +2 -2
  125. nautobot/project-static/docs/development/core/getting-started.html +7 -6
  126. nautobot/project-static/docs/development/core/homepage.html +2 -2
  127. nautobot/project-static/docs/development/core/index.html +2 -2
  128. nautobot/project-static/docs/development/core/model-checklist.html +2 -2
  129. nautobot/project-static/docs/development/core/model-features.html +2 -2
  130. nautobot/project-static/docs/development/core/natural-keys.html +2 -2
  131. nautobot/project-static/docs/development/core/navigation-menu.html +2 -2
  132. nautobot/project-static/docs/development/core/release-checklist.html +2 -2
  133. nautobot/project-static/docs/development/core/role-internals.html +2 -2
  134. nautobot/project-static/docs/development/core/settings.html +2 -2
  135. nautobot/project-static/docs/development/core/style-guide.html +2 -2
  136. nautobot/project-static/docs/development/core/templates.html +2 -2
  137. nautobot/project-static/docs/development/core/testing.html +12 -4
  138. nautobot/project-static/docs/development/core/user-preferences.html +2 -2
  139. nautobot/project-static/docs/development/index.html +2 -2
  140. nautobot/project-static/docs/development/jobs/index.html +2 -2
  141. nautobot/project-static/docs/development/jobs/migration/from-v1.html +2 -2
  142. nautobot/project-static/docs/index.html +2 -2
  143. nautobot/project-static/docs/objects.inv +0 -0
  144. nautobot/project-static/docs/overview/application_stack.html +2 -2
  145. nautobot/project-static/docs/overview/design_philosophy.html +2 -2
  146. nautobot/project-static/docs/release-notes/index.html +2 -2
  147. nautobot/project-static/docs/release-notes/version-1.0.html +2 -2
  148. nautobot/project-static/docs/release-notes/version-1.1.html +2 -2
  149. nautobot/project-static/docs/release-notes/version-1.2.html +2 -2
  150. nautobot/project-static/docs/release-notes/version-1.3.html +2 -2
  151. nautobot/project-static/docs/release-notes/version-1.4.html +2 -2
  152. nautobot/project-static/docs/release-notes/version-1.5.html +2 -2
  153. nautobot/project-static/docs/release-notes/version-1.6.html +2 -2
  154. nautobot/project-static/docs/release-notes/version-2.0.html +2 -2
  155. nautobot/project-static/docs/release-notes/version-2.1.html +2 -2
  156. nautobot/project-static/docs/release-notes/version-2.2.html +2 -2
  157. nautobot/project-static/docs/release-notes/version-2.3.html +402 -80
  158. nautobot/project-static/docs/search/search_index.json +1 -1
  159. nautobot/project-static/docs/sitemap.xml +269 -269
  160. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  161. nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +2 -2
  162. nautobot/project-static/docs/user-guide/administration/configuration/authentication/remote.html +2 -2
  163. nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +2 -2
  164. nautobot/project-static/docs/user-guide/administration/configuration/index.html +2 -2
  165. nautobot/project-static/docs/user-guide/administration/configuration/redis.html +2 -2
  166. nautobot/project-static/docs/user-guide/administration/configuration/settings.html +2 -2
  167. nautobot/project-static/docs/user-guide/administration/configuration/time-zones.html +2 -2
  168. nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +2 -2
  169. nautobot/project-static/docs/user-guide/administration/guides/docker.html +2 -2
  170. nautobot/project-static/docs/user-guide/administration/guides/health-checks.html +2 -2
  171. nautobot/project-static/docs/user-guide/administration/guides/permissions.html +2 -2
  172. nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +2 -2
  173. nautobot/project-static/docs/user-guide/administration/guides/replicating-nautobot.html +2 -2
  174. nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +2 -2
  175. nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +2 -2
  176. nautobot/project-static/docs/user-guide/administration/guides/selinux-troubleshooting.html +2 -2
  177. nautobot/project-static/docs/user-guide/administration/installation/app-install.html +2 -2
  178. nautobot/project-static/docs/user-guide/administration/installation/external-authentication.html +2 -2
  179. nautobot/project-static/docs/user-guide/administration/installation/http-server.html +2 -2
  180. nautobot/project-static/docs/user-guide/administration/installation/index.html +2 -2
  181. nautobot/project-static/docs/user-guide/administration/installation/install_system.html +2 -2
  182. nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +2 -2
  183. nautobot/project-static/docs/user-guide/administration/installation/services.html +2 -2
  184. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-netbox.html +2 -2
  185. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +2 -2
  186. nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +2 -2
  187. nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +2 -2
  188. nautobot/project-static/docs/user-guide/administration/upgrading/database-backup.html +2 -2
  189. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/after-you-upgrade.html +2 -2
  190. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/before-you-upgrade.html +2 -2
  191. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/for-developers.html +2 -2
  192. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/index.html +2 -2
  193. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/whats-changed.html +2 -2
  194. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/region-and-site-data-migration-guide.html +2 -2
  195. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +2 -2
  196. nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +2 -2
  197. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuit.html +2 -2
  198. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittermination.html +2 -2
  199. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittype.html +2 -2
  200. nautobot/project-static/docs/user-guide/core-data-model/circuits/provider.html +2 -2
  201. nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +2 -2
  202. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloud.html +2 -2
  203. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudaccount.html +2 -2
  204. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetwork.html +2 -2
  205. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetworkprefixassignment.html +2 -2
  206. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudresourcetype.html +2 -2
  207. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservice.html +2 -2
  208. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservicenetworkassignment.html +2 -2
  209. nautobot/project-static/docs/user-guide/core-data-model/dcim/cable.html +2 -2
  210. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +2 -2
  211. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +2 -2
  212. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +2 -2
  213. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +2 -2
  214. nautobot/project-static/docs/user-guide/core-data-model/dcim/controller.html +2 -2
  215. nautobot/project-static/docs/user-guide/core-data-model/dcim/controllermanageddevicegroup.html +2 -2
  216. nautobot/project-static/docs/user-guide/core-data-model/dcim/device.html +2 -2
  217. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +2 -2
  218. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +2 -2
  219. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicefamily.html +2 -2
  220. nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +2 -2
  221. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +2 -2
  222. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +2 -2
  223. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +2 -2
  224. nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +2 -2
  225. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +2 -2
  226. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +2 -2
  227. nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +2 -2
  228. nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +2 -2
  229. nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +2 -2
  230. nautobot/project-static/docs/user-guide/core-data-model/dcim/manufacturer.html +2 -2
  231. nautobot/project-static/docs/user-guide/core-data-model/dcim/module.html +2 -2
  232. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebay.html +2 -2
  233. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebaytemplate.html +2 -2
  234. nautobot/project-static/docs/user-guide/core-data-model/dcim/moduletype.html +2 -2
  235. nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +2 -2
  236. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerfeed.html +2 -2
  237. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +2 -2
  238. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +2 -2
  239. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerpanel.html +2 -2
  240. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +2 -2
  241. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +2 -2
  242. nautobot/project-static/docs/user-guide/core-data-model/dcim/rack.html +2 -2
  243. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackgroup.html +2 -2
  244. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackreservation.html +2 -2
  245. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +2 -2
  246. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +2 -2
  247. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +2 -2
  248. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareversion.html +2 -2
  249. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualchassis.html +2 -2
  250. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +2 -2
  251. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +2 -2
  252. nautobot/project-static/docs/user-guide/core-data-model/extras/contact.html +2 -2
  253. nautobot/project-static/docs/user-guide/core-data-model/extras/team.html +2 -2
  254. nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +2 -2
  255. nautobot/project-static/docs/user-guide/core-data-model/ipam/namespace.html +2 -2
  256. nautobot/project-static/docs/user-guide/core-data-model/ipam/prefix.html +2 -2
  257. nautobot/project-static/docs/user-guide/core-data-model/ipam/rir.html +2 -2
  258. nautobot/project-static/docs/user-guide/core-data-model/ipam/routetarget.html +2 -2
  259. nautobot/project-static/docs/user-guide/core-data-model/ipam/service.html +2 -2
  260. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +2 -2
  261. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlangroup.html +307 -2
  262. nautobot/project-static/docs/user-guide/core-data-model/ipam/vrf.html +2 -2
  263. nautobot/project-static/docs/user-guide/core-data-model/overview/introduction.html +2 -2
  264. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenant.html +2 -2
  265. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenantgroup.html +2 -2
  266. nautobot/project-static/docs/user-guide/core-data-model/virtualization/cluster.html +2 -2
  267. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustergroup.html +2 -2
  268. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustertype.html +2 -2
  269. nautobot/project-static/docs/user-guide/core-data-model/virtualization/virtualmachine.html +2 -2
  270. nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +2 -2
  271. nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +2 -2
  272. nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +2 -2
  273. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +2 -2
  274. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +2 -2
  275. nautobot/project-static/docs/user-guide/feature-guides/getting-started/index.html +2 -2
  276. nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +2 -2
  277. nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +2 -2
  278. nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +2 -2
  279. nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +2 -2
  280. nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +2 -2
  281. nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +2 -2
  282. nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +2 -2
  283. nautobot/project-static/docs/user-guide/feature-guides/graphql.html +2 -2
  284. nautobot/project-static/docs/user-guide/feature-guides/ip-address-merge-tool.html +2 -2
  285. nautobot/project-static/docs/user-guide/feature-guides/relationships.html +2 -2
  286. nautobot/project-static/docs/user-guide/feature-guides/software-image-files-and-versions.html +2 -2
  287. nautobot/project-static/docs/user-guide/index.html +2 -2
  288. nautobot/project-static/docs/user-guide/platform-functionality/change-logging.html +2 -2
  289. nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +2 -2
  290. nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +2 -2
  291. nautobot/project-static/docs/user-guide/platform-functionality/customlink.html +2 -2
  292. nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +2 -2
  293. nautobot/project-static/docs/user-guide/platform-functionality/exporttemplate.html +2 -2
  294. nautobot/project-static/docs/user-guide/platform-functionality/externalintegration.html +2 -2
  295. nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +2 -2
  296. nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +2 -2
  297. nautobot/project-static/docs/user-guide/platform-functionality/graphqlquery.html +2 -2
  298. nautobot/project-static/docs/user-guide/platform-functionality/imageattachment.html +2 -2
  299. nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +2 -2
  300. nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +2 -2
  301. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +2 -2
  302. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +2 -2
  303. nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +2 -2
  304. nautobot/project-static/docs/user-guide/platform-functionality/napalm.html +2 -2
  305. nautobot/project-static/docs/user-guide/platform-functionality/note.html +2 -2
  306. nautobot/project-static/docs/user-guide/platform-functionality/objectmetadata.html +2 -2
  307. nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +2 -2
  308. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +2 -2
  309. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +2 -2
  310. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +2 -2
  311. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/ui-related-endpoints.html +2 -2
  312. nautobot/project-static/docs/user-guide/platform-functionality/role.html +2 -2
  313. nautobot/project-static/docs/user-guide/platform-functionality/savedview.html +2 -2
  314. nautobot/project-static/docs/user-guide/platform-functionality/secret.html +2 -2
  315. nautobot/project-static/docs/user-guide/platform-functionality/staticgroupassociation.html +2 -2
  316. nautobot/project-static/docs/user-guide/platform-functionality/status.html +2 -2
  317. nautobot/project-static/docs/user-guide/platform-functionality/tag.html +2 -2
  318. nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +2 -2
  319. nautobot/project-static/docs/user-guide/platform-functionality/users/objectpermission.html +2 -2
  320. nautobot/project-static/docs/user-guide/platform-functionality/users/token.html +2 -2
  321. nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +2 -2
  322. {nautobot-2.3.4.dist-info → nautobot-2.3.6.dist-info}/METADATA +2 -2
  323. {nautobot-2.3.4.dist-info → nautobot-2.3.6.dist-info}/RECORD +327 -325
  324. {nautobot-2.3.4.dist-info → nautobot-2.3.6.dist-info}/LICENSE.txt +0 -0
  325. {nautobot-2.3.4.dist-info → nautobot-2.3.6.dist-info}/NOTICE +0 -0
  326. {nautobot-2.3.4.dist-info → nautobot-2.3.6.dist-info}/WHEEL +0 -0
  327. {nautobot-2.3.4.dist-info → nautobot-2.3.6.dist-info}/entry_points.txt +0 -0
@@ -65,7 +65,6 @@ from nautobot.dcim.models import (
65
65
  from nautobot.extras import context_managers
66
66
  from nautobot.extras.choices import CustomFieldTypeChoices
67
67
  from nautobot.extras.models import CustomField, Role, SecretsGroup, Status
68
- from nautobot.ipam.factory import VLANGroupFactory
69
68
  from nautobot.ipam.models import IPAddress, IPAddressToInterface, Namespace, Prefix, VLAN, VLANGroup
70
69
  from nautobot.tenancy.models import Tenant
71
70
  from nautobot.users.models import User
@@ -2351,7 +2350,6 @@ class InterfaceTestCase(ModularDeviceComponentTestCaseMixin, ModelTestCases.Base
2351
2350
  vid=100,
2352
2351
  location=location_2,
2353
2352
  status=vlan_status,
2354
- vlan_group=VLANGroupFactory.create(location=location_2),
2355
2353
  )
2356
2354
 
2357
2355
  cls.namespace = Namespace.objects.create(name="dcim_test_interface_ip_addresses")
nautobot/dcim/views.py CHANGED
@@ -3148,7 +3148,7 @@ class DeviceBayPopulateView(generic.ObjectEditView):
3148
3148
  f"Added {device_bay.installed_device} to {device_bay}.",
3149
3149
  )
3150
3150
 
3151
- return redirect("dcim:device", pk=device_bay.device.pk)
3151
+ return redirect("dcim:device_devicebays", pk=device_bay.device.pk)
3152
3152
 
3153
3153
  return render(
3154
3154
  request,
@@ -3191,7 +3191,7 @@ class DeviceBayDepopulateView(generic.ObjectEditView):
3191
3191
  f"Removed {removed_device} from {device_bay}.",
3192
3192
  )
3193
3193
 
3194
- return redirect("dcim:device", pk=device_bay.device.pk)
3194
+ return redirect("dcim:device_devicebays", pk=device_bay.device.pk)
3195
3195
 
3196
3196
  return render(
3197
3197
  request,
@@ -4139,9 +4139,12 @@ class DeviceFamilyUIViewSet(NautobotUIViewSet):
4139
4139
  context["device_type_table"] = device_type_table
4140
4140
 
4141
4141
  total_devices = 0
4142
+ device_type_count = 0
4142
4143
  for device_type in device_types:
4143
4144
  total_devices += device_type.device_count
4145
+ device_type_count += 1
4144
4146
  context["total_devices"] = total_devices
4147
+ context["device_type_count"] = device_type_count
4145
4148
 
4146
4149
  return context
4147
4150
 
@@ -1,5 +1,6 @@
1
1
  from django.conf import settings
2
2
  from django.contrib.contenttypes.models import ContentType
3
+ from django.db.models import ProtectedError
3
4
  from django.forms import ValidationError as FormsValidationError
4
5
  from django.http import FileResponse, Http404
5
6
  from django.shortcuts import get_object_or_404
@@ -729,6 +730,14 @@ class JobViewSet(
729
730
  ):
730
731
  lookup_value_regex = r"[-0-9a-fA-F]+"
731
732
 
733
+ def perform_destroy(self, obj):
734
+ if obj.module_name.startswith("nautobot."):
735
+ raise ProtectedError(
736
+ f"Unable to delete Job {obj}. System Job cannot be deleted",
737
+ [],
738
+ )
739
+ super().perform_destroy(obj)
740
+
732
741
 
733
742
  @extend_schema_view(
734
743
  destroy=extend_schema(operation_id="extras_jobs_destroy_by_name"),
@@ -246,7 +246,8 @@ class MetadataTypeFactory(PrimaryModelFactory):
246
246
  lambda: ContentType.objects.filter(
247
247
  FeatureQuery("metadata").get_query(), pk__in=existing_content_type_pks
248
248
  ),
249
- minimum=1,
249
+ minimum=3,
250
+ maximum=5,
250
251
  )
251
252
  )
252
253
 
@@ -11,7 +11,7 @@ from django.contrib.contenttypes.models import ContentType
11
11
  from django.core.exceptions import ValidationError
12
12
  from django.core.validators import MinValueValidator
13
13
  from django.db import models, transaction
14
- from django.db.models import signals
14
+ from django.db.models import ProtectedError, signals
15
15
  from django.utils import timezone
16
16
  from django.utils.functional import cached_property
17
17
  from django_celery_beat.clockedschedule import clocked
@@ -234,6 +234,14 @@ class Job(PrimaryModel):
234
234
  def __str__(self):
235
235
  return self.name
236
236
 
237
+ def delete(self):
238
+ if self.module_name.startswith("nautobot."):
239
+ raise ProtectedError(
240
+ f"Unable to delete Job {self}. System Job cannot be deleted",
241
+ [],
242
+ )
243
+ super().delete()
244
+
237
245
  @property
238
246
  def job_class(self):
239
247
  """
@@ -575,6 +575,8 @@ class FileAttachment(BaseModel):
575
575
  filename = models.CharField(max_length=CHARFIELD_MAX_LENGTH)
576
576
  mimetype = models.CharField(max_length=CHARFIELD_MAX_LENGTH)
577
577
 
578
+ is_metadata_associable_model = False
579
+
578
580
  natural_key_field_names = ["pk"]
579
581
 
580
582
  def __str__(self):
@@ -1,6 +1,6 @@
1
1
  from django.conf import settings
2
2
  from django.contrib.contenttypes.models import ContentType
3
- from django.db.models import F, Model, OuterRef, Q, Subquery
3
+ from django.db.models import F, Model, OuterRef, ProtectedError, Q, Subquery
4
4
  from django.db.models.functions import JSONObject
5
5
 
6
6
  from nautobot.core.models.query_functions import EmptyGroupByJSONBAgg
@@ -224,6 +224,15 @@ class JobQuerySet(RestrictedQuerySet):
224
224
  Extend the standard queryset with a get_for_class_path method.
225
225
  """
226
226
 
227
+ def delete(self):
228
+ for job in self:
229
+ if job.module_name.startswith("nautobot."):
230
+ raise ProtectedError(
231
+ f"Unable to delete Job {job}. System Job cannot be deleted",
232
+ [],
233
+ )
234
+ return super().delete()
235
+
227
236
  def get_for_class_path(self, class_path):
228
237
  try:
229
238
  module_name, job_class_name = class_path.rsplit(".", 1)
nautobot/extras/tables.py CHANGED
@@ -1031,6 +1031,9 @@ class ObjectMetadataTable(BaseTable):
1031
1031
  class NoteTable(BaseTable):
1032
1032
  actions = ButtonsColumn(Note)
1033
1033
  created = tables.LinkColumn()
1034
+ note = tables.Column(
1035
+ attrs={"td": {"class": "rendered-markdown"}},
1036
+ )
1034
1037
 
1035
1038
  class Meta(BaseTable.Meta):
1036
1039
  model = Note
@@ -1388,6 +1388,42 @@ class JobTest(
1388
1388
  job_model = Job.objects.get_for_class_path(class_path)
1389
1389
  return reverse("extras-api:job-run", kwargs={"pk": job_model.pk})
1390
1390
 
1391
+ def get_deletable_object(self):
1392
+ """
1393
+ Get an instance that can be deleted.
1394
+ Exclude system jobs
1395
+ """
1396
+ # filter out the system jobs:
1397
+ queryset = self._get_queryset().exclude(module_name__startswith="nautobot.")
1398
+ instance = get_deletable_objects(self.model, queryset).first()
1399
+ if instance is None:
1400
+ self.fail("Couldn't find a single deletable object!")
1401
+ return instance
1402
+
1403
+ def get_deletable_object_pks(self):
1404
+ """
1405
+ Get a list of PKs corresponding to jobs that can be safely bulk-deleted.
1406
+ Exclude system jobs
1407
+ """
1408
+ queryset = self._get_queryset().exclude(module_name__startswith="nautobot.")
1409
+ instances = get_deletable_objects(self.model, queryset).values_list("pk", flat=True)[:3]
1410
+ if len(instances) < 3:
1411
+ self.fail(f"Couldn't find 3 deletable objects, only found {len(instances)}!")
1412
+ return instances
1413
+
1414
+ def test_delete_system_jobs_fail(self):
1415
+ self.add_permissions("extras.delete_job")
1416
+ instance = self._get_queryset().filter(module_name__startswith="nautobot.").first()
1417
+ job_name = instance.name
1418
+ url = self._get_detail_url(instance)
1419
+ self.client.delete(url, **self.header)
1420
+ # assert Job still exists
1421
+ self.assertTrue(self._get_queryset().filter(name=job_name).exists())
1422
+ self.user.is_superuser = True
1423
+ self.client.delete(url, **self.header)
1424
+ # assert Job still exists
1425
+ self.assertTrue(self._get_queryset().filter(name=job_name).exists())
1426
+
1391
1427
  def test_get_job_variables(self):
1392
1428
  """Test the job/<pk>/variables API endpoint."""
1393
1429
  self.add_permissions("extras.view_job")
@@ -4044,14 +4080,14 @@ class TagTest(APIViewTestCases.APIViewTestCase):
4044
4080
  def test_create_tags_with_invalid_content_types(self):
4045
4081
  self.add_permissions("extras.add_tag")
4046
4082
 
4047
- # VLANGroup is an OrganizationalModel, not a PrimaryModel, and therefore does not support tags
4048
- data = {**self.create_data[0], "content_types": [VLANGroup._meta.label_lower]}
4083
+ # Manufacturer is an OrganizationalModel, not a PrimaryModel, and therefore does not support tags
4084
+ data = {**self.create_data[0], "content_types": [Manufacturer._meta.label_lower]}
4049
4085
  response = self.client.post(self._get_list_url(), data, format="json", **self.header)
4050
4086
 
4051
4087
  tag = Tag.objects.filter(name=data["name"])
4052
4088
  self.assertHttpStatus(response, 400)
4053
4089
  self.assertFalse(tag.exists())
4054
- self.assertIn(f"Invalid content type: {VLANGroup._meta.label_lower}", response.data["content_types"])
4090
+ self.assertIn(f"Invalid content type: {Manufacturer._meta.label_lower}", response.data["content_types"])
4055
4091
 
4056
4092
  def test_create_tags_without_content_types(self):
4057
4093
  self.add_permissions("extras.add_tag")
@@ -388,6 +388,7 @@ class RelationshipModelFormTestCase(TestCase):
388
388
  cls.vlangroup_form_base_data = {
389
389
  "location": cls.location.pk,
390
390
  "name": "New VLAN Group",
391
+ "range": "1-4094",
391
392
  }
392
393
 
393
394
  def test_create_relationship_associations_valid_1(self):
@@ -665,6 +666,7 @@ class RelationshipModelFormTestCase(TestCase):
665
666
  data={
666
667
  "name": self.vlangroup_1.name,
667
668
  "location": self.location,
669
+ "range": "1-4094",
668
670
  f"cr_{self.relationship_2.key}__source": self.device_2.pk,
669
671
  },
670
672
  )
@@ -18,7 +18,7 @@ from nautobot.core.choices import ColorChoices
18
18
  from nautobot.core.models.fields import slugify_dashes_to_underscores
19
19
  from nautobot.core.templatetags.helpers import bettertitle
20
20
  from nautobot.core.testing import extract_form_failures, extract_page_body, ModelViewTestCase, TestCase, ViewTestCases
21
- from nautobot.core.testing.utils import disable_warnings, post_data
21
+ from nautobot.core.testing.utils import disable_warnings, get_deletable_objects, post_data
22
22
  from nautobot.core.utils.permissions import get_permission_for_model
23
23
  from nautobot.dcim.models import (
24
24
  ConsolePort,
@@ -1840,6 +1840,9 @@ class ApprovalQueueTestCase(
1840
1840
  return reverse("extras:scheduledjob_approval_request_view", kwargs={"pk": instance.pk})
1841
1841
  raise ValueError("This override is only valid for list and view test cases")
1842
1842
 
1843
+ def get_list_url(self):
1844
+ return reverse("extras:scheduledjob_approval_queue_list")
1845
+
1843
1846
  def setUp(self):
1844
1847
  super().setUp()
1845
1848
  self.job_model = Job.objects.get_for_class_path("dry_run.TestDryRun")
@@ -2363,6 +2366,78 @@ class JobTestCase(
2363
2366
  "clear_task_queues_override": False,
2364
2367
  }
2365
2368
 
2369
+ def get_deletable_object(self):
2370
+ """
2371
+ Get an instance that can be deleted.
2372
+ Exclude system jobs
2373
+ """
2374
+ # filter out the system jobs:
2375
+ queryset = self._get_queryset().exclude(module_name__startswith="nautobot.")
2376
+ return get_deletable_objects(self.model, queryset).first()
2377
+
2378
+ def get_deletable_object_pks(self):
2379
+ """
2380
+ Get a list of PKs corresponding to jobs that can be safely bulk-deleted.
2381
+ Excluding system jobs
2382
+ """
2383
+ queryset = self._get_queryset().exclude(module_name__startswith="nautobot.")
2384
+ return get_deletable_objects(self.model, queryset).values_list("pk", flat=True)[:3]
2385
+
2386
+ def test_delete_system_jobs_fail(self):
2387
+ instance = self._get_queryset().filter(module_name__startswith="nautobot.").first()
2388
+ job_name = instance.name
2389
+ request = {
2390
+ "path": self._get_url("delete", instance),
2391
+ "data": post_data({"confirm": True}),
2392
+ }
2393
+
2394
+ # Try delete with delete job permission
2395
+ self.add_permissions("extras.delete_job")
2396
+ response = self.client.post(**request, follow=True)
2397
+ self.assertHttpStatus(response, 403)
2398
+ response_body = extract_page_body(response.content.decode(response.charset))
2399
+ self.assertIn(f"Unable to delete Job {instance}. System Job cannot be deleted", response_body)
2400
+ # assert Job still exists
2401
+ self.assertTrue(self._get_queryset().filter(name=job_name).exists())
2402
+
2403
+ # Try delete as a superuser
2404
+ self.user.is_superuser = True
2405
+ response = self.client.post(**request, follow=True)
2406
+ self.assertHttpStatus(response, 403)
2407
+ response_body = extract_page_body(response.content.decode(response.charset))
2408
+ self.assertIn(f"Unable to delete Job {instance}. System Job cannot be deleted", response_body)
2409
+ # assert Job still exists
2410
+ self.assertTrue(self._get_queryset().filter(name=job_name).exists())
2411
+
2412
+ def test_bulk_delete_system_jobs_fail(self):
2413
+ system_job_queryset = self.model.objects.filter(module_name__startswith="nautobot.")
2414
+ pk_list = system_job_queryset.values_list("pk", flat=True)[:3]
2415
+ initial_count = self._get_queryset().count()
2416
+ data = {
2417
+ "pk": pk_list,
2418
+ "confirm": True,
2419
+ "_confirm": True, # Form button
2420
+ }
2421
+ # Try bulk delete with delete job permission
2422
+ self.add_permissions("extras.delete_job")
2423
+ response = self.client.post(self._get_url("bulk_delete"), data, follow=True)
2424
+ self.assertHttpStatus(response, 403)
2425
+ self.assertEqual(self._get_queryset().count(), initial_count)
2426
+ response_body = extract_page_body(response.content.decode(response.charset))
2427
+ self.assertIn(
2428
+ f"Unable to delete Job {system_job_queryset.first()}. System Job cannot be deleted", response_body
2429
+ )
2430
+
2431
+ # Try bulk delete as a superuser
2432
+ self.user.is_superuser = True
2433
+ response = self.client.post(self._get_url("bulk_delete"), data, follow=True)
2434
+ self.assertHttpStatus(response, 403)
2435
+ self.assertEqual(self._get_queryset().count(), initial_count)
2436
+ response_body = extract_page_body(response.content.decode(response.charset))
2437
+ self.assertIn(
2438
+ f"Unable to delete Job {system_job_queryset.first()}. System Job cannot be deleted", response_body
2439
+ )
2440
+
2366
2441
  def validate_job_data_after_bulk_edit(self, pk_list, old_data):
2367
2442
  # Name is bulk-editable
2368
2443
  overridable_fields = [field for field in JOB_OVERRIDABLE_FIELDS if field != "name"]
@@ -3520,11 +3595,11 @@ class TagTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
3520
3595
 
3521
3596
  def test_create_tags_with_invalid_content_types(self):
3522
3597
  self.add_permissions("extras.add_tag")
3523
- vlangroup_content_type = ContentType.objects.get_for_model(VLANGroup)
3598
+ manufacturer_content_type = ContentType.objects.get_for_model(Manufacturer)
3524
3599
 
3525
3600
  form_data = {
3526
3601
  **self.form_data,
3527
- "content_types": [vlangroup_content_type.id],
3602
+ "content_types": [manufacturer_content_type.id],
3528
3603
  }
3529
3604
 
3530
3605
  request = {
nautobot/extras/views.py CHANGED
@@ -4,7 +4,6 @@ from urllib.parse import parse_qs
4
4
  from celery import chain
5
5
  from django.conf import settings
6
6
  from django.contrib import messages
7
- from django.contrib.auth.models import AnonymousUser
8
7
  from django.contrib.contenttypes.models import ContentType
9
8
  from django.core.exceptions import ObjectDoesNotExist, ValidationError
10
9
  from django.db import IntegrityError, transaction
@@ -23,6 +22,7 @@ from django.views.generic import View
23
22
  from django_tables2 import RequestConfig
24
23
  from jsonschema.validators import Draft7Validator
25
24
  from rest_framework.decorators import action
25
+ from rest_framework.permissions import IsAuthenticated
26
26
 
27
27
  try:
28
28
  from zoneinfo import ZoneInfo
@@ -1650,6 +1650,9 @@ class SavedViewUIViewSet(
1650
1650
  serializer_class = serializers.SavedViewSerializer
1651
1651
  table_class = tables.SavedViewTable
1652
1652
  action_buttons = ("export",)
1653
+ permission_classes = [
1654
+ IsAuthenticated,
1655
+ ]
1653
1656
 
1654
1657
  def alter_queryset(self, request):
1655
1658
  """
@@ -1676,15 +1679,15 @@ class SavedViewUIViewSet(
1676
1679
 
1677
1680
  def check_permissions(self, request):
1678
1681
  """
1679
- Override this method to not check any permissions.
1682
+ Override this method to not check any nautobot-specific object permissions and to only check if the user is authenticated.
1680
1683
  Since users with <app_label>.view_<model_name> permissions should be able to view saved views related to this model.
1681
1684
  And those permissions will be enforced in the related view.
1682
1685
  """
1683
-
1684
- def dispatch(self, request, *args, **kwargs):
1685
- if isinstance(request.user, AnonymousUser):
1686
- return self.handle_no_permission()
1687
- return super().dispatch(request, *args, **kwargs)
1686
+ for permission in self.get_permissions():
1687
+ if not permission.has_permission(request, self):
1688
+ self.permission_denied(
1689
+ request, message=getattr(permission, "message", None), code=getattr(permission, "code", None)
1690
+ )
1688
1691
 
1689
1692
  def extra_message_context(self, obj):
1690
1693
  """
@@ -118,7 +118,7 @@ class RIRSerializer(NautobotModelSerializer):
118
118
  #
119
119
 
120
120
 
121
- class VLANGroupSerializer(NautobotModelSerializer):
121
+ class VLANGroupSerializer(NautobotModelSerializer, TaggedModelSerializerMixin):
122
122
  vlan_count = serializers.IntegerField(read_only=True)
123
123
 
124
124
  class Meta:
@@ -481,6 +481,35 @@ class IPAllocationSerializer(NautobotModelSerializer, TaggedModelSerializerMixin
481
481
  return super().validate(data)
482
482
 
483
483
 
484
+ class VLANAllocationSerializer(NautobotModelSerializer, TaggedModelSerializerMixin):
485
+ """
486
+ Input serializer for POST to /api/ipam/vlan-groups/<id>/available-vlans/, i.e. allocating VLAN from VLANGroup.
487
+ """
488
+
489
+ vid = serializers.IntegerField(required=False, min_value=constants.VLAN_VID_MIN, max_value=constants.VLAN_VID_MAX)
490
+
491
+ def validate(self, data):
492
+ """Skip `ValidatedModel` validation.
493
+ This allows to skip `vid` attribute of `VLAN` model, while validate name and status.
494
+ """
495
+ return data
496
+
497
+ class Meta(VLANSerializer.Meta):
498
+ model = VLAN
499
+ fields = (
500
+ # permit "vid" and "vlan_group" for `VLAN` consistency.
501
+ # validate them under `VLANGroupViewSet`
502
+ "vid",
503
+ "vlan_group",
504
+ "name",
505
+ "status",
506
+ "role",
507
+ "tenant",
508
+ "description",
509
+ "custom_fields",
510
+ )
511
+
512
+
484
513
  #
485
514
  # IP address to interface
486
515
  #
@@ -6,12 +6,15 @@ from rest_framework import status
6
6
  from rest_framework.decorators import action
7
7
  from rest_framework.exceptions import APIException
8
8
  from rest_framework.response import Response
9
+ from rest_framework.serializers import IntegerField, ListSerializer
9
10
 
11
+ from nautobot.core.api.authentication import TokenPermissions
10
12
  from nautobot.core.models.querysets import count_related
11
13
  from nautobot.core.utils.config import get_settings_or_config
12
14
  from nautobot.dcim.models import Location
13
15
  from nautobot.extras.api.views import NautobotModelViewSet
14
16
  from nautobot.ipam import filters
17
+ from nautobot.ipam.api import serializers
15
18
  from nautobot.ipam.models import (
16
19
  IPAddress,
17
20
  IPAddressToInterface,
@@ -29,8 +32,6 @@ from nautobot.ipam.models import (
29
32
  VRFPrefixAssignment,
30
33
  )
31
34
 
32
- from . import serializers
33
-
34
35
  #
35
36
  # Namespace
36
37
  #
@@ -387,10 +388,171 @@ class IPAddressToInterfaceViewSet(NautobotModelViewSet):
387
388
 
388
389
 
389
390
  class VLANGroupViewSet(NautobotModelViewSet):
390
- queryset = VLANGroup.objects.select_related("location").annotate(vlan_count=count_related(VLAN, "vlan_group"))
391
+ queryset = (
392
+ VLANGroup.objects.select_related("location")
393
+ .prefetch_related("tags")
394
+ .annotate(vlan_count=count_related(VLAN, "vlan_group"))
395
+ )
391
396
  serializer_class = serializers.VLANGroupSerializer
392
397
  filterset_class = filters.VLANGroupFilterSet
393
398
 
399
+ def restrict_queryset(self, request, *args, **kwargs):
400
+ """
401
+ Apply "view" permissions on the POST /available-vlans/ endpoint, otherwise as ModelViewSetMixin.
402
+ """
403
+ if request.user.is_authenticated and self.action == "available_vlans":
404
+ self.queryset = self.queryset.restrict(request.user, "view")
405
+ else:
406
+ super().restrict_queryset(request, *args, **kwargs)
407
+
408
+ class AvailableVLANPermissions(TokenPermissions):
409
+ """As nautobot.core.api.authentication.TokenPermissions, but enforcing add_vlan permission."""
410
+
411
+ perms_map = {
412
+ "GET": ["ipam.view_vlangroup"],
413
+ "POST": ["ipam.view_vlangroup", "ipam.add_vlan"],
414
+ }
415
+
416
+ @extend_schema(methods=["get"], responses={200: ListSerializer(child=IntegerField())})
417
+ @extend_schema(
418
+ methods=["post"],
419
+ responses={201: serializers.VLANSerializer(many=True)},
420
+ request=serializers.VLANAllocationSerializer(many=True),
421
+ )
422
+ @action(
423
+ detail=True,
424
+ name="Available VLAN IDs",
425
+ url_path="available-vlans",
426
+ methods=["get", "post"],
427
+ permission_classes=[AvailableVLANPermissions],
428
+ filterset_class=None,
429
+ )
430
+ def available_vlans(self, request, pk=None):
431
+ """
432
+ A convenience method for listing available VLAN IDs within a VLANGroup.
433
+ By default, the number of VIDs returned will be equivalent to PAGINATE_COUNT.
434
+ An arbitrary limit (up to MAX_PAGE_SIZE, if set) may be passed, however results will not be paginated.
435
+ """
436
+ vlan_group = get_object_or_404(self.queryset, pk=pk)
437
+
438
+ if request.method == "POST":
439
+ with cache.lock(
440
+ "nautobot.ipam.api.views.available_vlans", blocking_timeout=5, timeout=settings.REDIS_LOCK_TIMEOUT
441
+ ):
442
+ # Normalize to a list of objects
443
+ serializer = serializers.VLANAllocationSerializer(
444
+ data=request.data if isinstance(request.data, list) else [request.data],
445
+ many=True,
446
+ context={
447
+ "request": request,
448
+ "vlan_group": vlan_group,
449
+ },
450
+ )
451
+ serializer.is_valid(raise_exception=True)
452
+ requested_vlans = serializer.validated_data
453
+
454
+ # Determine if the requested number of VLANs is available
455
+ available_vids = vlan_group.available_vids
456
+ if len(available_vids) < len(requested_vlans):
457
+ return Response(
458
+ {
459
+ "detail": (
460
+ f"An insufficient number of VLANs are available within the VLANGroup {vlan_group} "
461
+ f"({len(requested_vlans)} requested, {len(available_vids)} available)"
462
+ )
463
+ },
464
+ status=status.HTTP_204_NO_CONTENT,
465
+ )
466
+
467
+ # Prioritise and check for explicitly requested VIDs. Remove them from available_vids
468
+ for requested_vlan in requested_vlans:
469
+ # Check requested `vid` for availability.
470
+ # This will also catch if same `vid` was requested multiple times in a request.
471
+ if "vid" in requested_vlan and requested_vlan["vid"] not in available_vids:
472
+ return Response(
473
+ {"detail": f"VLAN {requested_vlan['vid']} is not available within the VLANGroup."},
474
+ status=status.HTTP_204_NO_CONTENT,
475
+ )
476
+ elif "vid" in requested_vlan and requested_vlan["vid"] in available_vids:
477
+ available_vids.remove(requested_vlan["vid"])
478
+
479
+ # Assign VLAN IDs from the list of VLANGroup's available VLAN IDs.
480
+ # Available_vids now does not contain explicitly requested vids.
481
+ _available_vids = iter(available_vids)
482
+
483
+ for requested_vlan in requested_vlans:
484
+ if "vid" not in requested_vlan:
485
+ requested_vlan["vid"] = next(_available_vids)
486
+
487
+ # Check requested `vlan_group`
488
+ if "vlan_group" in requested_vlan and requested_vlan["vlan_group"] != vlan_group:
489
+ return Response(
490
+ {
491
+ "detail": f"Invalid VLAN Group requested: {requested_vlan['vlan_group']}. "
492
+ f"Only VLAN Group {vlan_group} is permitted."
493
+ },
494
+ status=status.HTTP_204_NO_CONTENT,
495
+ )
496
+ else:
497
+ requested_vlan["vlan_group"] = vlan_group.pk
498
+
499
+ # Rewrite custom field data
500
+ requested_vlan["custom_fields"] = requested_vlan.pop("_custom_field_data", {})
501
+
502
+ # Initialize the serializer with a list or a single object depending on what was requested
503
+ context = {"request": request, "depth": 0}
504
+
505
+ if isinstance(request.data, list):
506
+ serializer = serializers.VLANSerializer(data=requested_vlans, many=True, context=context)
507
+ else:
508
+ serializer = serializers.VLANSerializer(data=requested_vlans[0], context=context)
509
+
510
+ # Create the new VLANs
511
+ serializer.is_valid(raise_exception=True)
512
+ serializer.save()
513
+
514
+ data = serializer.data
515
+
516
+ return Response(
517
+ data={
518
+ "count": len(data),
519
+ "next": None,
520
+ "previous": None,
521
+ "results": data,
522
+ },
523
+ status=status.HTTP_201_CREATED,
524
+ )
525
+
526
+ else:
527
+ try:
528
+ limit = int(request.query_params.get("limit", get_settings_or_config("PAGINATE_COUNT")))
529
+ except ValueError:
530
+ limit = get_settings_or_config("PAGINATE_COUNT")
531
+
532
+ if get_settings_or_config("MAX_PAGE_SIZE"):
533
+ limit = min(limit, get_settings_or_config("MAX_PAGE_SIZE"))
534
+
535
+ if isinstance(limit, int) and limit >= 0:
536
+ vids = vlan_group.available_vids[0:limit]
537
+ else:
538
+ vids = vlan_group.available_vids
539
+
540
+ serializer = ListSerializer(
541
+ child=IntegerField(),
542
+ data=vids,
543
+ )
544
+ serializer.is_valid(raise_exception=True)
545
+ data = serializer.validated_data
546
+
547
+ return Response(
548
+ {
549
+ "count": len(data),
550
+ "next": None,
551
+ "previous": None,
552
+ "results": data,
553
+ }
554
+ )
555
+
394
556
 
395
557
  #
396
558
  # VLANs
nautobot/ipam/filters.py CHANGED
@@ -539,7 +539,7 @@ class IPAddressToInterfaceFilterSet(NautobotFilterSet):
539
539
  class VLANGroupFilterSet(NautobotFilterSet, LocatableModelFilterSetMixin, NameSearchFilterSet):
540
540
  class Meta:
541
541
  model = VLANGroup
542
- fields = ["id", "name", "description"]
542
+ fields = ["id", "name", "description", "tags"]
543
543
 
544
544
 
545
545
  class VLANFilterSet(
nautobot/ipam/forms.py CHANGED
@@ -727,7 +727,9 @@ class VLANGroupForm(LocatableModelFormMixin, NautobotModelForm):
727
727
  fields = [
728
728
  "location",
729
729
  "name",
730
+ "range",
730
731
  "description",
732
+ "tags",
731
733
  ]
732
734
 
733
735
 
@@ -873,6 +875,12 @@ class ServiceForm(NautobotModelForm):
873
875
  base_field=forms.IntegerField(min_value=SERVICE_PORT_MIN, max_value=SERVICE_PORT_MAX),
874
876
  help_text="Comma-separated list of one or more port numbers. A range may be specified using a hyphen.",
875
877
  )
878
+ ip_addresses = DynamicModelMultipleChoiceField(
879
+ queryset=IPAddress.objects.all(),
880
+ required=False,
881
+ label="IP addresses",
882
+ query_params={"device_id": "$device", "virtual_machine_id": "$virtual_machine"},
883
+ )
876
884
 
877
885
  class Meta:
878
886
  model = Service
@@ -892,7 +900,6 @@ class ServiceForm(NautobotModelForm):
892
900
  }
893
901
  widgets = {
894
902
  "protocol": StaticSelect2(),
895
- "ip_addresses": StaticSelect2Multiple(),
896
903
  }
897
904
 
898
905
  def __init__(self, *args, **kwargs):