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
@@ -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
@@ -889,22 +890,16 @@ class Prefix(PrimaryModel):
889
890
  )
890
891
  return available_ips
891
892
 
892
- def get_child_ips(self):
893
+ def get_all_ips(self):
893
894
  """
894
- Return IP addresses with this prefix as parent.
895
-
896
- In a future release, if this prefix is a pool, it will return IP addresses within the pool's address space.
895
+ Return all IP addresses contained within this prefix, including child prefixes' IP addresses.
897
896
 
898
897
  Returns:
899
898
  IPAddress QuerySet
900
899
  """
901
- # 3.0 TODO: uncomment this to enable this logic
902
- # if self.type == choices.PrefixTypeChoices.TYPE_POOL:
903
- # return IPAddress.objects.filter(
904
- # parent__namespace=self.namespace, host__gte=self.network, host__lte=self.broadcast
905
- # )
906
- # else:
907
- return self.ip_addresses.all()
900
+ return IPAddress.objects.filter(
901
+ parent__namespace=self.namespace, host__gte=self.network, host__lte=self.broadcast
902
+ )
908
903
 
909
904
  def get_first_available_prefix(self):
910
905
  """
@@ -1280,11 +1275,14 @@ class IPAddressToInterface(BaseModel):
1280
1275
 
1281
1276
 
1282
1277
  @extras_features(
1278
+ "custom_links",
1283
1279
  "custom_validators",
1280
+ "export_templates",
1284
1281
  "graphql",
1285
1282
  "locations",
1283
+ "webhooks",
1286
1284
  )
1287
- class VLANGroup(OrganizationalModel):
1285
+ class VLANGroup(PrimaryModel):
1288
1286
  """
1289
1287
  A VLAN group is an arbitrary collection of VLANs within which VLAN IDs and names must be unique.
1290
1288
  """
@@ -1299,11 +1297,36 @@ class VLANGroup(OrganizationalModel):
1299
1297
  )
1300
1298
  description = models.CharField(max_length=CHARFIELD_MAX_LENGTH, blank=True)
1301
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
+
1302
1308
  class Meta:
1303
1309
  ordering = ("name",)
1304
1310
  verbose_name = "VLAN group"
1305
1311
  verbose_name_plural = "VLAN groups"
1306
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
+
1307
1330
  def clean(self):
1308
1331
  super().clean()
1309
1332
 
@@ -1314,18 +1337,25 @@ class VLANGroup(OrganizationalModel):
1314
1337
  {"location": f'VLAN groups may not associate to locations of type "{self.location.location_type}".'}
1315
1338
  )
1316
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
+
1317
1350
  def __str__(self):
1318
1351
  return self.name
1319
1352
 
1320
1353
  def get_next_available_vid(self):
1321
1354
  """
1322
- Return the first available VLAN ID (1-4094) in the group.
1355
+ Return the first available VLAN ID in the group's range.
1323
1356
  """
1324
- vlan_ids = VLAN.objects.filter(vlan_group=self).values_list("vid", flat=True)
1325
- for i in range(1, 4095):
1326
- if i not in vlan_ids:
1327
- return i
1328
- return None
1357
+ _available_vids = self.available_vids
1358
+ return _available_vids[0] if _available_vids else None
1329
1359
 
1330
1360
 
1331
1361
  @extras_features(
@@ -1444,6 +1474,13 @@ class VLAN(PrimaryModel):
1444
1474
  # Return all VM interfaces assigned to this VLAN
1445
1475
  return VMInterface.objects.filter(Q(untagged_vlan_id=self.pk) | Q(tagged_vlans=self.pk)).distinct()
1446
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
+
1447
1484
 
1448
1485
  @extras_features("graphql")
1449
1486
  class VLANLocationAssignment(BaseModel):
@@ -191,7 +191,14 @@ menu_items = (
191
191
  permissions=[
192
192
  "ipam.view_service",
193
193
  ],
194
- buttons=(),
194
+ buttons=(
195
+ NavMenuAddButton(
196
+ link="ipam:service_add",
197
+ permissions=[
198
+ "ipam.add_service",
199
+ ],
200
+ ),
201
+ ),
195
202
  ),
196
203
  ),
197
204
  ),
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
  #
@@ -33,7 +33,7 @@
33
33
  </li>
34
34
  {% if perms.ipam.view_ipaddress %}
35
35
  <li role="presentation"{% if active_tab == 'ip-addresses' %} class="active"{% endif %}>
36
- <a href="{% url 'ipam:prefix_ipaddresses' pk=object.pk %}">IP Addresses <span class="badge">{{ object.get_child_ips.count }}</span></a>
36
+ <a href="{% url 'ipam:prefix_ipaddresses' pk=object.pk %}">IP Addresses <span class="badge">{{ object.get_all_ips.count }}</span></a>
37
37
  </li>
38
38
  {% endif %}
39
39
  {% endblock extra_nav_tabs %}
@@ -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>
@@ -991,6 +991,15 @@ class VLANGroupTest(APIViewTestCases.APIViewTestCase):
991
991
  "description": "New description",
992
992
  }
993
993
 
994
+ @classmethod
995
+ def setUpTestData(cls):
996
+ cls.vlan_group = VLANGroup.objects.create(name="Test", range="5-10,15-20")
997
+ cls.default_status = Status.objects.first()
998
+ VLAN.objects.create(name="vlan_5", vid=5, status=cls.default_status, vlan_group=cls.vlan_group)
999
+ VLAN.objects.create(name="vlan_10", vid=10, status=cls.default_status, vlan_group=cls.vlan_group)
1000
+ VLAN.objects.create(name="vlan_17", vid=17, status=cls.default_status, vlan_group=cls.vlan_group)
1001
+ cls.unused_vids = [6, 7, 8, 9, 15, 16, 18, 19, 20]
1002
+
994
1003
  def get_deletable_object(self):
995
1004
  return VLANGroup.objects.create(name="DELETE ME")
996
1005
 
@@ -1002,6 +1011,171 @@ class VLANGroupTest(APIViewTestCases.APIViewTestCase):
1002
1011
  ]
1003
1012
  return [vg.pk for vg in vlangroups]
1004
1013
 
1014
+ def test_list_available_vlans(self):
1015
+ """
1016
+ Test retrieval of all available VLAN IDs within a VLANGroup.
1017
+ """
1018
+ url = reverse("ipam-api:vlangroup-available-vlans", kwargs={"pk": self.vlan_group.pk})
1019
+ self.add_permissions("ipam.view_vlangroup")
1020
+
1021
+ # Retrieve all available VLAN IDs
1022
+ response = self.client.get(url, **self.header)
1023
+
1024
+ self.assertEqual(response.data["results"], self.unused_vids)
1025
+ self.assertEqual(response.data["count"], len(self.unused_vids))
1026
+
1027
+ def test_create_single_available_vlan(self):
1028
+ """
1029
+ Test creation of the first available VLAN within a VLANGroup.
1030
+ """
1031
+ cf = CustomField.objects.create(key="sor", label="Source of Record Field", type="text")
1032
+ cf.content_types.add(ContentType.objects.get_for_model(VLAN))
1033
+ url = reverse("ipam-api:vlangroup-available-vlans", kwargs={"pk": self.vlan_group.pk})
1034
+ self.add_permissions(
1035
+ "ipam.view_vlangroup",
1036
+ "ipam.add_vlan",
1037
+ )
1038
+
1039
+ # Create all nine available VLANs with individual requests
1040
+ for unused_vid in self.unused_vids:
1041
+ data = {
1042
+ "name": f"VLAN_{unused_vid}",
1043
+ "description": f"Test VLAN {unused_vid}",
1044
+ "status": self.default_status.pk,
1045
+ "custom_fields": {"sor": "Nautobot"},
1046
+ }
1047
+ response = self.client.post(url, data, format="json", **self.header)
1048
+ self.assertHttpStatus(response, status.HTTP_201_CREATED)
1049
+ self.assertEqual(response.data["results"]["name"], data["name"])
1050
+ self.assertEqual(response.data["results"]["vid"], unused_vid)
1051
+ self.assertEqual(response.data["results"]["description"], data["description"])
1052
+ self.assertEqual(response.data["results"]["vlan_group"]["id"], self.vlan_group.pk)
1053
+ self.assertIn("custom_fields", response.data["results"])
1054
+ self.assertIn("sor", response.data["results"]["custom_fields"])
1055
+ self.assertEqual("Nautobot", response.data["results"]["custom_fields"]["sor"])
1056
+
1057
+ # Try to create one more VLAN
1058
+ response = self.client.post(
1059
+ url, {"name": "UTILIZED_VLAN_GROUP", "status": self.default_status.pk}, format="json", **self.header
1060
+ )
1061
+ self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
1062
+ self.assertIn("detail", response.data)
1063
+ self.assertIn(
1064
+ f"An insufficient number of VLANs are available within the VLANGroup {self.vlan_group}",
1065
+ response.data["detail"],
1066
+ )
1067
+
1068
+ def test_create_multiple_available_vlans(self):
1069
+ """
1070
+ Test the creation of available VLANS within a VLANGroup.
1071
+ """
1072
+ cf = CustomField.objects.create(key="sor", label="Source of Record Field", type="text")
1073
+ cf.content_types.add(ContentType.objects.get_for_model(VLAN))
1074
+ url = reverse("ipam-api:vlangroup-available-vlans", kwargs={"pk": self.vlan_group.pk})
1075
+ self.add_permissions(
1076
+ "ipam.view_vlangroup",
1077
+ "ipam.add_vlan",
1078
+ )
1079
+
1080
+ # Try to create ten VLANs (only nine are available)
1081
+ data = [ # First nine VLANs
1082
+ {
1083
+ "name": f"VLAN_{unused_vid}",
1084
+ "description": f"Test VLAN {unused_vid}",
1085
+ "status": self.default_status.pk,
1086
+ "custom_fields": {"sor": "Nautobot"},
1087
+ }
1088
+ for unused_vid in self.unused_vids
1089
+ ]
1090
+ additional_vlan = [
1091
+ {
1092
+ "name": "VLAN_10", # Out of range VLAN
1093
+ "description": "Test VLAN 10",
1094
+ "status": self.default_status.pk,
1095
+ "custom_fields": {"sor": "Nautobot"},
1096
+ }
1097
+ ]
1098
+ response = self.client.post(url, data + additional_vlan, format="json", **self.header)
1099
+ self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
1100
+ self.assertIn("detail", response.data)
1101
+
1102
+ # Create all nine available VLANs in a single request
1103
+ response = self.client.post(url, data, format="json", **self.header)
1104
+ self.assertHttpStatus(response, status.HTTP_201_CREATED)
1105
+ self.assertEqual(len(response.data["results"]), 9)
1106
+
1107
+ for i, vlan_data in enumerate(data):
1108
+ self.assertEqual(response.data["results"][i]["name"], vlan_data["name"])
1109
+ self.assertEqual(response.data["results"][i]["vid"], int(vlan_data["name"].replace("VLAN_", "")))
1110
+ self.assertEqual(response.data["results"][i]["description"], vlan_data["description"])
1111
+ self.assertEqual(response.data["results"][i]["vlan_group"]["id"], self.vlan_group.pk)
1112
+ self.assertIn("custom_fields", response.data["results"][i])
1113
+ self.assertIn("sor", response.data["results"][i]["custom_fields"])
1114
+ self.assertEqual("Nautobot", response.data["results"][i]["custom_fields"]["sor"])
1115
+
1116
+ def test_create_multiple_explicit_vlans(self):
1117
+ """
1118
+ Test the creation of available VLANS within a VLANGroup requesting explicit VLAN IDs.
1119
+ """
1120
+ url = reverse("ipam-api:vlangroup-available-vlans", kwargs={"pk": self.vlan_group.pk})
1121
+ self.add_permissions(
1122
+ "ipam.view_vlangroup",
1123
+ "ipam.add_vlan",
1124
+ )
1125
+
1126
+ # Try to create VLANs with specified VLAN IDs. Also, explicitly (and redundantly) specify a VLAN Group.
1127
+ data = [
1128
+ {"name": "VLAN_6", "status": self.default_status.pk, "vid": 6},
1129
+ {"name": "VLAN_7", "status": self.default_status.pk, "vid": 7},
1130
+ {"name": "VLAN_8", "status": self.default_status.pk},
1131
+ {"name": "VLAN_9", "status": self.default_status.pk, "vid": 9, "vlan_group": self.vlan_group.pk},
1132
+ {"name": "VLAN_15", "status": self.default_status.pk},
1133
+ {"name": "VLAN_16", "status": self.default_status.pk, "vid": 16, "vlan_group": self.vlan_group.pk},
1134
+ ]
1135
+
1136
+ response = self.client.post(url, data, format="json", **self.header)
1137
+ self.assertHttpStatus(response, status.HTTP_201_CREATED)
1138
+ self.assertEqual(len(response.data["results"]), 6)
1139
+
1140
+ for i, vlan_data in enumerate(data):
1141
+ self.assertEqual(response.data["results"][i]["name"], vlan_data["name"])
1142
+ self.assertEqual(response.data["results"][i]["vid"], int(vlan_data["name"].replace("VLAN_", "")))
1143
+ self.assertEqual(response.data["results"][i]["vlan_group"]["id"], self.vlan_group.pk)
1144
+
1145
+ def test_create_invalid_vlans(self):
1146
+ """
1147
+ Test the creation of VLANs using invalid requests.
1148
+ """
1149
+ url = reverse("ipam-api:vlangroup-available-vlans", kwargs={"pk": self.vlan_group.pk})
1150
+ self.add_permissions(
1151
+ "ipam.view_vlangroup",
1152
+ "ipam.add_vlan",
1153
+ )
1154
+
1155
+ # Try to create VLANs using same vid
1156
+ data = [
1157
+ {"name": "VLAN_6", "status": self.default_status.pk, "vid": 6},
1158
+ {"name": "VLAN_7", "status": self.default_status.pk, "vid": 6},
1159
+ {"name": "VLAN_8", "status": self.default_status.pk},
1160
+ ]
1161
+
1162
+ response = self.client.post(url, data, format="json", **self.header)
1163
+ self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
1164
+ self.assertIn("detail", response.data)
1165
+ self.assertEqual("VLAN 6 is not available within the VLANGroup.", response.data["detail"])
1166
+
1167
+ # Try to create VLANs specifying other VLAN Group
1168
+ some_other_vlan_group = VLANGroup.objects.create(name="VLAN Group 100-200", range="100-200")
1169
+ data = [{"name": "VLAN_7", "status": self.default_status.pk, "vlan_group": some_other_vlan_group.pk}]
1170
+
1171
+ response = self.client.post(url, data, format="json", **self.header)
1172
+ self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
1173
+ self.assertIn("detail", response.data)
1174
+ self.assertEqual(
1175
+ f"Invalid VLAN Group requested: {some_other_vlan_group}. Only VLAN Group {self.vlan_group} is permitted.",
1176
+ response.data["detail"],
1177
+ )
1178
+
1005
1179
 
1006
1180
  class VLANTest(APIViewTestCases.APIViewTestCase):
1007
1181
  model = VLAN
@@ -1256,7 +1256,7 @@ class VLANLocationAssignmentTestCase(FilterTestCases.FilterTestCase):
1256
1256
  params = {"q": vlan_vid}
1257
1257
  queryset = VLANLocationAssignment.objects.exclude(location__name__icontains=vlan_vid)
1258
1258
  filterset = VLANLocationAssignmentFilterSet(params, queryset).qs
1259
- expected_queryset = VLANLocationAssignment.objects.filter(vlan__vid__exact=vlan_vid)
1259
+ expected_queryset = queryset.filter(vlan__vid__exact=vlan_vid)
1260
1260
  self.assertQuerysetEqualAndNotEmpty(filterset, expected_queryset)
1261
1261
 
1262
1262
 
@@ -1302,7 +1302,7 @@ class TestVLANGroup(ModelTestCases.BaseModelTestCase):
1302
1302
  self.assertIn(f'VLAN groups may not associate to locations of type "{location_type.name}"', str(cm.exception))
1303
1303
 
1304
1304
  def test_get_next_available_vid(self):
1305
- vlangroup = VLANGroup.objects.create(name="VLAN Group 1")
1305
+ vlangroup = VLANGroup.objects.create(name="VLAN Group 1", range="1-6")
1306
1306
  status = Status.objects.get_for_model(VLAN).first()
1307
1307
  VLAN.objects.bulk_create(
1308
1308
  (
@@ -1316,6 +1316,40 @@ class TestVLANGroup(ModelTestCases.BaseModelTestCase):
1316
1316
 
1317
1317
  VLAN.objects.bulk_create((VLAN(name="VLAN 4", vid=4, vlan_group=vlangroup, status=status),))
1318
1318
  self.assertEqual(vlangroup.get_next_available_vid(), 6)
1319
+ # Next out of range.
1320
+ VLAN.objects.bulk_create((VLAN(name="VLAN 6", vid=6, vlan_group=vlangroup, status=status),))
1321
+ self.assertEqual(vlangroup.get_next_available_vid(), None)
1322
+
1323
+ def test_range_resize(self):
1324
+ vlangroup = VLANGroup.objects.create(name="VLAN Group 1", range="1-3")
1325
+ status = Status.objects.get_for_model(VLAN).first()
1326
+ VLAN.objects.bulk_create(
1327
+ (
1328
+ VLAN(name="VLAN 1", vid=1, vlan_group=vlangroup, status=status),
1329
+ VLAN(name="VLAN 2", vid=2, vlan_group=vlangroup, status=status),
1330
+ VLAN(name="VLAN 3", vid=3, vlan_group=vlangroup, status=status),
1331
+ )
1332
+ )
1333
+ with self.assertRaises(ValidationError) as exc:
1334
+ vlangroup.range = "1-2"
1335
+ vlangroup.validated_save()
1336
+ self.assertEqual(
1337
+ str(exc.exception), "{'range': ['VLAN group range may not be re-sized due to existing VLANs (IDs: 3).']}"
1338
+ )
1339
+
1340
+ def test_assign_vlan_out_of_range(self):
1341
+ vlangroup = VLANGroup.objects.create(name="VLAN Group 1", range="1-2")
1342
+ status = Status.objects.get_for_model(VLAN).first()
1343
+ VLAN.objects.bulk_create(
1344
+ (
1345
+ VLAN(name="VLAN 1", vid=1, vlan_group=vlangroup, status=status),
1346
+ VLAN(name="VLAN 2", vid=2, vlan_group=vlangroup, status=status),
1347
+ )
1348
+ )
1349
+ with self.assertRaises(ValidationError) as exc:
1350
+ vlan = VLAN(name="VLAN 3", vid=3, vlan_group=vlangroup, status=status)
1351
+ vlan.validated_save()
1352
+ self.assertEqual(str(exc.exception), "{'vid': ['VLAN ID is not contained in VLAN Group range (1-2)']}")
1319
1353
 
1320
1354
 
1321
1355
  class TestVLAN(ModelTestCases.BaseModelTestCase):
@@ -0,0 +1,61 @@
1
+ from django.test import TestCase
2
+
3
+ from nautobot.core.forms.utils import parse_numeric_range
4
+ from nautobot.extras.models import Status
5
+ from nautobot.ipam.models import VLAN, VLANGroup
6
+ from nautobot.ipam.utils import add_available_vlans
7
+
8
+
9
+ class AddAvailableVlansTest(TestCase):
10
+ """Tests for add_available_vlans()."""
11
+
12
+ def test_add_available_vlans(self):
13
+ vlan_group = VLANGroup.objects.create(name="VLAN Group 1", range="100-105,110-112,115")
14
+ status = Status.objects.get_for_model(VLAN).first()
15
+ vlan_100 = {"vid": 100, "available": 2, "range": "100-101"}
16
+ vlan_102 = VLAN.objects.create(name="VLAN 102", vid=102, vlan_group=vlan_group, status=status)
17
+ vlan_103 = VLAN.objects.create(name="VLAN 103", vid=103, vlan_group=vlan_group, status=status)
18
+ vlan_104 = {"vid": 104, "available": 2, "range": "104-105"}
19
+ vlan_110 = VLAN.objects.create(name="VLAN 110", vid=110, vlan_group=vlan_group, status=status)
20
+ vlan_111 = VLAN.objects.create(name="VLAN 111", vid=111, vlan_group=vlan_group, status=status)
21
+ vlan_112 = {"vid": 112, "available": 1, "range": "112"}
22
+ vlan_115 = VLAN.objects.create(name="VLAN 115", vid=115, vlan_group=vlan_group, status=status)
23
+
24
+ self.assertEqual(
25
+ list(add_available_vlans(vlan_group=vlan_group, vlans=vlan_group.vlans.all())),
26
+ [vlan_100, vlan_102, vlan_103, vlan_104, vlan_110, vlan_111, vlan_112, vlan_115],
27
+ )
28
+
29
+
30
+ class ParseNumericRangeTest(TestCase):
31
+ """Tests for add_available_vlans()."""
32
+
33
+ def test_parse(self):
34
+ self.assertEqual(parse_numeric_range(input_string="5"), [5])
35
+ self.assertEqual(parse_numeric_range(input_string="5-5"), [5])
36
+ self.assertEqual(parse_numeric_range(input_string="5-5,5,5"), [5])
37
+ self.assertEqual(parse_numeric_range(input_string="1-5"), [1, 2, 3, 4, 5])
38
+ self.assertEqual(parse_numeric_range(input_string="1,2,3,4,5"), [1, 2, 3, 4, 5])
39
+ self.assertEqual(parse_numeric_range(input_string="5,4,3,1,2"), [1, 2, 3, 4, 5])
40
+ self.assertEqual(parse_numeric_range(input_string="1-5,10"), [1, 2, 3, 4, 5, 10])
41
+ self.assertEqual(parse_numeric_range(input_string="1,5,10-11"), [1, 5, 10, 11])
42
+ self.assertEqual(parse_numeric_range(input_string="10-11,1,5"), [1, 5, 10, 11])
43
+ self.assertEqual(parse_numeric_range(input_string="a", base=16), [10])
44
+ self.assertEqual(parse_numeric_range(input_string="a,b", base=16), [10, 11])
45
+ self.assertEqual(parse_numeric_range(input_string="9-c,f", base=16), [9, 10, 11, 12, 15])
46
+ self.assertEqual(parse_numeric_range(input_string="15-19", base=16), [21, 22, 23, 24, 25])
47
+ self.assertEqual(parse_numeric_range(input_string="fa-ff", base=16), [250, 251, 252, 253, 254, 255])
48
+
49
+ def test_invalid_input(self):
50
+ invalid_inputs = [
51
+ [1, 2, 3],
52
+ None,
53
+ 1,
54
+ "",
55
+ "3-",
56
+ ]
57
+
58
+ for x in invalid_inputs:
59
+ with self.assertRaises(TypeError) as exc:
60
+ parse_numeric_range(input_string=x)
61
+ self.assertEqual(str(exc.exception), "Input value must be a string using a range format.")
@@ -328,6 +328,44 @@ class PrefixTestCase(ViewTestCases.PrimaryObjectViewTestCase, ViewTestCases.List
328
328
  strip_tags(content),
329
329
  )
330
330
 
331
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
332
+ def test_prefix_ipaddresses_table_list_includes_child_ips(self):
333
+ ip_status = Status.objects.get_for_model(IPAddress).first()
334
+ instance = Prefix.objects.create(
335
+ prefix="5.5.10.0/23",
336
+ namespace=self.namespace,
337
+ type=PrefixTypeChoices.TYPE_NETWORK,
338
+ status=self.statuses[1],
339
+ )
340
+ Prefix.objects.create(
341
+ prefix="5.5.10.0/30",
342
+ namespace=self.namespace,
343
+ type=PrefixTypeChoices.TYPE_POOL,
344
+ status=self.statuses[1],
345
+ )
346
+ IPAddress.objects.create(
347
+ address="5.5.10.1/23",
348
+ status=ip_status,
349
+ namespace=self.namespace,
350
+ )
351
+ IPAddress.objects.create(
352
+ address="5.5.10.4/23",
353
+ status=ip_status,
354
+ namespace=self.namespace,
355
+ )
356
+ url = reverse("ipam:prefix_ipaddresses", args=(instance.pk,))
357
+ response = self.client.get(url)
358
+ self.assertHttpStatus(response, 200)
359
+ content = response.content.decode(response.charset)
360
+ # This validates that both parent prefix and child prefix IPAddresses are present in parent prefix IPAddresses list
361
+ self.assertIn("5.5.10.1/23", strip_tags(content))
362
+ self.assertIn("5.5.10.4/23", strip_tags(content))
363
+ print(response.content.decode(response.charset))
364
+ ip_address_tab = (
365
+ f'<li role="presentation" class="active"><a href="{url}">IP Addresses <span class="badge">2</span></a></li>'
366
+ )
367
+ self.assertInHTML(ip_address_tab, content)
368
+
331
369
 
332
370
  class IPAddressTestCase(ViewTestCases.PrimaryObjectViewTestCase):
333
371
  model = IPAddress
@@ -993,6 +1031,8 @@ class VLANGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
993
1031
  "name": "VLAN Group X",
994
1032
  "location": location.pk,
995
1033
  "description": "A new VLAN group",
1034
+ "range": "1-4094",
1035
+ "tags": [t.pk for t in Tag.objects.get_for_model(VLANGroup)],
996
1036
  }
997
1037
 
998
1038
  def get_deletable_object(self):
@@ -1005,7 +1045,6 @@ class VLANTestCase(ViewTestCases.PrimaryObjectViewTestCase):
1005
1045
  @classmethod
1006
1046
  def setUpTestData(cls):
1007
1047
  cls.locations = Location.objects.filter(location_type=LocationType.objects.get(name="Campus"))
1008
- location_1 = cls.locations.first()
1009
1048
 
1010
1049
  vlangroups = (
1011
1050
  VLANGroup.objects.create(name="VLAN Group 1", location=cls.locations.first()),
@@ -1014,51 +1053,14 @@ class VLANTestCase(ViewTestCases.PrimaryObjectViewTestCase):
1014
1053
 
1015
1054
  roles = Role.objects.get_for_model(VLAN)[:2]
1016
1055
 
1017
- statuses = Status.objects.get_for_model(VLAN)
1018
- status_1 = statuses[0]
1019
- status_2 = statuses[1]
1020
-
1021
- vlans = (
1022
- VLAN.objects.create(
1023
- vlan_group=vlangroups[0],
1024
- vid=101,
1025
- name="VLAN101",
1026
- role=roles[0],
1027
- status=status_1,
1028
- _custom_field_data={"custom_field": "Value"},
1029
- ),
1030
- VLAN.objects.create(
1031
- vlan_group=vlangroups[0],
1032
- vid=102,
1033
- name="VLAN102",
1034
- role=roles[0],
1035
- status=status_1,
1036
- _custom_field_data={"custom_field": "Value"},
1037
- ),
1038
- VLAN.objects.create(
1039
- vlan_group=vlangroups[0],
1040
- vid=103,
1041
- name="VLAN103",
1042
- role=roles[0],
1043
- status=status_1,
1044
- _custom_field_data={"custom_field": "Value"},
1045
- ),
1046
- )
1047
- vlans[0].locations.add(location_1)
1048
- vlans[1].locations.add(location_1)
1049
- vlans[2].locations.add(location_1)
1050
-
1051
- custom_field = CustomField.objects.create(
1052
- type=CustomFieldTypeChoices.TYPE_TEXT, label="Custom Field", default=""
1053
- )
1054
- custom_field.content_types.set([ContentType.objects.get_for_model(VLAN)])
1056
+ status = Status.objects.get_for_model(VLAN).first()
1055
1057
 
1056
1058
  cls.form_data = {
1057
1059
  "vlan_group": vlangroups[1].pk,
1058
1060
  "vid": 999,
1059
1061
  "name": "VLAN999 with an unwieldy long name since we increased the limit to more than 64 characters",
1060
1062
  "tenant": None,
1061
- "status": status_2.pk,
1063
+ "status": status.pk,
1062
1064
  "role": roles[1].pk,
1063
1065
  "locations": list(cls.locations.values_list("pk", flat=True)[:2]),
1064
1066
  "description": "A new VLAN",
@@ -1068,7 +1070,7 @@ class VLANTestCase(ViewTestCases.PrimaryObjectViewTestCase):
1068
1070
  cls.bulk_edit_data = {
1069
1071
  "vlan_group": vlangroups[0].pk,
1070
1072
  "tenant": Tenant.objects.first().pk,
1071
- "status": status_2.pk,
1073
+ "status": status.pk,
1072
1074
  "role": roles[0].pk,
1073
1075
  "description": "New description",
1074
1076
  }