nautobot 2.3.5__py3-none-any.whl → 2.3.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of nautobot might be problematic. Click here for more details.

Files changed (337) hide show
  1. nautobot/__init__.py +4 -2
  2. nautobot/circuits/tests/test_views.py +4 -5
  3. nautobot/core/api/utils.py +12 -2
  4. nautobot/core/api/views.py +15 -3
  5. nautobot/core/forms/fields.py +5 -2
  6. nautobot/core/forms/utils.py +31 -6
  7. nautobot/core/models/fields.py +56 -0
  8. nautobot/core/templates/inc/javascript.html +2 -0
  9. nautobot/core/templates/inc/nav_menu.html +0 -251
  10. nautobot/core/testing/mixins.py +59 -2
  11. nautobot/core/testing/views.py +45 -61
  12. nautobot/core/tests/runner.py +6 -3
  13. nautobot/core/tests/test_paginator.py +4 -3
  14. nautobot/core/tests/test_utils.py +83 -0
  15. nautobot/core/tests/test_views.py +39 -56
  16. nautobot/core/views/__init__.py +27 -11
  17. nautobot/dcim/tests/test_api.py +4 -1
  18. nautobot/dcim/tests/test_views.py +26 -67
  19. nautobot/extras/datasources/git.py +6 -1
  20. nautobot/extras/factory.py +2 -1
  21. nautobot/extras/migrations/0112_dynamic_group_group_type_data_migration.py +3 -0
  22. nautobot/extras/migrations/0116_fix_dynamic_group_group_type_data_migration.py +16 -0
  23. nautobot/extras/models/models.py +2 -0
  24. nautobot/extras/tests/test_api.py +3 -3
  25. nautobot/extras/tests/test_customfields.py +9 -16
  26. nautobot/extras/tests/test_dynamicgroups.py +116 -0
  27. nautobot/extras/tests/test_forms.py +2 -0
  28. nautobot/extras/tests/test_plugins.py +4 -6
  29. nautobot/extras/tests/test_utils.py +5 -0
  30. nautobot/extras/tests/test_views.py +63 -161
  31. nautobot/extras/utils.py +50 -11
  32. nautobot/ipam/api/serializers.py +30 -1
  33. nautobot/ipam/api/views.py +165 -3
  34. nautobot/ipam/filters.py +1 -1
  35. nautobot/ipam/forms.py +2 -0
  36. nautobot/ipam/migrations/0050_vlangroup_range.py +24 -0
  37. nautobot/ipam/models.py +51 -8
  38. nautobot/ipam/tables.py +4 -4
  39. nautobot/ipam/templates/ipam/vlangroup.html +4 -0
  40. nautobot/ipam/tests/test_api.py +192 -12
  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 +8 -15
  44. nautobot/ipam/utils/__init__.py +10 -17
  45. nautobot/ipam/views.py +1 -1
  46. nautobot/project-static/docs/404.html +3 -3
  47. nautobot/project-static/docs/apps/index.html +3 -3
  48. nautobot/project-static/docs/apps/nautobot-apps.html +3 -3
  49. nautobot/project-static/docs/assets/javascripts/bundle.525ec568.min.js +16 -0
  50. nautobot/project-static/docs/assets/javascripts/{bundle.56dfad97.min.js.map → bundle.525ec568.min.js.map} +4 -4
  51. nautobot/project-static/docs/assets/stylesheets/main.8c3ca2c6.min.css +1 -0
  52. nautobot/project-static/docs/assets/stylesheets/main.8c3ca2c6.min.css.map +1 -0
  53. nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +3 -3
  54. nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +3 -3
  55. nautobot/project-static/docs/code-reference/nautobot/apps/api.html +3 -3
  56. nautobot/project-static/docs/code-reference/nautobot/apps/change_logging.html +3 -3
  57. nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +3 -3
  58. nautobot/project-static/docs/code-reference/nautobot/apps/config.html +3 -3
  59. nautobot/project-static/docs/code-reference/nautobot/apps/constants.html +3 -3
  60. nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +3 -3
  61. nautobot/project-static/docs/code-reference/nautobot/apps/exceptions.html +3 -3
  62. nautobot/project-static/docs/code-reference/nautobot/apps/factory.html +3 -3
  63. nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +3 -3
  64. nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +5 -5
  65. nautobot/project-static/docs/code-reference/nautobot/apps/graphql.html +3 -3
  66. nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +3 -3
  67. nautobot/project-static/docs/code-reference/nautobot/apps/models.html +3 -3
  68. nautobot/project-static/docs/code-reference/nautobot/apps/querysets.html +3 -3
  69. nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +3 -3
  70. nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +3 -3
  71. nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +124 -3
  72. nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +3 -3
  73. nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +3 -3
  74. nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +3 -3
  75. nautobot/project-static/docs/code-reference/nautobot/apps/views.html +3 -3
  76. nautobot/project-static/docs/development/apps/api/configuration-view.html +3 -3
  77. nautobot/project-static/docs/development/apps/api/database-backend-config.html +3 -3
  78. nautobot/project-static/docs/development/apps/api/models/django-admin.html +3 -3
  79. nautobot/project-static/docs/development/apps/api/models/global-search.html +3 -3
  80. nautobot/project-static/docs/development/apps/api/models/graphql.html +3 -3
  81. nautobot/project-static/docs/development/apps/api/models/index.html +5 -5
  82. nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +3 -3
  83. nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +3 -3
  84. nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +3 -3
  85. nautobot/project-static/docs/development/apps/api/platform-features/git-repository-content.html +3 -3
  86. nautobot/project-static/docs/development/apps/api/platform-features/index.html +3 -3
  87. nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +3 -3
  88. nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +3 -3
  89. nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +3 -3
  90. nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +3 -3
  91. nautobot/project-static/docs/development/apps/api/platform-features/uniquely-identify-objects.html +3 -3
  92. nautobot/project-static/docs/development/apps/api/prometheus.html +3 -3
  93. nautobot/project-static/docs/development/apps/api/setup.html +3 -3
  94. nautobot/project-static/docs/development/apps/api/testing.html +3 -3
  95. nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +3 -3
  96. nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +3 -3
  97. nautobot/project-static/docs/development/apps/api/ui-extensions/index.html +3 -3
  98. nautobot/project-static/docs/development/apps/api/ui-extensions/navigation.html +3 -3
  99. nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +3 -3
  100. nautobot/project-static/docs/development/apps/api/views/base-template.html +3 -3
  101. nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +3 -3
  102. nautobot/project-static/docs/development/apps/api/views/django-generic-views.html +3 -3
  103. nautobot/project-static/docs/development/apps/api/views/help-documentation.html +3 -3
  104. nautobot/project-static/docs/development/apps/api/views/index.html +3 -3
  105. nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +3 -3
  106. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +3 -3
  107. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +3 -3
  108. nautobot/project-static/docs/development/apps/api/views/notes.html +3 -3
  109. nautobot/project-static/docs/development/apps/api/views/rest-api.html +3 -3
  110. nautobot/project-static/docs/development/apps/api/views/urls.html +3 -3
  111. nautobot/project-static/docs/development/apps/index.html +3 -3
  112. nautobot/project-static/docs/development/apps/migration/code-updates.html +3 -3
  113. nautobot/project-static/docs/development/apps/migration/dependency-updates.html +3 -3
  114. nautobot/project-static/docs/development/apps/migration/from-v1.html +3 -3
  115. nautobot/project-static/docs/development/apps/migration/model-updates/dcim.html +3 -3
  116. nautobot/project-static/docs/development/apps/migration/model-updates/extras.html +3 -3
  117. nautobot/project-static/docs/development/apps/migration/model-updates/global.html +3 -3
  118. nautobot/project-static/docs/development/apps/migration/model-updates/ipam.html +3 -3
  119. nautobot/project-static/docs/development/apps/porting-from-netbox.html +3 -3
  120. nautobot/project-static/docs/development/core/application-registry.html +3 -3
  121. nautobot/project-static/docs/development/core/best-practices.html +3 -3
  122. nautobot/project-static/docs/development/core/bootstrap-ui.html +3 -3
  123. nautobot/project-static/docs/development/core/caching.html +3 -3
  124. nautobot/project-static/docs/development/core/controllers.html +3 -3
  125. nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +3 -3
  126. nautobot/project-static/docs/development/core/generic-views.html +3 -3
  127. nautobot/project-static/docs/development/core/getting-started.html +3 -3
  128. nautobot/project-static/docs/development/core/homepage.html +3 -3
  129. nautobot/project-static/docs/development/core/index.html +3 -3
  130. nautobot/project-static/docs/development/core/model-checklist.html +3 -3
  131. nautobot/project-static/docs/development/core/model-features.html +3 -3
  132. nautobot/project-static/docs/development/core/natural-keys.html +3 -3
  133. nautobot/project-static/docs/development/core/navigation-menu.html +3 -3
  134. nautobot/project-static/docs/development/core/release-checklist.html +3 -3
  135. nautobot/project-static/docs/development/core/role-internals.html +3 -3
  136. nautobot/project-static/docs/development/core/settings.html +3 -3
  137. nautobot/project-static/docs/development/core/style-guide.html +3 -3
  138. nautobot/project-static/docs/development/core/templates.html +3 -3
  139. nautobot/project-static/docs/development/core/testing.html +3 -3
  140. nautobot/project-static/docs/development/core/user-preferences.html +3 -3
  141. nautobot/project-static/docs/development/index.html +3 -3
  142. nautobot/project-static/docs/development/jobs/index.html +3 -3
  143. nautobot/project-static/docs/development/jobs/migration/from-v1.html +3 -3
  144. nautobot/project-static/docs/index.html +3 -3
  145. nautobot/project-static/docs/objects.inv +0 -0
  146. nautobot/project-static/docs/overview/application_stack.html +3 -3
  147. nautobot/project-static/docs/overview/design_philosophy.html +3 -3
  148. nautobot/project-static/docs/release-notes/index.html +3 -3
  149. nautobot/project-static/docs/release-notes/version-1.0.html +3 -3
  150. nautobot/project-static/docs/release-notes/version-1.1.html +3 -3
  151. nautobot/project-static/docs/release-notes/version-1.2.html +3 -3
  152. nautobot/project-static/docs/release-notes/version-1.3.html +3 -3
  153. nautobot/project-static/docs/release-notes/version-1.4.html +3 -3
  154. nautobot/project-static/docs/release-notes/version-1.5.html +3 -3
  155. nautobot/project-static/docs/release-notes/version-1.6.html +3 -3
  156. nautobot/project-static/docs/release-notes/version-2.0.html +3 -3
  157. nautobot/project-static/docs/release-notes/version-2.1.html +3 -3
  158. nautobot/project-static/docs/release-notes/version-2.2.html +3 -3
  159. nautobot/project-static/docs/release-notes/version-2.3.html +392 -95
  160. nautobot/project-static/docs/requirements.txt +1 -1
  161. nautobot/project-static/docs/search/search_index.json +1 -1
  162. nautobot/project-static/docs/sitemap.xml +269 -269
  163. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  164. nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +3 -3
  165. nautobot/project-static/docs/user-guide/administration/configuration/authentication/remote.html +3 -3
  166. nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +3 -3
  167. nautobot/project-static/docs/user-guide/administration/configuration/index.html +3 -3
  168. nautobot/project-static/docs/user-guide/administration/configuration/redis.html +3 -3
  169. nautobot/project-static/docs/user-guide/administration/configuration/settings.html +3 -3
  170. nautobot/project-static/docs/user-guide/administration/configuration/time-zones.html +3 -3
  171. nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +3 -3
  172. nautobot/project-static/docs/user-guide/administration/guides/docker.html +3 -3
  173. nautobot/project-static/docs/user-guide/administration/guides/health-checks.html +3 -3
  174. nautobot/project-static/docs/user-guide/administration/guides/permissions.html +3 -3
  175. nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +3 -3
  176. nautobot/project-static/docs/user-guide/administration/guides/replicating-nautobot.html +3 -3
  177. nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +3 -3
  178. nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +3 -3
  179. nautobot/project-static/docs/user-guide/administration/guides/selinux-troubleshooting.html +3 -3
  180. nautobot/project-static/docs/user-guide/administration/installation/app-install.html +3 -3
  181. nautobot/project-static/docs/user-guide/administration/installation/external-authentication.html +3 -3
  182. nautobot/project-static/docs/user-guide/administration/installation/http-server.html +3 -3
  183. nautobot/project-static/docs/user-guide/administration/installation/index.html +3 -3
  184. nautobot/project-static/docs/user-guide/administration/installation/install_system.html +3 -3
  185. nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +3 -3
  186. nautobot/project-static/docs/user-guide/administration/installation/services.html +3 -3
  187. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-netbox.html +3 -3
  188. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +3 -3
  189. nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +3 -3
  190. nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +3 -3
  191. nautobot/project-static/docs/user-guide/administration/upgrading/database-backup.html +3 -3
  192. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/after-you-upgrade.html +3 -3
  193. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/before-you-upgrade.html +3 -3
  194. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/for-developers.html +3 -3
  195. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/index.html +3 -3
  196. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/whats-changed.html +3 -3
  197. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/region-and-site-data-migration-guide.html +3 -3
  198. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +3 -3
  199. nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +3 -3
  200. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuit.html +3 -3
  201. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittermination.html +3 -3
  202. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittype.html +3 -3
  203. nautobot/project-static/docs/user-guide/core-data-model/circuits/provider.html +3 -3
  204. nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +3 -3
  205. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloud.html +3 -3
  206. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudaccount.html +3 -3
  207. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetwork.html +3 -3
  208. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetworkprefixassignment.html +3 -3
  209. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudresourcetype.html +3 -3
  210. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservice.html +3 -3
  211. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservicenetworkassignment.html +3 -3
  212. nautobot/project-static/docs/user-guide/core-data-model/dcim/cable.html +3 -3
  213. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +3 -3
  214. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +3 -3
  215. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +3 -3
  216. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +3 -3
  217. nautobot/project-static/docs/user-guide/core-data-model/dcim/controller.html +3 -3
  218. nautobot/project-static/docs/user-guide/core-data-model/dcim/controllermanageddevicegroup.html +3 -3
  219. nautobot/project-static/docs/user-guide/core-data-model/dcim/device.html +3 -3
  220. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +3 -3
  221. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +3 -3
  222. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicefamily.html +3 -3
  223. nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +3 -3
  224. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +3 -3
  225. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +3 -3
  226. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +3 -3
  227. nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +3 -3
  228. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +3 -3
  229. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +3 -3
  230. nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +3 -3
  231. nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +3 -3
  232. nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +3 -3
  233. nautobot/project-static/docs/user-guide/core-data-model/dcim/manufacturer.html +3 -3
  234. nautobot/project-static/docs/user-guide/core-data-model/dcim/module.html +3 -3
  235. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebay.html +3 -3
  236. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebaytemplate.html +3 -3
  237. nautobot/project-static/docs/user-guide/core-data-model/dcim/moduletype.html +3 -3
  238. nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +3 -3
  239. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerfeed.html +3 -3
  240. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +3 -3
  241. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +3 -3
  242. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerpanel.html +3 -3
  243. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +3 -3
  244. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +3 -3
  245. nautobot/project-static/docs/user-guide/core-data-model/dcim/rack.html +3 -3
  246. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackgroup.html +3 -3
  247. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackreservation.html +3 -3
  248. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +3 -3
  249. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +3 -3
  250. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +3 -3
  251. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareversion.html +3 -3
  252. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualchassis.html +3 -3
  253. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +3 -3
  254. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +3 -3
  255. nautobot/project-static/docs/user-guide/core-data-model/extras/contact.html +3 -3
  256. nautobot/project-static/docs/user-guide/core-data-model/extras/team.html +3 -3
  257. nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +3 -3
  258. nautobot/project-static/docs/user-guide/core-data-model/ipam/namespace.html +3 -3
  259. nautobot/project-static/docs/user-guide/core-data-model/ipam/prefix.html +3 -3
  260. nautobot/project-static/docs/user-guide/core-data-model/ipam/rir.html +3 -3
  261. nautobot/project-static/docs/user-guide/core-data-model/ipam/routetarget.html +3 -3
  262. nautobot/project-static/docs/user-guide/core-data-model/ipam/service.html +3 -3
  263. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +3 -3
  264. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlangroup.html +308 -3
  265. nautobot/project-static/docs/user-guide/core-data-model/ipam/vrf.html +3 -3
  266. nautobot/project-static/docs/user-guide/core-data-model/overview/introduction.html +3 -3
  267. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenant.html +3 -3
  268. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenantgroup.html +3 -3
  269. nautobot/project-static/docs/user-guide/core-data-model/virtualization/cluster.html +3 -3
  270. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustergroup.html +3 -3
  271. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustertype.html +3 -3
  272. nautobot/project-static/docs/user-guide/core-data-model/virtualization/virtualmachine.html +3 -3
  273. nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +3 -3
  274. nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +3 -3
  275. nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +3 -3
  276. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +3 -3
  277. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +3 -3
  278. nautobot/project-static/docs/user-guide/feature-guides/getting-started/index.html +3 -3
  279. nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +3 -3
  280. nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +3 -3
  281. nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +3 -3
  282. nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +3 -3
  283. nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +3 -3
  284. nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +3 -3
  285. nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +3 -3
  286. nautobot/project-static/docs/user-guide/feature-guides/graphql.html +3 -3
  287. nautobot/project-static/docs/user-guide/feature-guides/ip-address-merge-tool.html +3 -3
  288. nautobot/project-static/docs/user-guide/feature-guides/relationships.html +3 -3
  289. nautobot/project-static/docs/user-guide/feature-guides/software-image-files-and-versions.html +3 -3
  290. nautobot/project-static/docs/user-guide/index.html +3 -3
  291. nautobot/project-static/docs/user-guide/platform-functionality/change-logging.html +3 -3
  292. nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +3 -3
  293. nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +3 -3
  294. nautobot/project-static/docs/user-guide/platform-functionality/customlink.html +3 -3
  295. nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +3 -3
  296. nautobot/project-static/docs/user-guide/platform-functionality/exporttemplate.html +3 -3
  297. nautobot/project-static/docs/user-guide/platform-functionality/externalintegration.html +3 -3
  298. nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +3 -3
  299. nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +3 -3
  300. nautobot/project-static/docs/user-guide/platform-functionality/graphqlquery.html +3 -3
  301. nautobot/project-static/docs/user-guide/platform-functionality/imageattachment.html +3 -3
  302. nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +3 -3
  303. nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +3 -3
  304. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +3 -3
  305. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +3 -3
  306. nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +3 -3
  307. nautobot/project-static/docs/user-guide/platform-functionality/napalm.html +3 -3
  308. nautobot/project-static/docs/user-guide/platform-functionality/note.html +3 -3
  309. nautobot/project-static/docs/user-guide/platform-functionality/objectmetadata.html +3 -3
  310. nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +3 -3
  311. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +3 -3
  312. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +3 -3
  313. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +3 -3
  314. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/ui-related-endpoints.html +3 -3
  315. nautobot/project-static/docs/user-guide/platform-functionality/role.html +3 -3
  316. nautobot/project-static/docs/user-guide/platform-functionality/savedview.html +3 -3
  317. nautobot/project-static/docs/user-guide/platform-functionality/secret.html +3 -3
  318. nautobot/project-static/docs/user-guide/platform-functionality/staticgroupassociation.html +3 -3
  319. nautobot/project-static/docs/user-guide/platform-functionality/status.html +3 -3
  320. nautobot/project-static/docs/user-guide/platform-functionality/tag.html +3 -3
  321. nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +3 -3
  322. nautobot/project-static/docs/user-guide/platform-functionality/users/objectpermission.html +3 -3
  323. nautobot/project-static/docs/user-guide/platform-functionality/users/token.html +3 -3
  324. nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +3 -3
  325. nautobot/project-static/js/nav_menu.js +249 -0
  326. nautobot/tenancy/templates/tenancy/tenant.html +1 -1
  327. nautobot/users/tests/test_views.py +9 -11
  328. nautobot/virtualization/tests/test_views.py +3 -5
  329. {nautobot-2.3.5.dist-info → nautobot-2.3.7.dist-info}/METADATA +2 -1
  330. {nautobot-2.3.5.dist-info → nautobot-2.3.7.dist-info}/RECORD +334 -330
  331. {nautobot-2.3.5.dist-info → nautobot-2.3.7.dist-info}/WHEEL +1 -1
  332. nautobot/project-static/docs/assets/javascripts/bundle.56dfad97.min.js +0 -16
  333. nautobot/project-static/docs/assets/stylesheets/main.35f28582.min.css +0 -1
  334. nautobot/project-static/docs/assets/stylesheets/main.35f28582.min.css.map +0 -1
  335. {nautobot-2.3.5.dist-info → nautobot-2.3.7.dist-info}/LICENSE.txt +0 -0
  336. {nautobot-2.3.5.dist-info → nautobot-2.3.7.dist-info}/NOTICE +0 -0
  337. {nautobot-2.3.5.dist-info → nautobot-2.3.7.dist-info}/entry_points.txt +0 -0
@@ -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
 
@@ -0,0 +1,24 @@
1
+ # Generated by Django 4.2.16 on 2024-10-02 17:14
2
+
3
+ from django.db import migrations
4
+
5
+ import nautobot.core.models.fields
6
+
7
+
8
+ class Migration(migrations.Migration):
9
+ dependencies = [
10
+ ("ipam", "0049_vrf_data_migration"),
11
+ ]
12
+
13
+ operations = [
14
+ migrations.AddField(
15
+ model_name="vlangroup",
16
+ name="range",
17
+ field=nautobot.core.models.fields.PositiveRangeNumberTextField(default="1-4094"),
18
+ ),
19
+ migrations.AddField(
20
+ model_name="vlangroup",
21
+ name="tags",
22
+ field=nautobot.core.models.fields.TagsField(through="extras.TaggedItem", to="extras.Tag"),
23
+ ),
24
+ ]
nautobot/ipam/models.py CHANGED
@@ -10,8 +10,9 @@ from django.utils.functional import cached_property
10
10
  import netaddr
11
11
 
12
12
  from nautobot.core.constants import CHARFIELD_MAX_LENGTH
13
+ from nautobot.core.forms.utils import parse_numeric_range
13
14
  from nautobot.core.models import BaseManager, BaseModel
14
- from nautobot.core.models.fields import JSONArrayField
15
+ from nautobot.core.models.fields import JSONArrayField, PositiveRangeNumberTextField
15
16
  from nautobot.core.models.generics import OrganizationalModel, PrimaryModel
16
17
  from nautobot.core.models.utils import array_to_string
17
18
  from nautobot.core.utils.data import UtilizationData
@@ -1274,11 +1275,14 @@ class IPAddressToInterface(BaseModel):
1274
1275
 
1275
1276
 
1276
1277
  @extras_features(
1278
+ "custom_links",
1277
1279
  "custom_validators",
1280
+ "export_templates",
1278
1281
  "graphql",
1279
1282
  "locations",
1283
+ "webhooks",
1280
1284
  )
1281
- class VLANGroup(OrganizationalModel):
1285
+ class VLANGroup(PrimaryModel):
1282
1286
  """
1283
1287
  A VLAN group is an arbitrary collection of VLANs within which VLAN IDs and names must be unique.
1284
1288
  """
@@ -1293,11 +1297,36 @@ class VLANGroup(OrganizationalModel):
1293
1297
  )
1294
1298
  description = models.CharField(max_length=CHARFIELD_MAX_LENGTH, blank=True)
1295
1299
 
1300
+ range = PositiveRangeNumberTextField(
1301
+ blank=False,
1302
+ default="1-4094",
1303
+ help_text="Permitted VID range(s) as comma-separated list, default '1-4094' if left blank.",
1304
+ min_boundary=constants.VLAN_VID_MIN,
1305
+ max_boundary=constants.VLAN_VID_MAX,
1306
+ )
1307
+
1296
1308
  class Meta:
1297
1309
  ordering = ("name",)
1298
1310
  verbose_name = "VLAN group"
1299
1311
  verbose_name_plural = "VLAN groups"
1300
1312
 
1313
+ @property
1314
+ def expanded_range(self):
1315
+ """
1316
+ Expand VLAN's range into a list of integers (VLAN IDs).
1317
+ """
1318
+ return parse_numeric_range(self.range)
1319
+
1320
+ @property
1321
+ def available_vids(self):
1322
+ """
1323
+ Return all available VLAN IDs within this VLANGroup as a list.
1324
+ """
1325
+ used_ids = self.vlans.all().values_list("vid", flat=True)
1326
+ available = sorted([vid for vid in self.expanded_range if vid not in used_ids])
1327
+
1328
+ return available
1329
+
1301
1330
  def clean(self):
1302
1331
  super().clean()
1303
1332
 
@@ -1308,18 +1337,25 @@ class VLANGroup(OrganizationalModel):
1308
1337
  {"location": f'VLAN groups may not associate to locations of type "{self.location.location_type}".'}
1309
1338
  )
1310
1339
 
1340
+ # Validate ranges for related VLANs.
1341
+ _expanded_range = self.expanded_range
1342
+ out_of_range_vids = [_vlan.vid for _vlan in self.vlans.all() if _vlan.vid not in _expanded_range]
1343
+ if out_of_range_vids:
1344
+ raise ValidationError(
1345
+ {
1346
+ "range": f"VLAN group range may not be re-sized due to existing VLANs (IDs: {','.join(map(str, out_of_range_vids))})."
1347
+ }
1348
+ )
1349
+
1311
1350
  def __str__(self):
1312
1351
  return self.name
1313
1352
 
1314
1353
  def get_next_available_vid(self):
1315
1354
  """
1316
- Return the first available VLAN ID (1-4094) in the group.
1355
+ Return the first available VLAN ID in the group's range.
1317
1356
  """
1318
- vlan_ids = VLAN.objects.filter(vlan_group=self).values_list("vid", flat=True)
1319
- for i in range(1, 4095):
1320
- if i not in vlan_ids:
1321
- return i
1322
- return None
1357
+ _available_vids = self.available_vids
1358
+ return _available_vids[0] if _available_vids else None
1323
1359
 
1324
1360
 
1325
1361
  @extras_features(
@@ -1438,6 +1474,13 @@ class VLAN(PrimaryModel):
1438
1474
  # Return all VM interfaces assigned to this VLAN
1439
1475
  return VMInterface.objects.filter(Q(untagged_vlan_id=self.pk) | Q(tagged_vlans=self.pk)).distinct()
1440
1476
 
1477
+ def clean(self):
1478
+ super().clean()
1479
+
1480
+ # Validate Vlan Group Range
1481
+ if self.vlan_group and self.vid not in self.vlan_group.expanded_range:
1482
+ raise ValidationError({"vid": f"VLAN ID is not contained in VLAN Group range ({self.vlan_group.range})"})
1483
+
1441
1484
 
1442
1485
  @extras_features("graphql")
1443
1486
  class VLANLocationAssignment(BaseModel):
nautobot/ipam/tables.py CHANGED
@@ -161,9 +161,9 @@ VLAN_LINK = """
161
161
  {% url 'ipam:vlan_add' %}\
162
162
  ?vid={{ record.vid }}&vlan_group={{ vlan_group.pk }}\
163
163
  {% if vlan_group.location %}&location={{ vlan_group.location.pk }}{% endif %}\
164
- " class="btn btn-xs btn-success">{{ record.available }} VLAN{{ record.available|pluralize }} available</a>\
164
+ " class="btn btn-xs btn-success">{{ record.available }} VLAN{{ record.available|pluralize }} available ({{ record.range }})</a>\
165
165
  {% else %}
166
- {{ record.available }} VLAN{{ record.available|pluralize }} available
166
+ {{ record.available }} VLAN{{ record.available|pluralize }} available ({{ record.range }})
167
167
  {% endif %}
168
168
  """
169
169
 
@@ -665,8 +665,8 @@ class VLANGroupTable(BaseTable):
665
665
 
666
666
  class Meta(BaseTable.Meta):
667
667
  model = VLANGroup
668
- fields = ("pk", "name", "location", "vlan_count", "description", "actions")
669
- default_columns = ("pk", "name", "location", "vlan_count", "description", "actions")
668
+ fields = ("pk", "name", "location", "range", "vlan_count", "description", "actions")
669
+ default_columns = ("pk", "name", "range", "location", "vlan_count", "description", "actions")
670
670
 
671
671
 
672
672
  #
@@ -17,6 +17,10 @@
17
17
  <td>Location</td>
18
18
  <td>{% include 'dcim/inc/location_hierarchy.html' with location=object.location %}</td>
19
19
  </tr>
20
+ <tr>
21
+ <td>Range</td>
22
+ <td>{{ object.range }}</td>
23
+ </tr>
20
24
  <tr>
21
25
  <td>VLANs</td>
22
26
  <td>
@@ -200,14 +200,19 @@ class VRFDeviceAssignmentTest(APIViewTestCases.APIViewTestCase):
200
200
  }
201
201
  self.add_permissions("ipam.add_vrfdeviceassignment")
202
202
  response = self.client.post(self._get_list_url(), duplicate_device_create_data, format="json", **self.header)
203
- self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
204
- self.assertIn("The fields device, vrf must make a unique set.", str(response.content))
203
+ self.assertContains(
204
+ response, "The fields device, vrf must make a unique set.", status_code=status.HTTP_400_BAD_REQUEST
205
+ )
205
206
  response = self.client.post(self._get_list_url(), duplicate_vm_create_data, format="json", **self.header)
206
- self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
207
- self.assertIn("The fields virtual_machine, vrf must make a unique set.", str(response.content))
207
+ self.assertContains(
208
+ response, "The fields virtual_machine, vrf must make a unique set.", status_code=status.HTTP_400_BAD_REQUEST
209
+ )
208
210
  response = self.client.post(self._get_list_url(), invalid_create_data, format="json", **self.header)
209
- self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
210
- self.assertIn("A VRF cannot be associated with both a device and a virtual machine.", str(response.content))
211
+ self.assertContains(
212
+ response,
213
+ "A VRF cannot be associated with both a device and a virtual machine.",
214
+ status_code=status.HTTP_400_BAD_REQUEST,
215
+ )
211
216
 
212
217
 
213
218
  class VRFPrefixAssignmentTest(APIViewTestCases.APIViewTestCase):
@@ -266,14 +271,15 @@ class VRFPrefixAssignmentTest(APIViewTestCases.APIViewTestCase):
266
271
  }
267
272
  self.add_permissions("ipam.add_vrfprefixassignment")
268
273
  response = self.client.post(self._get_list_url(), duplicate_create_data, format="json", **self.header)
269
- self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
270
- self.assertIn("The fields vrf, prefix must make a unique set.", str(response.content))
274
+ self.assertContains(
275
+ response, "The fields vrf, prefix must make a unique set.", status_code=status.HTTP_400_BAD_REQUEST
276
+ )
271
277
  response = self.client.post(self._get_list_url(), wrong_namespace_create_data, format="json", **self.header)
272
- self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
273
- self.assertIn("Prefix must be in same namespace as VRF", str(response.content))
278
+ self.assertContains(
279
+ response, "Prefix must be in same namespace as VRF", status_code=status.HTTP_400_BAD_REQUEST
280
+ )
274
281
  response = self.client.post(self._get_list_url(), missing_field_create_data, format="json", **self.header)
275
- self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
276
- self.assertIn("This field may not be null.", str(response.content))
282
+ self.assertContains(response, "This field may not be null.", status_code=status.HTTP_400_BAD_REQUEST)
277
283
 
278
284
 
279
285
  class RouteTargetTest(APIViewTestCases.APIViewTestCase):
@@ -991,6 +997,15 @@ class VLANGroupTest(APIViewTestCases.APIViewTestCase):
991
997
  "description": "New description",
992
998
  }
993
999
 
1000
+ @classmethod
1001
+ def setUpTestData(cls):
1002
+ cls.vlan_group = VLANGroup.objects.create(name="Test", range="5-10,15-20")
1003
+ cls.default_status = Status.objects.first()
1004
+ VLAN.objects.create(name="vlan_5", vid=5, status=cls.default_status, vlan_group=cls.vlan_group)
1005
+ VLAN.objects.create(name="vlan_10", vid=10, status=cls.default_status, vlan_group=cls.vlan_group)
1006
+ VLAN.objects.create(name="vlan_17", vid=17, status=cls.default_status, vlan_group=cls.vlan_group)
1007
+ cls.unused_vids = [6, 7, 8, 9, 15, 16, 18, 19, 20]
1008
+
994
1009
  def get_deletable_object(self):
995
1010
  return VLANGroup.objects.create(name="DELETE ME")
996
1011
 
@@ -1002,6 +1017,171 @@ class VLANGroupTest(APIViewTestCases.APIViewTestCase):
1002
1017
  ]
1003
1018
  return [vg.pk for vg in vlangroups]
1004
1019
 
1020
+ def test_list_available_vlans(self):
1021
+ """
1022
+ Test retrieval of all available VLAN IDs within a VLANGroup.
1023
+ """
1024
+ url = reverse("ipam-api:vlangroup-available-vlans", kwargs={"pk": self.vlan_group.pk})
1025
+ self.add_permissions("ipam.view_vlangroup")
1026
+
1027
+ # Retrieve all available VLAN IDs
1028
+ response = self.client.get(url, **self.header)
1029
+
1030
+ self.assertEqual(response.data["results"], self.unused_vids)
1031
+ self.assertEqual(response.data["count"], len(self.unused_vids))
1032
+
1033
+ def test_create_single_available_vlan(self):
1034
+ """
1035
+ Test creation of the first available VLAN within a VLANGroup.
1036
+ """
1037
+ cf = CustomField.objects.create(key="sor", label="Source of Record Field", type="text")
1038
+ cf.content_types.add(ContentType.objects.get_for_model(VLAN))
1039
+ url = reverse("ipam-api:vlangroup-available-vlans", kwargs={"pk": self.vlan_group.pk})
1040
+ self.add_permissions(
1041
+ "ipam.view_vlangroup",
1042
+ "ipam.add_vlan",
1043
+ )
1044
+
1045
+ # Create all nine available VLANs with individual requests
1046
+ for unused_vid in self.unused_vids:
1047
+ data = {
1048
+ "name": f"VLAN_{unused_vid}",
1049
+ "description": f"Test VLAN {unused_vid}",
1050
+ "status": self.default_status.pk,
1051
+ "custom_fields": {"sor": "Nautobot"},
1052
+ }
1053
+ response = self.client.post(url, data, format="json", **self.header)
1054
+ self.assertHttpStatus(response, status.HTTP_201_CREATED)
1055
+ self.assertEqual(response.data["results"]["name"], data["name"])
1056
+ self.assertEqual(response.data["results"]["vid"], unused_vid)
1057
+ self.assertEqual(response.data["results"]["description"], data["description"])
1058
+ self.assertEqual(response.data["results"]["vlan_group"]["id"], self.vlan_group.pk)
1059
+ self.assertIn("custom_fields", response.data["results"])
1060
+ self.assertIn("sor", response.data["results"]["custom_fields"])
1061
+ self.assertEqual("Nautobot", response.data["results"]["custom_fields"]["sor"])
1062
+
1063
+ # Try to create one more VLAN
1064
+ response = self.client.post(
1065
+ url, {"name": "UTILIZED_VLAN_GROUP", "status": self.default_status.pk}, format="json", **self.header
1066
+ )
1067
+ self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
1068
+ self.assertIn("detail", response.data)
1069
+ self.assertIn(
1070
+ f"An insufficient number of VLANs are available within the VLANGroup {self.vlan_group}",
1071
+ response.data["detail"],
1072
+ )
1073
+
1074
+ def test_create_multiple_available_vlans(self):
1075
+ """
1076
+ Test the creation of available VLANS within a VLANGroup.
1077
+ """
1078
+ cf = CustomField.objects.create(key="sor", label="Source of Record Field", type="text")
1079
+ cf.content_types.add(ContentType.objects.get_for_model(VLAN))
1080
+ url = reverse("ipam-api:vlangroup-available-vlans", kwargs={"pk": self.vlan_group.pk})
1081
+ self.add_permissions(
1082
+ "ipam.view_vlangroup",
1083
+ "ipam.add_vlan",
1084
+ )
1085
+
1086
+ # Try to create ten VLANs (only nine are available)
1087
+ data = [ # First nine VLANs
1088
+ {
1089
+ "name": f"VLAN_{unused_vid}",
1090
+ "description": f"Test VLAN {unused_vid}",
1091
+ "status": self.default_status.pk,
1092
+ "custom_fields": {"sor": "Nautobot"},
1093
+ }
1094
+ for unused_vid in self.unused_vids
1095
+ ]
1096
+ additional_vlan = [
1097
+ {
1098
+ "name": "VLAN_10", # Out of range VLAN
1099
+ "description": "Test VLAN 10",
1100
+ "status": self.default_status.pk,
1101
+ "custom_fields": {"sor": "Nautobot"},
1102
+ }
1103
+ ]
1104
+ response = self.client.post(url, data + additional_vlan, format="json", **self.header)
1105
+ self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
1106
+ self.assertIn("detail", response.data)
1107
+
1108
+ # Create all nine available VLANs in a single request
1109
+ response = self.client.post(url, data, format="json", **self.header)
1110
+ self.assertHttpStatus(response, status.HTTP_201_CREATED)
1111
+ self.assertEqual(len(response.data["results"]), 9)
1112
+
1113
+ for i, vlan_data in enumerate(data):
1114
+ self.assertEqual(response.data["results"][i]["name"], vlan_data["name"])
1115
+ self.assertEqual(response.data["results"][i]["vid"], int(vlan_data["name"].replace("VLAN_", "")))
1116
+ self.assertEqual(response.data["results"][i]["description"], vlan_data["description"])
1117
+ self.assertEqual(response.data["results"][i]["vlan_group"]["id"], self.vlan_group.pk)
1118
+ self.assertIn("custom_fields", response.data["results"][i])
1119
+ self.assertIn("sor", response.data["results"][i]["custom_fields"])
1120
+ self.assertEqual("Nautobot", response.data["results"][i]["custom_fields"]["sor"])
1121
+
1122
+ def test_create_multiple_explicit_vlans(self):
1123
+ """
1124
+ Test the creation of available VLANS within a VLANGroup requesting explicit VLAN IDs.
1125
+ """
1126
+ url = reverse("ipam-api:vlangroup-available-vlans", kwargs={"pk": self.vlan_group.pk})
1127
+ self.add_permissions(
1128
+ "ipam.view_vlangroup",
1129
+ "ipam.add_vlan",
1130
+ )
1131
+
1132
+ # Try to create VLANs with specified VLAN IDs. Also, explicitly (and redundantly) specify a VLAN Group.
1133
+ data = [
1134
+ {"name": "VLAN_6", "status": self.default_status.pk, "vid": 6},
1135
+ {"name": "VLAN_7", "status": self.default_status.pk, "vid": 7},
1136
+ {"name": "VLAN_8", "status": self.default_status.pk},
1137
+ {"name": "VLAN_9", "status": self.default_status.pk, "vid": 9, "vlan_group": self.vlan_group.pk},
1138
+ {"name": "VLAN_15", "status": self.default_status.pk},
1139
+ {"name": "VLAN_16", "status": self.default_status.pk, "vid": 16, "vlan_group": self.vlan_group.pk},
1140
+ ]
1141
+
1142
+ response = self.client.post(url, data, format="json", **self.header)
1143
+ self.assertHttpStatus(response, status.HTTP_201_CREATED)
1144
+ self.assertEqual(len(response.data["results"]), 6)
1145
+
1146
+ for i, vlan_data in enumerate(data):
1147
+ self.assertEqual(response.data["results"][i]["name"], vlan_data["name"])
1148
+ self.assertEqual(response.data["results"][i]["vid"], int(vlan_data["name"].replace("VLAN_", "")))
1149
+ self.assertEqual(response.data["results"][i]["vlan_group"]["id"], self.vlan_group.pk)
1150
+
1151
+ def test_create_invalid_vlans(self):
1152
+ """
1153
+ Test the creation of VLANs using invalid requests.
1154
+ """
1155
+ url = reverse("ipam-api:vlangroup-available-vlans", kwargs={"pk": self.vlan_group.pk})
1156
+ self.add_permissions(
1157
+ "ipam.view_vlangroup",
1158
+ "ipam.add_vlan",
1159
+ )
1160
+
1161
+ # Try to create VLANs using same vid
1162
+ data = [
1163
+ {"name": "VLAN_6", "status": self.default_status.pk, "vid": 6},
1164
+ {"name": "VLAN_7", "status": self.default_status.pk, "vid": 6},
1165
+ {"name": "VLAN_8", "status": self.default_status.pk},
1166
+ ]
1167
+
1168
+ response = self.client.post(url, data, format="json", **self.header)
1169
+ self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
1170
+ self.assertIn("detail", response.data)
1171
+ self.assertEqual("VLAN 6 is not available within the VLANGroup.", response.data["detail"])
1172
+
1173
+ # Try to create VLANs specifying other VLAN Group
1174
+ some_other_vlan_group = VLANGroup.objects.create(name="VLAN Group 100-200", range="100-200")
1175
+ data = [{"name": "VLAN_7", "status": self.default_status.pk, "vlan_group": some_other_vlan_group.pk}]
1176
+
1177
+ response = self.client.post(url, data, format="json", **self.header)
1178
+ self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
1179
+ self.assertIn("detail", response.data)
1180
+ self.assertEqual(
1181
+ f"Invalid VLAN Group requested: {some_other_vlan_group}. Only VLAN Group {self.vlan_group} is permitted.",
1182
+ response.data["detail"],
1183
+ )
1184
+
1005
1185
 
1006
1186
  class VLANTest(APIViewTestCases.APIViewTestCase):
1007
1187
  model = VLAN