nautobot 2.4.16__py3-none-any.whl → 2.4.18__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 (434) hide show
  1. nautobot/apps/utils.py +2 -0
  2. nautobot/apps/views.py +2 -0
  3. nautobot/circuits/templates/circuits/circuittermination_retrieve.html +1 -8
  4. nautobot/circuits/templates/circuits/inc/circuit_termination_speed_fragment.html +9 -0
  5. nautobot/circuits/tests/integration/test_circuit.py +2 -2
  6. nautobot/circuits/views.py +32 -15
  7. nautobot/cloud/templates/cloud/cloudresourcetype_retrieve.html +3 -3
  8. nautobot/cloud/views.py +7 -0
  9. nautobot/core/apps/__init__.py +1 -0
  10. nautobot/core/celery/__init__.py +2 -1
  11. nautobot/core/filters.py +2 -2
  12. nautobot/core/settings.py +1 -0
  13. nautobot/core/settings.yaml +9 -0
  14. nautobot/core/tables.py +21 -23
  15. nautobot/core/templates/components/breadcrumbs.html +19 -0
  16. nautobot/core/templates/components/panel/panel.html +1 -1
  17. nautobot/core/templates/generic/object_changelog.html +0 -2
  18. nautobot/core/templates/generic/object_list.html +15 -12
  19. nautobot/core/templates/generic/object_notes.html +0 -2
  20. nautobot/core/templates/generic/object_retrieve.html +16 -9
  21. nautobot/core/templates/inc/paginator.html +3 -3
  22. nautobot/core/templates/inc/table.html +2 -2
  23. nautobot/core/templatetags/helpers.py +104 -6
  24. nautobot/core/templatetags/ui_framework.py +40 -5
  25. nautobot/core/testing/filters.py +37 -21
  26. nautobot/core/testing/mixins.py +1 -1
  27. nautobot/core/testing/views.py +27 -4
  28. nautobot/core/tests/test_tables.py +43 -6
  29. nautobot/core/tests/test_templatetags_ui_framework.py +146 -0
  30. nautobot/core/tests/test_titles.py +2 -2
  31. nautobot/core/tests/test_ui.py +14 -1
  32. nautobot/core/tests/test_views.py +45 -0
  33. nautobot/core/ui/breadcrumbs.py +13 -8
  34. nautobot/core/ui/bulk_buttons.py +53 -53
  35. nautobot/core/ui/object_detail.py +52 -9
  36. nautobot/core/ui/titles.py +9 -5
  37. nautobot/core/utils/data.py +13 -0
  38. nautobot/core/utils/deprecation.py +2 -0
  39. nautobot/core/views/__init__.py +24 -3
  40. nautobot/core/views/generic.py +42 -17
  41. nautobot/core/views/mixins.py +146 -12
  42. nautobot/core/views/utils.py +117 -0
  43. nautobot/dcim/migrations/0073_alter_powerport_power_factor_and_more.py +41 -0
  44. nautobot/dcim/models/device_component_templates.py +4 -2
  45. nautobot/dcim/models/device_components.py +3 -2
  46. nautobot/dcim/models/devices.py +4 -0
  47. nautobot/dcim/tables/__init__.py +2 -0
  48. nautobot/dcim/tables/devices.py +24 -0
  49. nautobot/dcim/tables/power.py +2 -2
  50. nautobot/dcim/templates/dcim/device/base.html +1 -11
  51. nautobot/dcim/templates/dcim/device_component.html +0 -19
  52. nautobot/dcim/templates/dcim/modulebay_retrieve.html +0 -16
  53. nautobot/dcim/templates/dcim/rack_elevation_list.html +4 -4
  54. nautobot/dcim/templates/dcim/virtualchassis_retrieve.html +1 -50
  55. nautobot/dcim/tests/test_views.py +41 -0
  56. nautobot/dcim/views.py +169 -39
  57. nautobot/extras/filters/mixins.py +1 -1
  58. nautobot/extras/forms/forms.py +15 -0
  59. nautobot/extras/models/customfields.py +45 -9
  60. nautobot/extras/models/groups.py +10 -1
  61. nautobot/extras/models/jobs.py +2 -2
  62. nautobot/extras/plugins/views.py +18 -5
  63. nautobot/extras/tables.py +4 -2
  64. nautobot/extras/templates/extras/configcontext_retrieve.html +1 -1
  65. nautobot/extras/templates/extras/configcontext_update.html +49 -49
  66. nautobot/extras/templates/extras/configcontextschema_retrieve.html +47 -47
  67. nautobot/extras/templates/extras/configcontextschema_update.html +18 -18
  68. nautobot/extras/templates/extras/customfield_retrieve.html +1 -128
  69. nautobot/extras/templates/extras/dynamicgroup.html +2 -99
  70. nautobot/extras/templates/extras/dynamicgroup_edit.html +2 -199
  71. nautobot/extras/templates/extras/dynamicgroup_retrieve.html +99 -0
  72. nautobot/extras/templates/extras/dynamicgroup_update.html +199 -0
  73. nautobot/extras/templates/extras/gitrepository.html +2 -82
  74. nautobot/extras/templates/extras/gitrepository_object_edit.html +2 -13
  75. nautobot/extras/templates/extras/gitrepository_retrieve.html +82 -0
  76. nautobot/extras/templates/extras/gitrepository_update.html +13 -0
  77. nautobot/extras/templates/extras/inc/job_table.html +1 -1
  78. nautobot/extras/templates/extras/inc/object_contact_header.html +2 -2
  79. nautobot/extras/templates/extras/note_retrieve.html +1 -53
  80. nautobot/extras/templates/extras/plugin_detail.html +3 -7
  81. nautobot/extras/templates/extras/plugins_list.html +0 -2
  82. nautobot/extras/templates/extras/tag_retrieve.html +1 -1
  83. nautobot/extras/templates/extras/tag_update.html +14 -14
  84. nautobot/extras/templates/extras/team_retrieve.html +1 -1
  85. nautobot/extras/tests/test_dynamicgroups.py +73 -18
  86. nautobot/extras/tests/test_models.py +216 -0
  87. nautobot/extras/tests/test_views.py +7 -2
  88. nautobot/extras/urls.py +2 -94
  89. nautobot/extras/views.py +425 -430
  90. nautobot/ipam/apps.py +1 -0
  91. nautobot/ipam/jobs/__init__.py +10 -0
  92. nautobot/ipam/jobs/cleanup.py +296 -0
  93. nautobot/ipam/models.py +301 -178
  94. nautobot/ipam/querysets.py +3 -3
  95. nautobot/ipam/signals.py +6 -1
  96. nautobot/ipam/templates/ipam/inc/ipadress_edit_header.html +3 -3
  97. nautobot/ipam/templates/ipam/inc/toggle_available.html +2 -2
  98. nautobot/ipam/templates/ipam/ipaddress_assign.html +1 -1
  99. nautobot/ipam/templates/ipam/prefix.html +0 -8
  100. nautobot/ipam/templates/ipam/prefix_list.html +1 -1
  101. nautobot/ipam/templates/ipam/vlan_retrieve.html +1 -77
  102. nautobot/ipam/tests/test_api.py +5 -0
  103. nautobot/ipam/tests/test_jobs.py +454 -0
  104. nautobot/ipam/tests/test_models.py +677 -122
  105. nautobot/ipam/tests/test_querysets.py +46 -0
  106. nautobot/ipam/tests/test_views.py +40 -164
  107. nautobot/ipam/urls.py +0 -11
  108. nautobot/ipam/utils/migrations.py +1 -1
  109. nautobot/ipam/utils/testing.py +9 -4
  110. nautobot/ipam/views.py +175 -235
  111. nautobot/project-static/docs/404.html +9 -6
  112. nautobot/project-static/docs/apps/index.html +9 -6
  113. nautobot/project-static/docs/apps/nautobot-apps.html +9 -6
  114. nautobot/project-static/docs/assets/javascripts/bundle.92b07e13.min.js +16 -0
  115. nautobot/project-static/docs/assets/javascripts/{bundle.50899def.min.js.map → bundle.92b07e13.min.js.map} +2 -2
  116. nautobot/project-static/docs/assets/javascripts/workers/{search.d50fe291.min.js → search.973d3a69.min.js} +4 -4
  117. nautobot/project-static/docs/assets/javascripts/workers/{search.d50fe291.min.js.map → search.973d3a69.min.js.map} +1 -1
  118. nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +9 -6
  119. nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +9 -6
  120. nautobot/project-static/docs/code-reference/nautobot/apps/api.html +9 -6
  121. nautobot/project-static/docs/code-reference/nautobot/apps/change_logging.html +9 -6
  122. nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +10 -7
  123. nautobot/project-static/docs/code-reference/nautobot/apps/config.html +9 -6
  124. nautobot/project-static/docs/code-reference/nautobot/apps/constants.html +9 -6
  125. nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +9 -6
  126. nautobot/project-static/docs/code-reference/nautobot/apps/events.html +9 -6
  127. nautobot/project-static/docs/code-reference/nautobot/apps/exceptions.html +9 -6
  128. nautobot/project-static/docs/code-reference/nautobot/apps/factory.html +9 -6
  129. nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +9 -6
  130. nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +9 -6
  131. nautobot/project-static/docs/code-reference/nautobot/apps/graphql.html +9 -6
  132. nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +9 -6
  133. nautobot/project-static/docs/code-reference/nautobot/apps/models.html +11 -8
  134. nautobot/project-static/docs/code-reference/nautobot/apps/querysets.html +9 -6
  135. nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +9 -6
  136. nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +11 -8
  137. nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +81 -6
  138. nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +73 -18
  139. nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +9 -6
  140. nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +69 -7
  141. nautobot/project-static/docs/code-reference/nautobot/apps/views.html +402 -21
  142. nautobot/project-static/docs/development/apps/api/configuration-view.html +13 -10
  143. nautobot/project-static/docs/development/apps/api/database-backend-config.html +11 -8
  144. nautobot/project-static/docs/development/apps/api/models/django-admin.html +13 -10
  145. nautobot/project-static/docs/development/apps/api/models/global-search.html +10 -7
  146. nautobot/project-static/docs/development/apps/api/models/graphql.html +18 -15
  147. nautobot/project-static/docs/development/apps/api/models/index.html +14 -11
  148. nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +12 -9
  149. nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +15 -12
  150. nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +9 -6
  151. nautobot/project-static/docs/development/apps/api/platform-features/git-repository-content.html +15 -12
  152. nautobot/project-static/docs/development/apps/api/platform-features/index.html +9 -6
  153. nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +11 -8
  154. nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +16 -13
  155. nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +12 -10305
  156. nautobot/project-static/docs/development/apps/api/platform-features/prepopulating-data.html +10722 -0
  157. nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +15 -12
  158. nautobot/project-static/docs/development/apps/api/platform-features/table-extensions.html +14 -11
  159. nautobot/project-static/docs/development/apps/api/platform-features/uniquely-identify-objects.html +9 -6
  160. nautobot/project-static/docs/development/apps/api/prometheus.html +15 -12
  161. nautobot/project-static/docs/development/apps/api/setup.html +9 -6
  162. nautobot/project-static/docs/development/apps/api/testing.html +9 -6
  163. nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +12 -9
  164. nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +9 -6
  165. nautobot/project-static/docs/development/apps/api/ui-extensions/index.html +9 -6
  166. nautobot/project-static/docs/development/apps/api/ui-extensions/navigation.html +9 -6
  167. nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +20 -17
  168. nautobot/project-static/docs/development/apps/api/views/base-template.html +9 -6
  169. nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +15 -12
  170. nautobot/project-static/docs/development/apps/api/views/django-generic-views.html +14 -11
  171. nautobot/project-static/docs/development/apps/api/views/help-documentation.html +9 -6
  172. nautobot/project-static/docs/development/apps/api/views/index.html +9 -6
  173. nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +10 -7
  174. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +24 -21
  175. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +12 -9
  176. nautobot/project-static/docs/development/apps/api/views/notes.html +10 -7
  177. nautobot/project-static/docs/development/apps/api/views/rest-api.html +19 -16
  178. nautobot/project-static/docs/development/apps/api/views/urls.html +11 -8
  179. nautobot/project-static/docs/development/apps/index.html +9 -6
  180. nautobot/project-static/docs/development/apps/migration/code-updates.html +19 -16
  181. nautobot/project-static/docs/development/apps/migration/dependency-updates.html +9 -6
  182. nautobot/project-static/docs/development/apps/migration/from-v1.html +9 -6
  183. nautobot/project-static/docs/development/apps/migration/model-updates/dcim.html +22 -19
  184. nautobot/project-static/docs/development/apps/migration/model-updates/extras.html +9 -6
  185. nautobot/project-static/docs/development/apps/migration/model-updates/global.html +9 -6
  186. nautobot/project-static/docs/development/apps/migration/model-updates/ipam.html +9 -6
  187. nautobot/project-static/docs/development/apps/migration/ui-component-framework/best-practices.html +9 -6
  188. nautobot/project-static/docs/development/apps/migration/ui-component-framework/breadcrumbs-titles.html +14 -11
  189. nautobot/project-static/docs/development/apps/migration/ui-component-framework/custom-content.html +27 -24
  190. nautobot/project-static/docs/development/apps/migration/ui-component-framework/index.html +20 -17
  191. nautobot/project-static/docs/development/apps/migration/ui-component-framework/migration-steps.html +20 -17
  192. nautobot/project-static/docs/development/apps/porting-from-netbox.html +9 -6
  193. nautobot/project-static/docs/development/core/application-registry.html +23 -20
  194. nautobot/project-static/docs/development/core/best-practices.html +23 -20
  195. nautobot/project-static/docs/development/core/bootstrap-ui.html +9 -6
  196. nautobot/project-static/docs/development/core/caching.html +9 -6
  197. nautobot/project-static/docs/development/core/controllers.html +9 -6
  198. nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +10 -7
  199. nautobot/project-static/docs/development/core/generic-views.html +9 -6
  200. nautobot/project-static/docs/development/core/getting-started.html +9 -21
  201. nautobot/project-static/docs/development/core/homepage.html +12 -9
  202. nautobot/project-static/docs/development/core/index.html +9 -6
  203. nautobot/project-static/docs/development/core/minikube-dev-environment-for-k8s-jobs.html +9 -6
  204. nautobot/project-static/docs/development/core/model-checklist.html +9 -6
  205. nautobot/project-static/docs/development/core/model-features.html +11 -8
  206. nautobot/project-static/docs/development/core/natural-keys.html +21 -18
  207. nautobot/project-static/docs/development/core/navigation-menu.html +10 -7
  208. nautobot/project-static/docs/development/core/release-checklist.html +9 -6
  209. nautobot/project-static/docs/development/core/role-internals.html +9 -6
  210. nautobot/project-static/docs/development/core/settings.html +9 -6
  211. nautobot/project-static/docs/development/core/style-guide.html +32 -29
  212. nautobot/project-static/docs/development/core/templates.html +9 -6
  213. nautobot/project-static/docs/development/core/testing.html +10 -7
  214. nautobot/project-static/docs/development/core/ui-component-framework.html +42 -44
  215. nautobot/project-static/docs/development/core/user-preferences.html +9 -6
  216. nautobot/project-static/docs/development/index.html +9 -6
  217. nautobot/project-static/docs/development/jobs/getting-started.html +13 -10
  218. nautobot/project-static/docs/development/jobs/index.html +9 -6
  219. nautobot/project-static/docs/development/jobs/installation.html +23 -20
  220. nautobot/project-static/docs/development/jobs/job-extensions.html +25 -22
  221. nautobot/project-static/docs/development/jobs/job-logging.html +12 -9
  222. nautobot/project-static/docs/development/jobs/job-patterns.html +45 -42
  223. nautobot/project-static/docs/development/jobs/job-structure.html +53 -50
  224. nautobot/project-static/docs/development/jobs/migration/from-v1.html +23 -20
  225. nautobot/project-static/docs/development/jobs/testing.html +14 -11
  226. nautobot/project-static/docs/index.html +9 -6
  227. nautobot/project-static/docs/objects.inv +0 -0
  228. nautobot/project-static/docs/overview/application_stack.html +9 -6
  229. nautobot/project-static/docs/overview/design_philosophy.html +9 -6
  230. nautobot/project-static/docs/release-notes/index.html +9 -6
  231. nautobot/project-static/docs/release-notes/version-1.0.html +9 -6
  232. nautobot/project-static/docs/release-notes/version-1.1.html +9 -6
  233. nautobot/project-static/docs/release-notes/version-1.2.html +10 -7
  234. nautobot/project-static/docs/release-notes/version-1.3.html +9 -6
  235. nautobot/project-static/docs/release-notes/version-1.4.html +9 -6
  236. nautobot/project-static/docs/release-notes/version-1.5.html +13 -10
  237. nautobot/project-static/docs/release-notes/version-1.6.html +9 -6
  238. nautobot/project-static/docs/release-notes/version-2.0.html +9 -6
  239. nautobot/project-static/docs/release-notes/version-2.1.html +9 -6
  240. nautobot/project-static/docs/release-notes/version-2.2.html +9 -6
  241. nautobot/project-static/docs/release-notes/version-2.3.html +9 -6
  242. nautobot/project-static/docs/release-notes/version-2.4.html +489 -6
  243. nautobot/project-static/docs/search/search_index.json +1 -1
  244. nautobot/project-static/docs/sitemap.xml +301 -301
  245. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  246. nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +15 -12
  247. nautobot/project-static/docs/user-guide/administration/configuration/authentication/remote.html +9 -6
  248. nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +16 -13
  249. nautobot/project-static/docs/user-guide/administration/configuration/index.html +9 -6
  250. nautobot/project-static/docs/user-guide/administration/configuration/redis.html +9 -6
  251. nautobot/project-static/docs/user-guide/administration/configuration/settings.html +38 -8
  252. nautobot/project-static/docs/user-guide/administration/configuration/time-zones.html +9 -6
  253. nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +9 -6
  254. nautobot/project-static/docs/user-guide/administration/guides/docker.html +9 -6
  255. nautobot/project-static/docs/user-guide/administration/guides/health-checks.html +9 -6
  256. nautobot/project-static/docs/user-guide/administration/guides/permissions.html +9 -6
  257. nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +9 -6
  258. nautobot/project-static/docs/user-guide/administration/guides/replicating-nautobot.html +9 -6
  259. nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +9 -6
  260. nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +16 -13
  261. nautobot/project-static/docs/user-guide/administration/guides/selinux-troubleshooting.html +9 -6
  262. nautobot/project-static/docs/user-guide/administration/installation/app-install.html +9 -6
  263. nautobot/project-static/docs/user-guide/administration/installation/external-authentication.html +9 -6
  264. nautobot/project-static/docs/user-guide/administration/installation/http-server.html +9 -6
  265. nautobot/project-static/docs/user-guide/administration/installation/index.html +9 -6
  266. nautobot/project-static/docs/user-guide/administration/installation/install_system.html +9 -6
  267. nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +9 -6
  268. nautobot/project-static/docs/user-guide/administration/installation/services.html +12 -9
  269. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-netbox.html +13 -10
  270. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +10 -7
  271. nautobot/project-static/docs/user-guide/administration/security/index.html +9 -6
  272. nautobot/project-static/docs/user-guide/administration/security/notices.html +9 -6
  273. nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +9 -6
  274. nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +10 -7
  275. nautobot/project-static/docs/user-guide/administration/upgrading/database-backup.html +9 -6
  276. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/after-you-upgrade.html +9 -6
  277. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/before-you-upgrade.html +9 -6
  278. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/for-developers.html +9 -6
  279. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/index.html +9 -6
  280. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/whats-changed.html +9 -6
  281. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/region-and-site-data-migration-guide.html +9 -6
  282. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +15 -12
  283. nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +9 -6
  284. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuit.html +9 -6
  285. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittermination.html +9 -6
  286. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittype.html +9 -6
  287. nautobot/project-static/docs/user-guide/core-data-model/circuits/provider.html +9 -6
  288. nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +9 -6
  289. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloud.html +9 -6
  290. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudaccount.html +9 -6
  291. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetwork.html +9 -6
  292. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetworkprefixassignment.html +9 -6
  293. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudresourcetype.html +9 -6
  294. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservice.html +9 -6
  295. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservicenetworkassignment.html +9 -6
  296. nautobot/project-static/docs/user-guide/core-data-model/dcim/cable.html +9 -6
  297. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +9 -6
  298. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +9 -6
  299. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +9 -6
  300. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +9 -6
  301. nautobot/project-static/docs/user-guide/core-data-model/dcim/controller.html +9 -6
  302. nautobot/project-static/docs/user-guide/core-data-model/dcim/controllermanageddevicegroup.html +9 -6
  303. nautobot/project-static/docs/user-guide/core-data-model/dcim/device.html +9 -6
  304. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +9 -6
  305. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +9 -6
  306. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicefamily.html +9 -6
  307. nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +13 -10
  308. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +9 -6
  309. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +9 -6
  310. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +9 -6
  311. nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +9 -6
  312. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +9 -6
  313. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +9 -6
  314. nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +9 -6
  315. nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +9 -6
  316. nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +9 -6
  317. nautobot/project-static/docs/user-guide/core-data-model/dcim/manufacturer.html +9 -6
  318. nautobot/project-static/docs/user-guide/core-data-model/dcim/module.html +9 -6
  319. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebay.html +9 -6
  320. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebaytemplate.html +9 -6
  321. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulefamily.html +9 -6
  322. nautobot/project-static/docs/user-guide/core-data-model/dcim/moduletype.html +9 -6
  323. nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +9 -6
  324. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerfeed.html +9 -6
  325. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +9 -6
  326. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +9 -6
  327. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerpanel.html +9 -6
  328. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +9 -6
  329. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +9 -6
  330. nautobot/project-static/docs/user-guide/core-data-model/dcim/rack.html +9 -6
  331. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackgroup.html +9 -6
  332. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackreservation.html +9 -6
  333. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +9 -6
  334. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +9 -6
  335. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +9 -6
  336. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareversion.html +9 -6
  337. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualchassis.html +9 -6
  338. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualdevicecontext.html +9 -6
  339. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +9 -6
  340. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +9 -6
  341. nautobot/project-static/docs/user-guide/core-data-model/extras/contact.html +11 -8
  342. nautobot/project-static/docs/user-guide/core-data-model/extras/team.html +11 -8
  343. nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +41 -41
  344. nautobot/project-static/docs/user-guide/core-data-model/ipam/namespace.html +9 -6
  345. nautobot/project-static/docs/user-guide/core-data-model/ipam/prefix.html +197 -54
  346. nautobot/project-static/docs/user-guide/core-data-model/ipam/rir.html +9 -6
  347. nautobot/project-static/docs/user-guide/core-data-model/ipam/routetarget.html +9 -6
  348. nautobot/project-static/docs/user-guide/core-data-model/ipam/service.html +9 -6
  349. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +9 -6
  350. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlangroup.html +9 -6
  351. nautobot/project-static/docs/user-guide/core-data-model/ipam/vrf.html +9 -6
  352. nautobot/project-static/docs/user-guide/core-data-model/overview/introduction.html +9 -6
  353. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenant.html +9 -6
  354. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenantgroup.html +9 -6
  355. nautobot/project-static/docs/user-guide/core-data-model/virtualization/cluster.html +9 -6
  356. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustergroup.html +9 -6
  357. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustertype.html +9 -6
  358. nautobot/project-static/docs/user-guide/core-data-model/virtualization/virtualmachine.html +9 -6
  359. nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +9 -6
  360. nautobot/project-static/docs/user-guide/core-data-model/wireless/index.html +9 -6
  361. nautobot/project-static/docs/user-guide/core-data-model/wireless/radioprofile.html +9 -6
  362. nautobot/project-static/docs/user-guide/core-data-model/wireless/supporteddatarate.html +9 -6
  363. nautobot/project-static/docs/user-guide/core-data-model/wireless/wirelessnetwork.html +9 -6
  364. nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +9 -6
  365. nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +9 -6
  366. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +9 -6
  367. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +9 -6
  368. nautobot/project-static/docs/user-guide/feature-guides/getting-started/index.html +9 -6
  369. nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +9 -6
  370. nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +9 -6
  371. nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +9 -6
  372. nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +9 -6
  373. nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +9 -6
  374. nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +9 -6
  375. nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +9 -6
  376. nautobot/project-static/docs/user-guide/feature-guides/graphql.html +13 -10
  377. nautobot/project-static/docs/user-guide/feature-guides/ip-address-merge-tool.html +9 -6
  378. nautobot/project-static/docs/user-guide/feature-guides/relationships.html +9 -6
  379. nautobot/project-static/docs/user-guide/feature-guides/software-image-files-and-versions.html +9 -6
  380. nautobot/project-static/docs/user-guide/feature-guides/wireless-networks-and-controllers.html +9 -6
  381. nautobot/project-static/docs/user-guide/index.html +9 -6
  382. nautobot/project-static/docs/user-guide/platform-functionality/change-logging.html +9 -6
  383. nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +9 -6
  384. nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +10 -7
  385. nautobot/project-static/docs/user-guide/platform-functionality/customlink.html +9 -6
  386. nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +9 -6
  387. nautobot/project-static/docs/user-guide/platform-functionality/events.html +11 -8
  388. nautobot/project-static/docs/user-guide/platform-functionality/exporttemplate.html +9 -6
  389. nautobot/project-static/docs/user-guide/platform-functionality/externalintegration.html +9 -6
  390. nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +9 -6
  391. nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +9 -6
  392. nautobot/project-static/docs/user-guide/platform-functionality/graphqlquery.html +9 -6
  393. nautobot/project-static/docs/user-guide/platform-functionality/imageattachment.html +9 -6
  394. nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +9 -6
  395. nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +9 -6
  396. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +9 -6
  397. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +9 -6
  398. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobqueue.html +9 -6
  399. nautobot/project-static/docs/user-guide/platform-functionality/jobs/kubernetes-job-support.html +9 -6
  400. nautobot/project-static/docs/user-guide/platform-functionality/jobs/managing-jobs.html +9 -6
  401. nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +9 -6
  402. nautobot/project-static/docs/user-guide/platform-functionality/napalm.html +9 -6
  403. nautobot/project-static/docs/user-guide/platform-functionality/note.html +9 -6
  404. nautobot/project-static/docs/user-guide/platform-functionality/objectmetadata.html +12 -9
  405. nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +9 -6
  406. nautobot/project-static/docs/user-guide/platform-functionality/rendering-jinja-templates.html +9 -6
  407. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +9 -6
  408. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +9 -6
  409. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +9 -6
  410. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/ui-related-endpoints.html +9 -6
  411. nautobot/project-static/docs/user-guide/platform-functionality/role.html +9 -6
  412. nautobot/project-static/docs/user-guide/platform-functionality/savedview.html +9 -6
  413. nautobot/project-static/docs/user-guide/platform-functionality/secret.html +11 -8
  414. nautobot/project-static/docs/user-guide/platform-functionality/staticgroupassociation.html +9 -6
  415. nautobot/project-static/docs/user-guide/platform-functionality/status.html +9 -6
  416. nautobot/project-static/docs/user-guide/platform-functionality/tag.html +9 -6
  417. nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +9 -6
  418. nautobot/project-static/docs/user-guide/platform-functionality/users/objectpermission.html +9 -6
  419. nautobot/project-static/docs/user-guide/platform-functionality/users/token.html +9 -6
  420. nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +9 -6
  421. nautobot/project-static/fonts/UFL.txt +96 -96
  422. nautobot/project-static/img/nautobot_icon.svg +32 -34
  423. nautobot/project-static/js/forms.js +35 -2
  424. nautobot/project-static/js/table_sorting_indicator.js +0 -2
  425. nautobot/virtualization/filters.py +7 -0
  426. {nautobot-2.4.16.dist-info → nautobot-2.4.18.dist-info}/METADATA +8 -8
  427. {nautobot-2.4.16.dist-info → nautobot-2.4.18.dist-info}/RECORD +431 -421
  428. nautobot/core/templates/inc/breadcrumbs.html +0 -14
  429. nautobot/project-static/docs/assets/javascripts/bundle.50899def.min.js +0 -16
  430. nautobot/project-static/docs/requirements.txt +0 -14
  431. {nautobot-2.4.16.dist-info → nautobot-2.4.18.dist-info}/LICENSE.txt +0 -0
  432. {nautobot-2.4.16.dist-info → nautobot-2.4.18.dist-info}/NOTICE +0 -0
  433. {nautobot-2.4.16.dist-info → nautobot-2.4.18.dist-info}/WHEEL +0 -0
  434. {nautobot-2.4.16.dist-info → nautobot-2.4.18.dist-info}/entry_points.txt +0 -0
nautobot/ipam/models.py CHANGED
@@ -1,5 +1,6 @@
1
1
  import logging
2
2
  import operator
3
+ from typing import Optional
3
4
 
4
5
  from django.contrib.contenttypes.models import ContentType
5
6
  from django.core.exceptions import MultipleObjectsReturned, ValidationError
@@ -602,44 +603,40 @@ class Prefix(PrimaryModel):
602
603
  unique_together = ["namespace", "network", "prefix_length"]
603
604
  verbose_name_plural = "prefixes"
604
605
 
605
- def validate_unique(self, exclude=None):
606
- if self.namespace is None:
607
- if Prefix.objects.filter(
608
- network=self.network, prefix_length=self.prefix_length, namespace__isnull=True
609
- ).exists():
610
- raise ValidationError(
611
- {"__all__": "Prefix with this Namespace, Network and Prefix length already exists."}
612
- )
613
- super().validate_unique(exclude)
614
-
615
606
  def __init__(self, *args, **kwargs):
616
607
  prefix = kwargs.pop("prefix", None)
617
608
  self._location = kwargs.pop("location", None)
618
609
  super().__init__(*args, **kwargs)
619
610
 
620
611
  # Initialize cached fields
621
- self._parent = None
612
+ self._parent_id = None
622
613
  self._network = None
614
+ self._broadcast = None
623
615
  self._prefix_length = None
624
616
  self._namespace_id = None
617
+ self._ip_version = None
618
+ self._cleaned = False
625
619
 
626
620
  self._deconstruct_prefix(prefix)
627
621
 
628
- @staticmethod
629
- def _extract_field_value(field_name, field_names, values):
630
- for current_field, field_value in zip(field_names, values):
631
- if field_name == current_field:
632
- return field_value
633
- return None
634
-
635
622
  @classmethod
636
623
  def from_db(cls, db, field_names, values):
637
624
  instance = super().from_db(db, field_names, values)
638
625
  # These cached values are used to detect changes during save,
639
626
  # avoiding unnecessary re-parenting of subnets and IPs if these fields have not been updated.
640
- instance._network = cls._extract_field_value("network", field_names, values)
641
- instance._prefix_length = cls._extract_field_value("prefix_length", field_names, values)
642
- instance._namespace_id = cls._extract_field_value("namespace_id", field_names, values)
627
+ for field_name, value in zip(field_names, values):
628
+ if field_name == "broadcast":
629
+ instance._broadcast = value
630
+ elif field_name == "ip_version":
631
+ instance._ip_version = value
632
+ elif field_name == "namespace_id":
633
+ instance._namespace_id = value
634
+ elif field_name == "network":
635
+ instance._network = value
636
+ elif field_name == "parent_id":
637
+ instance._parent_id = value
638
+ elif field_name == "prefix_length":
639
+ instance._prefix_length = value
643
640
  return instance
644
641
 
645
642
  def __str__(self):
@@ -671,116 +668,128 @@ class Prefix(PrimaryModel):
671
668
 
672
669
  def delete(self, *args, **kwargs):
673
670
  """
674
- A Prefix with children will be impossible to delete and raise a `ProtectedError`.
675
-
676
- If a Prefix has children, this catches the error and explicitly updates the
677
- `protected_objects` from the exception setting their parent to the old parent of this
678
- prefix, and then this prefix will be deleted.
671
+ As a part of deleting this Prefix, reparent any child Prefixes or IPAddresses to this Prefix's parent if any.
679
672
  """
680
673
 
681
- try:
682
- return super().delete(*args, **kwargs)
683
- except models.ProtectedError as err:
684
- for instance in err.protected_objects:
685
- # This will be either IPAddress or Prefix.
686
- protected_model = instance._meta.model
687
-
688
- # IPAddress objects must have a valid parent.
689
- # 3.0 TODO: uncomment this check to enforce it
690
- # if protected_model == IPAddress and (
691
- # self.parent is None
692
- # or self.parent.type != choices.PrefixTypeChoices.TYPE_NETWORK
693
- # ):
694
- if protected_model == IPAddress and self.parent is None:
695
- raise models.ProtectedError(
696
- msg=(
697
- f"Cannot delete Prefix {self} because it has child IPAddress objects that "
698
- "would no longer have a valid parent."
699
- ),
700
- protected_objects=err.protected_objects,
701
- ) from err
702
-
703
- elif protected_model not in (IPAddress, Prefix):
704
- raise
705
- # 3.0 TODO: uncomment this check to enforce it
706
- # Prefix objects must have a valid parent
707
- # elif (
708
- # protected_model == Prefix
709
- # and self.parent is not None
710
- # and constants.PREFIX_ALLOWED_PARENT_TYPES[instance.type] != self.parent.type
711
- # ):
712
- # raise models.ProtectedError(
713
- # msg=(
714
- # f"Cannot delete Prefix {self} because it has child Prefix objects that "
715
- # "would no longer have a valid parent."
716
- # ),
717
- # protected_objects=err.protected_objects,
718
- # ) from err
719
-
720
- # Update protected objects to use the new parent and delete the old parent (self).
721
- protected_pks = (po.pk for po in err.protected_objects)
722
- protected_objects = protected_model.objects.filter(pk__in=protected_pks)
723
- protected_objects.update(parent=self.parent)
674
+ with transaction.atomic():
675
+ if self.parent is None and self.ip_addresses.exists():
676
+ raise models.ProtectedError(
677
+ msg=(
678
+ f"Cannot delete Prefix {self} because it has child IPAddress objects that "
679
+ "would no longer have a valid parent."
680
+ ),
681
+ protected_objects=self.ip_addresses.all(),
682
+ )
683
+
684
+ self.children.update(parent=self.parent)
685
+ self.ip_addresses.update(parent=self.parent)
724
686
  return super().delete(*args, **kwargs)
725
687
 
726
688
  def get_parent(self):
727
- # Determine if a parent exists and set it to the closest ancestor by `prefix_length`.
728
- if self._parent is not None:
729
- return self._parent
689
+ """
690
+ Identify the prefix in this namespace that should serve as the parent of this Prefix.
730
691
 
692
+ Note that for historical reasons this does not directly set `self.parent`, but just returns the candidate.
693
+ """
694
+ # Closest ancestor by `prefix_length`.
731
695
  if supernets := self.supernets():
732
- parent = max(supernets, key=operator.attrgetter("prefix_length"))
733
- self._parent = parent
734
- return parent
696
+ return max(supernets, key=operator.attrgetter("prefix_length"))
735
697
  return None
736
698
 
737
- get_parent.alters_data = True
699
+ @property
700
+ def _networking_values_changed(self) -> bool:
701
+ """
702
+ Check if the networking fields of this Prefix have changed compared to their database values.
703
+
704
+ Returns:
705
+ bool: True if any of (network, broadcast, prefix_length, ip_version) have changed.
706
+ """
707
+ if not self.present_in_database:
708
+ return False
709
+ return (
710
+ self._network != self.network
711
+ or self._broadcast != self.broadcast
712
+ or self._prefix_length != self.prefix_length
713
+ or self._ip_version != self.ip_version
714
+ )
738
715
 
739
716
  def clean(self):
740
- if self.prefix is not None: # missing network/prefix_length will be caught by super().clean()
741
- # Clear host bits from prefix
742
- # This also has the subtle side effect of calling self._deconstruct_prefix(),
743
- # which will (re)set the broadcast and ip_version values of this instance to their correct values.
744
- self.prefix = self.prefix.cidr
717
+ """
718
+ Perform various data sanitization and validation.
719
+
720
+ - Prevent simultaneously changing network fields and namespace (too complex to handle)
721
+ - Ensure that `network`/`broadcast`/`prefix_length`/`ip_version` are set and self-consistent.
722
+ This includes clearing any host bits from the `prefix` or `network` based on the `prefix_length`.
723
+ - (Re)calculate `self.parent` if any networking fields have changed or if this is a new record.
724
+ - Raise a `ValidationError` if changes would orphan any existing child IPAddresses.
725
+ """
726
+ if self.prefix is not None: # skip if missing a network/prefix_length; that will be caught by super().clean()
727
+ self._deconstruct_prefix(self.prefix)
745
728
 
746
- self.parent = self.get_parent()
729
+ # Determine correct `parent` from `namespace`/`prefix` if needed
730
+ if (
731
+ self._networking_values_changed
732
+ or self._namespace_id != self.namespace_id
733
+ or not self.present_in_database
734
+ ):
735
+ self.parent = self.get_parent()
747
736
 
748
737
  super().clean()
749
738
 
739
+ if self._networking_values_changed and self._namespace_id != self.namespace_id:
740
+ raise ValidationError(
741
+ {
742
+ "__all__": "Cannot change network and namespace in the same update. "
743
+ "Consider creating a new Prefix instead."
744
+ }
745
+ )
746
+
747
+ if self._networking_values_changed and self._parent_id is None:
748
+ orphaned_ips = self.ip_addresses.exclude(
749
+ ip_version=self.ip_version,
750
+ host__gte=self.network,
751
+ host__lte=self.broadcast,
752
+ )
753
+ if orphaned_ips_count := orphaned_ips.count():
754
+ raise ValidationError(
755
+ {
756
+ "__all__": f"{orphaned_ips_count} existing IP addresses (including "
757
+ f"{orphaned_ips.first().host}) would no longer have a valid parent Prefix after this change."
758
+ }
759
+ )
760
+
761
+ if self.present_in_database and self._namespace_id != self.namespace_id:
762
+ if self.vrfs.exists():
763
+ raise ValidationError(
764
+ {
765
+ "namespace": "Cannot move to a different Namespace while associated to VRFs in the current "
766
+ "Namespace. Remove all VRFs from this Prefix and descendants before making this change."
767
+ }
768
+ )
769
+ if VRFPrefixAssignment.objects.filter(
770
+ prefix__ip_version=self.ip_version,
771
+ prefix__network__gte=self.network,
772
+ prefix__broadcast__lte=self.broadcast,
773
+ prefix__prefix_length__gt=self.prefix_length,
774
+ prefix__namespace_id=self._namespace_id,
775
+ ).exists():
776
+ raise ValidationError(
777
+ {
778
+ "namespace": "Cannot move to a different Namespace with descendant Prefixes associated to VRFs "
779
+ "in the current Namespace. Remove all VRFs from all descendants before making this change."
780
+ }
781
+ )
782
+
783
+ self._cleaned = True
784
+
750
785
  clean.alters_data = True
751
786
 
752
787
  def save(self, *args, **kwargs):
753
- self.clean()
754
-
755
- # Validate that creation of this prefix does not create an invalid parent/child relationship
756
- # 3.0 TODO: uncomment this to enforce this constraint
757
- # if self.parent and self.parent.type != constants.PREFIX_ALLOWED_PARENT_TYPES[self.type]:
758
- # err_msg = f"{self.type.title()} prefixes cannot be children of {self.parent.type.title()} prefixes"
759
- # raise ValidationError({"type": err_msg})
760
-
761
- # This is filtering on prefixes that share my parent and will be reparented to me
762
- # but are not the correct type for this parent/child relationship
763
- # 3.0 TODO: uncomment the below to enforce this constraint
764
- # invalid_children = Prefix.objects.filter(
765
- # ~models.Q(id=self.id),
766
- # ~models.Q(type__in=constants.PREFIX_ALLOWED_CHILD_TYPES[self.type]),
767
- # parent_id=self.parent_id,
768
- # prefix_length__gt=self.prefix_length,
769
- # ip_version=self.ip_version,
770
- # network__gte=self.network,
771
- # broadcast__lte=self.broadcast,
772
- # namespace=self.namespace,
773
- # )
774
- #
775
- # if invalid_children.exists():
776
- # invalid_child_prefixes = [
777
- # f"{child.cidr_str} ({child.type})" for child in invalid_children.only("network", "prefix_length")
778
- # ]
779
- # err_msg = (
780
- # f'Creating prefix "{self.prefix}" in namespace "{self.namespace}" with type "{self.type}" '
781
- # f"would create an invalid parent/child relationship with prefixes {invalid_child_prefixes}"
782
- # )
783
- # raise ValidationError({"__all__": err_msg})
788
+ """
789
+ If not already cleaned, clean automatically before saving, and after saving, update related IPs and Prefixes.
790
+ """
791
+ if not self._cleaned:
792
+ self.clean()
784
793
 
785
794
  # cache the value of present_in_database; because after `super().save()`
786
795
  # `self.present_in_database` would always return True`
@@ -788,29 +797,66 @@ class Prefix(PrimaryModel):
788
797
 
789
798
  with transaction.atomic():
790
799
  super().save(*args, **kwargs)
800
+
801
+ # Backward-compatibility: if this was initialized with a `location`, update the `locations` M2M now.
791
802
  if self._location is not None:
792
803
  self.location = self._location
793
804
 
794
- # Only reparent subnets and ips if any of these fields has been updated.
795
- if (
796
- not present_in_database
797
- or self._network != self.network
798
- or self._namespace_id != self.namespace_id
799
- or self._prefix_length != self.prefix_length
800
- ):
801
- # Determine the subnets and reparent them to this prefix.
802
- self.reparent_subnets()
803
- # Determine the child IPs and reparent them to this prefix.
804
- self.reparent_ips()
805
+ if self._namespace_id != self.namespace_id and present_in_database:
806
+ # Namespace changed, move *all* descendants to the new namespace as well.
807
+ subnets = Prefix.objects.filter(
808
+ ip_version=self.ip_version,
809
+ network__gte=self.network,
810
+ broadcast__lte=self.broadcast,
811
+ prefix_length__gt=self.prefix_length,
812
+ namespace_id=self._namespace_id, # important!
813
+ )
814
+ # Is this a "merge" case where there are existing prefixes in the target namespace?
815
+ if (
816
+ Prefix.objects.filter(
817
+ ip_version=self.ip_version,
818
+ network__gte=self.network,
819
+ broadcast__lte=self.broadcast,
820
+ prefix_length__gt=self.prefix_length,
821
+ namespace_id=self.namespace_id, # important!
822
+ ).exists()
823
+ or IPAddress.objects.filter(
824
+ ip_version=self.ip_version,
825
+ host__gte=self.network,
826
+ host__lte=self.broadcast,
827
+ parent__namespace_id=self.namespace_id,
828
+ ).exists()
829
+ ):
830
+ for subnet in subnets.iterator():
831
+ # We don't use `subnets.update()` here because we need to have each subnet call its own
832
+ # reparent_ips() and reparent_subnets()
833
+ subnet.namespace_id = self.namespace_id
834
+ subnet.save()
835
+ else:
836
+ # Nothing to merge, much more efficient:
837
+ subnets.update(namespace_id=self.namespace_id)
838
+
839
+ if self._networking_values_changed or self._namespace_id != self.namespace_id or not present_in_database:
840
+ # Claim and/or un-claim child prefixes and IPs
841
+ self.reparent_subnets()
842
+ self.reparent_ips()
843
+
844
+ self._network = self.network
845
+ self._broadcast = self.broadcast
846
+ self._prefix_length = self.prefix_length
847
+ self._ip_version = self.ip_version
848
+ self._namespace_id = self.namespace_id
849
+ self._parent_id = self.parent_id
850
+ self._cleaned = False
805
851
 
806
852
  @property
807
- def cidr_str(self):
853
+ def cidr_str(self) -> Optional[str]:
808
854
  if self.network is not None and self.prefix_length is not None:
809
855
  return f"{self.network}/{self.prefix_length}"
810
856
  return None
811
857
 
812
858
  @property
813
- def prefix(self):
859
+ def prefix(self) -> Optional[netaddr.IPNetwork]:
814
860
  if self.cidr_str:
815
861
  return netaddr.IPNetwork(self.cidr_str)
816
862
  return None
@@ -818,6 +864,7 @@ class Prefix(PrimaryModel):
818
864
  @prefix.setter
819
865
  def prefix(self, prefix):
820
866
  self._deconstruct_prefix(prefix)
867
+ self._cleaned = False
821
868
 
822
869
  @property
823
870
  def location(self):
@@ -837,35 +884,87 @@ class Prefix(PrimaryModel):
837
884
 
838
885
  def reparent_subnets(self):
839
886
  """
840
- Determine the list of child Prefixes and set the parent to self.
887
+ Handle changes to the parentage of other Prefixes as a consequence of this Prefix's creation or update.
841
888
 
842
- This query is similiar performing update from the query returned by `subnets(direct=True)`,
843
- but explicitly filters for subnets of the parent of this Prefix so they can be reparented.
844
- """
845
- query = Prefix.objects.select_for_update().filter(
846
- ~models.Q(id=self.id), # Don't include yourself...
847
- parent_id=self.parent_id,
848
- prefix_length__gt=self.prefix_length,
849
- ip_version=self.ip_version,
850
- network__gte=self.network,
851
- broadcast__lte=self.broadcast,
852
- namespace=self.namespace,
853
- )
889
+ - Former child Prefixes of ours that are no longer our descendants can be reparented to our former parent.
890
+ - Subnets that we are now the closest parent of can be reparented to us.
891
+ - Former children that we are no longer the closest parent of can be reparented to one of our new descendants.
854
892
 
855
- return query.update(parent=self)
893
+ Called automatically by save(); generally not intended for use outside of that context.
894
+ """
895
+ if self._networking_values_changed:
896
+ # Former child Prefixes of ours that are no longer our descendants can be reparented to our former parent.
897
+ self.children.exclude(
898
+ ip_version=self.ip_version,
899
+ network__gte=self.network,
900
+ broadcast__lte=self.broadcast,
901
+ prefix_length__gt=self.prefix_length,
902
+ namespace_id=self.namespace_id,
903
+ ).update(parent_id=self._parent_id)
904
+
905
+ # Subnets that we are now the closest parent of can be reparented to us.
906
+ self.subnets().filter(
907
+ models.Q(parent__isnull=True) # No parent
908
+ | models.Q(parent__prefix_length__lt=self.prefix_length) # We're closer than the current parent
909
+ ).update(parent=self)
910
+
911
+ if self._networking_values_changed:
912
+ # Former children that we are no longer the closest parent can be reparented to one of our new descendants.
913
+ children_to_reparent = []
914
+ for child in self.children.filter(prefix_length__gt=self.prefix_length + 1):
915
+ try:
916
+ closest_parent = self.children.get_closest_parent(child.prefix) # pylint: disable=no-member
917
+ except Prefix.DoesNotExist:
918
+ closest_parent = self
919
+ if closest_parent != self:
920
+ child.parent = closest_parent
921
+ children_to_reparent.append(child)
922
+
923
+ Prefix.objects.bulk_update(children_to_reparent, ["parent"], batch_size=1000)
856
924
 
857
925
  reparent_subnets.alters_data = True
858
926
 
859
927
  def reparent_ips(self):
860
- """Determine the list of child IPAddresses and set the parent to self."""
861
- query = IPAddress.objects.select_for_update().filter(
862
- ip_version=self.ip_version,
863
- parent_id=self.parent_id,
864
- host__gte=self.network,
865
- host__lte=self.broadcast,
866
- )
928
+ """
929
+ Handle changes to the parentage of IPAddresses as a consequence of this Prefix's creation or update.
867
930
 
868
- return query.update(parent=self)
931
+ - Former child IPs of ours that are no longer our descendants can be reparented to our former parent.
932
+ - Former child IPs that we are no longer the closest parent of can be reparented to one of our new descendants.
933
+ - IPs that we are now the closest parent of can be reparented to us.
934
+
935
+ Called automatically by save(); generally not intended for use outside of that context.
936
+ """
937
+ if self._networking_values_changed:
938
+ # Former child IPs of ours that are no longer our descendants can be reparented to our former parent.
939
+ reparentable_ips = self.ip_addresses.exclude(
940
+ ip_version=self.ip_version,
941
+ host__gte=self.network,
942
+ host__lte=self.broadcast,
943
+ )
944
+ if self._parent_id is None and reparentable_ips.exists():
945
+ raise ValidationError(
946
+ {
947
+ "__all__": f"{reparentable_ips.count()} existing IP addresses would no longer have "
948
+ "a valid parent Prefix after this change."
949
+ }
950
+ )
951
+ reparentable_ips.update(parent_id=self._parent_id)
952
+
953
+ # Former child IPs that we are no longer closest parent of can be reparented to one of our new descendants.
954
+ ips_to_reparent = []
955
+ for ip in self.ip_addresses.all():
956
+ try:
957
+ closest_parent = self.children.get_closest_parent(ip.host, include_self=True) # pylint: disable=no-member
958
+ except Prefix.DoesNotExist:
959
+ closest_parent = self
960
+ if closest_parent != self:
961
+ ip.parent = closest_parent
962
+ ips_to_reparent.append(ip)
963
+
964
+ IPAddress.objects.bulk_update(ips_to_reparent, ["parent"], batch_size=1000)
965
+
966
+ # IPs that we are now the closest parent of can be reparented to us.
967
+ self.get_all_ips().select_for_update().filter(parent__prefix_length__lt=self.prefix_length).update(parent=self)
869
968
 
870
969
  reparent_ips.alters_data = True
871
970
 
@@ -1018,9 +1117,9 @@ class Prefix(PrimaryModel):
1018
1117
  # IPv6, pool, or IPv4 /31-32 sets are fully usable
1019
1118
  if any(
1020
1119
  [
1021
- self.ip_version == 6,
1120
+ self.ip_version == choices.IPAddressVersionChoices.VERSION_6,
1022
1121
  self.type == choices.PrefixTypeChoices.TYPE_POOL,
1023
- self.ip_version == 4 and self.prefix_length >= 31,
1122
+ self.ip_version == choices.IPAddressVersionChoices.VERSION_4 and self.prefix_length >= 31,
1024
1123
  ]
1025
1124
  ):
1026
1125
  return available_ips
@@ -1097,10 +1196,9 @@ class Prefix(PrimaryModel):
1097
1196
  def get_utilization(self):
1098
1197
  """Return the utilization of this prefix as a UtilizationData object.
1099
1198
 
1100
- For prefixes containing other prefixes, all direct child prefixes are considered fully utilized.
1199
+ For CONTAINER and NETWORK prefixes, all child prefixes are considered fully utilized.
1101
1200
 
1102
- For prefixes containing IP addresses and/or pools, pools are considered fully utilized while
1103
- only IP addresses that are not contained within pools are added to the utilization.
1201
+ For NETWORK and POOL prefixes, individual IP addresses not already covered by a child prefix are also counted.
1104
1202
 
1105
1203
  It is recommended that when using this method you add the following prefetch to the queryset when dealing with
1106
1204
  multiple prefixes to ensure good performance:
@@ -1120,9 +1218,7 @@ class Prefix(PrimaryModel):
1120
1218
  child_ips = netaddr.IPSet()
1121
1219
  child_prefixes = netaddr.IPSet()
1122
1220
 
1123
- # 3.0 TODO: In the long term, TYPE_POOL prefixes will be disallowed from directly containing IPAddresses,
1124
- # and the addresses will instead be parented to the containing TYPE_NETWORK prefix. It should be possible to
1125
- # change this when that is the case, see #3873 for historical context.
1221
+ # NETWORK and POOL prefixes, but not CONTAINER prefixes, count contained IPAddresses towards utilization.
1126
1222
  if self.type != choices.PrefixTypeChoices.TYPE_CONTAINER:
1127
1223
  pool_ips = IPAddress.objects.filter(
1128
1224
  parent__namespace_id=self.namespace_id,
@@ -1132,6 +1228,7 @@ class Prefix(PrimaryModel):
1132
1228
  ).values_list("host", flat=True)
1133
1229
  child_ips = netaddr.IPSet(pool_ips)
1134
1230
 
1231
+ # CONTAINER and NETWORK prefixes, but not POOL prefixes, count contained Prefixes towards utilization.
1135
1232
  if self.type != choices.PrefixTypeChoices.TYPE_POOL:
1136
1233
  # Using self.children.all over self.children.iterator (with chunk_size given or not) consistently shaves
1137
1234
  # off around 200 extra SQL queries and shows better performance.
@@ -1141,13 +1238,13 @@ class Prefix(PrimaryModel):
1141
1238
 
1142
1239
  numerator_set = child_ips | child_prefixes
1143
1240
 
1144
- # Exclude network and broadcast address from the denominator unless they've been assigned to an IPAddress or child pool.
1241
+ # Exclude network and broadcast IPs from the denominator unless they're assigned to an IPAddress or child pool.
1145
1242
  # Only applies to IPv4 network prefixes with a prefix length of /30 or shorter
1146
1243
  if all(
1147
1244
  [
1148
1245
  denominator > 2,
1149
1246
  self.type == choices.PrefixTypeChoices.TYPE_NETWORK,
1150
- self.ip_version == 4,
1247
+ self.ip_version == choices.IPAddressVersionChoices.VERSION_4,
1151
1248
  ]
1152
1249
  ):
1153
1250
  if not any([self.network in numerator_set, self.broadcast in numerator_set]):
@@ -1266,12 +1363,26 @@ class IPAddress(PrimaryModel):
1266
1363
  def __init__(self, *args, address=None, namespace=None, **kwargs):
1267
1364
  super().__init__(*args, **kwargs)
1268
1365
 
1366
+ self._parent = None
1367
+ self._host = None
1368
+
1269
1369
  if namespace is not None and not self.present_in_database:
1270
1370
  self._provided_namespace = namespace
1271
1371
 
1272
1372
  if address is not None and not self.present_in_database:
1273
1373
  self._deconstruct_address(address)
1274
1374
 
1375
+ @classmethod
1376
+ def from_db(cls, db, field_names, values):
1377
+ instance = super().from_db(db, field_names, values)
1378
+ # These cached values are used to detect changes during save
1379
+ for field_name, value in zip(field_names, values):
1380
+ if field_name == "host":
1381
+ instance._host = value
1382
+ elif field_name == "parent":
1383
+ instance._parent = value
1384
+ return instance
1385
+
1275
1386
  def __str__(self):
1276
1387
  return str(self.address)
1277
1388
 
@@ -1297,33 +1408,36 @@ class IPAddress(PrimaryModel):
1297
1408
  if self.host in empty_values or self.mask_length in empty_values:
1298
1409
  return None
1299
1410
  try:
1300
- closest_parent = (
1301
- Prefix.objects.filter(namespace=self._namespace)
1302
- # 3.0 TODO: disallow IPAddress from parenting to a TYPE_POOL prefix, instead pick closest TYPE_NETWORK
1303
- # .exclude(type=choices.PrefixTypeChoices.TYPE_POOL)
1304
- .get_closest_parent(self.host, include_self=True)
1411
+ closest_parent = Prefix.objects.filter(namespace=self._namespace).get_closest_parent(
1412
+ self.host, include_self=True
1305
1413
  )
1306
1414
  return closest_parent
1307
1415
  except Prefix.DoesNotExist as e:
1308
- raise ValidationError({"namespace": "No suitable parent Prefix exists in this Namespace"}) from e
1416
+ raise ValidationError(
1417
+ {"namespace": f"No suitable parent Prefix for {self.host} exists in Namespace {self._namespace}"}
1418
+ ) from e
1309
1419
 
1310
1420
  def clean(self):
1311
- self.address = self.address # not a no-op - forces re-calling of self._deconstruct_address()
1421
+ self._deconstruct_address(self.address)
1312
1422
 
1313
- # Validate that host is not being modified
1314
- if self.present_in_database:
1315
- ip_address = IPAddress.objects.get(id=self.id)
1316
- if ip_address.host != self.host:
1317
- raise ValidationError({"address": "Host address cannot be changed once created"})
1423
+ if self.present_in_database and self._host != self.host:
1424
+ raise ValidationError({"__all__": "Host address cannot be changed once created"})
1318
1425
 
1319
1426
  # Validate IP status selection
1320
- if self.type == choices.IPAddressTypeChoices.TYPE_SLAAC and self.ip_version != 6:
1427
+ if (
1428
+ self.type == choices.IPAddressTypeChoices.TYPE_SLAAC
1429
+ and self.ip_version != choices.IPAddressVersionChoices.VERSION_6
1430
+ ):
1321
1431
  raise ValidationError({"type": "Only IPv6 addresses can be assigned SLAAC type"})
1322
1432
 
1323
1433
  closest_parent = self._get_closest_parent()
1324
- # Validate `parent` can be used as the parent for this ipaddress
1325
1434
  if closest_parent is not None:
1326
- if self.parent is not None and self.parent != closest_parent:
1435
+ # If `parent` was explicitly set or changed, validate it and reject if invalid.
1436
+ if (
1437
+ self.parent is not None
1438
+ and (self.parent != self._parent or not self.present_in_database)
1439
+ and self.parent != closest_parent
1440
+ ):
1327
1441
  raise ValidationError(
1328
1442
  {
1329
1443
  "parent": (
@@ -1332,14 +1446,10 @@ class IPAddress(PrimaryModel):
1332
1446
  )
1333
1447
  }
1334
1448
  )
1449
+ # Otherwise, it was *implicitly* changed (e.g. by changing `namespace`), so just update it as appropriate.
1335
1450
  self.parent = closest_parent
1336
1451
  self._namespace = None
1337
1452
 
1338
- # 3.0 TODO: uncomment the below to enforce this constraint
1339
- # if self.parent.type != choices.PrefixTypeChoices.TYPE_NETWORK:
1340
- # err_msg = f"IP addresses cannot be created in {self.parent.type} prefixes. You must create a network prefix first."
1341
- # raise ValidationError({"address": err_msg})
1342
-
1343
1453
  # Force dns_name to lowercase
1344
1454
  if not self.dns_name.islower:
1345
1455
  self.dns_name = self.dns_name.lower()
@@ -1353,6 +1463,9 @@ class IPAddress(PrimaryModel):
1353
1463
 
1354
1464
  super().save(*args, **kwargs)
1355
1465
 
1466
+ self._parent = self.parent
1467
+ self._host = self.host
1468
+
1356
1469
  @property
1357
1470
  def address(self):
1358
1471
  if self.host is not None and self.mask_length is not None:
@@ -1371,6 +1484,8 @@ class IPAddress(PrimaryModel):
1371
1484
  Args:
1372
1485
  ascending (bool): If set, reverses the return order.
1373
1486
  """
1487
+ if self.parent is None: # invalid, but possible currently
1488
+ return Prefix.objects.none()
1374
1489
  return self.parent.ancestors(include_self=True, ascending=ascending)
1375
1490
 
1376
1491
  @cached_property
@@ -1674,6 +1789,14 @@ class VLAN(PrimaryModel):
1674
1789
  # Return all VM interfaces assigned to this VLAN
1675
1790
  return VMInterface.objects.filter(Q(untagged_vlan_id=self.pk) | Q(tagged_vlans=self.pk)).distinct()
1676
1791
 
1792
+ @property
1793
+ def interfaces(self):
1794
+ return self.get_interfaces()
1795
+
1796
+ @property
1797
+ def vminterfaces(self):
1798
+ return self.get_vminterfaces()
1799
+
1677
1800
  def clean(self):
1678
1801
  super().clean()
1679
1802