nautobot 3.0.0a2__py3-none-any.whl → 3.0.0a3__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 (420) hide show
  1. nautobot/apps/choices.py +0 -2
  2. nautobot/apps/filters.py +7 -9
  3. nautobot/apps/models.py +2 -2
  4. nautobot/apps/ui.py +9 -1
  5. nautobot/circuits/filters.py +3 -2
  6. nautobot/circuits/navigation.py +3 -2
  7. nautobot/circuits/templates/circuits/circuit.html +1 -1
  8. nautobot/circuits/templates/circuits/circuit_create.html +3 -3
  9. nautobot/circuits/templates/circuits/circuittermination.html +1 -1
  10. nautobot/circuits/templates/circuits/circuittermination_create.html +9 -24
  11. nautobot/circuits/templates/circuits/circuittype.html +1 -1
  12. nautobot/circuits/templates/circuits/inc/circuit_termination_cable_fragment.html +6 -6
  13. nautobot/circuits/templates/circuits/inc/speed_widget.html +12 -12
  14. nautobot/circuits/templates/circuits/providernetwork.html +1 -1
  15. nautobot/circuits/tests/integration/test_circuit.py +10 -13
  16. nautobot/cloud/filters.py +1 -1
  17. nautobot/cloud/navigation.py +3 -2
  18. nautobot/core/api/schema.py +1 -1
  19. nautobot/core/api/serializers.py +6 -1
  20. nautobot/core/api/urls.py +1 -0
  21. nautobot/core/api/views.py +8 -0
  22. nautobot/core/apps/__init__.py +11 -10
  23. nautobot/core/celery/__init__.py +3 -5
  24. nautobot/core/checks.py +46 -0
  25. nautobot/core/cli/bootstrap_v3_to_v5.py +70 -1
  26. nautobot/core/cli/migrate_deprecated_templates.py +200 -0
  27. nautobot/core/constants.py +3 -0
  28. nautobot/core/context_processors.py +9 -1
  29. nautobot/core/forms/forms.py +1 -1
  30. nautobot/core/jobs/__init__.py +6 -3
  31. nautobot/core/jobs/groups.py +31 -1
  32. nautobot/core/management/commands/generate_test_data.py +28 -9
  33. nautobot/core/models/generics.py +9 -1
  34. nautobot/core/models/tree_queries.py +10 -5
  35. nautobot/core/settings.py +18 -12
  36. nautobot/core/settings.yaml +13 -7
  37. nautobot/core/signals.py +12 -1
  38. nautobot/core/tables.py +13 -6
  39. nautobot/core/templates/40x.html +1 -1
  40. nautobot/core/templates/500.html +2 -2
  41. nautobot/core/templates/admin/config/config.html +12 -12
  42. nautobot/core/templates/admin/index.html +3 -3
  43. nautobot/core/templates/buttons/export.html +1 -1
  44. nautobot/core/templates/components/button/dropdown.html +5 -3
  45. nautobot/core/templates/components/panel/body_wrapper_generic_table.html +1 -1
  46. nautobot/core/templates/components/panel/panel.html +3 -3
  47. nautobot/core/templates/components/tab/content_wrapper.html +2 -3
  48. nautobot/core/templates/components/tab/label_wrapper_distinct_view.html +1 -1
  49. nautobot/core/templates/echarts/echarts.html +1 -1
  50. nautobot/core/templates/generic/object_bulk_add_component.html +2 -1
  51. nautobot/core/templates/generic/object_bulk_create.html +4 -3
  52. nautobot/core/templates/generic/object_bulk_destroy.html +3 -3
  53. nautobot/core/templates/generic/object_bulk_remove.html +2 -2
  54. nautobot/core/templates/generic/object_bulk_update.html +5 -4
  55. nautobot/core/templates/generic/object_create.html +5 -4
  56. nautobot/core/templates/generic/object_import.html +2 -1
  57. nautobot/core/templates/generic/object_list.html +12 -4
  58. nautobot/core/templates/generic/object_notes.html +5 -3
  59. nautobot/core/templates/generic/object_retrieve.html +2 -3
  60. nautobot/core/templates/graphene/graphiql.html +7 -7
  61. nautobot/core/templates/home.html +1 -1
  62. nautobot/core/templates/import_success.html +2 -1
  63. nautobot/core/templates/inc/computed_fields/panel_data.html +1 -1
  64. nautobot/core/templates/inc/created_updated.html +7 -3
  65. nautobot/core/templates/inc/custom_fields/panel_data.html +1 -1
  66. nautobot/core/templates/inc/form_static_field.html +6 -0
  67. nautobot/core/templates/inc/header.html +1 -1
  68. nautobot/core/templates/inc/image_attachments.html +2 -1
  69. nautobot/core/templates/inc/nav_menu.html +2 -1
  70. nautobot/core/templates/inc/search_panel.html +4 -4
  71. nautobot/core/templates/login.html +4 -2
  72. nautobot/core/templates/nautobot_config.py.j2 +6 -5
  73. nautobot/core/templates/redoc_ui.html +7 -0
  74. nautobot/core/templates/search.html +1 -1
  75. nautobot/core/templates/swagger_ui.html +17 -3
  76. nautobot/core/templates/system_jobs/import_objects.html +1 -2
  77. nautobot/core/templates/utilities/confirmation_form.html +2 -2
  78. nautobot/core/templates/utilities/obj_table.html +10 -2
  79. nautobot/core/templates/utilities/render_field.html +7 -7
  80. nautobot/core/templates/utilities/render_jinja2.html +2 -2
  81. nautobot/core/templates/utilities/templatetags/filter_form_drawer.html +4 -4
  82. nautobot/core/templates/utilities/theme_preview.html +16 -3
  83. nautobot/core/templates/widgets/selectwithdisabled_option.html +3 -1
  84. nautobot/core/templatetags/helpers.py +52 -6
  85. nautobot/core/testing/api.py +68 -9
  86. nautobot/core/testing/filters.py +0 -23
  87. nautobot/core/testing/integration.py +23 -10
  88. nautobot/core/testing/mixins.py +2 -0
  89. nautobot/core/testing/views.py +4 -0
  90. nautobot/core/tests/integration/test_app_home.py +34 -30
  91. nautobot/core/tests/integration/test_app_navbar.py +3 -0
  92. nautobot/core/tests/nautobot_config_without_example_apps.py +4 -0
  93. nautobot/core/tests/runner.py +9 -1
  94. nautobot/core/tests/test_api.py +5 -3
  95. nautobot/core/tests/test_breadcrumbs.py +6 -7
  96. nautobot/core/tests/test_checks.py +28 -0
  97. nautobot/core/tests/test_cli.py +40 -0
  98. nautobot/core/tests/test_config.py +2 -1
  99. nautobot/core/tests/test_forms.py +55 -13
  100. nautobot/core/tests/test_jobs.py +75 -1
  101. nautobot/core/tests/test_nautobot_server.py +2 -0
  102. nautobot/core/tests/test_navigations.py +76 -1
  103. nautobot/core/tests/test_patch_social_django.py +42 -0
  104. nautobot/core/tests/test_tables.py +3 -1
  105. nautobot/core/tests/test_templatetags_helpers.py +53 -13
  106. nautobot/core/tests/test_templatetags_ui_framework.py +4 -4
  107. nautobot/core/tests/test_tree_queries.py +14 -1
  108. nautobot/core/tests/test_ui.py +1 -1
  109. nautobot/core/tests/test_utils.py +31 -4
  110. nautobot/core/tests/test_views.py +159 -31
  111. nautobot/core/ui/breadcrumbs.py +2 -12
  112. nautobot/core/ui/choices.py +142 -10
  113. nautobot/core/ui/constants.py +76 -12
  114. nautobot/core/ui/object_detail.py +92 -12
  115. nautobot/core/urls.py +12 -1
  116. nautobot/core/utils/cache.py +2 -1
  117. nautobot/core/utils/filtering.py +17 -17
  118. nautobot/core/utils/lookup.py +3 -8
  119. nautobot/core/utils/module_loading.py +21 -0
  120. nautobot/core/utils/patch_social_django.py +128 -0
  121. nautobot/core/views/__init__.py +38 -1
  122. nautobot/core/views/generic.py +3 -3
  123. nautobot/core/views/mixins.py +15 -3
  124. nautobot/core/views/renderers.py +2 -0
  125. nautobot/core/views/viewsets.py +2 -1
  126. nautobot/data_validation/apps.py +1 -5
  127. nautobot/data_validation/custom_validators.py +4 -4
  128. nautobot/data_validation/filters.py +1 -1
  129. nautobot/data_validation/forms.py +40 -0
  130. nautobot/data_validation/migrations/0001_initial.py +0 -7
  131. nautobot/data_validation/migrations/0002_data_migration_from_app.py +0 -12
  132. nautobot/data_validation/models.py +16 -7
  133. nautobot/data_validation/navigation.py +8 -1
  134. nautobot/data_validation/tables.py +12 -5
  135. nautobot/data_validation/templates/data_validation/datacompliance_tab.html +1 -0
  136. nautobot/data_validation/templates/data_validation/device_constraints.html +61 -0
  137. nautobot/data_validation/tests/__init__.py +2 -2
  138. nautobot/data_validation/tests/migrations/test_migrations.py +83 -3
  139. nautobot/data_validation/tests/test_data_compliance_rules.py +12 -7
  140. nautobot/data_validation/tests/test_filters.py +8 -6
  141. nautobot/data_validation/tests/test_models.py +15 -0
  142. nautobot/data_validation/tests/test_views.py +190 -32
  143. nautobot/data_validation/urls.py +2 -5
  144. nautobot/data_validation/views.py +73 -40
  145. nautobot/dcim/api/serializers.py +0 -13
  146. nautobot/dcim/apps.py +4 -0
  147. nautobot/dcim/choices.py +16 -0
  148. nautobot/dcim/custom_validators.py +84 -0
  149. nautobot/dcim/filter_mixins.py +353 -4
  150. nautobot/dcim/{filters/__init__.py → filters.py} +2 -35
  151. nautobot/dcim/forms.py +1 -1
  152. nautobot/dcim/migrations/0078_remove_device_location_tenant_name_uniqueness.py +16 -0
  153. nautobot/dcim/migrations/0079_device_name_data_migration.py +59 -0
  154. nautobot/dcim/models/device_components.py +81 -68
  155. nautobot/dcim/models/devices.py +13 -16
  156. nautobot/dcim/navigation.py +7 -6
  157. nautobot/dcim/tables/devices.py +3 -0
  158. nautobot/dcim/tables/template_code.py +14 -14
  159. nautobot/dcim/templates/dcim/cable.html +2 -61
  160. nautobot/dcim/templates/dcim/cable_connect.html +28 -112
  161. nautobot/dcim/templates/dcim/cable_edit.html +2 -5
  162. nautobot/dcim/templates/dcim/cable_retrieve.html +61 -0
  163. nautobot/dcim/templates/dcim/cable_trace.html +1 -3
  164. nautobot/dcim/templates/dcim/cable_update.html +5 -0
  165. nautobot/dcim/templates/dcim/consoleport.html +6 -5
  166. nautobot/dcim/templates/dcim/consoleserverport.html +6 -5
  167. nautobot/dcim/templates/dcim/device/config.html +2 -2
  168. nautobot/dcim/templates/dcim/device/consoleports.html +1 -1
  169. nautobot/dcim/templates/dcim/device/consoleserverports.html +1 -1
  170. nautobot/dcim/templates/dcim/device/devicebays.html +1 -1
  171. nautobot/dcim/templates/dcim/device/frontports.html +1 -1
  172. nautobot/dcim/templates/dcim/device/interfaces.html +1 -1
  173. nautobot/dcim/templates/dcim/device/inventory.html +1 -1
  174. nautobot/dcim/templates/dcim/device/lldp_neighbors.html +1 -1
  175. nautobot/dcim/templates/dcim/device/modulebays.html +1 -1
  176. nautobot/dcim/templates/dcim/device/poweroutlets.html +1 -1
  177. nautobot/dcim/templates/dcim/device/powerports.html +1 -1
  178. nautobot/dcim/templates/dcim/device/rearports.html +1 -1
  179. nautobot/dcim/templates/dcim/device/status.html +8 -8
  180. nautobot/dcim/templates/dcim/device/wireless.html +1 -1
  181. nautobot/dcim/templates/dcim/device.html +1 -1
  182. nautobot/dcim/templates/dcim/device_component_add.html +2 -2
  183. nautobot/dcim/templates/dcim/device_create.html +5 -3
  184. nautobot/dcim/templates/dcim/device_interface_delete.html +1 -1
  185. nautobot/dcim/templates/dcim/device_list.html +73 -10
  186. nautobot/dcim/templates/dcim/devicebay_populate.html +2 -2
  187. nautobot/dcim/templates/dcim/devicetype.html +1 -1
  188. nautobot/dcim/templates/dcim/devicetype_component_add.html +2 -2
  189. nautobot/dcim/templates/dcim/footer_convert_to_contact_or_team_record.html +14 -0
  190. nautobot/dcim/templates/dcim/frontport.html +9 -8
  191. nautobot/dcim/templates/dcim/inc/edit_form_softwareversion_js.html +2 -2
  192. nautobot/dcim/templates/dcim/interface.html +26 -6
  193. nautobot/dcim/templates/dcim/interface_bulk_delete.html +1 -1
  194. nautobot/dcim/templates/dcim/inventoryitem_add.html +3 -1
  195. nautobot/dcim/templates/dcim/inventoryitem_bulk_delete.html +1 -1
  196. nautobot/dcim/templates/dcim/inventoryitem_edit.html +3 -1
  197. nautobot/dcim/templates/dcim/location_retrieve.html +1 -242
  198. nautobot/dcim/templates/dcim/module/base.html +49 -9
  199. nautobot/dcim/templates/dcim/module_list.html +57 -8
  200. nautobot/dcim/templates/dcim/modulefamily_retrieve.html +1 -1
  201. nautobot/dcim/templates/dcim/moduletype_retrieve.html +49 -9
  202. nautobot/dcim/templates/dcim/platform_create.html +1 -1
  203. nautobot/dcim/templates/dcim/powerfeed.html +1 -1
  204. nautobot/dcim/templates/dcim/powerpanel.html +1 -1
  205. nautobot/dcim/templates/dcim/powerport.html +5 -4
  206. nautobot/dcim/templates/dcim/rack_elevation_list.html +16 -4
  207. nautobot/dcim/templates/dcim/rack_retrieve.html +33 -15
  208. nautobot/dcim/templates/dcim/rearport.html +7 -6
  209. nautobot/dcim/templates/dcim/virtualchassis.html +1 -1
  210. nautobot/dcim/templates/dcim/virtualchassis_add_member.html +16 -14
  211. nautobot/dcim/templates/dcim/virtualchassis_update.html +14 -6
  212. nautobot/dcim/tests/integration/test_controller.py +1 -0
  213. nautobot/dcim/tests/test_api.py +8 -0
  214. nautobot/dcim/tests/test_custom_validators.py +229 -0
  215. nautobot/dcim/tests/test_filters.py +12 -6
  216. nautobot/dcim/tests/test_models.py +63 -4
  217. nautobot/dcim/tests/test_views.py +63 -22
  218. nautobot/dcim/urls.py +64 -21
  219. nautobot/dcim/utils.py +3 -3
  220. nautobot/dcim/views.py +547 -273
  221. nautobot/extras/api/views.py +9 -1
  222. nautobot/extras/choices.py +2 -13
  223. nautobot/extras/{filters/mixins.py → filter_mixins.py} +1 -1
  224. nautobot/extras/{filters/customfields.py → filter_mixins_customfields.py} +42 -6
  225. nautobot/extras/{filters/__init__.py → filters.py} +14 -46
  226. nautobot/extras/forms/forms.py +5 -13
  227. nautobot/extras/forms/mixins.py +0 -41
  228. nautobot/extras/management/__init__.py +9 -0
  229. nautobot/extras/migrations/0127_approval_workflow_models.py +6 -6
  230. nautobot/extras/migrations/0129_jobresult_debug_log_count_jobresult_error_log_count_and_more.py +37 -0
  231. nautobot/extras/migrations/0130_jobresult_generate_log_entry_counts.py +42 -0
  232. nautobot/extras/models/__init__.py +1 -2
  233. nautobot/extras/models/approvals.py +22 -13
  234. nautobot/extras/models/contacts.py +2 -0
  235. nautobot/extras/models/groups.py +44 -5
  236. nautobot/extras/models/jobs.py +59 -1
  237. nautobot/extras/models/mixins.py +28 -0
  238. nautobot/extras/models/models.py +13 -0
  239. nautobot/extras/models/secrets.py +1 -0
  240. nautobot/extras/models/statuses.py +0 -15
  241. nautobot/extras/navigation.py +13 -9
  242. nautobot/extras/plugins/__init__.py +33 -55
  243. nautobot/extras/plugins/tables.py +3 -3
  244. nautobot/extras/plugins/urls.py +2 -21
  245. nautobot/extras/plugins/utils.py +1 -33
  246. nautobot/extras/plugins/views.py +0 -4
  247. nautobot/extras/signals.py +20 -19
  248. nautobot/extras/tables.py +52 -68
  249. nautobot/extras/templates/extras/approval_dashboard.html +7 -5
  250. nautobot/extras/templates/extras/approvalworkflowdefinition_update.html +4 -2
  251. nautobot/extras/templates/extras/approvalworkflowstage_retrieve.html +20 -12
  252. nautobot/extras/templates/extras/computedfield.html +1 -1
  253. nautobot/extras/templates/extras/configcontext.html +1 -1
  254. nautobot/extras/templates/extras/configcontextschema_validation.html +2 -2
  255. nautobot/extras/templates/extras/customfield.html +1 -1
  256. nautobot/extras/templates/extras/dynamicgroup_retrieve.html +11 -5
  257. nautobot/extras/templates/extras/dynamicgroup_update.html +1 -1
  258. nautobot/extras/templates/extras/gitrepository_result.html +0 -2
  259. nautobot/extras/templates/extras/graphqlquery_retrieve.html +1 -96
  260. nautobot/extras/templates/extras/inc/approval_buttons_column.html +20 -6
  261. nautobot/extras/templates/extras/inc/bulk_edit_overridable_field.html +8 -7
  262. nautobot/extras/templates/extras/inc/configcontext_format.html +10 -3
  263. nautobot/extras/templates/extras/inc/graphqlquery_execute.html +71 -0
  264. nautobot/extras/templates/extras/inc/job_tiles.html +15 -3
  265. nautobot/extras/templates/extras/inc/json_format.html +10 -3
  266. nautobot/extras/templates/extras/inc/overridable_field.html +13 -12
  267. nautobot/extras/templates/extras/job.html +29 -12
  268. nautobot/extras/templates/extras/job_bulk_edit.html +18 -0
  269. nautobot/extras/templates/extras/job_edit.html +52 -46
  270. nautobot/extras/templates/extras/job_list.html +29 -25
  271. nautobot/extras/templates/extras/marketplace.html +5 -9
  272. nautobot/extras/templates/extras/object_configcontext.html +1 -1
  273. nautobot/extras/templates/extras/object_dynamicgroups.html +2 -2
  274. nautobot/extras/templates/extras/objectchange_retrieve.html +19 -37
  275. nautobot/extras/templates/extras/plugin_detail.html +26 -21
  276. nautobot/extras/templates/extras/plugins_list.html +16 -26
  277. nautobot/extras/templates/extras/role_retrieve.html +64 -0
  278. nautobot/extras/templates/extras/scheduledjob.html +4 -2
  279. nautobot/extras/templates/extras/secretsgroup.html +1 -1
  280. nautobot/extras/templates/extras/tag.html +1 -1
  281. nautobot/extras/templatetags/custom_links.py +12 -12
  282. nautobot/extras/templatetags/job_buttons.py +14 -12
  283. nautobot/extras/test_jobs/invalid_import.py +9 -0
  284. nautobot/extras/test_jobs/log_counts_by_level.py +23 -0
  285. nautobot/extras/test_jobs/missing_import.py +11 -0
  286. nautobot/extras/tests/integration/test_configcontextschema.py +27 -26
  287. nautobot/extras/tests/integration/test_customfields.py +8 -7
  288. nautobot/extras/tests/integration/test_dynamicgroups.py +5 -1
  289. nautobot/extras/tests/integration/test_plugin_banner.py +3 -0
  290. nautobot/extras/tests/integration/test_plugins.py +18 -6
  291. nautobot/extras/tests/test_api.py +27 -18
  292. nautobot/extras/tests/test_approvals.py +38 -38
  293. nautobot/extras/tests/test_changelog.py +35 -3
  294. nautobot/extras/tests/test_customfields.py +22 -13
  295. nautobot/extras/tests/test_customfields_filters.py +479 -0
  296. nautobot/extras/tests/test_dynamicgroups.py +39 -1
  297. nautobot/extras/tests/test_filters.py +21 -19
  298. nautobot/extras/tests/test_forms.py +18 -21
  299. nautobot/extras/tests/test_jobs.py +25 -4
  300. nautobot/extras/tests/test_migrations.py +1 -0
  301. nautobot/extras/tests/test_models.py +13 -31
  302. nautobot/extras/tests/test_plugins.py +36 -10
  303. nautobot/extras/tests/test_views.py +31 -30
  304. nautobot/extras/views.py +81 -19
  305. nautobot/ipam/factory.py +7 -0
  306. nautobot/ipam/filter_mixins.py +38 -0
  307. nautobot/ipam/filters.py +27 -38
  308. nautobot/ipam/formfields.py +1 -1
  309. nautobot/ipam/forms.py +6 -3
  310. nautobot/ipam/migrations/0030_ipam__namespaces.py +13 -0
  311. nautobot/ipam/migrations/0031_ipam___data_migrations.py +4 -1
  312. nautobot/ipam/migrations/0054_namespace_tenant.py +25 -0
  313. nautobot/ipam/models.py +29 -2
  314. nautobot/ipam/navigation.py +3 -2
  315. nautobot/ipam/signals.py +71 -0
  316. nautobot/ipam/tables.py +13 -6
  317. nautobot/ipam/templates/ipam/inc/toggle_available.html +10 -10
  318. nautobot/ipam/templates/ipam/inc/vlangroup_header.html +1 -0
  319. nautobot/ipam/templates/ipam/ipaddress.html +14 -0
  320. nautobot/ipam/templates/ipam/ipaddress_merge.html +3 -3
  321. nautobot/ipam/templates/ipam/ipaddresstointerface_retrieve.html +1 -0
  322. nautobot/ipam/templates/ipam/namespace_update.html +15 -0
  323. nautobot/ipam/templates/ipam/prefix_delete.html +1 -1
  324. nautobot/ipam/templates/ipam/prefix_list.html +14 -13
  325. nautobot/ipam/templates/ipam/service.html +1 -1
  326. nautobot/ipam/templates/ipam/vlan.html +1 -1
  327. nautobot/ipam/templates/ipam/vlan_interfaces.html +1 -1
  328. nautobot/ipam/templates/ipam/vlan_vminterfaces.html +1 -1
  329. nautobot/ipam/tests/migration/test_migrations.py +89 -0
  330. nautobot/ipam/tests/test_api.py +13 -6
  331. nautobot/ipam/tests/test_filters.py +10 -0
  332. nautobot/ipam/tests/test_forms.py +1 -1
  333. nautobot/ipam/tests/test_models.py +43 -1
  334. nautobot/ipam/tests/test_tables.py +1 -2
  335. nautobot/ipam/tests/test_utils.py +1 -1
  336. nautobot/ipam/tests/test_views.py +13 -14
  337. nautobot/ipam/ui.py +0 -17
  338. nautobot/ipam/utils/migrations.py +16 -2
  339. nautobot/ipam/utils/testing.py +9 -3
  340. nautobot/ipam/views.py +46 -6
  341. nautobot/project-static/dist/css/nautobot.css +1 -1
  342. nautobot/project-static/dist/css/nautobot.css.map +1 -1
  343. nautobot/project-static/dist/js/nautobot.js +1 -1
  344. nautobot/project-static/dist/js/nautobot.js.map +1 -1
  345. nautobot/project-static/js/cabletrace.js +1 -1
  346. nautobot/project-static/js/interface_filtering.js +20 -16
  347. nautobot/project-static/nautobot-icons/battery-3.svg +3 -0
  348. nautobot/project-static/nautobot-icons/cloud.svg +1 -1
  349. nautobot/project-static/nautobot-icons/control-panel.svg +1 -1
  350. nautobot/project-static/nautobot-icons/device-lifecycle.svg +1 -1
  351. nautobot/project-static/nautobot-icons/elements.svg +1 -1
  352. nautobot/project-static/nautobot-icons/extensibility.svg +3 -0
  353. nautobot/project-static/nautobot-icons/hammer.svg +1 -1
  354. nautobot/project-static/nautobot-icons/organization.svg +3 -0
  355. nautobot/project-static/nautobot-icons/secrets.svg +1 -1
  356. nautobot/project-static/nautobot-icons/security.svg +3 -0
  357. nautobot/project-static/nautobot-icons/server.svg +1 -1
  358. nautobot/project-static/nautobot-icons/star-filled.svg +1 -1
  359. nautobot/project-static/nautobot-icons/star.svg +1 -1
  360. nautobot/tenancy/api/serializers.py +1 -0
  361. nautobot/tenancy/api/views.py +2 -1
  362. nautobot/tenancy/{filters/__init__.py → filters.py} +2 -10
  363. nautobot/tenancy/navigation.py +3 -1
  364. nautobot/tenancy/tests/test_filters.py +0 -2
  365. nautobot/tenancy/views.py +2 -1
  366. nautobot/ui/src/js/collapse.js +3 -3
  367. nautobot/ui/src/js/nautobot.js +16 -0
  368. nautobot/ui/src/scss/colors.scss +1 -1
  369. nautobot/ui/src/scss/nautobot.scss +61 -28
  370. nautobot/users/templates/users/profile.html +45 -12
  371. nautobot/users/templates/users/sessionkey_delete.html +1 -1
  372. nautobot/users/tests/test_api.py +4 -0
  373. nautobot/users/views.py +4 -2
  374. nautobot/virtualization/models.py +1 -68
  375. nautobot/virtualization/navigation.py +3 -2
  376. nautobot/virtualization/templates/virtualization/virtual_machine_vminterface_delete.html +1 -1
  377. nautobot/virtualization/templates/virtualization/virtualmachine.html +1 -1
  378. nautobot/virtualization/templates/virtualization/virtualmachine_list.html +2 -2
  379. nautobot/virtualization/templates/virtualization/virtualmachine_update.html +3 -1
  380. nautobot/virtualization/tests/test_api.py +3 -0
  381. nautobot/virtualization/tests/test_models.py +44 -4
  382. nautobot/vpn/__init__.py +0 -0
  383. nautobot/vpn/api/serializers.py +113 -0
  384. nautobot/vpn/api/urls.py +19 -0
  385. nautobot/vpn/api/views.py +70 -0
  386. nautobot/vpn/apps.py +8 -0
  387. nautobot/vpn/choices.py +171 -0
  388. nautobot/vpn/factory.py +209 -0
  389. nautobot/vpn/filters.py +233 -0
  390. nautobot/vpn/forms.py +486 -0
  391. nautobot/vpn/homepage.py +19 -0
  392. nautobot/vpn/migrations/0001_initial.py +541 -0
  393. nautobot/vpn/migrations/0002_populate_defaults.py +199 -0
  394. nautobot/vpn/migrations/__init__.py +0 -0
  395. nautobot/vpn/models.py +527 -0
  396. nautobot/vpn/navigation.py +98 -0
  397. nautobot/vpn/tables.py +380 -0
  398. nautobot/vpn/templates/vpn/vpnprofile.html +2 -0
  399. nautobot/vpn/templates/vpn/vpnprofile_create.html +150 -0
  400. nautobot/vpn/tests/__init__.py +0 -0
  401. nautobot/vpn/tests/test_api.py +341 -0
  402. nautobot/vpn/tests/test_filters.py +139 -0
  403. nautobot/vpn/tests/test_forms.py +294 -0
  404. nautobot/vpn/tests/test_models.py +97 -0
  405. nautobot/vpn/tests/test_views.py +281 -0
  406. nautobot/vpn/urls.py +16 -0
  407. nautobot/vpn/views.py +437 -0
  408. nautobot/wireless/navigation.py +3 -2
  409. nautobot/wireless/tests/integration/test_radio_profile.py +1 -5
  410. nautobot/wireless/tests/test_api.py +1 -1
  411. {nautobot-3.0.0a2.dist-info → nautobot-3.0.0a3.dist-info}/METADATA +14 -14
  412. {nautobot-3.0.0a2.dist-info → nautobot-3.0.0a3.dist-info}/RECORD +417 -366
  413. {nautobot-3.0.0a2.dist-info → nautobot-3.0.0a3.dist-info}/entry_points.txt +1 -0
  414. nautobot/data_validation/template_content.py +0 -42
  415. nautobot/dcim/filters/mixins.py +0 -354
  416. nautobot/ipam/templates/ipam/inc/prefix_header_extra_content_table.html +0 -4
  417. /nautobot/tenancy/{filters/mixins.py → filter_mixins.py} +0 -0
  418. {nautobot-3.0.0a2.dist-info → nautobot-3.0.0a3.dist-info}/LICENSE.txt +0 -0
  419. {nautobot-3.0.0a2.dist-info → nautobot-3.0.0a3.dist-info}/NOTICE +0 -0
  420. {nautobot-3.0.0a2.dist-info → nautobot-3.0.0a3.dist-info}/WHEEL +0 -0
nautobot/ipam/forms.py CHANGED
@@ -72,10 +72,10 @@ IPADDRESS_MASK_LENGTH_CHOICES = add_blank_choice(
72
72
  #
73
73
 
74
74
 
75
- class NamespaceForm(LocatableModelFormMixin, NautobotModelForm):
75
+ class NamespaceForm(LocatableModelFormMixin, NautobotModelForm, TenancyForm):
76
76
  class Meta:
77
77
  model = Namespace
78
- fields = ["name", "description", "location", "tags"]
78
+ fields = ["name", "description", "tenant", "location", "tags"]
79
79
 
80
80
 
81
81
  class NamespaceBulkEditForm(
@@ -84,18 +84,21 @@ class NamespaceBulkEditForm(
84
84
  NautobotBulkEditForm,
85
85
  ):
86
86
  pk = forms.ModelMultipleChoiceField(queryset=Namespace.objects.all(), widget=forms.MultipleHiddenInput())
87
+ tenant = DynamicModelChoiceField(queryset=Tenant.objects.all(), required=False)
87
88
  description = forms.CharField(max_length=CHARFIELD_MAX_LENGTH, required=False)
88
89
 
89
90
  class Meta:
90
91
  model = Namespace
91
92
  nullable_fields = [
92
93
  "description",
94
+ "tenant",
93
95
  "location",
94
96
  ]
95
97
 
96
98
 
97
- class NamespaceFilterForm(LocatableModelFilterFormMixin, NautobotFilterForm):
99
+ class NamespaceFilterForm(LocatableModelFilterFormMixin, NautobotFilterForm, TenancyFilterForm):
98
100
  model = Namespace
101
+ field_order = ["q", "name", "tenant_group", "tenant"]
99
102
  q = forms.CharField(required=False, label="Search")
100
103
  name = forms.CharField(required=False)
101
104
 
@@ -10,6 +10,17 @@ import nautobot.extras.models.mixins
10
10
  import nautobot.ipam.models
11
11
 
12
12
 
13
+ def create_default_namespace(apps, schema):
14
+ Namespace = apps.get_model("ipam", "Namespace")
15
+
16
+ namespace, _ = Namespace.objects.get_or_create(
17
+ name="Global", defaults={"description": "Default Global namespace. Created by Nautobot."}
18
+ )
19
+
20
+ # Populate the contextvars cache so that subsequent calls to get_default_namespace_pk() do not error out
21
+ nautobot.ipam.models.default_namespace_pk.set(namespace.pk)
22
+
23
+
13
24
  class Migration(migrations.Migration):
14
25
  dependencies = [
15
26
  ("extras", "0072_rename_model_fields"),
@@ -130,6 +141,8 @@ class Migration(migrations.Migration):
130
141
  name="ip_version",
131
142
  field=models.IntegerField(db_index=True, editable=False, null=True),
132
143
  ),
144
+ # We shouldn't mix data migrations with schema migrations, but we didn't catch this data dependency until much later
145
+ migrations.RunPython(create_default_namespace, migrations.RunPython.noop),
133
146
  migrations.AddField(
134
147
  model_name="prefix",
135
148
  name="namespace",
@@ -17,7 +17,10 @@ def reverse_it(apps, schema_editor):
17
17
  Interface = apps.get_model("dcim", "Interface")
18
18
  VMInterface = apps.get_model("virtualization", "VMInterface")
19
19
 
20
- ns_global = Namespace.objects.get(name="Global")
20
+ # This may be overly defensive, but it doesn't hurt to be safe.
21
+ ns_global, _ = Namespace.objects.get_or_create(
22
+ name="Global", defaults={"description": "Default Global namespace. Created by Nautobot."}
23
+ )
21
24
  Prefix.objects.update(namespace=ns_global)
22
25
  VRF.objects.update(namespace=ns_global)
23
26
  Namespace.objects.exclude(name=ns_global.name).delete()
@@ -0,0 +1,25 @@
1
+ # Generated by Django 4.2.23 on 2025-08-30 13:10
2
+
3
+ from django.db import migrations, models
4
+ import django.db.models.deletion
5
+
6
+
7
+ class Migration(migrations.Migration):
8
+ dependencies = [
9
+ ("tenancy", "0009_update_all_charfields_max_length_to_255"),
10
+ ("ipam", "0053_alter_vrfdeviceassignment_options_and_more"),
11
+ ]
12
+
13
+ operations = [
14
+ migrations.AddField(
15
+ model_name="namespace",
16
+ name="tenant",
17
+ field=models.ForeignKey(
18
+ blank=True,
19
+ null=True,
20
+ on_delete=django.db.models.deletion.PROTECT,
21
+ related_name="namespaces",
22
+ to="tenancy.tenant",
23
+ ),
24
+ ),
25
+ ]
nautobot/ipam/models.py CHANGED
@@ -1,3 +1,4 @@
1
+ import contextvars
1
2
  import logging
2
3
  import operator
3
4
  from typing import Optional
@@ -48,6 +49,9 @@ __all__ = (
48
49
  logger = logging.getLogger(__name__)
49
50
 
50
51
 
52
+ default_namespace_pk = contextvars.ContextVar("default_namespace_pk", default=None)
53
+
54
+
51
55
  @extras_features(
52
56
  "custom_links",
53
57
  "custom_validators",
@@ -68,6 +72,13 @@ class Namespace(PrimaryModel):
68
72
  blank=True,
69
73
  null=True,
70
74
  )
75
+ tenant = models.ForeignKey(
76
+ to="tenancy.Tenant",
77
+ on_delete=models.PROTECT,
78
+ related_name="namespaces",
79
+ blank=True,
80
+ null=True,
81
+ )
71
82
 
72
83
  @property
73
84
  def ip_addresses(self):
@@ -80,9 +91,17 @@ class Namespace(PrimaryModel):
80
91
  def __str__(self):
81
92
  return self.name
82
93
 
94
+ def delete(self, *args, **kwargs):
95
+ if self.name == "Global":
96
+ default_namespace_pk.set(None)
97
+ super().delete(*args, **kwargs)
98
+
83
99
 
84
100
  def get_default_namespace():
85
- """Return the Global namespace."""
101
+ """Return the Global namespace.
102
+
103
+ Because this has no access to historical models, this MUST NOT be called during migrations.
104
+ """
86
105
  obj, _ = Namespace.objects.get_or_create(
87
106
  name="Global", defaults={"description": "Default Global namespace. Created by Nautobot."}
88
107
  )
@@ -91,7 +110,15 @@ def get_default_namespace():
91
110
 
92
111
  def get_default_namespace_pk():
93
112
  """Return the PK of the Global namespace for use in default value for foreign keys."""
94
- return get_default_namespace().pk
113
+ pk = default_namespace_pk.get()
114
+ if pk is None:
115
+ # MUST NEVER HAPPEN DURING MIGRATIONS, because get_default_namespace() doesn't use historical models.
116
+ # This is accommodated in migration ipam__0030 to directly set default_namespace_pk *from* the historical model
117
+ # but any other migrations using this function may need to implement similar workarounds.
118
+ pk = get_default_namespace().pk
119
+ default_namespace_pk.set(pk)
120
+
121
+ return pk
95
122
 
96
123
 
97
124
  @extras_features(
@@ -4,12 +4,13 @@ from nautobot.core.apps import (
4
4
  NavMenuItem,
5
5
  NavMenuTab,
6
6
  )
7
+ from nautobot.core.ui.choices import NavigationIconChoices, NavigationWeightChoices
7
8
 
8
9
  menu_items = (
9
10
  NavMenuTab(
10
11
  name="IPAM",
11
- icon="ip",
12
- weight=300,
12
+ icon=NavigationIconChoices.IPAM,
13
+ weight=NavigationWeightChoices.IPAM,
13
14
  groups=(
14
15
  NavMenuGroup(
15
16
  name="IP Addresses",
nautobot/ipam/signals.py CHANGED
@@ -5,6 +5,7 @@ from django.db.models.signals import m2m_changed, pre_delete, pre_save
5
5
  from django.dispatch import receiver
6
6
 
7
7
  from nautobot.ipam.models import (
8
+ IPAddress,
8
9
  IPAddressToInterface,
9
10
  Prefix,
10
11
  PrefixLocationAssignment,
@@ -136,3 +137,73 @@ def assert_locations_content_types(sender, instance, action, reverse, model, pk_
136
137
  raise ValidationError(
137
138
  {key: f"{instance} is a {instance.location_type} and may not have {label} associated to it."}
138
139
  )
140
+
141
+
142
+ def _validate_interface_ipaddress_assignments(sender, instance, pk_set, interface_field):
143
+ """
144
+ Helper function to validate IPAddressToInterface instances on Interface and VMInterface objects after M2M operations.
145
+
146
+ For example:
147
+ * interface.ip_addresses.add(ip_address)
148
+ * vm_interface.ip_addresses.add(ip_address)
149
+
150
+ Args:
151
+ sender: The through model class (IPAddressToInterface)
152
+ instance: The interface instance (Interface or VMInterface)
153
+ pk_set: Set of IP address PKs being modified
154
+ interface_field: Field name to filter on ('interface' or 'vm_interface')
155
+ """
156
+ # Get the through model instances that were just created
157
+ filter_kwargs = {interface_field: instance, "ip_address_id__in": pk_set}
158
+ through_instances = sender.objects.filter(**filter_kwargs)
159
+
160
+ # Validate each through model instance
161
+ for through_instance in through_instances:
162
+ through_instance.full_clean()
163
+
164
+
165
+ def _validate_ipaddress_interface_assignments(sender, instance, pk_set, interface_model):
166
+ """
167
+ Helper function to validate IPAddressToInterface instances on IPAddress objects after M2M operations.
168
+
169
+ For example:
170
+ * ip_address.interfaces.add(interface)
171
+ * ip_address.vm_interfaces.add(vm_interface)
172
+
173
+ Args:
174
+ sender: The through model class (IPAddressToInterface)
175
+ instance: The interface instance (Interface or VMInterface)
176
+ pk_set: Set of IP address PKs being modified
177
+ interface_model: Field name to filter on ('interface' or 'vm_interface')
178
+ """
179
+ filter_kwargs = {"ip_address": instance, interface_model + "_id__in": pk_set}
180
+ through_instances = sender.objects.filter(**filter_kwargs)
181
+ for through_instance in through_instances:
182
+ through_instance.full_clean()
183
+
184
+
185
+ @receiver(m2m_changed, sender=IPAddressToInterface)
186
+ def validate_interface_ip_assignments(sender, instance, action, pk_set, **kwargs):
187
+ """
188
+ Validate IPAddressToInterface instances after M2M operations.
189
+
190
+ Handles both physical Interface and VMInterface IP assignments.
191
+ Since Django's M2M add() with through_defaults bypasses save() methods,
192
+ we validate the through model instances via signal handler.
193
+ """
194
+ from nautobot.dcim.models import Interface
195
+ from nautobot.virtualization.models import VMInterface
196
+
197
+ if action == "post_add" and pk_set:
198
+ # Route to appropriate validation based on instance type
199
+ if isinstance(instance, Interface):
200
+ _validate_interface_ipaddress_assignments(sender, instance, pk_set, "interface")
201
+ elif isinstance(instance, VMInterface):
202
+ _validate_interface_ipaddress_assignments(sender, instance, pk_set, "vm_interface")
203
+ elif isinstance(instance, IPAddress):
204
+ interface_model = kwargs["model"]
205
+
206
+ if interface_model == Interface:
207
+ _validate_ipaddress_interface_assignments(sender, instance, pk_set, "interface")
208
+ elif interface_model == VMInterface:
209
+ _validate_ipaddress_interface_assignments(sender, instance, pk_set, "vm_interface")
nautobot/ipam/tables.py CHANGED
@@ -205,11 +205,12 @@ VLANGROUP_ADD_VLAN = """
205
205
  class NamespaceTable(BaseTable):
206
206
  pk = ToggleColumn()
207
207
  name = tables.LinkColumn()
208
+ tenant = TenantColumn()
208
209
  tags = TagColumn(url_name="ipam:namespace_list")
209
210
 
210
211
  class Meta(BaseTable.Meta):
211
212
  model = Namespace
212
- fields = ("pk", "name", "description", "location")
213
+ fields = ("pk", "name", "description", "tenant", "location")
213
214
 
214
215
 
215
216
  #
@@ -369,7 +370,7 @@ class PrefixTable(StatusTableMixin, RoleTableMixin, BaseTable):
369
370
  tenant = TenantColumn()
370
371
  namespace = tables.Column(linkify=True)
371
372
  vlan = tables.Column(linkify=True, verbose_name="VLAN")
372
- rir = tables.Column(linkify=True)
373
+ rir = tables.Column(linkify=True, verbose_name="RIR")
373
374
  children = tables.Column(accessor="descendants_count", orderable=False)
374
375
  date_allocated = tables.DateTimeColumn()
375
376
  location_count = LinkedCountColumn(
@@ -415,7 +416,7 @@ class PrefixTable(StatusTableMixin, RoleTableMixin, BaseTable):
415
416
  "actions",
416
417
  )
417
418
  row_attrs = {
418
- "class": lambda record: "success" if not record.present_in_database else "",
419
+ "class": lambda record: "table-success" if not record.present_in_database else "",
419
420
  }
420
421
 
421
422
 
@@ -449,6 +450,7 @@ class PrefixDetailTable(PrefixTable):
449
450
  "role",
450
451
  "description",
451
452
  "tags",
453
+ "actions",
452
454
  )
453
455
  default_columns = (
454
456
  "pk",
@@ -463,6 +465,7 @@ class PrefixDetailTable(PrefixTable):
463
465
  "vlan",
464
466
  "role",
465
467
  "description",
468
+ "actions",
466
469
  )
467
470
 
468
471
 
@@ -499,6 +502,7 @@ class IPAddressTable(StatusTableMixin, RoleTableMixin, BaseTable):
499
502
  distinct=True,
500
503
  verbose_name="Virtual Machines",
501
504
  )
505
+ actions = ButtonsColumn(Prefix)
502
506
 
503
507
  class Meta(BaseTable.Meta):
504
508
  model = IPAddress
@@ -516,9 +520,10 @@ class IPAddressTable(StatusTableMixin, RoleTableMixin, BaseTable):
516
520
  "interface_parent_count",
517
521
  "vm_interface_count",
518
522
  "vm_interface_parent_count",
523
+ "actions",
519
524
  )
520
525
  row_attrs = {
521
- "class": lambda record: "success" if not isinstance(record, IPAddress) else "",
526
+ "class": lambda record: "table-success" if not isinstance(record, IPAddress) else "",
522
527
  }
523
528
 
524
529
 
@@ -545,6 +550,7 @@ class IPAddressDetailTable(IPAddressTable):
545
550
  "dns_name",
546
551
  "description",
547
552
  "tags",
553
+ "actions",
548
554
  )
549
555
  default_columns = (
550
556
  "pk",
@@ -557,6 +563,7 @@ class IPAddressDetailTable(IPAddressTable):
557
563
  "assigned",
558
564
  "dns_name",
559
565
  "description",
566
+ "actions",
560
567
  )
561
568
 
562
569
 
@@ -651,7 +658,7 @@ class IPAddressInterfaceTable(InterfaceTable):
651
658
  "connection",
652
659
  ]
653
660
  row_attrs = {
654
- "style": cable_status_color_css,
661
+ "class": cable_status_color_css,
655
662
  "data-name": lambda record: record.name,
656
663
  }
657
664
 
@@ -744,7 +751,7 @@ class VLANTable(StatusTableMixin, RoleTableMixin, BaseTable):
744
751
  "description",
745
752
  )
746
753
  row_attrs = {
747
- "class": lambda record: "success" if not isinstance(record, VLAN) else "",
754
+ "class": lambda record: "table-success" if not isinstance(record, VLAN) else "",
748
755
  }
749
756
 
750
757
 
@@ -1,11 +1,11 @@
1
1
  {% load helpers %}
2
- {% if show_available is not None %}
3
- <div class="btn-group" role="group">
4
- <a href="{{ request.path }}{% legacy_querystring request show_available='true' %}" class="btn btn-secondary btn-sm{% if show_available %} active disabled{% endif %}">
5
- <span class="mdi mdi-eye-outline"></span> Show available
6
- </a>
7
- <a href="{{ request.path }}{% legacy_querystring request show_available='false' %}" class="btn btn-secondary btn-sm{% if not show_available %} active disabled{% endif %}">
8
- <span class="mdi mdi-eye-off-outline"></span> Hide available
9
- </a>
10
- </div>
11
- {% endif %}
2
+ <div class="btn-group" role="group">
3
+ <a href="{% django_querystring show_available='true' %}"
4
+ class="btn btn-primary{% if show_available %} bg-primary nb-text-body-bg{% endif %}">
5
+ <span class="mdi mdi-eye-outline"></span> Show {{ label }}
6
+ </a>
7
+ <a href="{% django_querystring show_available='false' %}"
8
+ class="btn btn-primary{% if not show_available %} bg-primary nb-text-body-bg{% endif %}">
9
+ <span class="mdi mdi-eye-off-outline"></span> Hide {{ label }}
10
+ </a>
11
+ </div>
@@ -1,3 +1,4 @@
1
+ {# TODO: this file is unused as far as I can tell - remove it? #}
1
2
  <div class="float-end">
2
3
  {% if perms.ipam.add_vlan and first_available_vlan %}
3
4
  <a href="{% url 'ipam:vlan_add' %}?vid={{ first_available_vlan }}&group={{ object.pk }}{% if object.location %}&location={{ object.location.pk }}{% endif %}" class="btn btn-success">
@@ -136,6 +136,20 @@
136
136
  {% endif %}
137
137
  </td>
138
138
  </tr>
139
+ <tr>
140
+ <td>VPN Endpoints</td>
141
+ <td>
142
+ {% if object.vpn_tunnel_endpoints_src_ip.exists %}
143
+ <ul class="list-unstyled">
144
+ {% for endpoint in object.vpn_tunnel_endpoints_src_ip.all %}
145
+ <li>{{ endpoint|hyperlinked_object }}</li>
146
+ {% endfor %}
147
+ </ul>
148
+ {% else %}
149
+ <span class="text-secondary">None</span>
150
+ {% endif %}
151
+ </td>
152
+ </tr>
139
153
  </table>
140
154
  </div>
141
155
  {% endblock content_right_page %}
@@ -16,8 +16,8 @@
16
16
  {% block content %}
17
17
  <form action="" method="post" enctype="multipart/form-data" class="h-100 vstack">
18
18
  {% csrf_token %}
19
- <div class="row align-content-start flex-fill">
20
- <div class="col-lg-10 offset-md-1">
19
+ <div class="row justify-content-center align-content-start flex-fill">
20
+ <div class="col-lg-10">
21
21
  <div class="card border-info">
22
22
  <div class="card-header bg-info-subtle border-info fw-medium text-body"><strong>Confirm Merging IP Addresses</strong></div>
23
23
  <div class="card-body">
@@ -26,7 +26,7 @@
26
26
  </div>
27
27
  </div>
28
28
  </div>
29
- <div class="col-lg-10 offset-md-1">
29
+ <div class="col-lg-10">
30
30
  <div class="card">
31
31
  <div class="table-responsive">
32
32
  <table class="table table-hover nb-table-headings">
@@ -1,4 +1,5 @@
1
1
  {% extends 'generic/object_retrieve.html' %}
2
+ {# TODO: this file is unused as far as I can tell. Remove it? #}
2
3
  {% load helpers %}
3
4
  {% load render_table from django_tables2 %}
4
5
 
@@ -0,0 +1,15 @@
1
+ {% extends 'generic/object_create.html' %}
2
+ {% load form_helpers %}
3
+
4
+ {% block form %}
5
+ <div class="card">
6
+ <div class="card-header"><strong>Namespace</strong></div>
7
+ <div class="card-body">
8
+ {% render_field form.name %}
9
+ {% render_field form.description %}
10
+ {% render_field form.location %}
11
+ </div>
12
+ </div>
13
+ {% include 'inc/tenancy_form_panel.html' %}
14
+ {% include 'inc/extras_features_edit_form_fields.html' %}
15
+ {% endblock %}
@@ -1,4 +1,4 @@
1
- {% extends 'generic/object_delete.html' %}
1
+ {% extends 'generic/object_destroy.html' %}
2
2
 
3
3
  {% block message_extra %}
4
4
  <p>Note: This will <strong>not</strong> delete any child prefixes or IP addresses.</p>
@@ -2,19 +2,20 @@
2
2
  {% load helpers %}
3
3
 
4
4
  {% block buttons %}
5
- <div class="btn-group" role="group">
6
- <div class="dropdown">
7
- <button class="btn btn-secondary dropdown-toggle" type="button" id="max_length" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
8
- Max Length{% if "prefix_length__lte" in request.GET %}: {{ request.GET.prefix_length__lte }}{% endif %}
9
- <span class="mdi mdi-chevron-down"></span>
10
- </button>
11
- <ul class="dropdown-menu" aria-labelledby="max_length">
12
- {% for i in "4,8,12,16,20,24,28,32,40,48,56,64"|split %}
13
- <li><a href="{% url 'ipam:prefix_list' %}{% legacy_querystring request prefix_length__lte=i page=1 %}">
5
+ <div class="dropdown">
6
+ <button class="btn btn-secondary dropdown-toggle" type="button" id="max_length" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
7
+ Max Length{% if "prefix_length__lte" in request.GET %}: {{ request.GET.prefix_length__lte }}{% endif %}
8
+ <span class="mdi mdi-chevron-down"></span>
9
+ </button>
10
+ <ul class="dropdown-menu" aria-labelledby="max_length">
11
+ {% for i in "4,8,12,16,20,24,28,32,40,48,56,64"|split %}
12
+ <li>
13
+ <a class="dropdown-item"
14
+ href="{% url 'ipam:prefix_list' %}{% legacy_querystring request prefix_length__lte=i page=1 %}">
14
15
  {{ i }} {% if request.GET.prefix_length__lte == i %}<span class="mdi mdi-check-bold"></span>{% endif %}
15
- </a></li>
16
- {% endfor %}
17
- </ul>
18
- </div>
16
+ </a>
17
+ </li>
18
+ {% endfor %}
19
+ </ul>
19
20
  </div>
20
21
  {% endblock %}
@@ -1,2 +1,2 @@
1
- {% extends 'ipam/service_retrieve.html' %}
1
+ {% extends 'generic/object_retrieve.html' %}
2
2
  {% comment %}3.0 TODO: remove this template, which only exists for backward compatibility with 2.4 and earlier{% endcomment %}
@@ -1,2 +1,2 @@
1
- {% extends 'ipam/vlan_retrieve.html' %}
1
+ {% extends 'generic/object_retrieve.html' %}
2
2
  {% comment %}3.0 TODO: remove this template, which only exists for backward compatibility with 2.4 and earlier{% endcomment %}
@@ -1,4 +1,4 @@
1
- {% extends 'ipam/vlan.html' %}
1
+ {% extends 'generic/object_retrieve.html' %}
2
2
 
3
3
  {% block content %}
4
4
  <div class="row">
@@ -1,4 +1,4 @@
1
- {% extends 'ipam/vlan.html' %}
1
+ {% extends 'generic/object_retrieve.html' %}
2
2
 
3
3
  {% block content %}
4
4
  <div class="row">
@@ -508,3 +508,92 @@ class IPAMDataMigration0031TestCase(MigratorTestCase):
508
508
  for ip in IPAddress.objects.iterator():
509
509
  self.assertLessEqual(netaddr.IPAddress(ip.parent.network), netaddr.IPAddress(ip.host))
510
510
  self.assertGreaterEqual(netaddr.IPAddress(ip.parent.broadcast), netaddr.IPAddress(ip.host))
511
+
512
+
513
+ class NamespaceTenantCircularDependencyTestCase(MigratorTestCase):
514
+ """Test that our 3-phase migration approach resolves circular dependencies and supports rollback."""
515
+
516
+ migrate_from = ("ipam", "0029_ip_address_to_interface_uniqueness_constraints") # Before namespace creation
517
+ migrate_to = ("ipam", "0054_namespace_tenant") # After our complete sequence
518
+
519
+ def prepare(self):
520
+ """Create minimal data to test namespace-tenant migration sequence."""
521
+ # Create basic tenancy data in old state
522
+ TenantGroup = self.old_state.apps.get_model("tenancy", "TenantGroup")
523
+ Tenant = self.old_state.apps.get_model("tenancy", "Tenant")
524
+
525
+ self.tenant_group = TenantGroup.objects.create(name="Test Group")
526
+ self.tenant1 = Tenant.objects.create(name="Test Tenant 1", tenant_group=self.tenant_group)
527
+ self.tenant2 = Tenant.objects.create(name="Test Tenant 2", tenant_group=self.tenant_group)
528
+
529
+ def test_forward_migration_resolves_circular_dependency(self):
530
+ """Test that fresh database creation works with tenant on Namespace."""
531
+ Namespace = self.new_state.apps.get_model("ipam", "Namespace")
532
+ VRF = self.new_state.apps.get_model("ipam", "VRF")
533
+ Prefix = self.new_state.apps.get_model("ipam", "Prefix")
534
+
535
+ # Verify Global namespace exists
536
+ global_namespace = Namespace.objects.get(name="Global")
537
+ self.assertIsNotNone(global_namespace)
538
+
539
+ # Verify tenant field exists on Namespace
540
+ self.assertTrue(hasattr(global_namespace, "tenant"))
541
+
542
+ # Verify VRF and Prefix have namespace defaults working
543
+ vrf_field = VRF._meta.get_field("namespace")
544
+ prefix_field = Prefix._meta.get_field("namespace")
545
+ self.assertIsNotNone(vrf_field.default)
546
+ self.assertIsNotNone(prefix_field.default)
547
+
548
+ def test_namespace_tenant_field_added(self):
549
+ """Test that tenant field was properly added to Namespace model."""
550
+ Namespace = self.new_state.apps.get_model("ipam", "Namespace")
551
+
552
+ # Create a namespace with tenant assignment
553
+ test_namespace = Namespace.objects.create(
554
+ name="Test Namespace", tenant_id=self.tenant1.pk, description="Test namespace with tenant"
555
+ )
556
+
557
+ # Verify tenant field exists and works
558
+ self.assertEqual(test_namespace.tenant_id, self.tenant1.pk)
559
+ self.assertEqual(test_namespace.name, "Test Namespace")
560
+
561
+ # Verify tenant relationship works
562
+ retrieved_namespace = Namespace.objects.get(name="Test Namespace")
563
+ self.assertEqual(retrieved_namespace.tenant_id, self.tenant1.pk)
564
+
565
+
566
+ class NamespaceTenantRollbackTestCase(MigratorTestCase):
567
+ """Test rolling back the namespace-tenant feature works safely."""
568
+
569
+ # For rollback testing: migrate_from is AFTER our changes, migrate_to is BEFORE
570
+ migrate_from = ("ipam", "0054_namespace_tenant") # After our complete sequence
571
+ migrate_to = ("ipam", "0052_alter_ipaddress_index_together_and_more") # Before our changes
572
+
573
+ def prepare(self):
574
+ """Create namespace data in the 'after' state to test rollback."""
575
+ # Create tenancy data first
576
+ TenantGroup = self.old_state.apps.get_model("tenancy", "TenantGroup")
577
+ Tenant = self.old_state.apps.get_model("tenancy", "Tenant")
578
+
579
+ self.tenant_group = TenantGroup.objects.create(name="Test Tenant Group")
580
+ self.tenant1 = Tenant.objects.create(name="Test Tenant 1", tenant_group=self.tenant_group)
581
+
582
+ # Create namespace with tenant assignment in the 'after' state
583
+ Namespace = self.old_state.apps.get_model("ipam", "Namespace")
584
+ self.test_namespace = Namespace.objects.create(
585
+ name="Test Namespace", tenant_id=self.tenant1.pk, description="Test namespace for rollback"
586
+ )
587
+
588
+ def test_rollback_migration_preserves_data_integrity(self):
589
+ """Test that rolling back preserves namespace data but removes tenant field."""
590
+ # In the rolled-back state, tenant field should be gone
591
+ Namespace = self.new_state.apps.get_model("ipam", "Namespace")
592
+ rolled_back_namespace = Namespace.objects.get(pk=self.test_namespace.pk)
593
+
594
+ # Verify core fields preserved
595
+ self.assertEqual(rolled_back_namespace.name, "Test Namespace")
596
+ self.assertEqual(rolled_back_namespace.description, "Test namespace for rollback")
597
+
598
+ # Verify tenant field no longer exists (rolled back)
599
+ self.assertFalse(hasattr(rolled_back_namespace, "tenant"))