nautobot 2.3.9__py3-none-any.whl → 2.3.11__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 (335) hide show
  1. nautobot/apps/utils.py +2 -0
  2. nautobot/cloud/tables.py +1 -0
  3. nautobot/core/forms/forms.py +5 -1
  4. nautobot/core/models/query_functions.py +147 -1
  5. nautobot/core/tables.py +88 -22
  6. nautobot/core/templates/generic/object_bulk_destroy.html +12 -3
  7. nautobot/core/templates/generic/object_bulk_update.html +4 -2
  8. nautobot/core/templates/generic/object_create.html +1 -1
  9. nautobot/core/templates/rest_framework/api.html +3 -0
  10. nautobot/core/testing/api.py +3 -1
  11. nautobot/core/testing/integration.py +64 -0
  12. nautobot/core/testing/views.py +33 -27
  13. nautobot/core/tests/integration/test_app_navbar.py +3 -3
  14. nautobot/core/tests/integration/test_navbar.py +1 -1
  15. nautobot/core/tests/test_csv.py +3 -0
  16. nautobot/core/tests/test_models_query_functions.py +108 -0
  17. nautobot/core/tests/test_utils.py +25 -5
  18. nautobot/core/utils/lookup.py +35 -0
  19. nautobot/core/views/generic.py +50 -39
  20. nautobot/core/views/mixins.py +97 -43
  21. nautobot/core/views/renderers.py +8 -5
  22. nautobot/dcim/tables/devices.py +3 -0
  23. nautobot/dcim/templates/dcim/device_component_add.html +8 -8
  24. nautobot/dcim/templates/dcim/modulebay_create.html +39 -0
  25. nautobot/dcim/templates/dcim/modulebay_update.html +39 -0
  26. nautobot/dcim/templates/dcim/virtualchassis_add_member.html +2 -2
  27. nautobot/dcim/templates/dcim/virtualchassis_edit.html +2 -2
  28. nautobot/dcim/tests/integration/test_create_device.py +86 -0
  29. nautobot/dcim/views.py +1 -1
  30. nautobot/extras/api/customfields.py +3 -10
  31. nautobot/extras/context_managers.py +23 -3
  32. nautobot/extras/jobs.py +20 -14
  33. nautobot/extras/models/customfields.py +12 -0
  34. nautobot/extras/signals.py +2 -0
  35. nautobot/extras/tasks.py +88 -69
  36. nautobot/extras/tests/test_context_managers.py +9 -4
  37. nautobot/extras/tests/test_relationships.py +1 -0
  38. nautobot/extras/tests/test_webhooks.py +1 -1
  39. nautobot/extras/views.py +1 -0
  40. nautobot/extras/webhooks.py +16 -7
  41. nautobot/ipam/factory.py +3 -0
  42. nautobot/ipam/filters.py +5 -0
  43. nautobot/ipam/forms.py +17 -0
  44. nautobot/ipam/models.py +2 -1
  45. nautobot/ipam/signals.py +2 -2
  46. nautobot/ipam/tables.py +3 -3
  47. nautobot/ipam/templates/ipam/ipaddress_assign.html +2 -2
  48. nautobot/ipam/tests/test_models.py +113 -1
  49. nautobot/ipam/tests/test_views.py +39 -5
  50. nautobot/project-static/docs/404.html +1 -1
  51. nautobot/project-static/docs/apps/index.html +1 -1
  52. nautobot/project-static/docs/apps/nautobot-apps.html +1 -1
  53. nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +1 -1
  54. nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +1 -1
  55. nautobot/project-static/docs/code-reference/nautobot/apps/api.html +1 -1
  56. nautobot/project-static/docs/code-reference/nautobot/apps/change_logging.html +1 -1
  57. nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +1 -1
  58. nautobot/project-static/docs/code-reference/nautobot/apps/config.html +1 -1
  59. nautobot/project-static/docs/code-reference/nautobot/apps/constants.html +1 -1
  60. nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +1 -1
  61. nautobot/project-static/docs/code-reference/nautobot/apps/exceptions.html +1 -1
  62. nautobot/project-static/docs/code-reference/nautobot/apps/factory.html +1 -1
  63. nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +1 -1
  64. nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +1 -1
  65. nautobot/project-static/docs/code-reference/nautobot/apps/graphql.html +1 -1
  66. nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +62 -5
  67. nautobot/project-static/docs/code-reference/nautobot/apps/models.html +1 -1
  68. nautobot/project-static/docs/code-reference/nautobot/apps/querysets.html +1 -1
  69. nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +1 -1
  70. nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +132 -7
  71. nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +176 -1
  72. nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +1 -1
  73. nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +1 -1
  74. nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +95 -1
  75. nautobot/project-static/docs/code-reference/nautobot/apps/views.html +5 -5
  76. nautobot/project-static/docs/development/apps/api/configuration-view.html +1 -1
  77. nautobot/project-static/docs/development/apps/api/database-backend-config.html +1 -1
  78. nautobot/project-static/docs/development/apps/api/models/django-admin.html +1 -1
  79. nautobot/project-static/docs/development/apps/api/models/global-search.html +1 -1
  80. nautobot/project-static/docs/development/apps/api/models/graphql.html +1 -1
  81. nautobot/project-static/docs/development/apps/api/models/index.html +1 -1
  82. nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +1 -1
  83. nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +1 -1
  84. nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +1 -1
  85. nautobot/project-static/docs/development/apps/api/platform-features/git-repository-content.html +1 -1
  86. nautobot/project-static/docs/development/apps/api/platform-features/index.html +1 -1
  87. nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +1 -1
  88. nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +1 -1
  89. nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +1 -1
  90. nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +1 -1
  91. nautobot/project-static/docs/development/apps/api/platform-features/table-extensions.html +1 -1
  92. nautobot/project-static/docs/development/apps/api/platform-features/uniquely-identify-objects.html +1 -1
  93. nautobot/project-static/docs/development/apps/api/prometheus.html +1 -1
  94. nautobot/project-static/docs/development/apps/api/setup.html +1 -1
  95. nautobot/project-static/docs/development/apps/api/testing.html +1 -1
  96. nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +1 -1
  97. nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +1 -1
  98. nautobot/project-static/docs/development/apps/api/ui-extensions/index.html +1 -1
  99. nautobot/project-static/docs/development/apps/api/ui-extensions/navigation.html +1 -1
  100. nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +1 -1
  101. nautobot/project-static/docs/development/apps/api/views/base-template.html +1 -1
  102. nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +1 -1
  103. nautobot/project-static/docs/development/apps/api/views/django-generic-views.html +1 -1
  104. nautobot/project-static/docs/development/apps/api/views/help-documentation.html +1 -1
  105. nautobot/project-static/docs/development/apps/api/views/index.html +1 -1
  106. nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +1 -1
  107. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +1 -1
  108. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +1 -1
  109. nautobot/project-static/docs/development/apps/api/views/notes.html +1 -1
  110. nautobot/project-static/docs/development/apps/api/views/rest-api.html +1 -1
  111. nautobot/project-static/docs/development/apps/api/views/urls.html +1 -1
  112. nautobot/project-static/docs/development/apps/index.html +1 -1
  113. nautobot/project-static/docs/development/apps/migration/code-updates.html +1 -1
  114. nautobot/project-static/docs/development/apps/migration/dependency-updates.html +1 -1
  115. nautobot/project-static/docs/development/apps/migration/from-v1.html +1 -1
  116. nautobot/project-static/docs/development/apps/migration/model-updates/dcim.html +1 -1
  117. nautobot/project-static/docs/development/apps/migration/model-updates/extras.html +1 -1
  118. nautobot/project-static/docs/development/apps/migration/model-updates/global.html +1 -1
  119. nautobot/project-static/docs/development/apps/migration/model-updates/ipam.html +1 -1
  120. nautobot/project-static/docs/development/apps/porting-from-netbox.html +1 -1
  121. nautobot/project-static/docs/development/core/application-registry.html +1 -1
  122. nautobot/project-static/docs/development/core/best-practices.html +1 -1
  123. nautobot/project-static/docs/development/core/bootstrap-ui.html +1 -1
  124. nautobot/project-static/docs/development/core/caching.html +1 -1
  125. nautobot/project-static/docs/development/core/controllers.html +1 -1
  126. nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +1 -1
  127. nautobot/project-static/docs/development/core/generic-views.html +1 -1
  128. nautobot/project-static/docs/development/core/getting-started.html +1 -1
  129. nautobot/project-static/docs/development/core/homepage.html +1 -1
  130. nautobot/project-static/docs/development/core/index.html +1 -1
  131. nautobot/project-static/docs/development/core/model-checklist.html +1 -1
  132. nautobot/project-static/docs/development/core/model-features.html +1 -1
  133. nautobot/project-static/docs/development/core/natural-keys.html +1 -1
  134. nautobot/project-static/docs/development/core/navigation-menu.html +1 -1
  135. nautobot/project-static/docs/development/core/release-checklist.html +1 -1
  136. nautobot/project-static/docs/development/core/role-internals.html +1 -1
  137. nautobot/project-static/docs/development/core/settings.html +1 -1
  138. nautobot/project-static/docs/development/core/style-guide.html +1 -1
  139. nautobot/project-static/docs/development/core/templates.html +1 -1
  140. nautobot/project-static/docs/development/core/testing.html +1 -1
  141. nautobot/project-static/docs/development/core/user-preferences.html +1 -1
  142. nautobot/project-static/docs/development/index.html +1 -1
  143. nautobot/project-static/docs/development/jobs/index.html +1 -1
  144. nautobot/project-static/docs/development/jobs/migration/from-v1.html +1 -1
  145. nautobot/project-static/docs/index.html +1 -1
  146. nautobot/project-static/docs/objects.inv +0 -0
  147. nautobot/project-static/docs/overview/application_stack.html +1 -1
  148. nautobot/project-static/docs/overview/design_philosophy.html +1 -1
  149. nautobot/project-static/docs/release-notes/index.html +1 -1
  150. nautobot/project-static/docs/release-notes/version-1.0.html +1 -1
  151. nautobot/project-static/docs/release-notes/version-1.1.html +1 -1
  152. nautobot/project-static/docs/release-notes/version-1.2.html +1 -1
  153. nautobot/project-static/docs/release-notes/version-1.3.html +1 -1
  154. nautobot/project-static/docs/release-notes/version-1.4.html +1 -1
  155. nautobot/project-static/docs/release-notes/version-1.5.html +1 -1
  156. nautobot/project-static/docs/release-notes/version-1.6.html +1 -1
  157. nautobot/project-static/docs/release-notes/version-2.0.html +1 -1
  158. nautobot/project-static/docs/release-notes/version-2.1.html +1 -1
  159. nautobot/project-static/docs/release-notes/version-2.2.html +1 -1
  160. nautobot/project-static/docs/release-notes/version-2.3.html +440 -140
  161. nautobot/project-static/docs/requirements.txt +1 -1
  162. nautobot/project-static/docs/search/search_index.json +1 -1
  163. nautobot/project-static/docs/sitemap.xml +270 -270
  164. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  165. nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +1 -1
  166. nautobot/project-static/docs/user-guide/administration/configuration/authentication/remote.html +1 -1
  167. nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +1 -1
  168. nautobot/project-static/docs/user-guide/administration/configuration/index.html +1 -1
  169. nautobot/project-static/docs/user-guide/administration/configuration/redis.html +1 -1
  170. nautobot/project-static/docs/user-guide/administration/configuration/settings.html +1 -1
  171. nautobot/project-static/docs/user-guide/administration/configuration/time-zones.html +1 -1
  172. nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +1 -1
  173. nautobot/project-static/docs/user-guide/administration/guides/docker.html +1 -1
  174. nautobot/project-static/docs/user-guide/administration/guides/health-checks.html +1 -1
  175. nautobot/project-static/docs/user-guide/administration/guides/permissions.html +1 -1
  176. nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +1 -1
  177. nautobot/project-static/docs/user-guide/administration/guides/replicating-nautobot.html +1 -1
  178. nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +40 -1
  179. nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +1 -1
  180. nautobot/project-static/docs/user-guide/administration/guides/selinux-troubleshooting.html +1 -1
  181. nautobot/project-static/docs/user-guide/administration/installation/app-install.html +1 -1
  182. nautobot/project-static/docs/user-guide/administration/installation/external-authentication.html +1 -1
  183. nautobot/project-static/docs/user-guide/administration/installation/http-server.html +1 -1
  184. nautobot/project-static/docs/user-guide/administration/installation/index.html +1 -1
  185. nautobot/project-static/docs/user-guide/administration/installation/install_system.html +1 -1
  186. nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +1 -1
  187. nautobot/project-static/docs/user-guide/administration/installation/services.html +1 -1
  188. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-netbox.html +1 -1
  189. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +1 -1
  190. nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +1 -1
  191. nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +1 -1
  192. nautobot/project-static/docs/user-guide/administration/upgrading/database-backup.html +1 -1
  193. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/after-you-upgrade.html +1 -1
  194. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/before-you-upgrade.html +1 -1
  195. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/for-developers.html +1 -1
  196. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/index.html +1 -1
  197. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/whats-changed.html +1 -1
  198. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/region-and-site-data-migration-guide.html +1 -1
  199. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +1 -1
  200. nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +1 -1
  201. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuit.html +1 -1
  202. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittermination.html +1 -1
  203. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittype.html +1 -1
  204. nautobot/project-static/docs/user-guide/core-data-model/circuits/provider.html +1 -1
  205. nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +1 -1
  206. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloud.html +1 -1
  207. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudaccount.html +1 -1
  208. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetwork.html +1 -1
  209. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetworkprefixassignment.html +1 -1
  210. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudresourcetype.html +1 -1
  211. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservice.html +1 -1
  212. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservicenetworkassignment.html +1 -1
  213. nautobot/project-static/docs/user-guide/core-data-model/dcim/cable.html +1 -1
  214. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +1 -1
  215. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +1 -1
  216. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +1 -1
  217. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +1 -1
  218. nautobot/project-static/docs/user-guide/core-data-model/dcim/controller.html +1 -1
  219. nautobot/project-static/docs/user-guide/core-data-model/dcim/controllermanageddevicegroup.html +1 -1
  220. nautobot/project-static/docs/user-guide/core-data-model/dcim/device.html +1 -1
  221. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +1 -1
  222. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +1 -1
  223. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicefamily.html +1 -1
  224. nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +1 -1
  225. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +1 -1
  226. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +1 -1
  227. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +1 -1
  228. nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +1 -1
  229. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +1 -1
  230. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +1 -1
  231. nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +1 -1
  232. nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +1 -1
  233. nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +1 -1
  234. nautobot/project-static/docs/user-guide/core-data-model/dcim/manufacturer.html +1 -1
  235. nautobot/project-static/docs/user-guide/core-data-model/dcim/module.html +1 -1
  236. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebay.html +1 -1
  237. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebaytemplate.html +1 -1
  238. nautobot/project-static/docs/user-guide/core-data-model/dcim/moduletype.html +1 -1
  239. nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +1 -1
  240. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerfeed.html +1 -1
  241. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +1 -1
  242. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +1 -1
  243. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerpanel.html +1 -1
  244. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +1 -1
  245. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +1 -1
  246. nautobot/project-static/docs/user-guide/core-data-model/dcim/rack.html +1 -1
  247. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackgroup.html +1 -1
  248. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackreservation.html +1 -1
  249. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +1 -1
  250. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +1 -1
  251. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +1 -1
  252. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareversion.html +1 -1
  253. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualchassis.html +1 -1
  254. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +1 -1
  255. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +1 -1
  256. nautobot/project-static/docs/user-guide/core-data-model/extras/contact.html +1 -1
  257. nautobot/project-static/docs/user-guide/core-data-model/extras/team.html +1 -1
  258. nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +1 -1
  259. nautobot/project-static/docs/user-guide/core-data-model/ipam/namespace.html +1 -1
  260. nautobot/project-static/docs/user-guide/core-data-model/ipam/prefix.html +1 -1
  261. nautobot/project-static/docs/user-guide/core-data-model/ipam/rir.html +1 -1
  262. nautobot/project-static/docs/user-guide/core-data-model/ipam/routetarget.html +1 -1
  263. nautobot/project-static/docs/user-guide/core-data-model/ipam/service.html +1 -1
  264. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +1 -1
  265. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlangroup.html +1 -1
  266. nautobot/project-static/docs/user-guide/core-data-model/ipam/vrf.html +1 -1
  267. nautobot/project-static/docs/user-guide/core-data-model/overview/introduction.html +1 -1
  268. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenant.html +1 -1
  269. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenantgroup.html +1 -1
  270. nautobot/project-static/docs/user-guide/core-data-model/virtualization/cluster.html +1 -1
  271. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustergroup.html +1 -1
  272. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustertype.html +1 -1
  273. nautobot/project-static/docs/user-guide/core-data-model/virtualization/virtualmachine.html +1 -1
  274. nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +1 -1
  275. nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +1 -1
  276. nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +1 -1
  277. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +1 -1
  278. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +1 -1
  279. nautobot/project-static/docs/user-guide/feature-guides/getting-started/index.html +1 -1
  280. nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +1 -1
  281. nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +1 -1
  282. nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +1 -1
  283. nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +1 -1
  284. nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +1 -1
  285. nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +1 -1
  286. nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +1 -1
  287. nautobot/project-static/docs/user-guide/feature-guides/graphql.html +1 -1
  288. nautobot/project-static/docs/user-guide/feature-guides/ip-address-merge-tool.html +1 -1
  289. nautobot/project-static/docs/user-guide/feature-guides/relationships.html +1 -1
  290. nautobot/project-static/docs/user-guide/feature-guides/software-image-files-and-versions.html +1 -1
  291. nautobot/project-static/docs/user-guide/index.html +1 -1
  292. nautobot/project-static/docs/user-guide/platform-functionality/change-logging.html +1 -1
  293. nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +1 -1
  294. nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +1 -1
  295. nautobot/project-static/docs/user-guide/platform-functionality/customlink.html +1 -1
  296. nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +1 -1
  297. nautobot/project-static/docs/user-guide/platform-functionality/exporttemplate.html +1 -1
  298. nautobot/project-static/docs/user-guide/platform-functionality/externalintegration.html +1 -1
  299. nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +1 -1
  300. nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +1 -1
  301. nautobot/project-static/docs/user-guide/platform-functionality/graphqlquery.html +1 -1
  302. nautobot/project-static/docs/user-guide/platform-functionality/imageattachment.html +1 -1
  303. nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +1 -1
  304. nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +1 -1
  305. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +1 -1
  306. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +1 -1
  307. nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +1 -1
  308. nautobot/project-static/docs/user-guide/platform-functionality/napalm.html +1 -1
  309. nautobot/project-static/docs/user-guide/platform-functionality/note.html +1 -1
  310. nautobot/project-static/docs/user-guide/platform-functionality/objectmetadata.html +1 -1
  311. nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +1 -1
  312. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +1 -1
  313. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +1 -1
  314. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +1 -1
  315. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/ui-related-endpoints.html +1 -1
  316. nautobot/project-static/docs/user-guide/platform-functionality/role.html +1 -1
  317. nautobot/project-static/docs/user-guide/platform-functionality/savedview.html +1 -1
  318. nautobot/project-static/docs/user-guide/platform-functionality/secret.html +1 -1
  319. nautobot/project-static/docs/user-guide/platform-functionality/staticgroupassociation.html +1 -1
  320. nautobot/project-static/docs/user-guide/platform-functionality/status.html +1 -1
  321. nautobot/project-static/docs/user-guide/platform-functionality/tag.html +1 -1
  322. nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +1 -1
  323. nautobot/project-static/docs/user-guide/platform-functionality/users/objectpermission.html +1 -1
  324. nautobot/project-static/docs/user-guide/platform-functionality/users/token.html +1 -1
  325. nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +1 -1
  326. nautobot/project-static/js/forms.js +0 -38
  327. nautobot/virtualization/forms.py +24 -0
  328. nautobot/virtualization/templates/virtualization/vminterface_edit.html +1 -0
  329. nautobot/virtualization/tests/test_views.py +7 -2
  330. {nautobot-2.3.9.dist-info → nautobot-2.3.11.dist-info}/METADATA +2 -2
  331. {nautobot-2.3.9.dist-info → nautobot-2.3.11.dist-info}/RECORD +335 -331
  332. {nautobot-2.3.9.dist-info → nautobot-2.3.11.dist-info}/LICENSE.txt +0 -0
  333. {nautobot-2.3.9.dist-info → nautobot-2.3.11.dist-info}/NOTICE +0 -0
  334. {nautobot-2.3.9.dist-info → nautobot-2.3.11.dist-info}/WHEEL +0 -0
  335. {nautobot-2.3.9.dist-info → nautobot-2.3.11.dist-info}/entry_points.txt +0 -0
@@ -391,6 +391,18 @@ class CustomFieldManager(BaseManager.from_queryset(RestrictedQuerySet)):
391
391
 
392
392
  get_for_model.cache_key_prefix = "nautobot.extras.customfield.get_for_model"
393
393
 
394
+ def keys_for_model(self, model):
395
+ """Return list of all keys for CustomFields assigned to the given model."""
396
+ concrete_model = model._meta.concrete_model
397
+ cache_key = f"{self.keys_for_model.cache_key_prefix}.{concrete_model._meta.label_lower}"
398
+ keys = cache.get(cache_key)
399
+ if keys is None:
400
+ keys = list(self.get_for_model(model).values_list("key", flat=True))
401
+ cache.set(cache_key, keys)
402
+ return keys
403
+
404
+ keys_for_model.cache_key_prefix = "nautobot.extras.customfield.keys_for_model"
405
+
394
406
 
395
407
  @extras_features("webhooks")
396
408
  class CustomField(
@@ -91,6 +91,8 @@ def invalidate_models_cache(sender, **kwargs):
91
91
  with contextlib.suppress(redis.exceptions.ConnectionError):
92
92
  # TODO: *maybe* target more narrowly, e.g. only clear the cache for specific related content-types?
93
93
  cache.delete_pattern(f"{manager.get_for_model.cache_key_prefix}.*")
94
+ if hasattr(manager, "keys_for_model"):
95
+ cache.delete_pattern(f"{manager.keys_for_model.cache_key_prefix}.*")
94
96
 
95
97
 
96
98
  @receiver(post_save, sender=Relationship)
nautobot/extras/tasks.py CHANGED
@@ -2,18 +2,42 @@ from logging import getLogger
2
2
 
3
3
  from django.conf import settings
4
4
  from django.contrib.contenttypes.models import ContentType
5
- from django.db import transaction
6
5
  from jinja2.exceptions import TemplateError
7
6
  import requests
8
7
 
9
8
  from nautobot.core.celery import nautobot_task
9
+ from nautobot.core.models.query_functions import JSONRemove, JSONSet
10
10
  from nautobot.extras.choices import CustomFieldTypeChoices, ObjectChangeActionChoices
11
11
  from nautobot.extras.utils import generate_signature
12
12
 
13
13
  logger = getLogger("nautobot.extras.tasks")
14
14
 
15
15
 
16
- @nautobot_task
16
+ def _generate_bulk_object_changes(context, queryset, task_logger):
17
+ # Circular import
18
+ from nautobot.extras.context_managers import (
19
+ change_logging,
20
+ ChangeContext,
21
+ deferred_change_logging_for_bulk_operation,
22
+ )
23
+ from nautobot.extras.signals import _handle_changed_object
24
+
25
+ task_logger.info("Creating deferred ObjectChange records for bulk operation...")
26
+
27
+ # Note: we use change_logging() here instead of web_request_context() because we don't want these change records to
28
+ # trigger jobhooks and webhooks.
29
+ # TODO: this could be made much faster if we ensure the queryset has appropriate select_related/prefetch_related?
30
+ change_context = ChangeContext(**context)
31
+ i = 0
32
+ with change_logging(change_context):
33
+ with deferred_change_logging_for_bulk_operation():
34
+ for i, instance in enumerate(queryset.iterator(), start=1):
35
+ _handle_changed_object(queryset.model, instance, created=False)
36
+
37
+ task_logger.info("Created %d ObjectChange records", i)
38
+
39
+
40
+ @nautobot_task(soft_time_limit=1800, time_limit=2000)
17
41
  def update_custom_field_choice_data(field_id, old_value, new_value, change_context=None):
18
42
  """
19
43
  Update the values for a custom field choice used in objects' _custom_field_data for the given field.
@@ -22,47 +46,48 @@ def update_custom_field_choice_data(field_id, old_value, new_value, change_conte
22
46
  field_id (uuid4): The PK of the custom field to which this choice value relates
23
47
  old_value (str): The existing value of the choice
24
48
  new_value (str): The value which will be used as replacement
49
+ change_context (dict): Optional dict representation of change context for ObjectChange creation
25
50
  """
26
51
  # Circular Import
27
52
  from nautobot.extras.context_managers import web_request_context
28
53
  from nautobot.extras.models import CustomField
29
54
 
55
+ task_logger = getLogger("celery.task.update_custom_field_choice_data")
56
+
30
57
  try:
31
58
  field = CustomField.objects.get(pk=field_id)
32
59
  except CustomField.DoesNotExist:
33
- logger.error(f"Custom field with ID {field_id} not found, failing to act on choice data.")
34
- return False
60
+ task_logger.error("Custom field with ID %s not found, failing to act on choice data.", field_id)
61
+ raise
35
62
 
36
63
  if field.type == CustomFieldTypeChoices.TYPE_SELECT:
37
64
  # Loop through all field content types and search for values to update
38
65
  for ct in field.content_types.all():
39
66
  model = ct.model_class()
67
+ queryset = model.objects.filter(**{f"_custom_field_data__{field.key}": old_value})
40
68
  if change_context is not None:
41
- with web_request_context(
42
- user=change_context.get("user"),
43
- change_id=change_context.get("change_id"),
44
- context_detail=change_context.get("context_detail"),
45
- context=change_context.get("context"),
46
- ):
47
- for obj in model.objects.filter(**{f"_custom_field_data__{field.key}": old_value}):
48
- obj._custom_field_data[field.key] = new_value
49
- obj.save()
50
- else:
51
- for obj in model.objects.filter(**{f"_custom_field_data__{field.key}": old_value}):
52
- obj._custom_field_data[field.key] = new_value
53
- obj.save()
69
+ pk_list = list(queryset.values_list("pk", flat=True))
70
+ task_logger.info(
71
+ "Updating selection for custom field `%s` from `%s` to `%s` on %s records...",
72
+ field.key,
73
+ old_value,
74
+ new_value,
75
+ ct.model,
76
+ extra={"object": field},
77
+ )
78
+ count = queryset.update(_custom_field_data=JSONSet("_custom_field_data", field.key, new_value))
79
+ task_logger.info("Updated %d records", count)
80
+ if change_context is not None:
81
+ # Since we used update() above, we bypassed ObjectChange automatic creation via signals. Create them now
82
+ _generate_bulk_object_changes(change_context, model.objects.filter(pk__in=pk_list), task_logger)
54
83
 
55
84
  elif field.type == CustomFieldTypeChoices.TYPE_MULTISELECT:
56
85
  # Loop through all field content types and search for values to update
86
+ # TODO: can we implement a bulk operator for this?
57
87
  for ct in field.content_types.all():
58
88
  model = ct.model_class()
59
89
  if change_context is not None:
60
- with web_request_context(
61
- user=change_context.get("user"),
62
- change_id=change_context.get("change_id"),
63
- context_detail=change_context.get("context_detail"),
64
- context=change_context.get("context"),
65
- ):
90
+ with web_request_context(**change_context):
66
91
  for obj in model.objects.filter(**{f"_custom_field_data__{field.key}__contains": old_value}):
67
92
  old_list = obj._custom_field_data[field.key]
68
93
  new_list = [new_value if e == old_value else e for e in old_list]
@@ -76,13 +101,13 @@ def update_custom_field_choice_data(field_id, old_value, new_value, change_conte
76
101
  obj.save()
77
102
 
78
103
  else:
79
- logger.error(f"Unknown field type, failing to act on choice data for this field {field.key}.")
80
- return False
104
+ task_logger.error(f"Unknown field type, failing to act on choice data for this field {field.key}.")
105
+ raise ValueError
81
106
 
82
107
  return True
83
108
 
84
109
 
85
- @nautobot_task
110
+ @nautobot_task(soft_time_limit=1800, time_limit=2000)
86
111
  def delete_custom_field_data(field_key, content_type_pk_set, change_context=None):
87
112
  """
88
113
  Delete the values for a custom field
@@ -90,30 +115,23 @@ def delete_custom_field_data(field_key, content_type_pk_set, change_context=None
90
115
  Args:
91
116
  field_key (str): The key of the custom field which is being deleted
92
117
  content_type_pk_set (list): List of PKs for content types to act upon
118
+ change_context (dict): Optional change context for ObjectChange creation
93
119
  """
94
- # Circular Import
95
- from nautobot.extras.context_managers import web_request_context
96
-
97
- with transaction.atomic():
98
- for ct in ContentType.objects.filter(pk__in=content_type_pk_set):
99
- model = ct.model_class()
100
- if change_context is not None:
101
- with web_request_context(
102
- user=change_context.get("user"),
103
- change_id=change_context.get("change_id"),
104
- context_detail=change_context.get("context_detail"),
105
- context=change_context.get("context"),
106
- ):
107
- for obj in model.objects.filter(**{f"_custom_field_data__{field_key}__isnull": False}):
108
- del obj._custom_field_data[field_key]
109
- obj.save()
110
- else:
111
- for obj in model.objects.filter(**{f"_custom_field_data__{field_key}__isnull": False}):
112
- del obj._custom_field_data[field_key]
113
- obj.save()
114
-
115
-
116
- @nautobot_task
120
+ task_logger = getLogger("celery.task.delete_custom_field_data")
121
+ for ct in ContentType.objects.filter(pk__in=content_type_pk_set):
122
+ model = ct.model_class()
123
+ queryset = model.objects.filter(**{f"_custom_field_data__{field_key}__isnull": False})
124
+ if change_context is not None:
125
+ pk_list = list(queryset.values_list("pk", flat=True))
126
+ task_logger.info("Deleting existing values for custom field `%s` from %s records...", field_key, ct.model)
127
+ count = queryset.update(_custom_field_data=JSONRemove("_custom_field_data", field_key))
128
+ task_logger.info("Updated %d records", count)
129
+ if change_context is not None:
130
+ # Since we used update() above, we bypassed ObjectChange automatic creation via signals. Create them now
131
+ _generate_bulk_object_changes(change_context, model.objects.filter(pk__in=pk_list), task_logger)
132
+
133
+
134
+ @nautobot_task(soft_time_limit=1800, time_limit=2000)
117
135
  def provision_field(field_id, content_type_pk_set, change_context=None):
118
136
  """
119
137
  Provision a new custom field on all relevant content type object instances.
@@ -121,34 +139,35 @@ def provision_field(field_id, content_type_pk_set, change_context=None):
121
139
  Args:
122
140
  field_id (uuid4): The PK of the custom field being provisioned
123
141
  content_type_pk_set (list): List of PKs for content types to act upon
142
+ change_context (dict): Optional change context for ObjectChange creation.
124
143
  """
125
144
  # Circular Import
126
- from nautobot.extras.context_managers import web_request_context
127
145
  from nautobot.extras.models import CustomField
128
146
 
147
+ task_logger = getLogger("celery.task.provision_field")
148
+
129
149
  try:
130
150
  field = CustomField.objects.get(pk=field_id)
131
151
  except CustomField.DoesNotExist:
132
- logger.error(f"Custom field with ID {field_id} not found, failing to provision.")
133
- return False
152
+ task_logger.error(f"Custom field with ID {field_id} not found, failing to provision.")
153
+ raise
134
154
 
135
- with transaction.atomic():
136
- for ct in ContentType.objects.filter(pk__in=content_type_pk_set):
137
- model = ct.model_class()
138
- if change_context is not None:
139
- with web_request_context(
140
- user=change_context.get("user"),
141
- change_id=change_context.get("change_id"),
142
- context_detail=change_context.get("context_detail"),
143
- context=change_context.get("context"),
144
- ):
145
- for obj in model.objects.all():
146
- obj._custom_field_data.setdefault(field.key, field.default)
147
- obj.save()
148
- else:
149
- for obj in model.objects.all():
150
- obj._custom_field_data.setdefault(field.key, field.default)
151
- obj.save()
155
+ for ct in ContentType.objects.filter(pk__in=content_type_pk_set):
156
+ model = ct.model_class()
157
+ queryset = model.objects.filter(**{f"_custom_field_data__{field.key}__isnull": True})
158
+ if change_context is not None:
159
+ pk_list = list(queryset.values_list("pk", flat=True))
160
+ task_logger.info(
161
+ "Adding data for custom field `%s` to %s records...",
162
+ field.key,
163
+ ct.model,
164
+ extra={"object": field},
165
+ )
166
+ count = queryset.update(_custom_field_data=JSONSet("_custom_field_data", field.key, field.default))
167
+ task_logger.info("Updated %d records.", count)
168
+ if change_context is not None:
169
+ # Since we used update() above, we bypassed ObjectChange automatic creation via signals. Create them now
170
+ _generate_bulk_object_changes(change_context, model.objects.filter(pk__in=pk_list), task_logger)
152
171
 
153
172
  return True
154
173
 
@@ -74,8 +74,8 @@ class WebRequestContextTestCase(TestCase):
74
74
  self.assertEqual(oc_list[0].changed_object, location)
75
75
  self.assertEqual(oc_list[0].action, ObjectChangeActionChoices.ACTION_CREATE)
76
76
 
77
- @mock.patch("nautobot.extras.jobs.enqueue_job_hooks", return_value=True)
78
- @mock.patch("nautobot.extras.context_managers.enqueue_webhooks")
77
+ @mock.patch("nautobot.extras.jobs.enqueue_job_hooks", return_value=(True, None))
78
+ @mock.patch("nautobot.extras.context_managers.enqueue_webhooks", return_value=None)
79
79
  def test_create_then_delete(self, mock_enqueue_webhooks, mock_enqueue_job_hooks):
80
80
  """Test that a create followed by a delete is logged as two changes"""
81
81
  location_type = LocationType.objects.get(name="Campus")
@@ -93,9 +93,14 @@ class WebRequestContextTestCase(TestCase):
93
93
  self.assertEqual(oc_list[0].action, ObjectChangeActionChoices.ACTION_CREATE)
94
94
  self.assertEqual(oc_list[1].action, ObjectChangeActionChoices.ACTION_DELETE)
95
95
  mock_enqueue_job_hooks.assert_has_calls(
96
- [mock.call(oc_list[0], may_reload_jobs=True), mock.call(oc_list[1], may_reload_jobs=False)],
96
+ [
97
+ mock.call(oc_list[0], may_reload_jobs=True, jobhook_queryset=None),
98
+ mock.call(oc_list[1], may_reload_jobs=False, jobhook_queryset=None),
99
+ ],
100
+ )
101
+ mock_enqueue_webhooks.assert_has_calls(
102
+ [mock.call(oc_list[0], webhook_queryset=None), mock.call(oc_list[1], webhook_queryset=None)]
97
103
  )
98
- mock_enqueue_webhooks.assert_has_calls([mock.call(oc_list[0]), mock.call(oc_list[1])])
99
104
 
100
105
  def test_update_then_delete(self):
101
106
  """Test that an update followed by a delete is logged as a single delete"""
@@ -1395,6 +1395,7 @@ class RequiredRelationshipTestMixin:
1395
1395
  # Protected FK to SoftwareVersion prevents deletion
1396
1396
  Controller.objects.all().delete()
1397
1397
  Device.objects.all().update(software_version=None)
1398
+ Device.objects.all().delete()
1398
1399
 
1399
1400
  # Create required relationships:
1400
1401
  device_ct = ContentType.objects.get_for_model(Device)
@@ -345,7 +345,7 @@ class WebhookTest(APITestCase):
345
345
 
346
346
  all_changes = get_changes_for_model(location)
347
347
  self.assertEqual(all_changes.count(), 1)
348
- mock_enqueue_webhooks.assert_called_once_with(all_changes.first())
348
+ mock_enqueue_webhooks.assert_called_once_with(all_changes.first(), webhook_queryset=None)
349
349
 
350
350
  def test_all_webhook_supported_models(self):
351
351
  """
nautobot/extras/views.py CHANGED
@@ -2846,6 +2846,7 @@ class StatusBulkDeleteView(generic.BulkDeleteView):
2846
2846
 
2847
2847
  queryset = Status.objects.all()
2848
2848
  table = tables.StatusTable
2849
+ filterset = filters.StatusFilterSet
2849
2850
 
2850
2851
 
2851
2852
  class StatusDeleteView(generic.ObjectDeleteView):
@@ -6,16 +6,22 @@ from nautobot.extras.registry import registry
6
6
  from nautobot.extras.tasks import process_webhook
7
7
 
8
8
 
9
- def enqueue_webhooks(object_change):
9
+ def enqueue_webhooks(object_change, webhook_queryset=None):
10
10
  """
11
- Find Webhook(s) assigned to this instance + action and enqueue them
12
- to be processed
11
+ Find Webhook(s) assigned to this instance + action and enqueue them to be processed.
12
+
13
+ Args:
14
+ object_change (ObjectChange): The change that may trigger Webhooks to be sent.
15
+ webhook_queryset (QuerySet): Previously retrieved set of Webhooks to potentially send.
16
+
17
+ Returns:
18
+ webhook_queryset (QuerySet): for reuse when processing multiple ObjectChange with the same content-type+action.
13
19
  """
14
20
  # Determine whether this type of object supports webhooks
15
21
  app_label = object_change.changed_object_type.app_label
16
22
  model_name = object_change.changed_object_type.model
17
23
  if model_name not in registry["model_features"]["webhooks"].get(app_label, []):
18
- return
24
+ return webhook_queryset
19
25
 
20
26
  # Retrieve any applicable Webhooks
21
27
  content_type = object_change.changed_object_type
@@ -24,16 +30,17 @@ def enqueue_webhooks(object_change):
24
30
  ObjectChangeActionChoices.ACTION_UPDATE: "type_update",
25
31
  ObjectChangeActionChoices.ACTION_DELETE: "type_delete",
26
32
  }[object_change.action]
27
- webhooks = Webhook.objects.filter(content_types=content_type, enabled=True, **{action_flag: True})
33
+ if webhook_queryset is None:
34
+ webhook_queryset = Webhook.objects.filter(content_types=content_type, enabled=True, **{action_flag: True})
28
35
 
29
- if webhooks.exists():
36
+ if webhook_queryset: # not .exists() as we *want* to populate the queryset cache
30
37
  # fall back to object_data if object_data_v2 is not available
31
38
  serialized_data = object_change.object_data_v2
32
39
  if serialized_data is None:
33
40
  serialized_data = object_change.object_data
34
41
 
35
42
  # Enqueue the webhooks
36
- for webhook in webhooks:
43
+ for webhook in webhook_queryset:
37
44
  args = [
38
45
  webhook.pk,
39
46
  serialized_data,
@@ -45,3 +52,5 @@ def enqueue_webhooks(object_change):
45
52
  object_change.get_snapshots(),
46
53
  ]
47
54
  process_webhook.apply_async(args=args)
55
+
56
+ return webhook_queryset
nautobot/ipam/factory.py CHANGED
@@ -238,6 +238,9 @@ class VLANFactory(PrimaryModelFactory):
238
238
  lambda: Location.objects.filter(location_type__content_types__in=[vlan_ct]), minimum=0
239
239
  )
240
240
  )
241
+ if self.vlan_group and self.vlan_group.location:
242
+ # add the parent of the vlan group location to the vlan locations
243
+ self.locations.add(self.vlan_group.location.ancestors(include_self=True)[0])
241
244
 
242
245
 
243
246
  class VLANGetOrCreateFactory(VLANFactory):
nautobot/ipam/filters.py CHANGED
@@ -90,6 +90,11 @@ class VRFFilterSet(NautobotFilterSet, StatusModelFilterSetMixin, TenancyModelFil
90
90
  to_field_name="name",
91
91
  label="Device (ID or name)",
92
92
  )
93
+ virtual_machines = NaturalKeyOrPKMultipleChoiceFilter(
94
+ queryset=VirtualMachine.objects.all(),
95
+ to_field_name="name",
96
+ label="Virtual Machine (ID or name)",
97
+ )
93
98
  prefix = NaturalKeyOrPKMultipleChoiceFilter(
94
99
  field_name="prefixes",
95
100
  queryset=Prefix.objects.all(),
nautobot/ipam/forms.py CHANGED
@@ -752,6 +752,7 @@ class VLANForm(NautobotModelForm, TenancyForm):
752
752
  )
753
753
  vlan_group = DynamicModelChoiceField(
754
754
  queryset=VLANGroup.objects.all(),
755
+ query_params={"location": "$locations"},
755
756
  required=False,
756
757
  )
757
758
 
@@ -779,6 +780,22 @@ class VLANForm(NautobotModelForm, TenancyForm):
779
780
  }
780
781
 
781
782
  def clean(self):
783
+ vlan_group = self.cleaned_data["vlan_group"]
784
+ locations = self.cleaned_data["locations"]
785
+ # Validate Vlan Group Location is one of the ancestors of the VLAN locations specified.
786
+ if vlan_group and vlan_group.location and locations:
787
+ vlan_group_location = vlan_group.location
788
+ is_vlan_group_valid = False
789
+ for location in locations:
790
+ if vlan_group_location in location.ancestors(include_self=True):
791
+ is_vlan_group_valid = True
792
+ break
793
+
794
+ if not is_vlan_group_valid:
795
+ locations = list(locations.values_list("name", flat=True))
796
+ raise ValidationError(
797
+ {"vlan_group": [f"VLAN Group {vlan_group} is not in locations {locations} or their ancestors."]}
798
+ )
782
799
  # Validation error raised in signal is not properly handled in form clean
783
800
  # Hence handling any validationError that might occur.
784
801
  try:
nautobot/ipam/models.py CHANGED
@@ -1296,7 +1296,8 @@ class IPAddressToInterface(BaseModel):
1296
1296
 
1297
1297
  def __str__(self):
1298
1298
  if self.interface:
1299
- return f"{self.ip_address!s} {self.interface.device.name} {self.interface.name}"
1299
+ parent_name = self.interface.parent.name if self.interface.parent else "No Parent"
1300
+ return f"{self.ip_address!s} {parent_name} {self.interface.name}"
1300
1301
  else:
1301
1302
  return f"{self.ip_address!s} {self.vm_interface.virtual_machine.name} {self.vm_interface.name}"
1302
1303
 
nautobot/ipam/signals.py CHANGED
@@ -65,9 +65,9 @@ def ip_address_to_interface_pre_delete(instance, raw=False, **kwargs):
65
65
  # that is the primary_v{version} of the host machine.
66
66
 
67
67
  if getattr(instance, "interface"):
68
- host = instance.interface.device
68
+ host = instance.interface.parent
69
69
  other_assignments_exist = (
70
- IPAddressToInterface.objects.filter(interface__device=host, ip_address=instance.ip_address)
70
+ IPAddressToInterface.objects.filter(interface__in=host.all_interfaces, ip_address=instance.ip_address)
71
71
  .exclude(id=instance.id)
72
72
  .exists()
73
73
  )
nautobot/ipam/tables.py CHANGED
@@ -346,7 +346,7 @@ class PrefixTable(StatusTableMixin, RoleTableMixin, BaseTable):
346
346
  prefix = tables.TemplateColumn(
347
347
  template_code=PREFIX_COPY_LINK, attrs={"td": {"class": "text-nowrap"}}, order_by=("network", "prefix_length")
348
348
  )
349
- # vrf = tables.TemplateColumn(template_code=VRF_LINK, verbose_name="VRF")
349
+ vrf_count = LinkedCountColumn(viewname="ipam:vrf_list", url_params={"prefixes": "pk"}, verbose_name="VRFs")
350
350
  tenant = TenantColumn()
351
351
  namespace = tables.Column(linkify=True)
352
352
  vlan = tables.Column(linkify=True, verbose_name="VLAN")
@@ -368,7 +368,7 @@ class PrefixTable(StatusTableMixin, RoleTableMixin, BaseTable):
368
368
  "type",
369
369
  "status",
370
370
  "children",
371
- # "vrf",
371
+ "vrf_count",
372
372
  "namespace",
373
373
  "tenant",
374
374
  "location_count",
@@ -384,7 +384,7 @@ class PrefixTable(StatusTableMixin, RoleTableMixin, BaseTable):
384
384
  "prefix",
385
385
  "type",
386
386
  "status",
387
- # "vrf",
387
+ "vrf_count",
388
388
  "namespace",
389
389
  "tenant",
390
390
  "location_count",
@@ -18,7 +18,7 @@
18
18
  {% endif %}
19
19
  {% endfor %}
20
20
  <div class="row">
21
- <div class="col-md-6 col-md-offset-3">
21
+ <div class="col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1">
22
22
  <h3>Assign an IP Address</h3>
23
23
  {% include 'ipam/inc/ipadress_edit_header.html' with active_tab='assign' %}
24
24
  {% if form.non_field_errors %}
@@ -38,7 +38,7 @@
38
38
  </div>
39
39
  </div>
40
40
  <div class="row">
41
- <div class="col-md-6 col-md-offset-3 text-right">
41
+ <div class="col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1 text-right">
42
42
  <button type="submit" class="btn btn-primary">Search</button>
43
43
  <a href="{{ return_url }}" class="btn btn-default">Cancel</a>
44
44
  </div>
@@ -9,7 +9,7 @@ import netaddr
9
9
 
10
10
  from nautobot.core.testing.models import ModelTestCases
11
11
  from nautobot.dcim import choices as dcim_choices
12
- from nautobot.dcim.models import Device, DeviceType, Interface, Location, LocationType
12
+ from nautobot.dcim.models import Device, DeviceType, Interface, Location, LocationType, Module, ModuleBay, ModuleType
13
13
  from nautobot.extras.models import Role, Status
14
14
  from nautobot.ipam.choices import IPAddressTypeChoices, PrefixTypeChoices, ServiceProtocolChoices
15
15
  from nautobot.ipam.models import (
@@ -171,6 +171,118 @@ class IPAddressToInterfaceTest(TestCase):
171
171
  IPAddressToInterface.objects.create(vm_interface=None, interface=None, ip_address=ip_addr)
172
172
  self.assertIn("Must associate to either an Interface or a VMInterface.", str(cm.exception))
173
173
 
174
+ def test_primary_ip_retained_when_deleted_from_device_or_module_interface(self):
175
+ """Test primary_ip4 remains set when the same IP is assigned to multiple interfaces and deleted from one."""
176
+
177
+ # Create a module bay on the existing device
178
+ device_module_bay = ModuleBay.objects.create(parent_device=self.test_device, name="Test Bay")
179
+
180
+ # Create a module with an interface and add it to the module bay on the device
181
+ module = Module.objects.create(
182
+ module_type=ModuleType.objects.first(),
183
+ status=Status.objects.get_for_model(Module).first(),
184
+ parent_module_bay=device_module_bay,
185
+ )
186
+
187
+ # Set status for the module interface
188
+ int_status = Status.objects.get_for_model(Interface).first()
189
+
190
+ # Create an interface on the module
191
+ interface_module = Interface.objects.create(
192
+ name="eth0_module",
193
+ module=module,
194
+ type=dcim_choices.InterfaceTypeChoices.TYPE_1GE_FIXED,
195
+ status=int_status,
196
+ )
197
+
198
+ # Link the module to the device
199
+ self.test_device.installed_device = interface_module
200
+ self.test_device.save()
201
+
202
+ # Create IP and assign it to multiple interfaces
203
+ ip_address = IPAddress.objects.create(address="192.0.2.1/24", namespace=self.namespace, status=self.status)
204
+ assignment_device_int1 = IPAddressToInterface.objects.create(interface=self.test_int1, ip_address=ip_address)
205
+ assignment_module_int1 = IPAddressToInterface.objects.create(interface=interface_module, ip_address=ip_address)
206
+
207
+ # Set the primary IP on the device
208
+ self.test_device.primary_ip4 = assignment_device_int1.ip_address
209
+ self.test_device.save()
210
+
211
+ # Verify that the primary IP is set
212
+ self.assertEqual(self.test_device.primary_ip4, ip_address)
213
+
214
+ # Delete the IP assignment from one interface
215
+ assignment_device_int1.delete()
216
+
217
+ # Refresh and check that the primary IP is still assigned
218
+ self.test_device.refresh_from_db()
219
+ self.assertEqual(self.test_device.primary_ip4, ip_address)
220
+
221
+ # Verify remaining IP assignments on the IP object
222
+ remaining_assignments = ip_address.interface_assignments.all()
223
+ self.assertEqual(remaining_assignments.count(), 1)
224
+ self.assertIn(assignment_module_int1, remaining_assignments)
225
+
226
+ def test_primary_ip_retained_when_deleted_from_device_interface_with_nested_module(self):
227
+ """Test primary_ip4 remains set when the same IP is assigned to a device and nested module interfaces, and deleted from the device interface."""
228
+
229
+ # Create a module bay on the existing device
230
+ device_module_bay = ModuleBay.objects.create(parent_device=self.test_device, name="Primary Module Bay")
231
+
232
+ # Create a primary module with an interface and add it to the module bay on the device
233
+ primary_module = Module.objects.create(
234
+ module_type=ModuleType.objects.first(),
235
+ status=Status.objects.get_for_model(Module).first(),
236
+ parent_module_bay=device_module_bay,
237
+ )
238
+
239
+ # Create a secondary module bay within the primary module for nested module creation
240
+ nested_module_bay = ModuleBay.objects.create(parent_module=primary_module, name="Nested Module Bay")
241
+
242
+ # Create a nested module within the primary module's module bay
243
+ nested_module = Module.objects.create(
244
+ module_type=ModuleType.objects.first(),
245
+ status=Status.objects.get_for_model(Module).first(),
246
+ parent_module_bay=nested_module_bay,
247
+ )
248
+
249
+ # Set status for the nested module interface
250
+ int_status = Status.objects.get_for_model(Interface).first()
251
+
252
+ # Create an interface on the nested module and assign an IP
253
+ nested_interface = Interface.objects.create(
254
+ name="eth0_nested",
255
+ module=nested_module,
256
+ type=dcim_choices.InterfaceTypeChoices.TYPE_1GE_FIXED,
257
+ status=int_status,
258
+ )
259
+
260
+ # Create IP and assign it to both the device and the nested module interface
261
+ ip_address = IPAddress.objects.create(address="192.0.2.1/24", namespace=self.namespace, status=self.status)
262
+ assignment_device_int1 = IPAddressToInterface.objects.create(interface=self.test_int1, ip_address=ip_address)
263
+ assignment_nested_module = IPAddressToInterface.objects.create(
264
+ interface=nested_interface, ip_address=ip_address
265
+ )
266
+
267
+ # Set the primary IP on the device to the IP on the device interface
268
+ self.test_device.primary_ip4 = assignment_nested_module.ip_address
269
+ self.test_device.save()
270
+
271
+ # Verify that the primary IP is correctly set
272
+ self.assertEqual(self.test_device.primary_ip4, ip_address)
273
+
274
+ # Delete the IP assignment from the device interface
275
+ assignment_device_int1.delete()
276
+
277
+ # Refresh and check that the primary IP is still assigned to the device
278
+ self.test_device.refresh_from_db()
279
+ self.assertEqual(self.test_device.primary_ip4, ip_address)
280
+
281
+ # Confirm that the IP is still associated with the nested module interface
282
+ remaining_assignments = ip_address.interface_assignments.all()
283
+ self.assertEqual(remaining_assignments.count(), 1)
284
+ self.assertIn(assignment_nested_module, remaining_assignments)
285
+
174
286
 
175
287
  class TestVarbinaryIPField(TestCase):
176
288
  """Tests for `nautobot.ipam.fields.VarbinaryIPField`."""