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
@@ -284,6 +284,393 @@ class IPAddressToInterfaceTest(TestCase):
284
284
  self.assertEqual(remaining_assignments.count(), 1)
285
285
  self.assertIn(assignment_nested_module, remaining_assignments)
286
286
 
287
+ def test_ip_address_to_interface_delete_signal_no_save_when_device_has_no_primary_ips(self):
288
+ """
289
+ Test that Device is not saved when removing an IP from an interface and the device
290
+ has no primary IPs assigned.
291
+ """
292
+ # Create an IP address
293
+ ip_addr = IPAddress.objects.create(address="192.0.2.1/24", status=self.status, namespace=self.namespace)
294
+
295
+ # Assign IP to interface
296
+ assignment_device_int1 = IPAddressToInterface.objects.create(interface=self.test_int1, ip_address=ip_addr)
297
+
298
+ # Mock the device save method to verify it's not called
299
+ with patch.object(self.test_device, "save") as mock_save:
300
+ # Remove the IP assignment from interface - this should NOT trigger a device save
301
+ assignment_device_int1.delete()
302
+
303
+ # Assert save was not called since no primary IPs were affected
304
+ mock_save.assert_not_called()
305
+
306
+ def test_ip_address_to_interface_delete_signal_no_save_when_removing_non_primary_ip_from_device(self):
307
+ """
308
+ Test that Device is not saved when removing an IP from an interface and the IP
309
+ is not the device's primary IP.
310
+ """
311
+ # Test removing non-primary IPv4 assignment
312
+ with self.subTest("IPv4 non-primary IP removal"):
313
+ # Create IPv4 addresses
314
+ primary_ipv4_addr = IPAddress.objects.create(
315
+ address="192.0.2.1/24", status=self.status, namespace=self.namespace
316
+ )
317
+ other_ipv4_addr = IPAddress.objects.create(
318
+ address="192.0.2.2/24", status=self.status, namespace=self.namespace
319
+ )
320
+
321
+ # Assign primary IP to interface first (required before setting as primary)
322
+ IPAddressToInterface.objects.create(interface=self.test_int2, ip_address=primary_ipv4_addr)
323
+
324
+ # Set it as the device's primary IP
325
+ self.test_device.primary_ip4 = primary_ipv4_addr
326
+ self.test_device.save()
327
+
328
+ # Assign the other IP to a different interface
329
+ assignment_other_ipv4 = IPAddressToInterface.objects.create(
330
+ interface=self.test_int1, ip_address=other_ipv4_addr
331
+ )
332
+
333
+ with patch.object(self.test_device, "save") as mock_save:
334
+ assignment_other_ipv4.delete()
335
+ mock_save.assert_not_called()
336
+
337
+ # Test removing non-primary IPv6 assignment
338
+ with self.subTest("IPv6 non-primary IP removal"):
339
+ # Create IPv6 prefix first
340
+ Prefix.objects.create(prefix="2001:db8::/64", status=self.status, namespace=self.namespace)
341
+
342
+ # Create IPv6 addresses
343
+ primary_ipv6_addr = IPAddress.objects.create(
344
+ address="2001:db8::1/64", status=self.status, namespace=self.namespace
345
+ )
346
+ other_ipv6_addr = IPAddress.objects.create(
347
+ address="2001:db8::2/64", status=self.status, namespace=self.namespace
348
+ )
349
+
350
+ # Assign primary IP to interface first (required before setting as primary)
351
+ IPAddressToInterface.objects.create(interface=self.test_int2, ip_address=primary_ipv6_addr)
352
+
353
+ # Set it as the device's primary IP
354
+ self.test_device.primary_ip6 = primary_ipv6_addr
355
+ self.test_device.save()
356
+
357
+ # Assign the other IP to a different interface
358
+ assignment_other_ipv6 = IPAddressToInterface.objects.create(
359
+ interface=self.test_int1, ip_address=other_ipv6_addr
360
+ )
361
+
362
+ with patch.object(self.test_device, "save") as mock_save:
363
+ assignment_other_ipv6.delete()
364
+ mock_save.assert_not_called()
365
+
366
+ def test_ip_address_to_interface_delete_signal_save_when_removing_primary_ip_from_device(self):
367
+ """
368
+ Test that Device is saved when removing an IP from an interface and the IP is the device's
369
+ primary IP and no other assignments exist.
370
+ """
371
+ # Test removing primary IPv4 assignment
372
+ with self.subTest("IPv4 primary IP removal"):
373
+ # Create primary IPv4 address
374
+ primary_ipv4_addr = IPAddress.objects.create(
375
+ address="192.0.2.1/24", status=self.status, namespace=self.namespace
376
+ )
377
+
378
+ # Assign primary IPv4 to an interface first
379
+ assignment_primary_ipv4 = IPAddressToInterface.objects.create(
380
+ interface=self.test_int1, ip_address=primary_ipv4_addr
381
+ )
382
+
383
+ # Set it as the device's primary IP
384
+ self.test_device.primary_ip4 = primary_ipv4_addr
385
+ self.test_device.save()
386
+
387
+ # Mock the device save method to verify it's called
388
+ with patch.object(self.test_device, "save") as mock_save:
389
+ # Remove the primary IP assignment - this SHOULD trigger a device save
390
+ assignment_primary_ipv4.delete()
391
+
392
+ # Assert save was called to nullify primary_ip4
393
+ mock_save.assert_called_once()
394
+
395
+ # Test removing primary IPv6 assignment
396
+ with self.subTest("IPv6 primary IP removal"):
397
+ # Create IPv6 prefix first
398
+ Prefix.objects.create(prefix="2001:db8::/64", status=self.status, namespace=self.namespace)
399
+
400
+ # Create primary IPv6 address
401
+ primary_ipv6_addr = IPAddress.objects.create(
402
+ address="2001:db8::1/64", status=self.status, namespace=self.namespace
403
+ )
404
+
405
+ # Assign primary IPv6 to an interface first
406
+ assignment_primary_ipv6 = IPAddressToInterface.objects.create(
407
+ interface=self.test_int1, ip_address=primary_ipv6_addr
408
+ )
409
+
410
+ # Set it as the device's primary IP
411
+ self.test_device.primary_ip6 = primary_ipv6_addr
412
+ self.test_device.save()
413
+
414
+ # Mock the device save method to verify it's called
415
+ with patch.object(self.test_device, "save") as mock_save:
416
+ # Remove the primary IP assignment - this SHOULD trigger a device save
417
+ assignment_primary_ipv6.delete()
418
+
419
+ # Assert save was called to nullify primary_ip6
420
+ mock_save.assert_called_once()
421
+
422
+ def test_ip_address_to_interface_delete_signal_no_save_when_device_primary_ip_has_multiple_assignments(self):
423
+ """
424
+ Test that Device is not saved when removing an IP from an interface and the IP is the device's
425
+ primary IP but is assigned to other interfaces on that device.
426
+ """
427
+ # Test IPv4 primary IP with multiple assignments
428
+ with self.subTest("IPv4 primary IP with multiple assignments"):
429
+ # Create primary IPv4 address
430
+ primary_ipv4_addr = IPAddress.objects.create(
431
+ address="192.0.2.1/24", status=self.status, namespace=self.namespace
432
+ )
433
+ # Assign primary IPv4 to multiple interfaces
434
+ assignment_ipv4_int1 = IPAddressToInterface.objects.create(
435
+ interface=self.test_int1, ip_address=primary_ipv4_addr
436
+ )
437
+ IPAddressToInterface.objects.create(interface=self.test_int2, ip_address=primary_ipv4_addr)
438
+
439
+ # Set it as the device's primary IP
440
+ self.test_device.primary_ip4 = primary_ipv4_addr
441
+ self.test_device.save()
442
+
443
+ # Mock the device save method to verify it's not called
444
+ with patch.object(self.test_device, "save") as mock_save:
445
+ # Remove primary IP from one interface - should NOT trigger save since other assignment exists
446
+ assignment_ipv4_int1.delete()
447
+
448
+ # Assert save was not called since IP is still assigned to test_int2
449
+ mock_save.assert_not_called()
450
+
451
+ # Test IPv6 primary IP with multiple assignments
452
+ with self.subTest("IPv6 primary IP with multiple assignments"):
453
+ # Create IPv6 prefix first
454
+ Prefix.objects.create(prefix="2001:db8::/64", status=self.status, namespace=self.namespace)
455
+
456
+ # Create primary IPv6 address
457
+ primary_ipv6_addr = IPAddress.objects.create(
458
+ address="2001:db8::1/64", status=self.status, namespace=self.namespace
459
+ )
460
+
461
+ # Assign primary IPv6 to multiple interfaces
462
+ assignment_ipv6_int1 = IPAddressToInterface.objects.create(
463
+ interface=self.test_int1, ip_address=primary_ipv6_addr
464
+ )
465
+ IPAddressToInterface.objects.create(interface=self.test_int2, ip_address=primary_ipv6_addr)
466
+
467
+ # Set it as the device's primary IP
468
+ self.test_device.primary_ip6 = primary_ipv6_addr
469
+ self.test_device.save()
470
+
471
+ # Mock the device save method to verify it's not called
472
+ with patch.object(self.test_device, "save") as mock_save:
473
+ # Remove primary IP from one interface - should NOT trigger save since other assignment exists
474
+ assignment_ipv6_int1.delete()
475
+
476
+ # Assert save was not called since IP is still assigned to test_int2
477
+ mock_save.assert_not_called()
478
+
479
+ def test_ip_address_to_interface_delete_signal_no_save_when_vm_has_no_primary_ips(self):
480
+ """
481
+ Test that VM is not saved when removing an IP from a VM interface and the VM
482
+ has no primary IPs assigned.
483
+ """
484
+ # Create an IP address
485
+ ip_addr = IPAddress.objects.create(address="192.0.2.1/24", status=self.status, namespace=self.namespace)
486
+
487
+ # VM has no primary IPs by default (both are None)
488
+
489
+ # Assign IP to VM interface
490
+ assignment_vm_int1 = IPAddressToInterface.objects.create(vm_interface=self.test_vmint1, ip_address=ip_addr)
491
+
492
+ # Mock the VM save method to verify it's not called
493
+ with patch.object(self.test_vm, "save") as mock_save:
494
+ # Remove the IP assignment from VM interface - this should NOT trigger a VM save
495
+ assignment_vm_int1.delete()
496
+
497
+ # Assert save was not called since no primary IPs were affected
498
+ mock_save.assert_not_called()
499
+
500
+ def test_ip_address_to_interface_delete_signal_no_save_when_removing_non_primary_ip_from_vm(self):
501
+ """
502
+ Test that VM is not saved when removing an IP from a VM interface and the IP
503
+ is not the VM's primary IP.
504
+ """
505
+ # Test removing non-primary IPv4 assignment
506
+ with self.subTest("IPv4 non-primary IP removal"):
507
+ # Create IPv4 addresses
508
+ primary_ipv4_addr = IPAddress.objects.create(
509
+ address="192.0.2.1/24", status=self.status, namespace=self.namespace
510
+ )
511
+ other_ipv4_addr = IPAddress.objects.create(
512
+ address="192.0.2.2/24", status=self.status, namespace=self.namespace
513
+ )
514
+
515
+ # Assign primary IP to VM interface first (required before setting as primary)
516
+ IPAddressToInterface.objects.create(vm_interface=self.test_vmint2, ip_address=primary_ipv4_addr)
517
+
518
+ # Set it as the VM's primary IP
519
+ self.test_vm.primary_ip4 = primary_ipv4_addr
520
+ self.test_vm.save()
521
+
522
+ # Assign the other IP to a different VM interface
523
+ assignment_vm_int1 = IPAddressToInterface.objects.create(
524
+ vm_interface=self.test_vmint1, ip_address=other_ipv4_addr
525
+ )
526
+
527
+ with patch.object(self.test_vm, "save") as mock_save:
528
+ assignment_vm_int1.delete()
529
+ mock_save.assert_not_called()
530
+
531
+ # Test removing non-primary IPv6 assignment
532
+ with self.subTest("IPv6 non-primary IP removal"):
533
+ # Create IPv6 prefix first
534
+ Prefix.objects.create(prefix="2001:db8::/64", status=self.status, namespace=self.namespace)
535
+
536
+ # Create IPv6 addresses
537
+ primary_ipv6_addr = IPAddress.objects.create(
538
+ address="2001:db8::1/64", status=self.status, namespace=self.namespace
539
+ )
540
+ other_ipv6_addr = IPAddress.objects.create(
541
+ address="2001:db8::2/64", status=self.status, namespace=self.namespace
542
+ )
543
+
544
+ # Assign primary IP to VM interface first (required before setting as primary)
545
+ IPAddressToInterface.objects.create(vm_interface=self.test_vmint2, ip_address=primary_ipv6_addr)
546
+
547
+ # Set it as the VM's primary IP
548
+ self.test_vm.primary_ip6 = primary_ipv6_addr
549
+ self.test_vm.save()
550
+
551
+ # Assign the other IP to a different VM interface
552
+ assignment_vm_int1 = IPAddressToInterface.objects.create(
553
+ vm_interface=self.test_vmint1, ip_address=other_ipv6_addr
554
+ )
555
+
556
+ with patch.object(self.test_vm, "save") as mock_save:
557
+ assignment_vm_int1.delete()
558
+ mock_save.assert_not_called()
559
+
560
+ def test_ip_address_to_interface_delete_signal_save_when_removing_primary_ip_from_vm(self):
561
+ """
562
+ Test that VM is saved when removing an IP from a VM interface and the IP is the VM's
563
+ primary IP and no other assignments exist.
564
+ """
565
+ # Test removing primary IPv4 assignment
566
+ with self.subTest("IPv4 primary IP removal"):
567
+ # Create primary IPv4 address
568
+ primary_ipv4_addr = IPAddress.objects.create(
569
+ address="192.0.2.1/24", status=self.status, namespace=self.namespace
570
+ )
571
+
572
+ # Assign primary IPv4 to VM interface first
573
+ assignment_primary_ipv4 = IPAddressToInterface.objects.create(
574
+ vm_interface=self.test_vmint1, ip_address=primary_ipv4_addr
575
+ )
576
+
577
+ # Set it as the VM's primary IP
578
+ self.test_vm.primary_ip4 = primary_ipv4_addr
579
+ self.test_vm.save()
580
+
581
+ # Mock the VM save method to verify it's called
582
+ with patch.object(self.test_vm, "save") as mock_save:
583
+ # Remove the primary IP assignment - this SHOULD trigger a VM save
584
+ assignment_primary_ipv4.delete()
585
+
586
+ # Assert save was called to nullify primary_ip4
587
+ mock_save.assert_called_once()
588
+
589
+ # Test removing primary IPv6 assignment
590
+ with self.subTest("IPv6 primary IP removal"):
591
+ # Create IPv6 prefix first
592
+ Prefix.objects.create(prefix="2001:db8::/64", status=self.status, namespace=self.namespace)
593
+
594
+ # Create primary IPv6 address
595
+ primary_ipv6_addr = IPAddress.objects.create(
596
+ address="2001:db8::1/64", status=self.status, namespace=self.namespace
597
+ )
598
+
599
+ # Assign primary IPv6 to VM interface first
600
+ assignment_primary_ipv6 = IPAddressToInterface.objects.create(
601
+ vm_interface=self.test_vmint1, ip_address=primary_ipv6_addr
602
+ )
603
+
604
+ # Set it as the VM's primary IP
605
+ self.test_vm.primary_ip6 = primary_ipv6_addr
606
+ self.test_vm.save()
607
+
608
+ # Mock the VM save method to verify it's called
609
+ with patch.object(self.test_vm, "save") as mock_save:
610
+ # Remove the primary IP assignment - this SHOULD trigger a VM save
611
+ assignment_primary_ipv6.delete()
612
+
613
+ # Assert save was called to nullify primary_ip6
614
+ mock_save.assert_called_once()
615
+
616
+ def test_ip_address_to_interface_delete_signal_no_save_when_vm_primary_ip_has_multiple_assignments(self):
617
+ """
618
+ Test that VM is not saved when removing an IP from a VM interface and the IP is the VM's
619
+ primary IP but is assigned to other VM interfaces on that VM.
620
+ """
621
+ # Test IPv4 primary IP with multiple assignments
622
+ with self.subTest("IPv4 primary IP with multiple assignments"):
623
+ # Create primary IPv4 address
624
+ primary_ipv4_addr = IPAddress.objects.create(
625
+ address="192.0.2.1/24", status=self.status, namespace=self.namespace
626
+ )
627
+
628
+ # Assign primary IPv4 to multiple VM interfaces
629
+ assignment_ipv4_vmint1 = IPAddressToInterface.objects.create(
630
+ vm_interface=self.test_vmint1, ip_address=primary_ipv4_addr
631
+ )
632
+ IPAddressToInterface.objects.create(vm_interface=self.test_vmint2, ip_address=primary_ipv4_addr)
633
+
634
+ # Set it as the VM's primary IP
635
+ self.test_vm.primary_ip4 = primary_ipv4_addr
636
+ self.test_vm.save()
637
+
638
+ # Mock the VM save method to verify it's not called
639
+ with patch.object(self.test_vm, "save") as mock_save:
640
+ # Remove primary IP from one VM interface - should NOT trigger save since other assignment exists
641
+ assignment_ipv4_vmint1.delete()
642
+
643
+ # Assert save was not called since IP is still assigned to test_vmint2
644
+ mock_save.assert_not_called()
645
+
646
+ # Test IPv6 primary IP with multiple assignments
647
+ with self.subTest("IPv6 primary IP with multiple assignments"):
648
+ # Create IPv6 prefix first
649
+ Prefix.objects.create(prefix="2001:db8::/64", status=self.status, namespace=self.namespace)
650
+
651
+ # Create primary IPv6 address
652
+ primary_ipv6_addr = IPAddress.objects.create(
653
+ address="2001:db8::1/64", status=self.status, namespace=self.namespace
654
+ )
655
+
656
+ # Assign primary IPv6 to multiple VM interfaces
657
+ assignment_ipv6_vmint1 = IPAddressToInterface.objects.create(
658
+ vm_interface=self.test_vmint1, ip_address=primary_ipv6_addr
659
+ )
660
+ IPAddressToInterface.objects.create(vm_interface=self.test_vmint2, ip_address=primary_ipv6_addr)
661
+
662
+ # Set it as the VM's primary IP
663
+ self.test_vm.primary_ip6 = primary_ipv6_addr
664
+ self.test_vm.save()
665
+
666
+ # Mock the VM save method to verify it's not called
667
+ with patch.object(self.test_vm, "save") as mock_save:
668
+ # Remove primary IP from one VM interface - should NOT trigger save since other assignment exists
669
+ assignment_ipv6_vmint1.delete()
670
+
671
+ # Assert save was not called since IP is still assigned to test_vmint2
672
+ mock_save.assert_not_called()
673
+
287
674
 
288
675
  class TestVarbinaryIPField(TestCase):
289
676
  """Tests for `nautobot.ipam.fields.VarbinaryIPField`."""
@@ -406,17 +793,20 @@ class TestPrefix(ModelTestCases.BaseModelTestCase):
406
793
  self.status = self.statuses.first()
407
794
  self.status.content_types.add(ContentType.objects.get_for_model(IPAddress))
408
795
  self.root = Prefix.objects.create(
409
- prefix="101.102.0.0/24", status=self.status, namespace=self.namespace, type=PrefixTypeChoices.TYPE_CONTAINER
796
+ prefix="101.102.0.0/16", status=self.status, namespace=self.namespace, type=PrefixTypeChoices.TYPE_CONTAINER
410
797
  )
411
798
  self.parent = Prefix.objects.create(
412
- prefix="101.102.0.0/25", status=self.status, namespace=self.namespace, type=PrefixTypeChoices.TYPE_CONTAINER
799
+ prefix="101.102.103.0/24",
800
+ status=self.status,
801
+ namespace=self.namespace,
802
+ type=PrefixTypeChoices.TYPE_CONTAINER,
413
803
  )
414
- self.child1 = Prefix.objects.create(prefix="101.102.0.0/26", status=self.status, namespace=self.namespace)
415
- self.child2 = Prefix.objects.create(prefix="101.102.0.64/26", status=self.status, namespace=self.namespace)
804
+ self.child1 = Prefix.objects.create(prefix="101.102.103.0/26", status=self.status, namespace=self.namespace)
805
+ self.child2 = Prefix.objects.create(prefix="101.102.103.104/32", status=self.status, namespace=self.namespace)
416
806
 
417
807
  def test_parent_exists_after_model_clean(self):
418
808
  prefix = Prefix(
419
- prefix="101.102.0.128/26",
809
+ prefix="101.102.1.0/24",
420
810
  status=self.status,
421
811
  namespace=self.namespace,
422
812
  type=PrefixTypeChoices.TYPE_CONTAINER,
@@ -626,18 +1016,18 @@ class TestPrefix(ModelTestCases.BaseModelTestCase):
626
1016
  # siblings()
627
1017
  self.assertEqual(list(self.child1.siblings()), [self.child2])
628
1018
  self.assertEqual(list(self.child1.siblings(include_self=True)), [self.child1, self.child2])
629
- parent2 = Prefix.objects.create(prefix="101.102.0.128/25", status=self.status, namespace=self.namespace)
1019
+ parent2 = Prefix.objects.create(prefix="101.102.128.0/24", status=self.status, namespace=self.namespace)
630
1020
  self.assertEqual(list(self.parent.siblings()), [parent2])
631
1021
  self.assertEqual(list(self.parent.siblings(include_self=True)), [self.parent, parent2])
632
1022
 
633
- def test_reparenting(self):
1023
+ def test_reparenting_on_create_and_delete(self):
634
1024
  """Test that reparenting algorithm works in its most basic form."""
635
1025
  # tree hierarchy
636
1026
  self.assertIsNone(self.root.parent)
637
1027
  self.assertEqual(self.parent.parent, self.root)
638
1028
  self.assertEqual(self.child1.parent, self.parent)
639
1029
 
640
- # Delete the parent (/25); child1/child2 now have root (/24) as their parent.
1030
+ # Delete the parent (/24); child1/child2 now have root (/16) as their parent.
641
1031
  num_deleted, _ = self.parent.delete()
642
1032
  self.assertEqual(num_deleted, 1)
643
1033
 
@@ -648,9 +1038,14 @@ class TestPrefix(ModelTestCases.BaseModelTestCase):
648
1038
  self.assertEqual(self.child2.parent, self.root)
649
1039
  self.assertEqual(list(self.child1.ancestors()), [self.root])
650
1040
 
651
- # Add /25 back in as a parent and assert that child1/child2 now have it as their parent, and
652
- # /24 is its parent.
653
- self.parent.save() # This creates another Prefix using the same instance.
1041
+ # Add /24 back in as a parent and assert that child1/child2 now have it as their parent, and
1042
+ # /16 is its parent.
1043
+ self.parent = Prefix.objects.create(
1044
+ prefix="101.102.103.0/24",
1045
+ status=self.status,
1046
+ namespace=self.namespace,
1047
+ type=PrefixTypeChoices.TYPE_CONTAINER,
1048
+ )
654
1049
  self.child1.refresh_from_db()
655
1050
  self.child2.refresh_from_db()
656
1051
  self.assertEqual(self.child1.parent, self.parent)
@@ -687,13 +1082,266 @@ class TestPrefix(ModelTestCases.BaseModelTestCase):
687
1082
 
688
1083
  # Add /25 back in as a parent and assert that child1/child2 now have it as their parent, and
689
1084
  # /24 is its parent.
690
- parent.save() # This creates another Prefix using the same instance.
1085
+ parent = Prefix.objects.create(
1086
+ prefix="101.102.0.0/25", status=self.status, namespace=namespace, type=PrefixTypeChoices.TYPE_CONTAINER
1087
+ )
691
1088
  child1.refresh_from_db()
692
1089
  child2.refresh_from_db()
693
1090
  self.assertEqual(child1.parent, parent)
694
1091
  self.assertEqual(child2.parent, parent)
695
1092
  self.assertEqual(list(child1.ancestors()), [root, parent])
696
1093
 
1094
+ def test_reparenting_on_field_updates(self):
1095
+ """Test that reparenting occurs when network, prefix_length, etc. are updated."""
1096
+ self.assertIsNone(self.root.parent)
1097
+ self.assertEqual(self.parent.parent, self.root)
1098
+ self.assertEqual(self.child1.parent, self.parent)
1099
+ self.assertEqual(self.child2.parent, self.parent)
1100
+
1101
+ ip1 = IPAddress.objects.create(address="101.102.103.127/32", status=self.status, namespace=self.namespace)
1102
+ ip2 = IPAddress.objects.create(address="101.102.103.128/32", status=self.status, namespace=self.namespace)
1103
+ self.assertEqual(ip1.parent, self.parent)
1104
+ self.assertEqual(ip2.parent, self.parent)
1105
+
1106
+ with self.subTest("Decrease prefix_length, gaining children"):
1107
+ self.child1.prefix_length = 25
1108
+ self.child1.save()
1109
+ self.child1.refresh_from_db()
1110
+ self.child2.refresh_from_db()
1111
+ self.assertEqual(self.child2.parent, self.child1)
1112
+ ip1.refresh_from_db()
1113
+ self.assertEqual(ip1.parent, self.child1)
1114
+
1115
+ with self.subTest("Increase prefix_length, losing children"):
1116
+ self.child1.prefix_length = 26
1117
+ self.child1.save()
1118
+ self.child1.refresh_from_db()
1119
+ self.child2.refresh_from_db()
1120
+ self.assertEqual(self.child2.parent, self.parent)
1121
+ ip1.refresh_from_db()
1122
+ self.assertEqual(ip1.parent, self.parent)
1123
+
1124
+ with self.subTest("Broaden prefix, becoming parent of former parent"):
1125
+ self.parent.prefix = "101.0.0.0/8"
1126
+ self.parent.save()
1127
+ self.assertIsNone(self.parent.parent)
1128
+ # Former root is now a child of parent
1129
+ self.root.refresh_from_db()
1130
+ self.assertEqual(self.root.parent, self.parent)
1131
+ # Former children are now children of former root
1132
+ self.child1.refresh_from_db()
1133
+ self.assertEqual(self.child1.parent, self.root)
1134
+ self.child2.refresh_from_db()
1135
+ self.assertEqual(self.child2.parent, self.root)
1136
+ ip1.refresh_from_db()
1137
+ self.assertEqual(ip1.parent, self.root)
1138
+ ip2.refresh_from_db()
1139
+ self.assertEqual(ip2.parent, self.root)
1140
+
1141
+ with self.subTest("Narrow prefix, becoming child of former child"):
1142
+ self.parent.prefix = "101.102.103.0/24"
1143
+ self.parent.save()
1144
+ self.assertEqual(self.parent.parent, self.root)
1145
+ # Former root is now again root
1146
+ self.root.refresh_from_db()
1147
+ self.assertIsNone(self.root.parent)
1148
+ # Former children are again children of parent
1149
+ self.child1.refresh_from_db()
1150
+ self.assertEqual(self.child1.parent, self.parent)
1151
+ self.child2.refresh_from_db()
1152
+ self.assertEqual(self.child2.parent, self.parent)
1153
+ ip1.refresh_from_db()
1154
+ self.assertEqual(ip1.parent, self.parent)
1155
+ ip2.refresh_from_db()
1156
+ self.assertEqual(ip2.parent, self.parent)
1157
+
1158
+ with self.subTest("Change former root on multiple dimensions"):
1159
+ self.root.network = "101.102.103.0"
1160
+ self.root.prefix_length = 25
1161
+ self.root.save()
1162
+ self.assertEqual(self.root.parent, self.parent)
1163
+ self.parent.refresh_from_db()
1164
+ self.assertEqual(self.parent.parent, None)
1165
+ self.child1.refresh_from_db()
1166
+ self.assertEqual(self.child1.parent, self.root)
1167
+ self.child2.refresh_from_db()
1168
+ self.assertEqual(self.child2.parent, self.root)
1169
+ ip1.refresh_from_db()
1170
+ self.assertEqual(ip1.parent, self.root)
1171
+ ip2.refresh_from_db()
1172
+ self.assertEqual(ip2.parent, self.parent)
1173
+
1174
+ with self.subTest("Reclaim root position"):
1175
+ self.root.network = "101.0.0.0"
1176
+ self.root.prefix_length = 8
1177
+ self.root.save()
1178
+ self.assertIsNone(self.root.parent)
1179
+ self.parent.refresh_from_db()
1180
+ self.assertEqual(self.parent.parent, self.root)
1181
+ self.child1.refresh_from_db()
1182
+ self.assertEqual(self.child1.parent, self.parent)
1183
+ self.child2.refresh_from_db()
1184
+ self.assertEqual(self.child2.parent, self.parent)
1185
+ ip1.refresh_from_db()
1186
+ self.assertEqual(ip1.parent, self.parent)
1187
+ ip2.refresh_from_db()
1188
+ self.assertEqual(ip2.parent, self.parent)
1189
+
1190
+ def test_clean_fails_if_would_orphan_ips(self):
1191
+ """Test that clean() fails if reparenting would orphan IPs."""
1192
+ self.ip = IPAddress.objects.create(address="101.102.1.1/32", status=self.status, namespace=self.namespace)
1193
+ self.assertEqual(self.ip.parent, self.root)
1194
+ with self.assertRaises(ValidationError) as cm:
1195
+ self.root.prefix = "102.103.0.0/16"
1196
+ self.root.clean()
1197
+ self.assertIn(
1198
+ f"1 existing IP addresses (including {self.ip.host}) would no longer have a valid parent", str(cm.exception)
1199
+ )
1200
+ self.root.refresh_from_db()
1201
+ self.ip2 = IPAddress.objects.create(address="101.102.1.2/32", status=self.status, namespace=self.namespace)
1202
+ self.assertEqual(self.ip2.parent, self.root)
1203
+ with self.assertRaises(ValidationError) as cm:
1204
+ self.root.prefix = "102.103.0.0/16"
1205
+ self.root.clean()
1206
+ self.assertIn(
1207
+ f"2 existing IP addresses (including {self.ip.host}) would no longer have a valid parent",
1208
+ str(cm.exception),
1209
+ )
1210
+
1211
+ def test_clean_fails_if_namespace_changed_and_vrfs_involved(self):
1212
+ vrf = VRF.objects.create(name="VRF Red", namespace=self.namespace)
1213
+ vrf.add_prefix(self.root)
1214
+
1215
+ new_namespace = Namespace.objects.exclude(id=self.namespace.id).first()
1216
+
1217
+ self.root.namespace = new_namespace
1218
+ with self.assertRaises(ValidationError) as cm:
1219
+ self.root.clean()
1220
+ self.assertIn("Cannot move to a different Namespace while associated to VRFs", str(cm.exception))
1221
+
1222
+ vrf.remove_prefix(self.root)
1223
+ self.root.clean()
1224
+
1225
+ vrf.add_prefix(self.parent)
1226
+ with self.assertRaises(ValidationError) as cm:
1227
+ self.root.clean()
1228
+ self.assertIn(
1229
+ "Cannot move to a different Namespace with descendant Prefixes associated to VRFs", str(cm.exception)
1230
+ )
1231
+
1232
+ def test_namespace_change_success_updates_descendants_and_claims_new_children(self):
1233
+ new_namespace = Namespace.objects.exclude(id=self.namespace.id).first()
1234
+ new_catchall = Prefix.objects.create(prefix="0.0.0.0/0", status=self.status, namespace=new_namespace)
1235
+ new_parent = Prefix.objects.create(prefix="101.102.200.0/24", status=self.status, namespace=new_namespace)
1236
+ new_child = Prefix.objects.create(prefix="101.102.103.64/26", status=self.status, namespace=new_namespace)
1237
+ new_grandchild = Prefix.objects.create(prefix="101.102.103.0/27", status=self.status, namespace=new_namespace)
1238
+ new_ip = IPAddress.objects.create(address="101.102.150.200/32", status=self.status, namespace=new_namespace)
1239
+
1240
+ # Before:
1241
+ # self.namespace
1242
+ # self.root 101.102.0.0/16
1243
+ # self.parent 101.102.103.0/24
1244
+ # self.child1 101.102.103.0/26
1245
+ # self.child2 101.102.103.104/32
1246
+ # new_namespace
1247
+ # new_catchall 0.0.0.0/0
1248
+ # new_grandchild 101.102.103.0/27
1249
+ # new_child 101.102.103.64/26
1250
+ # new_ip 101.102.150.200/32
1251
+ # new_parent 101.102.200.0/24
1252
+ #
1253
+ # After:
1254
+ # new_namespace
1255
+ # new_catchall 0.0.0.0/0
1256
+ # self.root 101.102.0.0/16
1257
+ # self.parent 101.102.103.0/24
1258
+ # self.child1 101.102.103.0/26
1259
+ # new_grandchild 101.102.103.0/27
1260
+ # new_child 101.102.103.64/26
1261
+ # self.child2 101.102.103.104/32
1262
+ # new_ip 101.102.150.200/32
1263
+ # new_parent 101.102.200.0/24
1264
+
1265
+ self.root.namespace = new_namespace
1266
+ self.root.save()
1267
+ self.assertEqual(self.root.namespace, new_namespace)
1268
+ self.assertEqual(self.root.parent, new_catchall) # automatically updated
1269
+ self.parent.refresh_from_db()
1270
+ self.assertEqual(self.parent.namespace, new_namespace) # automatically updated
1271
+ self.assertEqual(self.parent.parent, self.root) # unchanged
1272
+ self.child1.refresh_from_db()
1273
+ self.assertEqual(self.child1.namespace, new_namespace) # automatically updated
1274
+ self.assertEqual(self.child1.parent, self.parent) # unchanged
1275
+ self.child2.refresh_from_db()
1276
+ self.assertEqual(self.child2.namespace, new_namespace) # automatically updated
1277
+ self.assertEqual(self.child2.parent, new_child) # automatically updated
1278
+ new_parent.refresh_from_db()
1279
+ self.assertEqual(new_parent.namespace, new_namespace) # unchanged
1280
+ self.assertEqual(new_parent.parent, self.root) # automatically updated
1281
+ new_child.refresh_from_db()
1282
+ self.assertEqual(new_child.namespace, new_namespace) # unchanged
1283
+ self.assertEqual(new_child.parent, self.parent) # automatically updated
1284
+ new_grandchild.refresh_from_db()
1285
+ self.assertEqual(new_grandchild.namespace, new_namespace) # unchanged
1286
+ self.assertEqual(new_grandchild.parent, self.child1) # automatically updated
1287
+ new_ip.refresh_from_db()
1288
+ self.assertEqual(new_ip.parent, self.root)
1289
+
1290
+ def test_namespace_change_results_in_merge_collisions(self):
1291
+ new_namespace = Namespace.objects.exclude(id=self.namespace.id).first()
1292
+ new_root = Prefix.objects.create(prefix="101.102.0.0/16", status=self.status, namespace=new_namespace)
1293
+
1294
+ self.root.namespace = new_namespace
1295
+ with self.assertRaises(IntegrityError):
1296
+ self.root.save()
1297
+ self.root.refresh_from_db()
1298
+ self.assertEqual(self.root.namespace, self.namespace)
1299
+ self.parent.refresh_from_db()
1300
+ self.assertEqual(self.parent.namespace, self.namespace)
1301
+ self.child1.refresh_from_db()
1302
+ self.assertEqual(self.child1.namespace, self.namespace)
1303
+ self.child2.refresh_from_db()
1304
+ self.assertEqual(self.child2.namespace, self.namespace)
1305
+
1306
+ new_root.delete()
1307
+ new_parent = Prefix.objects.create(prefix="101.102.103.0/24", status=self.status, namespace=new_namespace)
1308
+
1309
+ self.root.namespace = new_namespace
1310
+ with self.assertRaises(IntegrityError):
1311
+ self.root.save()
1312
+ self.root.refresh_from_db()
1313
+ self.assertEqual(self.root.namespace, self.namespace)
1314
+ self.parent.refresh_from_db()
1315
+ self.assertEqual(self.parent.namespace, self.namespace)
1316
+ self.child1.refresh_from_db()
1317
+ self.assertEqual(self.child1.namespace, self.namespace)
1318
+ self.child2.refresh_from_db()
1319
+ self.assertEqual(self.child2.namespace, self.namespace)
1320
+
1321
+ new_parent.delete()
1322
+
1323
+ existing_ip = IPAddress.objects.create(address="101.102.103.1/32", status=self.status, namespace=self.namespace)
1324
+ new_prefix = Prefix.objects.create(prefix="0.0.0.0/0", status=self.status, namespace=new_namespace)
1325
+ new_ip = IPAddress.objects.create(address="101.102.103.1/32", status=self.status, namespace=new_namespace)
1326
+ self.assertEqual(new_ip.parent, new_prefix)
1327
+
1328
+ self.root.namespace = new_namespace
1329
+ with self.assertRaises(IntegrityError):
1330
+ self.root.save()
1331
+ self.root.refresh_from_db()
1332
+ self.assertIsNone(self.root.parent)
1333
+ self.assertEqual(self.root.namespace, self.namespace)
1334
+ self.parent.refresh_from_db()
1335
+ self.assertEqual(self.parent.namespace, self.namespace)
1336
+ self.child1.refresh_from_db()
1337
+ self.assertEqual(self.child1.namespace, self.namespace)
1338
+ self.child2.refresh_from_db()
1339
+ self.assertEqual(self.child2.namespace, self.namespace)
1340
+ existing_ip.refresh_from_db()
1341
+ self.assertEqual(existing_ip.parent, self.child1)
1342
+ new_ip.refresh_from_db()
1343
+ self.assertEqual(new_ip.parent, new_prefix)
1344
+
697
1345
  def test_descendants(self):
698
1346
  prefixes = (
699
1347
  Prefix.objects.create(
@@ -901,8 +1549,10 @@ class TestPrefix(ModelTestCases.BaseModelTestCase):
901
1549
  self.assertEqual(slash25.get_utilization(), (4, 128))
902
1550
 
903
1551
  # When the pool does not overlap with broadcast or network address, the denominator decrements by 2
904
- pool.network = "10.0.0.132"
905
- pool.save()
1552
+ pool.delete()
1553
+ pool = Prefix.objects.create(
1554
+ prefix="10.0.0.132/30", type=PrefixTypeChoices.TYPE_POOL, status=self.status, namespace=self.namespace
1555
+ )
906
1556
  self.assertEqual(slash25.get_utilization(), (4, 126))
907
1557
 
908
1558
  # Further distinguishing between get_child_ips() and get_all_ips():
@@ -1026,86 +1676,10 @@ class TestPrefix(ModelTestCases.BaseModelTestCase):
1026
1676
  Prefix.objects.create(
1027
1677
  prefix="11.0.0.0/24", status=self.status, namespace=namespace, type=PrefixTypeChoices.TYPE_NETWORK
1028
1678
  )
1029
- # 3.0 TODO: replace with the commented below once type enforcement is enabled
1030
- # pool_prefix = Prefix.objects.create(
1031
1679
  Prefix.objects.create(
1032
1680
  prefix="12.0.0.0/24", status=self.status, namespace=namespace, type=PrefixTypeChoices.TYPE_POOL
1033
1681
  )
1034
1682
 
1035
- # 3.0 TODO: uncomment the below tests once type enforcement is enabled
1036
-
1037
- # with self.assertRaises(ValidationError, msg="Network prefix parent cannot be a network"):
1038
- # Prefix.objects.create(
1039
- # prefix="11.0.0.0/30", status=self.status, namespace=namespace, type=PrefixTypeChoices.TYPE_NETWORK
1040
- # )
1041
-
1042
- # with self.assertRaises(ValidationError, msg="Network prefix parent cannot be a pool"):
1043
- # Prefix.objects.create(
1044
- # prefix="12.0.0.0/30", status=self.status, namespace=namespace, type=PrefixTypeChoices.TYPE_NETWORK
1045
- # )
1046
-
1047
- # with self.assertRaises(ValidationError, msg="Container prefix parent cannot be a network"):
1048
- # Prefix.objects.create(
1049
- # prefix="11.0.0.0/30", status=self.status, namespace=namespace, type=PrefixTypeChoices.TYPE_CONTAINER
1050
- # )
1051
-
1052
- # with self.assertRaises(ValidationError, msg="Container prefix parent cannot be a pool"):
1053
- # Prefix.objects.create(
1054
- # prefix="12.0.0.0/30", status=self.status, namespace=namespace, type=PrefixTypeChoices.TYPE_CONTAINER
1055
- # )
1056
-
1057
- # with self.assertRaises(ValidationError, msg="Pool prefix parent cannot be a container"):
1058
- # Prefix.objects.create(
1059
- # prefix="10.0.0.0/30", status=self.status, namespace=namespace, type=PrefixTypeChoices.TYPE_POOL
1060
- # )
1061
-
1062
- # with self.assertRaises(ValidationError, msg="Pool prefix parent cannot be a pool"):
1063
- # Prefix.objects.create(
1064
- # prefix="12.0.0.0/30", status=self.status, namespace=namespace, type=PrefixTypeChoices.TYPE_POOL
1065
- # )
1066
-
1067
- # with self.assertRaises(
1068
- # ValidationError, msg="Test that an invalid parent cannot be created (network parenting container)"
1069
- # ):
1070
- # Prefix.objects.create(
1071
- # prefix="10.0.0.0/16", status=self.status, namespace=namespace, type=PrefixTypeChoices.TYPE_NETWORK
1072
- # )
1073
-
1074
- # with self.assertRaises(
1075
- # ValidationError, msg="Test that an invalid parent cannot be created (pool parenting container)"
1076
- # ):
1077
- # Prefix.objects.create(
1078
- # prefix="10.0.0.0/16", status=self.status, namespace=namespace, type=PrefixTypeChoices.TYPE_POOL
1079
- # )
1080
-
1081
- # with self.assertRaises(
1082
- # ValidationError, msg="Test that an invalid parent cannot be created (network parenting network)"
1083
- # ):
1084
- # Prefix.objects.create(
1085
- # prefix="11.0.0.0/16", status=self.status, namespace=namespace, type=PrefixTypeChoices.TYPE_NETWORK
1086
- # )
1087
-
1088
- # with self.assertRaises(
1089
- # ValidationError, msg="Test that an invalid parent cannot be created (pool parenting network)"
1090
- # ):
1091
- # Prefix.objects.create(
1092
- # prefix="11.0.0.0/16", status=self.status, namespace=namespace, type=PrefixTypeChoices.TYPE_POOL
1093
- # )
1094
-
1095
- # with self.assertRaises(
1096
- # ValidationError, msg="Test that an invalid parent cannot be created (container parenting pool)"
1097
- # ):
1098
- # Prefix.objects.create(
1099
- # prefix="12.0.0.0/16", status=self.status, namespace=namespace, type=PrefixTypeChoices.TYPE_CONTAINER
1100
- # )
1101
-
1102
- # with self.assertRaises(
1103
- # ValidationError, msg="Test that an invalid parent cannot be created (pool parenting pool)"
1104
- # ):
1105
- # Prefix.objects.create(
1106
- # prefix="12.0.0.0/16", status=self.status, namespace=namespace, type=PrefixTypeChoices.TYPE_POOL
1107
- # )
1108
-
1109
1683
  with self.subTest("Test that valid parents can be created"):
1110
1684
  Prefix.objects.create(
1111
1685
  prefix="12.0.0.0/16", status=self.status, namespace=namespace, type=PrefixTypeChoices.TYPE_NETWORK
@@ -1125,14 +1699,6 @@ class TestPrefix(ModelTestCases.BaseModelTestCase):
1125
1699
  prefix="10.0.0.0/26", status=self.status, namespace=namespace, type=PrefixTypeChoices.TYPE_CONTAINER
1126
1700
  )
1127
1701
 
1128
- # 3.0 TODO: uncomment once type enforcement is enabled
1129
- # with self.assertRaises(
1130
- # ValidationError,
1131
- # msg="Test that modifying a prefix's type fails if it would result in an invalid parent/child relationship",
1132
- # ):
1133
- # pool_prefix.type = PrefixTypeChoices.TYPE_NETWORK
1134
- # pool_prefix.validated_save()
1135
-
1136
1702
  with self.subTest(
1137
1703
  "Test that modifying a prefix's type is allowed if it does not create an invalid relationship"
1138
1704
  ):
@@ -1159,13 +1725,6 @@ class TestPrefix(ModelTestCases.BaseModelTestCase):
1159
1725
  prefix="10.0.0.0/26", status=self.status, namespace=namespace, type=PrefixTypeChoices.TYPE_POOL
1160
1726
  )
1161
1727
 
1162
- # 3.0 TODO: uncomment once type enforcement is enabled
1163
- # with self.assertRaises(
1164
- # ProtectedError,
1165
- # msg="Test that deleting a network prefix that would make a pool prefix's parent a container raises a ProtectedError",
1166
- # ):
1167
- # network.delete()
1168
-
1169
1728
  with self.subTest("Test that deleting a parent prefix properly reparents the child prefixes"):
1170
1729
  container.delete()
1171
1730
  root.refresh_from_db()
@@ -1178,18 +1737,11 @@ class TestPrefix(ModelTestCases.BaseModelTestCase):
1178
1737
  ip = IPAddress.objects.create(address="10.0.0.1/32", status=self.status, namespace=namespace)
1179
1738
 
1180
1739
  with self.subTest("Test that deleting a pool prefix containing IPs succeeds"):
1181
- self.assertEqual(ip.parent, pool) # 3.0 TODO: change this to ", network)" once IP-to-pool is disallowed
1740
+ self.assertEqual(ip.parent, pool)
1182
1741
  pool.delete()
1183
1742
  ip.refresh_from_db()
1184
1743
  self.assertEqual(ip.parent, network)
1185
1744
 
1186
- # 3.0 TODO: uncomment once type enforcement is enabled
1187
- # with self.assertRaises(
1188
- # ProtectedError,
1189
- # msg="Test that deleting a network prefix that would make an IP's parent a container raises a ProtectedError",
1190
- # ):
1191
- # network.delete()
1192
-
1193
1745
  with self.subTest("Test that deleting the root prefix succeeds"):
1194
1746
  root.delete()
1195
1747
  network.refresh_from_db()
@@ -1433,17 +1985,11 @@ class TestIPAddress(ModelTestCases.BaseModelTestCase):
1433
1985
  prefix="12.0.0.0/24", status=self.status, namespace=namespace, type=PrefixTypeChoices.TYPE_POOL
1434
1986
  )
1435
1987
 
1436
- # 3.0 TODO: uncomment once type enforcement is enabled
1437
- # with self.assertRaises(ValidationError, msg="IP Address parent cannot be a container"):
1438
- # IPAddress.objects.create(address="10.0.0.1/32", status=self.status, namespace=namespace)
1439
-
1440
- # with self.assertRaises(Prefix.DoesNotExist, msg="IP Address parent cannot be a pool"):
1441
- # IPAddress.objects.create(address="12.0.0.1/32", status=self.status, namespace=namespace)
1442
-
1443
1988
  with self.assertRaises(ValidationError) as err:
1444
1989
  IPAddress.objects.create(address="13.0.0.1/32", status=self.status, namespace=namespace)
1445
1990
  self.assertEqual(
1446
- err.exception.message_dict["namespace"][0], "No suitable parent Prefix exists in this Namespace"
1991
+ err.exception.message_dict["namespace"][0],
1992
+ "No suitable parent Prefix for 13.0.0.1 exists in Namespace test_parenting_constraints",
1447
1993
  )
1448
1994
 
1449
1995
  with self.subTest("Test that IP address can be assigned to a valid parent"):
@@ -1483,7 +2029,7 @@ class TestIPAddress(ModelTestCases.BaseModelTestCase):
1483
2029
  self.assertIn("namespace", err.exception.message_dict)
1484
2030
  self.assertEqual(
1485
2031
  err.exception.message_dict["namespace"][0],
1486
- "No suitable parent Prefix exists in this Namespace",
2032
+ "No suitable parent Prefix for 1976:2023::1 exists in Namespace Global",
1487
2033
  )
1488
2034
 
1489
2035
  # Appropriate parent exists in the default namespace --> no error
@@ -1523,6 +2069,15 @@ class TestIPAddress(ModelTestCases.BaseModelTestCase):
1523
2069
  ip.validated_save()
1524
2070
  self.assertEqual(ip.parent, prefixes[0])
1525
2071
 
2072
+ def test_change_host(self):
2073
+ ip = IPAddress.objects.create(address="192.0.2.1/32", status=self.status, namespace=self.namespace)
2074
+ self.assertEqual(ip.parent, self.prefix)
2075
+
2076
+ ip.host = "192.168.1.1"
2077
+ with self.assertRaises(ValidationError) as cm:
2078
+ ip.validated_save()
2079
+ self.assertIn("Host address cannot be changed once created", str(cm.exception))
2080
+
1526
2081
  def test_varbinary_ip_fields_with_empty_values_do_not_violate_not_null_constrains(self):
1527
2082
  # Assert that an error is triggered when the host is not provided.
1528
2083
  # Initially, VarbinaryIPField fields with None values are stored as the binary representation of b'',