nautobot 2.3.7__py3-none-any.whl → 2.3.9__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 (336) hide show
  1. nautobot/apps/tables.py +2 -0
  2. nautobot/core/forms/__init__.py +4 -0
  3. nautobot/core/forms/fields.py +32 -0
  4. nautobot/core/jobs/__init__.py +24 -8
  5. nautobot/core/models/tree_queries.py +8 -0
  6. nautobot/core/settings.py +7 -0
  7. nautobot/core/settings.yaml +10 -0
  8. nautobot/core/signals.py +5 -4
  9. nautobot/core/templates/inc/paginator.html +3 -0
  10. nautobot/core/templates/nautobot_config.py.j2 +4 -0
  11. nautobot/core/templates/utilities/obj_table.html +1 -1
  12. nautobot/dcim/api/serializers.py +10 -5
  13. nautobot/dcim/forms.py +41 -34
  14. nautobot/dcim/models/device_components.py +12 -4
  15. nautobot/dcim/tables/devices.py +4 -2
  16. nautobot/dcim/tests/test_api.py +28 -0
  17. nautobot/dcim/tests/test_forms.py +17 -1
  18. nautobot/dcim/tests/test_models.py +58 -4
  19. nautobot/dcim/utils.py +9 -6
  20. nautobot/extras/context_managers.py +7 -8
  21. nautobot/extras/datasources/__init__.py +2 -0
  22. nautobot/extras/datasources/git.py +30 -49
  23. nautobot/extras/datasources/registry.py +2 -2
  24. nautobot/extras/jobs.py +17 -5
  25. nautobot/extras/models/datasources.py +6 -0
  26. nautobot/extras/models/groups.py +47 -33
  27. nautobot/extras/models/jobs.py +1 -1
  28. nautobot/extras/plugins/__init__.py +165 -0
  29. nautobot/extras/plugins/views.py +18 -3
  30. nautobot/extras/templates/extras/plugin_detail.html +33 -0
  31. nautobot/extras/tests/test_context_managers.py +16 -7
  32. nautobot/extras/tests/test_datasources.py +88 -1
  33. nautobot/extras/tests/test_dynamicgroups.py +12 -0
  34. nautobot/extras/tests/test_plugins.py +94 -0
  35. nautobot/extras/views.py +3 -1
  36. nautobot/ipam/filters.py +2 -2
  37. nautobot/ipam/models.py +29 -2
  38. nautobot/ipam/templates/ipam/ipaddress.html +2 -2
  39. nautobot/ipam/templates/ipam/ipaddress_interfaces.html +3 -0
  40. nautobot/ipam/templates/ipam/ipaddress_vm_interfaces.html +3 -0
  41. nautobot/ipam/templates/ipam/prefix.html +3 -3
  42. nautobot/ipam/templates/ipam/routetarget.html +2 -2
  43. nautobot/ipam/templates/ipam/vlan.html +3 -0
  44. nautobot/ipam/templates/ipam/vrf.html +7 -4
  45. nautobot/ipam/tests/test_models.py +68 -12
  46. nautobot/ipam/views.py +43 -0
  47. nautobot/project-static/docs/404.html +24 -3
  48. nautobot/project-static/docs/apps/index.html +24 -3
  49. nautobot/project-static/docs/apps/nautobot-apps.html +24 -3
  50. nautobot/project-static/docs/assets/javascripts/{bundle.525ec568.min.js → bundle.83f73b43.min.js} +2 -2
  51. nautobot/project-static/docs/assets/javascripts/{bundle.525ec568.min.js.map → bundle.83f73b43.min.js.map} +3 -3
  52. nautobot/project-static/docs/assets/stylesheets/main.0253249f.min.css +1 -0
  53. nautobot/project-static/docs/assets/stylesheets/main.0253249f.min.css.map +1 -0
  54. nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +24 -3
  55. nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +24 -3
  56. nautobot/project-static/docs/code-reference/nautobot/apps/api.html +24 -3
  57. nautobot/project-static/docs/code-reference/nautobot/apps/change_logging.html +24 -3
  58. nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +24 -3
  59. nautobot/project-static/docs/code-reference/nautobot/apps/config.html +24 -3
  60. nautobot/project-static/docs/code-reference/nautobot/apps/constants.html +24 -3
  61. nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +24 -3
  62. nautobot/project-static/docs/code-reference/nautobot/apps/exceptions.html +24 -3
  63. nautobot/project-static/docs/code-reference/nautobot/apps/factory.html +24 -3
  64. nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +24 -3
  65. nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +24 -3
  66. nautobot/project-static/docs/code-reference/nautobot/apps/graphql.html +24 -3
  67. nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +49 -6
  68. nautobot/project-static/docs/code-reference/nautobot/apps/models.html +24 -3
  69. nautobot/project-static/docs/code-reference/nautobot/apps/querysets.html +24 -3
  70. nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +24 -3
  71. nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +138 -3
  72. nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +24 -3
  73. nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +24 -3
  74. nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +24 -3
  75. nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +24 -3
  76. nautobot/project-static/docs/code-reference/nautobot/apps/views.html +24 -3
  77. nautobot/project-static/docs/development/apps/api/configuration-view.html +24 -3
  78. nautobot/project-static/docs/development/apps/api/database-backend-config.html +24 -3
  79. nautobot/project-static/docs/development/apps/api/models/django-admin.html +24 -3
  80. nautobot/project-static/docs/development/apps/api/models/global-search.html +24 -3
  81. nautobot/project-static/docs/development/apps/api/models/graphql.html +24 -3
  82. nautobot/project-static/docs/development/apps/api/models/index.html +24 -3
  83. nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +24 -3
  84. nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +24 -3
  85. nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +24 -3
  86. nautobot/project-static/docs/development/apps/api/platform-features/git-repository-content.html +24 -3
  87. nautobot/project-static/docs/development/apps/api/platform-features/index.html +24 -3
  88. nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +24 -3
  89. nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +24 -3
  90. nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +24 -3
  91. nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +27 -6
  92. nautobot/project-static/docs/development/apps/api/platform-features/table-extensions.html +8823 -0
  93. nautobot/project-static/docs/development/apps/api/platform-features/uniquely-identify-objects.html +27 -6
  94. nautobot/project-static/docs/development/apps/api/prometheus.html +24 -3
  95. nautobot/project-static/docs/development/apps/api/setup.html +33 -11
  96. nautobot/project-static/docs/development/apps/api/testing.html +24 -3
  97. nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +24 -3
  98. nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +24 -3
  99. nautobot/project-static/docs/development/apps/api/ui-extensions/index.html +24 -3
  100. nautobot/project-static/docs/development/apps/api/ui-extensions/navigation.html +24 -3
  101. nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +24 -3
  102. nautobot/project-static/docs/development/apps/api/views/base-template.html +24 -3
  103. nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +24 -3
  104. nautobot/project-static/docs/development/apps/api/views/django-generic-views.html +24 -3
  105. nautobot/project-static/docs/development/apps/api/views/help-documentation.html +24 -3
  106. nautobot/project-static/docs/development/apps/api/views/index.html +24 -3
  107. nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +24 -3
  108. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +24 -3
  109. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +24 -3
  110. nautobot/project-static/docs/development/apps/api/views/notes.html +24 -3
  111. nautobot/project-static/docs/development/apps/api/views/rest-api.html +24 -3
  112. nautobot/project-static/docs/development/apps/api/views/urls.html +24 -3
  113. nautobot/project-static/docs/development/apps/index.html +24 -3
  114. nautobot/project-static/docs/development/apps/migration/code-updates.html +24 -3
  115. nautobot/project-static/docs/development/apps/migration/dependency-updates.html +24 -3
  116. nautobot/project-static/docs/development/apps/migration/from-v1.html +24 -3
  117. nautobot/project-static/docs/development/apps/migration/model-updates/dcim.html +24 -3
  118. nautobot/project-static/docs/development/apps/migration/model-updates/extras.html +24 -3
  119. nautobot/project-static/docs/development/apps/migration/model-updates/global.html +24 -3
  120. nautobot/project-static/docs/development/apps/migration/model-updates/ipam.html +24 -3
  121. nautobot/project-static/docs/development/apps/porting-from-netbox.html +24 -3
  122. nautobot/project-static/docs/development/core/application-registry.html +24 -3
  123. nautobot/project-static/docs/development/core/best-practices.html +24 -3
  124. nautobot/project-static/docs/development/core/bootstrap-ui.html +24 -3
  125. nautobot/project-static/docs/development/core/caching.html +24 -3
  126. nautobot/project-static/docs/development/core/controllers.html +24 -3
  127. nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +24 -3
  128. nautobot/project-static/docs/development/core/generic-views.html +24 -3
  129. nautobot/project-static/docs/development/core/getting-started.html +24 -3
  130. nautobot/project-static/docs/development/core/homepage.html +24 -3
  131. nautobot/project-static/docs/development/core/index.html +24 -3
  132. nautobot/project-static/docs/development/core/model-checklist.html +24 -3
  133. nautobot/project-static/docs/development/core/model-features.html +24 -3
  134. nautobot/project-static/docs/development/core/natural-keys.html +24 -3
  135. nautobot/project-static/docs/development/core/navigation-menu.html +24 -3
  136. nautobot/project-static/docs/development/core/release-checklist.html +24 -3
  137. nautobot/project-static/docs/development/core/role-internals.html +24 -3
  138. nautobot/project-static/docs/development/core/settings.html +24 -3
  139. nautobot/project-static/docs/development/core/style-guide.html +24 -3
  140. nautobot/project-static/docs/development/core/templates.html +24 -3
  141. nautobot/project-static/docs/development/core/testing.html +24 -3
  142. nautobot/project-static/docs/development/core/user-preferences.html +24 -3
  143. nautobot/project-static/docs/development/index.html +24 -3
  144. nautobot/project-static/docs/development/jobs/index.html +24 -3
  145. nautobot/project-static/docs/development/jobs/migration/from-v1.html +24 -3
  146. nautobot/project-static/docs/index.html +24 -3
  147. nautobot/project-static/docs/objects.inv +0 -0
  148. nautobot/project-static/docs/overview/application_stack.html +24 -3
  149. nautobot/project-static/docs/overview/design_philosophy.html +24 -3
  150. nautobot/project-static/docs/release-notes/index.html +24 -3
  151. nautobot/project-static/docs/release-notes/version-1.0.html +24 -3
  152. nautobot/project-static/docs/release-notes/version-1.1.html +24 -3
  153. nautobot/project-static/docs/release-notes/version-1.2.html +24 -3
  154. nautobot/project-static/docs/release-notes/version-1.3.html +24 -3
  155. nautobot/project-static/docs/release-notes/version-1.4.html +24 -3
  156. nautobot/project-static/docs/release-notes/version-1.5.html +24 -3
  157. nautobot/project-static/docs/release-notes/version-1.6.html +24 -3
  158. nautobot/project-static/docs/release-notes/version-2.0.html +24 -3
  159. nautobot/project-static/docs/release-notes/version-2.1.html +24 -3
  160. nautobot/project-static/docs/release-notes/version-2.2.html +24 -3
  161. nautobot/project-static/docs/release-notes/version-2.3.html +337 -109
  162. nautobot/project-static/docs/requirements.txt +1 -1
  163. nautobot/project-static/docs/search/search_index.json +1 -1
  164. nautobot/project-static/docs/sitemap.xml +273 -269
  165. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  166. nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +24 -3
  167. nautobot/project-static/docs/user-guide/administration/configuration/authentication/remote.html +24 -3
  168. nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +24 -3
  169. nautobot/project-static/docs/user-guide/administration/configuration/index.html +24 -3
  170. nautobot/project-static/docs/user-guide/administration/configuration/redis.html +24 -3
  171. nautobot/project-static/docs/user-guide/administration/configuration/settings.html +29 -4
  172. nautobot/project-static/docs/user-guide/administration/configuration/time-zones.html +24 -3
  173. nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +24 -3
  174. nautobot/project-static/docs/user-guide/administration/guides/docker.html +24 -3
  175. nautobot/project-static/docs/user-guide/administration/guides/health-checks.html +24 -3
  176. nautobot/project-static/docs/user-guide/administration/guides/permissions.html +24 -3
  177. nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +24 -3
  178. nautobot/project-static/docs/user-guide/administration/guides/replicating-nautobot.html +24 -3
  179. nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +24 -3
  180. nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +24 -3
  181. nautobot/project-static/docs/user-guide/administration/guides/selinux-troubleshooting.html +24 -3
  182. nautobot/project-static/docs/user-guide/administration/installation/app-install.html +24 -3
  183. nautobot/project-static/docs/user-guide/administration/installation/external-authentication.html +24 -3
  184. nautobot/project-static/docs/user-guide/administration/installation/http-server.html +24 -3
  185. nautobot/project-static/docs/user-guide/administration/installation/index.html +24 -3
  186. nautobot/project-static/docs/user-guide/administration/installation/install_system.html +24 -3
  187. nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +24 -3
  188. nautobot/project-static/docs/user-guide/administration/installation/services.html +24 -3
  189. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-netbox.html +24 -3
  190. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +24 -3
  191. nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +24 -3
  192. nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +24 -3
  193. nautobot/project-static/docs/user-guide/administration/upgrading/database-backup.html +24 -3
  194. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/after-you-upgrade.html +24 -3
  195. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/before-you-upgrade.html +24 -3
  196. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/for-developers.html +24 -3
  197. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/index.html +24 -3
  198. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/whats-changed.html +24 -3
  199. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/region-and-site-data-migration-guide.html +24 -3
  200. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +24 -3
  201. nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +24 -3
  202. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuit.html +24 -3
  203. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittermination.html +24 -3
  204. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittype.html +24 -3
  205. nautobot/project-static/docs/user-guide/core-data-model/circuits/provider.html +24 -3
  206. nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +24 -3
  207. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloud.html +24 -3
  208. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudaccount.html +24 -3
  209. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetwork.html +24 -3
  210. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetworkprefixassignment.html +24 -3
  211. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudresourcetype.html +24 -3
  212. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservice.html +24 -3
  213. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservicenetworkassignment.html +24 -3
  214. nautobot/project-static/docs/user-guide/core-data-model/dcim/cable.html +24 -3
  215. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +24 -3
  216. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +24 -3
  217. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +24 -3
  218. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +24 -3
  219. nautobot/project-static/docs/user-guide/core-data-model/dcim/controller.html +24 -3
  220. nautobot/project-static/docs/user-guide/core-data-model/dcim/controllermanageddevicegroup.html +24 -3
  221. nautobot/project-static/docs/user-guide/core-data-model/dcim/device.html +24 -3
  222. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +24 -3
  223. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +24 -3
  224. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicefamily.html +24 -3
  225. nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +24 -3
  226. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +24 -3
  227. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +24 -3
  228. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +24 -3
  229. nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +24 -3
  230. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +24 -3
  231. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +24 -3
  232. nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +24 -3
  233. nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +24 -3
  234. nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +24 -3
  235. nautobot/project-static/docs/user-guide/core-data-model/dcim/manufacturer.html +24 -3
  236. nautobot/project-static/docs/user-guide/core-data-model/dcim/module.html +24 -3
  237. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebay.html +24 -3
  238. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebaytemplate.html +24 -3
  239. nautobot/project-static/docs/user-guide/core-data-model/dcim/moduletype.html +24 -3
  240. nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +24 -3
  241. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerfeed.html +24 -3
  242. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +24 -3
  243. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +24 -3
  244. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerpanel.html +24 -3
  245. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +24 -3
  246. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +24 -3
  247. nautobot/project-static/docs/user-guide/core-data-model/dcim/rack.html +24 -3
  248. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackgroup.html +24 -3
  249. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackreservation.html +24 -3
  250. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +24 -3
  251. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +24 -3
  252. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +24 -3
  253. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareversion.html +24 -3
  254. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualchassis.html +24 -3
  255. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +24 -3
  256. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +24 -3
  257. nautobot/project-static/docs/user-guide/core-data-model/extras/contact.html +24 -3
  258. nautobot/project-static/docs/user-guide/core-data-model/extras/team.html +24 -3
  259. nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +24 -3
  260. nautobot/project-static/docs/user-guide/core-data-model/ipam/namespace.html +24 -3
  261. nautobot/project-static/docs/user-guide/core-data-model/ipam/prefix.html +24 -3
  262. nautobot/project-static/docs/user-guide/core-data-model/ipam/rir.html +24 -3
  263. nautobot/project-static/docs/user-guide/core-data-model/ipam/routetarget.html +24 -3
  264. nautobot/project-static/docs/user-guide/core-data-model/ipam/service.html +24 -3
  265. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +24 -3
  266. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlangroup.html +24 -3
  267. nautobot/project-static/docs/user-guide/core-data-model/ipam/vrf.html +24 -3
  268. nautobot/project-static/docs/user-guide/core-data-model/overview/introduction.html +24 -3
  269. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenant.html +24 -3
  270. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenantgroup.html +24 -3
  271. nautobot/project-static/docs/user-guide/core-data-model/virtualization/cluster.html +24 -3
  272. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustergroup.html +24 -3
  273. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustertype.html +24 -3
  274. nautobot/project-static/docs/user-guide/core-data-model/virtualization/virtualmachine.html +24 -3
  275. nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +24 -3
  276. nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +24 -3
  277. nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +24 -3
  278. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +24 -3
  279. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +24 -3
  280. nautobot/project-static/docs/user-guide/feature-guides/getting-started/index.html +24 -3
  281. nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +24 -3
  282. nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +24 -3
  283. nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +24 -3
  284. nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +24 -3
  285. nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +24 -3
  286. nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +24 -3
  287. nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +24 -3
  288. nautobot/project-static/docs/user-guide/feature-guides/graphql.html +24 -3
  289. nautobot/project-static/docs/user-guide/feature-guides/ip-address-merge-tool.html +24 -3
  290. nautobot/project-static/docs/user-guide/feature-guides/relationships.html +24 -3
  291. nautobot/project-static/docs/user-guide/feature-guides/software-image-files-and-versions.html +24 -3
  292. nautobot/project-static/docs/user-guide/index.html +24 -3
  293. nautobot/project-static/docs/user-guide/platform-functionality/change-logging.html +24 -3
  294. nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +24 -3
  295. nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +24 -3
  296. nautobot/project-static/docs/user-guide/platform-functionality/customlink.html +24 -3
  297. nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +24 -3
  298. nautobot/project-static/docs/user-guide/platform-functionality/exporttemplate.html +24 -3
  299. nautobot/project-static/docs/user-guide/platform-functionality/externalintegration.html +24 -3
  300. nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +24 -3
  301. nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +24 -3
  302. nautobot/project-static/docs/user-guide/platform-functionality/graphqlquery.html +24 -3
  303. nautobot/project-static/docs/user-guide/platform-functionality/imageattachment.html +24 -3
  304. nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +24 -3
  305. nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +24 -3
  306. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +24 -3
  307. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +24 -3
  308. nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +24 -3
  309. nautobot/project-static/docs/user-guide/platform-functionality/napalm.html +24 -3
  310. nautobot/project-static/docs/user-guide/platform-functionality/note.html +24 -3
  311. nautobot/project-static/docs/user-guide/platform-functionality/objectmetadata.html +24 -3
  312. nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +24 -3
  313. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +24 -3
  314. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +24 -3
  315. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +24 -3
  316. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/ui-related-endpoints.html +24 -3
  317. nautobot/project-static/docs/user-guide/platform-functionality/role.html +24 -3
  318. nautobot/project-static/docs/user-guide/platform-functionality/savedview.html +24 -3
  319. nautobot/project-static/docs/user-guide/platform-functionality/secret.html +24 -3
  320. nautobot/project-static/docs/user-guide/platform-functionality/staticgroupassociation.html +24 -3
  321. nautobot/project-static/docs/user-guide/platform-functionality/status.html +24 -3
  322. nautobot/project-static/docs/user-guide/platform-functionality/tag.html +24 -3
  323. nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +24 -3
  324. nautobot/project-static/docs/user-guide/platform-functionality/users/objectpermission.html +24 -3
  325. nautobot/project-static/docs/user-guide/platform-functionality/users/token.html +24 -3
  326. nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +24 -3
  327. nautobot/project-static/js/forms.js +41 -5
  328. nautobot/virtualization/tables.py +1 -1
  329. {nautobot-2.3.7.dist-info → nautobot-2.3.9.dist-info}/METADATA +2 -2
  330. {nautobot-2.3.7.dist-info → nautobot-2.3.9.dist-info}/RECORD +334 -333
  331. nautobot/project-static/docs/assets/stylesheets/main.8c3ca2c6.min.css +0 -1
  332. nautobot/project-static/docs/assets/stylesheets/main.8c3ca2c6.min.css.map +0 -1
  333. {nautobot-2.3.7.dist-info → nautobot-2.3.9.dist-info}/LICENSE.txt +0 -0
  334. {nautobot-2.3.7.dist-info → nautobot-2.3.9.dist-info}/NOTICE +0 -0
  335. {nautobot-2.3.7.dist-info → nautobot-2.3.9.dist-info}/WHEEL +0 -0
  336. {nautobot-2.3.7.dist-info → nautobot-2.3.9.dist-info}/entry_points.txt +0 -0
nautobot/apps/tables.py CHANGED
@@ -15,6 +15,7 @@ from nautobot.core.tables import (
15
15
  TagColumn,
16
16
  ToggleColumn,
17
17
  )
18
+ from nautobot.extras.plugins import TableExtension
18
19
  from nautobot.extras.tables import RoleTableMixin, StatusTableMixin
19
20
 
20
21
  __all__ = (
@@ -32,5 +33,6 @@ __all__ = (
32
33
  "RoleTableMixin",
33
34
  "StatusTableMixin",
34
35
  "TagColumn",
36
+ "TableExtension",
35
37
  "ToggleColumn",
36
38
  )
@@ -7,6 +7,8 @@ from nautobot.core.forms.constants import (
7
7
  NUMERIC_EXPANSION_PATTERN,
8
8
  )
9
9
  from nautobot.core.forms.fields import (
10
+ AutoPositionField,
11
+ AutoPositionPatternField,
10
12
  CommentField,
11
13
  CSVChoiceField,
12
14
  CSVContentTypeField,
@@ -80,6 +82,8 @@ __all__ = (
80
82
  "ALPHANUMERIC_EXPANSION_PATTERN",
81
83
  "APISelect",
82
84
  "APISelectMultiple",
85
+ "AutoPositionField",
86
+ "AutoPositionPatternField",
83
87
  "BOOLEAN_CHOICES",
84
88
  "BOOLEAN_WITH_BLANK_CHOICES",
85
89
  "BootstrapMixin",
@@ -444,6 +444,38 @@ class SlugField(django_forms.SlugField):
444
444
  self.widget.attrs["slug-source"] = slug_source
445
445
 
446
446
 
447
+ class AutoPositionField(django_forms.CharField):
448
+ def __init__(self, source="name", *args, **kwargs):
449
+ """
450
+ Instantiate a AutoPositionField.
451
+
452
+ Args:
453
+ source (str, tuple): Name of the field (or a list of field names) that will be used to suggest a position.
454
+ """
455
+ kwargs.setdefault("label", "Position")
456
+ kwargs.setdefault("widget", forms.SlugWidget)
457
+ super().__init__(*args, **kwargs)
458
+ if isinstance(source, (tuple, list)):
459
+ source = " ".join(source)
460
+ self.widget.attrs["source"] = source
461
+
462
+
463
+ class AutoPositionPatternField(ExpandableNameField):
464
+ def __init__(self, source="name_pattern", *args, **kwargs):
465
+ """
466
+ Instantiate a AutoPositionPatternField.
467
+
468
+ Args:
469
+ source (str, tuple): Name pattern of the field (or a list of field names) that will be used to suggest a position pattern.
470
+ """
471
+ kwargs.setdefault("label", "Position")
472
+ kwargs.setdefault("widget", forms.SlugWidget)
473
+ super().__init__(*args, **kwargs)
474
+ if isinstance(source, (tuple, list)):
475
+ source = " ".join(source)
476
+ self.widget.attrs["source"] = source
477
+
478
+
447
479
  class DynamicModelChoiceMixin:
448
480
  """
449
481
  :param display_field: The name of the attribute of an API response object to display in the selection list
@@ -19,7 +19,12 @@ from nautobot.core.jobs.cleanup import LogsCleanup
19
19
  from nautobot.core.jobs.groups import RefreshDynamicGroupCaches
20
20
  from nautobot.core.utils.lookup import get_filterset_for_model
21
21
  from nautobot.core.utils.requests import get_filterable_params_from_filter_params
22
- from nautobot.extras.datasources import ensure_git_repository, git_repository_dry_run, refresh_datasource_content
22
+ from nautobot.extras.datasources import (
23
+ ensure_git_repository,
24
+ git_repository_dry_run,
25
+ refresh_datasource_content,
26
+ refresh_job_code_from_repository,
27
+ )
23
28
  from nautobot.extras.jobs import BooleanVar, ChoiceVar, FileVar, Job, ObjectVar, RunJobTaskFailed, StringVar, TextVar
24
29
  from nautobot.extras.models import ExportTemplate, GitRepository
25
30
 
@@ -49,13 +54,24 @@ class GitRepositorySync(Job):
49
54
  self.logger.info(f'Creating/refreshing local copy of Git repository "{repository.name}"...')
50
55
 
51
56
  try:
52
- ensure_git_repository(repository, logger=self.logger)
53
- refresh_datasource_content("extras.gitrepository", repository, user, job_result, delete=False)
54
- # Given that the above succeeded, tell all workers (including ourself) to call ensure_git_repository()
55
- app.control.broadcast("refresh_git_repository", repository_pk=repository.pk, head=repository.current_head)
56
- finally:
57
- if job_result.duration:
58
- self.logger.info("Repository synchronization completed in %s", job_result.duration)
57
+ with transaction.atomic():
58
+ ensure_git_repository(repository, logger=self.logger)
59
+ refresh_datasource_content("extras.gitrepository", repository, user, job_result, delete=False)
60
+ # Given that the above succeeded, tell all workers (including ourself) to call ensure_git_repository()
61
+ app.control.broadcast(
62
+ "refresh_git_repository", repository_pk=repository.pk, head=repository.current_head
63
+ )
64
+ if job_result.duration:
65
+ self.logger.info("Repository synchronization completed in %s", job_result.duration)
66
+ except Exception:
67
+ job_result.log("Changes to database records have been reverted.")
68
+ # Re-check-out previous commit if any
69
+ repository.refresh_from_db()
70
+ if repository.current_head:
71
+ job_result.log(f"Attempting to revert local repository clone to commit {repository.current_head}")
72
+ ensure_git_repository(repository, logger=self.logger, head=repository.current_head)
73
+ refresh_job_code_from_repository(repository.slug, ignore_import_errors=True)
74
+ raise
59
75
 
60
76
 
61
77
  class GitRepositoryDryRun(Job):
@@ -1,9 +1,11 @@
1
1
  from django.core.cache import cache
2
2
  from django.db.models import Case, When
3
+ from django.db.models.signals import post_delete, post_save
3
4
  from tree_queries.models import TreeNode
4
5
  from tree_queries.query import TreeManager as TreeManager_, TreeQuerySet as TreeQuerySet_
5
6
 
6
7
  from nautobot.core.models import BaseManager, querysets
8
+ from nautobot.core.signals import invalidate_max_depth_cache
7
9
 
8
10
 
9
11
  class TreeQuerySet(TreeQuerySet_, querysets.RestrictedQuerySet):
@@ -117,3 +119,9 @@ class TreeModel(TreeNode):
117
119
  display_str += self.name
118
120
  cache.set(cache_key, display_str, 5)
119
121
  return display_str
122
+
123
+ @classmethod
124
+ def __init_subclass__(cls, **kwargs):
125
+ super().__init_subclass__(**kwargs)
126
+ post_save.connect(invalidate_max_depth_cache, sender=cls)
127
+ post_delete.connect(invalidate_max_depth_cache, sender=cls)
nautobot/core/settings.py CHANGED
@@ -418,7 +418,14 @@ if "NAUTOBOT_ALLOWED_HOSTS" in os.environ and os.environ["NAUTOBOT_ALLOWED_HOSTS
418
418
  ALLOWED_HOSTS = os.environ["NAUTOBOT_ALLOWED_HOSTS"].split(" ")
419
419
  else:
420
420
  ALLOWED_HOSTS = []
421
+
422
+ # Allow CSRF trusted origins to be set via an environment variable
423
+ # This allows Nautobot to be run behind a reverse proxy that terminates TLS
424
+ # and fix potential issues with CSRF validation
421
425
  CSRF_TRUSTED_ORIGINS = []
426
+ if "NAUTOBOT_CSRF_TRUSTED_ORIGINS" in os.environ and os.environ["NAUTOBOT_CSRF_TRUSTED_ORIGINS"] != "":
427
+ CSRF_TRUSTED_ORIGINS = os.getenv("NAUTOBOT_CSRF_TRUSTED_ORIGINS", "").split(_CONFIG_SETTING_SEPARATOR)
428
+
422
429
  CSRF_FAILURE_VIEW = "nautobot.core.views.csrf_failure"
423
430
  DATE_FORMAT = os.getenv("NAUTOBOT_DATE_FORMAT", "N j, Y")
424
431
  DATETIME_FORMAT = os.getenv("NAUTOBOT_DATETIME_FORMAT", "N j, Y g:i a")
@@ -587,6 +587,16 @@ properties:
587
587
  description: >-
588
588
  A list of hosts (fully-qualified domain names (FQDNs) or subdomains) that are considered trusted origins
589
589
  for cross-site secure requests such as HTTPS POST.
590
+
591
+ You might need to set this if you are using a reverse proxy or load balancer that changes the host header or if you face the error
592
+ 'Invalid Form Submission - CSRF failure has occured' and 'Origin checking failed - https://subdomain.example.com does not match any trusted origins.'
593
+
594
+ Example:
595
+
596
+ ```python
597
+ CSRF_TRUSTED_ORIGINS = ['https://subdomain.example.com', 'https://*.example.com']
598
+ ```
599
+ environment_variable: "NAUTOBOT_CSRF_TRUSTED_ORIGINS"
590
600
  items:
591
601
  type: "string"
592
602
  see_also:
nautobot/core/signals.py CHANGED
@@ -7,7 +7,6 @@ import logging
7
7
 
8
8
  from django.contrib.auth.signals import user_logged_in, user_logged_out
9
9
  from django.core.cache import cache
10
- from django.db.models.signals import post_delete, post_save
11
10
  from django.dispatch import receiver, Signal
12
11
  import redis.exceptions
13
12
 
@@ -62,10 +61,12 @@ def disable_for_loaddata(signal_handler):
62
61
  return wrapper
63
62
 
64
63
 
65
- @receiver(post_save)
66
- @receiver(post_delete)
67
64
  def invalidate_max_depth_cache(sender, **kwargs):
68
- """Clear the appropriate TreeManager.max_depth cache as the create/update/delete may have changed the tree."""
65
+ """
66
+ Clear the appropriate TreeManager.max_depth cache as the create/update/delete may have changed the tree.
67
+
68
+ Note that this signal is connected in `TreeModel.__init_subclass__()` so as to only apply to those models.
69
+ """
69
70
  from nautobot.core.models.tree_queries import TreeManager
70
71
 
71
72
  if not isinstance(sender.objects, TreeManager):
@@ -29,6 +29,9 @@
29
29
  {% endif %}
30
30
  {% endfor %}
31
31
  <select name="per_page" id="per_page" class="form-control">
32
+ {% if page.paginator.per_page not in "PER_PAGE_DEFAULTS"|settings_or_config %}
33
+ <option value="{{ page.paginator.per_page }}" selected="selected">{{ page.paginator.per_page }}</option>
34
+ {% endif %}
32
35
  {% for n in "PER_PAGE_DEFAULTS"|settings_or_config %}
33
36
  <option value="{{ n }}"{% if page.paginator.per_page == n %} selected="selected"{% endif %}>{{ n }}</option>
34
37
  {% endfor %}
@@ -94,9 +94,13 @@ SECRET_KEY = os.getenv("NAUTOBOT_SECRET_KEY", "{{ secret_key }}")
94
94
  # FQDNs that are considered trusted origins for secure, cross-domain, requests such as HTTPS POST.
95
95
  # If running Nautobot under a single domain, you may not need to set this variable;
96
96
  # if running on multiple domains, you *may* need to set this variable to more or less the same as ALLOWED_HOSTS above.
97
+ # You also want to set this variable if you are facing CSRF validation issues such as
98
+ # 'CSRF failure has occured' or 'Origin checking failed - https://subdomain.example.com does not match any trusted origins.'
97
99
  # https://docs.djangoproject.com/en/stable/ref/settings/#csrf-trusted-origins
98
100
  #
99
101
  # CSRF_TRUSTED_ORIGINS = []
102
+ # if "NAUTOBOT_CSRF_TRUSTED_ORIGINS" in os.environ and os.environ["NAUTOBOT_CSRF_TRUSTED_ORIGINS"] != "":
103
+ # CSRF_TRUSTED_ORIGINS = os.getenv("NAUTOBOT_CSRF_TRUSTED_ORIGINS", "").split(_CONFIG_SETTING_SEPARATOR)
100
104
 
101
105
  # Date/time formatting. See the following link for supported formats:
102
106
  # https://docs.djangoproject.com/en/stable/ref/templates/builtins/#date
@@ -45,7 +45,7 @@
45
45
  {% else %}
46
46
  {% include table_template|default:'responsive_table.html' %}
47
47
  {% endif %}
48
- {% if not disable_pagination %}
48
+ {% if table.paginator.num_pages > 1 and not disable_pagination %}
49
49
  {% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
50
50
  {% endif %}
51
51
  <div class="clearfix"></div>
@@ -798,14 +798,19 @@ class InterfaceSerializer(
798
798
  def validate(self, data):
799
799
  # Validate many-to-many VLAN assignments
800
800
  device = self.instance.device if self.instance else data.get("device")
801
- # TODO: after Location model replaced Site, which was not a hierarchical model, should we allow users to assign a VLAN belongs to
802
- # the parent Location or the child location of `device.location`?
801
+ location = None
802
+ if device:
803
+ location = device.location
804
+ if location:
805
+ location_ids = location.ancestors(include_self=True).values_list("id", flat=True)
806
+ else:
807
+ location_ids = []
803
808
  for vlan in data.get("tagged_vlans", []):
804
- if vlan.locations.exists() and not vlan.locations.filter(pk=device.location.pk).exists():
809
+ if vlan.locations.exists() and not vlan.locations.filter(pk__in=location_ids).exists():
805
810
  raise serializers.ValidationError(
806
811
  {
807
- "tagged_vlans": f"VLAN {vlan} must have a common location as the interface's parent device, or "
808
- f"it must be global."
812
+ "tagged_vlans": f"VLAN {vlan} must have the same location as the interface's parent device, "
813
+ f"or is in one of the parents of the interface's parent device's location, or it must be global."
809
814
  }
810
815
  )
811
816
 
nautobot/dcim/forms.py CHANGED
@@ -13,6 +13,8 @@ from nautobot.core.forms import (
13
13
  add_blank_choice,
14
14
  APISelect,
15
15
  APISelectMultiple,
16
+ AutoPositionField,
17
+ AutoPositionPatternField,
16
18
  BootstrapMixin,
17
19
  BulkEditNullBooleanSelect,
18
20
  ColorSelect,
@@ -213,23 +215,27 @@ class InterfaceCommonForm(forms.Form):
213
215
  elif mode == InterfaceModeChoices.MODE_TAGGED_ALL:
214
216
  self.cleaned_data["tagged_vlans"] = []
215
217
 
216
- # Validate tagged VLANs; must be a global VLAN or in the same location
217
- # TODO: after Location model replaced Site, which was not a hierarchical model, should we allow users to add a VLAN
218
- # belongs to the parent Location or the child location of the parent device to the `tagged_vlan` field of the interface?
218
+ # Validate tagged VLANs; must be a global VLAN or in the same location as the
219
+ # parent device/VM or any of that location's parent locations
219
220
  elif mode == InterfaceModeChoices.MODE_TAGGED:
220
- valid_location = self.cleaned_data[parent_field].location
221
+ location = self.cleaned_data[parent_field].location
222
+ if location:
223
+ location_ids = location.ancestors(include_self=True).values_list("id", flat=True)
224
+ else:
225
+ location_ids = []
221
226
  invalid_vlans = [
222
227
  str(v)
223
228
  for v in tagged_vlans
224
229
  if v.locations.without_tree_fields().exists()
225
- and not VLANLocationAssignment.objects.filter(location=valid_location, vlan=v).exists()
230
+ and not VLANLocationAssignment.objects.filter(location__in=location_ids, vlan=v).exists()
226
231
  ]
227
232
 
228
233
  if invalid_vlans:
229
234
  raise forms.ValidationError(
230
235
  {
231
- "tagged_vlans": f"The tagged VLANs ({', '.join(invalid_vlans)}) must belong to the same location as "
232
- f"the interface's parent device/VM, or they must be global"
236
+ "tagged_vlans": f"The tagged VLANs ({', '.join(invalid_vlans)}) must have the same location as the "
237
+ "interface's parent device, or is in one of the parents of the interface's parent device's location, "
238
+ "or it must be global."
233
239
  }
234
240
  )
235
241
 
@@ -265,23 +271,7 @@ class ComponentForm(BootstrapMixin, forms.Form):
265
271
 
266
272
 
267
273
  class ModularComponentForm(ComponentForm):
268
- name_pattern = ExpandableNameField(
269
- label="Name",
270
- help_text="""
271
- Alphanumeric ranges are supported for bulk creation. Mixed cases and types within a single range
272
- are not supported. Examples:
273
- <ul>
274
- <li><code>[ge,xe]-0/0/[0-9]</code></li>
275
- <li><code>e[0-3][a-d,f]</code></li>
276
- </ul>
277
-
278
- The variables <code>{module}</code>, <code>{module.parent}</code>, <code>{module.parent.parent}</code>, etc.
279
- may be used in the name field and will be replaced by the <code>position</code> of the module bay that the
280
- module occupies (skipping over any bays with a blank <code>position</code>). These variables can be used
281
- multiple times in the component name and there is no limit to the depth of parent levels.
282
- Any variables that cannot be replaced by a suitable position value will remain unchanged.
283
- """,
284
- )
274
+ """Base class for forms for components that can be assigned to either a device or a module."""
285
275
 
286
276
 
287
277
  #
@@ -1060,11 +1050,27 @@ class ComponentTemplateCreateForm(ComponentForm):
1060
1050
  description = forms.CharField(required=False)
1061
1051
 
1062
1052
 
1063
- class ModularComponentTemplateCreateForm(ModularComponentForm):
1053
+ class ModularComponentTemplateCreateForm(ComponentTemplateCreateForm):
1064
1054
  """
1065
1055
  Base form for the creation of modular device component templates (subclassed from ModularComponentTemplateModel).
1066
1056
  """
1067
1057
 
1058
+ name_pattern = ExpandableNameField(
1059
+ label="Name",
1060
+ help_text="""
1061
+ Alphanumeric ranges are supported for bulk creation. Mixed cases and types within a single range
1062
+ are not supported. Examples:
1063
+ <ul>
1064
+ <li><code>[ge,xe]-0/0/[0-9]</code></li>
1065
+ <li><code>e[0-3][a-d,f]</code></li>
1066
+ </ul>
1067
+
1068
+ The variables <code>{module}</code>, <code>{module.parent}</code>, <code>{module.parent.parent}</code>, etc.
1069
+ may be used in the name field and will be replaced by the <code>position</code> of the module bay that the
1070
+ module occupies (skipping over any bays with a blank <code>position</code>). These variables can be used
1071
+ multiple times in the component name and there is no limit to the depth of parent levels.
1072
+ Any variables that cannot be replaced by a suitable position value will remain unchanged.""",
1073
+ )
1068
1074
  device_type = DynamicModelChoiceField(
1069
1075
  queryset=DeviceType.objects.all(),
1070
1076
  required=False,
@@ -1073,7 +1079,6 @@ class ModularComponentTemplateCreateForm(ModularComponentForm):
1073
1079
  queryset=ModuleType.objects.all(),
1074
1080
  required=False,
1075
1081
  )
1076
- description = forms.CharField(required=False)
1077
1082
 
1078
1083
 
1079
1084
  class ConsolePortTemplateForm(ModularComponentTemplateForm):
@@ -1566,10 +1571,10 @@ class ModuleBayBaseCreateForm(BootstrapMixin, forms.Form):
1566
1571
  required=False,
1567
1572
  help_text="Alphanumeric ranges are supported. (Must match the number of names being created.)",
1568
1573
  )
1569
- position_pattern = ExpandableNameField(
1570
- label="Position",
1574
+ position_pattern = AutoPositionPatternField(
1571
1575
  required=False,
1572
- help_text="Alphanumeric ranges are supported. (Must match the number of names being created.)",
1576
+ help_text="Alphanumeric ranges are supported. (Must match the number of names being created.)"
1577
+ " Default to the names of the module bays unless manually supplied by the user.",
1573
1578
  )
1574
1579
  description = forms.CharField(max_length=CHARFIELD_MAX_LENGTH, required=False)
1575
1580
 
@@ -2538,12 +2543,8 @@ class DeviceBulkAddComponentForm(ComponentForm, CustomFieldModelBulkEditFormMixi
2538
2543
  nullable_fields = []
2539
2544
 
2540
2545
 
2541
- class ModuleBulkAddComponentForm(ModularComponentForm, CustomFieldModelBulkEditFormMixin):
2546
+ class ModuleBulkAddComponentForm(DeviceBulkAddComponentForm):
2542
2547
  pk = forms.ModelMultipleChoiceField(queryset=Module.objects.all(), widget=forms.MultipleHiddenInput())
2543
- description = forms.CharField(max_length=CHARFIELD_MAX_LENGTH, required=False)
2544
-
2545
- class Meta:
2546
- nullable_fields = []
2547
2548
 
2548
2549
 
2549
2550
  #
@@ -3562,6 +3563,12 @@ class ModuleBayFilterForm(NautobotFilterForm):
3562
3563
 
3563
3564
 
3564
3565
  class ModuleBayForm(NautobotModelForm):
3566
+ position = AutoPositionField(
3567
+ max_length=CHARFIELD_MAX_LENGTH,
3568
+ help_text="The position of the module bay within the parent device/module. "
3569
+ "Defaults to the name of the module bay unless overridden.",
3570
+ required=False,
3571
+ )
3565
3572
  parent_device = DynamicModelChoiceField(
3566
3573
  queryset=Device.objects.all(),
3567
3574
  required=False,
@@ -727,19 +727,22 @@ class Interface(ModularComponentModel, CableTermination, PathEndpoint, BaseInter
727
727
  )
728
728
 
729
729
  # Validate untagged VLAN
730
- # TODO: after Location model replaced Site, which was not a hierarchical model, should we allow users to assign a VLAN belongs to
731
- # the parent Locations or the child locations of `device.location`?
730
+ location = self.parent.location if self.parent is not None else None
731
+ if location:
732
+ location_ids = location.ancestors(include_self=True).values_list("id", flat=True)
733
+ else:
734
+ location_ids = []
732
735
  if (
733
736
  self.untagged_vlan
734
737
  and self.untagged_vlan.locations.exists()
735
738
  and self.parent
736
- and not self.untagged_vlan.locations.filter(id=self.parent.location_id).exists()
739
+ and not self.untagged_vlan.locations.filter(pk__in=location_ids).exists()
737
740
  ):
738
741
  raise ValidationError(
739
742
  {
740
743
  "untagged_vlan": (
741
744
  f"The untagged VLAN ({self.untagged_vlan}) must have a common location as the interface's parent "
742
- f"device, or it must be global."
745
+ f"device, or is in one of the parents of the interface's parent device's location, or it must be global."
743
746
  )
744
747
  }
745
748
  )
@@ -1277,3 +1280,8 @@ class ModuleBay(PrimaryModel):
1277
1280
 
1278
1281
  if not (self.parent_device or self.parent_module):
1279
1282
  raise ValidationError("Either parent_device or parent_module must be set")
1283
+
1284
+ # Populate the position field with the name of the module bay if it is not supplied by the user.
1285
+
1286
+ if not self.position:
1287
+ self.position = self.name
@@ -173,6 +173,7 @@ class DeviceTable(StatusTableMixin, RoleTableMixin, BaseTable):
173
173
  template_code="""{% if record.device_redundancy_group %}<span class="badge badge-default">{{ record.device_redundancy_group_priority|default:'None' }}</span>{% else %}—{% endif %}"""
174
174
  )
175
175
  controller_managed_device_group = tables.Column(linkify=True)
176
+ software_version = tables.Column(linkify=True, verbose_name="Software Version")
176
177
  secrets_group = tables.Column(linkify=True)
177
178
  tags = TagColumn(url_name="dcim:device_list")
178
179
 
@@ -201,6 +202,7 @@ class DeviceTable(StatusTableMixin, RoleTableMixin, BaseTable):
201
202
  "vc_priority",
202
203
  "device_redundancy_group",
203
204
  "device_redundancy_group_priority",
205
+ "software_version",
204
206
  "controller_managed_device_group",
205
207
  "secrets_group",
206
208
  "tags",
@@ -616,7 +618,7 @@ class DeviceModulePowerOutletTable(PowerOutletTable):
616
618
  row_attrs = {"class": cable_status_color_css}
617
619
 
618
620
 
619
- class BaseInterfaceTable(BaseTable):
621
+ class BaseInterfaceTable(StatusTableMixin, RoleTableMixin, BaseTable):
620
622
  enabled = BooleanColumn()
621
623
  ip_addresses = tables.TemplateColumn(
622
624
  template_code=INTERFACE_IPADDRESSES,
@@ -632,7 +634,7 @@ class BaseInterfaceTable(BaseTable):
632
634
  vrf = tables.Column(linkify=True, verbose_name="VRF")
633
635
 
634
636
 
635
- class InterfaceTable(StatusTableMixin, ModularDeviceComponentTable, BaseInterfaceTable, PathEndpointTable):
637
+ class InterfaceTable(ModularDeviceComponentTable, BaseInterfaceTable, PathEndpointTable):
636
638
  mgmt_only = BooleanColumn()
637
639
  tags = TagColumn(url_name="dcim:interface_list")
638
640
 
@@ -2198,6 +2198,34 @@ class InterfaceTest(Mixins.ModularDeviceComponentMixin, Mixins.BasePortTestMixin
2198
2198
  self.client.post(url, self.untagged_vlan_data, format="json", **self.header), status.HTTP_201_CREATED
2199
2199
  )
2200
2200
 
2201
+ def test_tagged_vlan_must_be_in_the_location_or_parent_locations_of_the_parent_device(self):
2202
+ self.add_permissions("dcim.add_interface")
2203
+
2204
+ interface_status = Status.objects.get_for_model(Interface).first()
2205
+ location = self.devices[0].location
2206
+ location_ids = [ancestor.id for ancestor in location.ancestors()]
2207
+ non_valid_locations = Location.objects.exclude(pk__in=location_ids)
2208
+ faulty_vlan = self.vlans[0]
2209
+ faulty_vlan.locations.set([non_valid_locations.first().pk])
2210
+ faulty_vlan.validated_save()
2211
+ faulty_data = {
2212
+ "device": self.devices[0].pk,
2213
+ "name": "Test Vlans Interface",
2214
+ "type": "virtual",
2215
+ "status": interface_status.pk,
2216
+ "mode": InterfaceModeChoices.MODE_TAGGED,
2217
+ "parent_interface": self.interfaces[1].pk,
2218
+ "tagged_vlans": [faulty_vlan.pk, self.vlans[1].pk],
2219
+ "untagged_vlan": self.vlans[2].pk,
2220
+ }
2221
+ response = self.client.post(self._get_list_url(), data=faulty_data, format="json", **self.header)
2222
+ self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
2223
+ self.assertIn(
2224
+ b"must have the same location as the interface's parent device, or is in one of the parents of the interface's parent device's location, or "
2225
+ b"it must be global.",
2226
+ response.content,
2227
+ )
2228
+
2201
2229
  def test_interface_belonging_to_common_device_or_vc_allowed(self):
2202
2230
  """Test parent, bridge, and LAG interfaces belonging to common device or VC is valid"""
2203
2231
  self.add_permissions("dcim.add_interface")
@@ -341,9 +341,25 @@ class InterfaceTestCase(TestCase):
341
341
  "tagged_vlans": [cls.vlan.pk],
342
342
  }
343
343
 
344
+ def test_interface_form_clean_vlan_location_success(self):
345
+ """Assert that form validation succeeds when matching locations/parent locations are associated to tagged VLAN"""
346
+ location = self.device.location
347
+ location_ids = location.ancestors(include_self=True).values_list("id", flat=True)
348
+ self.vlan.locations.set([location.id])
349
+ self.data["tagged_vlans"] = [self.vlan]
350
+ form = InterfaceForm(data=self.data, instance=self.interface)
351
+ self.assertTrue(form.is_valid())
352
+ self.vlan.locations.set(location_ids[:2])
353
+ self.data["tagged_vlans"] = [self.vlan]
354
+ form = InterfaceForm(data=self.data, instance=self.interface)
355
+ self.assertTrue(form.is_valid())
356
+
344
357
  def test_interface_form_clean_vlan_location_fail(self):
345
358
  """Assert that form validation fails when no matching locations are associated to tagged VLAN"""
346
- self.vlan.locations.set(list(Location.objects.exclude(pk=self.device.location.pk))[:2])
359
+ location = self.device.location
360
+ location_ids = location.ancestors(include_self=True).values_list("id", flat=True)
361
+ self.vlan.locations.set(list(Location.objects.exclude(pk__in=location_ids))[:2])
362
+ self.data["tagged_vlans"] = [self.vlan]
347
363
  form = InterfaceForm(data=self.data, instance=self.interface)
348
364
  self.assertFalse(form.is_valid())
349
365
 
@@ -2376,19 +2376,57 @@ class InterfaceTestCase(ModularDeviceComponentTestCaseMixin, ModelTestCases.Base
2376
2376
  )
2377
2377
 
2378
2378
  def test_error_raised_when_adding_tagged_vlan_with_different_location_from_interface_parent_location(self):
2379
+ intf_status = Status.objects.get_for_model(Interface).first()
2380
+ intf_role = Role.objects.get_for_model(Interface).first()
2381
+ location_type = LocationType.objects.get(name="Campus")
2382
+ child_location = Location.objects.filter(parent__isnull=False, location_type=location_type).first()
2383
+ self.device.location = child_location
2384
+ self.device.validated_save()
2385
+ # Same location as the device
2386
+ interface = Interface.objects.create(
2387
+ name="Test Interface 2",
2388
+ mode=InterfaceModeChoices.MODE_TAGGED,
2389
+ device=self.device,
2390
+ status=intf_status,
2391
+ role=intf_role,
2392
+ )
2393
+ self.other_location_vlan.locations.set([self.device.location.pk])
2394
+ interface.tagged_vlans.set([self.other_location_vlan.pk])
2395
+
2396
+ # One of the parent locations of the device's location
2397
+ interface = Interface.objects.create(
2398
+ name="Test Interface 3",
2399
+ mode=InterfaceModeChoices.MODE_TAGGED,
2400
+ device=self.device,
2401
+ status=intf_status,
2402
+ role=intf_role,
2403
+ )
2404
+ self.other_location_vlan.locations.set([self.device.location.ancestors().first().pk])
2405
+ interface.tagged_vlans.set([self.other_location_vlan.pk])
2406
+
2379
2407
  with self.assertRaises(ValidationError) as err:
2380
2408
  interface = Interface.objects.create(
2381
- name="Test Interface",
2409
+ name="Test Interface 1",
2382
2410
  mode=InterfaceModeChoices.MODE_TAGGED,
2383
2411
  device=self.device,
2384
- status=Status.objects.get_for_model(Interface).first(),
2385
- role=Role.objects.get_for_model(Interface).first(),
2412
+ status=intf_status,
2413
+ role=intf_role,
2386
2414
  )
2415
+ location_3 = Location.objects.create(
2416
+ name="Invalid VLAN Location",
2417
+ location_type=LocationType.objects.get(name="Campus"),
2418
+ status=Status.objects.get_for_model(Location).first(),
2419
+ )
2420
+ # clear the valid locations
2421
+ self.other_location_vlan.locations.set([])
2422
+ # assign the invalid location
2423
+ self.other_location_vlan.location = location_3
2424
+ self.other_location_vlan.validated_save()
2387
2425
  interface.tagged_vlans.add(self.other_location_vlan)
2388
2426
  self.assertEqual(
2389
2427
  err.exception.message_dict["tagged_vlans"][0],
2390
2428
  f"Tagged VLAN with names {[self.other_location_vlan.name]} must all belong to the "
2391
- f"same location as the interface's parent device, or it must be global.",
2429
+ f"same location as the interface's parent device, one of the parent locations of the interface's parent device's location, or it must be global.",
2392
2430
  )
2393
2431
 
2394
2432
  def test_add_ip_addresses(self):
@@ -2811,6 +2849,22 @@ class ModuleBayTestCase(ModularDeviceComponentTestCaseMixin, ModelTestCases.Base
2811
2849
  self.assertIsNone(child_module_bay.parent)
2812
2850
  self.assertIsNone(grandchild_module_bay.parent)
2813
2851
 
2852
+ def test_position_value_auto_population(self):
2853
+ """
2854
+ Assert that the value of the module bay position is auto-populated by its name if position is not provided by the user.
2855
+ """
2856
+
2857
+ module_bay = ModuleBay.objects.create(
2858
+ parent_device=self.device,
2859
+ name="1111",
2860
+ )
2861
+ module_bay.validated_save()
2862
+ self.assertEqual(module_bay.position, module_bay.name)
2863
+ # Test the default value is overriden if the user provides a position value.
2864
+ module_bay.position = "1222"
2865
+ module_bay.validated_save()
2866
+ self.assertEqual(module_bay.position, "1222")
2867
+
2814
2868
 
2815
2869
  class ModuleBayTemplateTestCase(ModularDeviceComponentTemplateTestCaseMixin, ModelTestCases.BaseModelTestCase):
2816
2870
  model = ModuleBayTemplate