nautobot 3.0.0a2__py3-none-any.whl → 3.0.0rc1__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.
Files changed (659) hide show
  1. nautobot/apps/choices.py +4 -2
  2. nautobot/apps/filters.py +7 -9
  3. nautobot/apps/models.py +2 -2
  4. nautobot/apps/ui.py +13 -1
  5. nautobot/apps/utils.py +8 -0
  6. nautobot/circuits/filters.py +3 -2
  7. nautobot/circuits/navigation.py +3 -2
  8. nautobot/circuits/templates/circuits/circuit_create.html +3 -3
  9. nautobot/circuits/templates/circuits/circuittermination_create.html +9 -24
  10. nautobot/circuits/templates/circuits/inc/circuit_termination_cable_fragment.html +6 -6
  11. nautobot/circuits/templates/circuits/inc/speed_widget.html +12 -12
  12. nautobot/circuits/tests/integration/test_circuit.py +10 -13
  13. nautobot/circuits/tests/integration/test_circuits_bulk_operations.py +0 -3
  14. nautobot/circuits/views.py +6 -2
  15. nautobot/cloud/filters.py +1 -1
  16. nautobot/cloud/navigation.py +3 -2
  17. nautobot/core/api/schema.py +1 -1
  18. nautobot/core/api/serializers.py +6 -1
  19. nautobot/core/api/urls.py +2 -0
  20. nautobot/core/api/views.py +12 -0
  21. nautobot/core/apps/__init__.py +11 -10
  22. nautobot/core/celery/__init__.py +3 -5
  23. nautobot/core/checks.py +46 -0
  24. nautobot/core/choices.py +1 -1
  25. nautobot/core/cli/bootstrap_v3_to_v5.py +105 -13
  26. nautobot/core/cli/migrate_deprecated_templates.py +227 -0
  27. nautobot/core/constants.py +3 -0
  28. nautobot/core/context_processors.py +9 -1
  29. nautobot/core/filters.py +4 -0
  30. nautobot/core/forms/__init__.py +2 -0
  31. nautobot/core/forms/forms.py +1 -1
  32. nautobot/core/forms/widgets.py +21 -2
  33. nautobot/core/jobs/__init__.py +62 -3
  34. nautobot/core/jobs/groups.py +31 -1
  35. nautobot/core/management/commands/generate_test_data.py +28 -9
  36. nautobot/core/models/__init__.py +11 -0
  37. nautobot/core/models/generics.py +9 -1
  38. nautobot/core/models/tree_queries.py +10 -5
  39. nautobot/core/models/utils.py +1 -1
  40. nautobot/core/settings.py +35 -19
  41. nautobot/core/settings.yaml +17 -33
  42. nautobot/core/signals.py +12 -1
  43. nautobot/core/tables.py +13 -6
  44. nautobot/core/templates/40x.html +1 -1
  45. nautobot/core/templates/500.html +2 -2
  46. nautobot/core/templates/admin/base.html +1 -2
  47. nautobot/core/templates/admin/change_list.html +9 -12
  48. nautobot/core/templates/admin/config/config.html +12 -12
  49. nautobot/core/templates/admin/index.html +3 -3
  50. nautobot/core/templates/base_django.html +1 -2
  51. nautobot/core/templates/buttons/export.html +1 -1
  52. nautobot/core/templates/components/button/dropdown.html +5 -3
  53. nautobot/core/templates/components/panel/body_wrapper_generic_table.html +1 -1
  54. nautobot/core/templates/components/panel/header_extra_content_table.html +1 -1
  55. nautobot/core/templates/components/panel/panel.html +3 -3
  56. nautobot/core/templates/components/tab/content_wrapper.html +6 -7
  57. nautobot/core/templates/components/tab/label_wrapper_distinct_view.html +1 -1
  58. nautobot/core/templates/echarts/echarts.html +22 -9
  59. nautobot/core/templates/generic/object_bulk_add_component.html +2 -1
  60. nautobot/core/templates/generic/object_bulk_create.html +6 -5
  61. nautobot/core/templates/generic/object_bulk_delete.html +1 -1
  62. nautobot/core/templates/generic/object_bulk_destroy.html +3 -3
  63. nautobot/core/templates/generic/object_bulk_edit.html +1 -1
  64. nautobot/core/templates/generic/object_bulk_import.html +1 -1
  65. nautobot/core/templates/generic/object_bulk_remove.html +2 -2
  66. nautobot/core/templates/generic/object_bulk_update.html +5 -4
  67. nautobot/core/templates/generic/object_create.html +5 -4
  68. nautobot/core/templates/generic/object_delete.html +1 -1
  69. nautobot/core/templates/generic/object_detail.html +1 -1
  70. nautobot/core/templates/generic/object_edit.html +1 -1
  71. nautobot/core/templates/generic/object_import.html +2 -1
  72. nautobot/core/templates/generic/object_list.html +12 -4
  73. nautobot/core/templates/generic/object_notes.html +5 -3
  74. nautobot/core/templates/generic/object_retrieve.html +4 -5
  75. nautobot/core/templates/graphene/graphiql.html +7 -8
  76. nautobot/core/templates/home.html +1 -1
  77. nautobot/core/templates/import_success.html +2 -1
  78. nautobot/core/templates/inc/computed_fields/panel_data.html +1 -1
  79. nautobot/core/templates/inc/created_updated.html +7 -3
  80. nautobot/core/templates/inc/custom_fields/panel_data.html +1 -1
  81. nautobot/core/templates/inc/footer.html +3 -1
  82. nautobot/core/templates/inc/form_static_field.html +6 -0
  83. nautobot/core/templates/inc/header.html +11 -1
  84. nautobot/core/templates/inc/image_attachments.html +2 -1
  85. nautobot/core/templates/inc/media.html +14 -0
  86. nautobot/core/templates/inc/nav_menu.html +3 -9
  87. nautobot/core/templates/inc/object_details_advanced_panel.html +2 -2
  88. nautobot/core/templates/inc/search_panel.html +4 -4
  89. nautobot/core/templates/login.html +4 -2
  90. nautobot/core/templates/nautobot_config.py.j2 +6 -11
  91. nautobot/core/templates/redoc_ui.html +7 -0
  92. nautobot/core/templates/rest_framework/api.html +103 -2
  93. nautobot/core/templates/search.html +1 -1
  94. nautobot/core/templates/swagger_ui.html +17 -3
  95. nautobot/core/templates/system_jobs/import_objects.html +1 -2
  96. nautobot/core/templates/utilities/confirmation_form.html +2 -2
  97. nautobot/core/templates/utilities/obj_table.html +10 -2
  98. nautobot/core/templates/utilities/render_field.html +7 -7
  99. nautobot/core/templates/utilities/render_jinja2.html +2 -2
  100. nautobot/core/templates/utilities/templatetags/filter_form_drawer.html +37 -4
  101. nautobot/core/templates/utilities/theme_preview.html +19 -3
  102. nautobot/core/templates/widgets/number_input_with_choices.html +44 -0
  103. nautobot/core/templates/widgets/selectwithdisabled_option.html +3 -1
  104. nautobot/core/templatetags/helpers.py +76 -18
  105. nautobot/core/testing/api.py +68 -9
  106. nautobot/core/testing/filters.py +0 -23
  107. nautobot/core/testing/integration.py +41 -17
  108. nautobot/core/testing/mixins.py +2 -0
  109. nautobot/core/testing/utils.py +18 -4
  110. nautobot/core/testing/views.py +104 -13
  111. nautobot/core/tests/integration/test_app_home.py +34 -30
  112. nautobot/core/tests/integration/test_app_navbar.py +3 -0
  113. nautobot/core/tests/integration/test_filters.py +48 -11
  114. nautobot/core/tests/integration/test_theme.py +22 -21
  115. nautobot/core/tests/nautobot_config.py +3 -0
  116. nautobot/core/tests/nautobot_config_without_example_apps.py +4 -0
  117. nautobot/core/tests/runner.py +8 -1
  118. nautobot/core/tests/test_api.py +5 -3
  119. nautobot/core/tests/test_breadcrumbs.py +27 -28
  120. nautobot/core/tests/test_checks.py +28 -0
  121. nautobot/core/tests/test_cli.py +40 -0
  122. nautobot/core/tests/test_config.py +2 -1
  123. nautobot/core/tests/test_forms.py +55 -13
  124. nautobot/core/tests/test_jobs.py +144 -3
  125. nautobot/core/tests/test_nautobot_server.py +2 -0
  126. nautobot/core/tests/test_navigations.py +76 -1
  127. nautobot/core/tests/test_patch_social_django.py +42 -0
  128. nautobot/core/tests/test_renderers.py +59 -0
  129. nautobot/core/tests/test_settings_schema.py +1 -0
  130. nautobot/core/tests/test_tables.py +3 -1
  131. nautobot/core/tests/test_templatetags_helpers.py +62 -13
  132. nautobot/core/tests/test_templatetags_ui_framework.py +4 -4
  133. nautobot/core/tests/test_titles.py +0 -16
  134. nautobot/core/tests/test_tree_queries.py +14 -1
  135. nautobot/core/tests/test_ui.py +123 -4
  136. nautobot/core/tests/test_utils.py +72 -5
  137. nautobot/core/tests/test_views.py +159 -31
  138. nautobot/core/ui/breadcrumbs.py +70 -29
  139. nautobot/core/ui/bulk_buttons.py +1 -1
  140. nautobot/core/ui/choices.py +143 -27
  141. nautobot/core/ui/constants.py +76 -12
  142. nautobot/core/ui/echarts.py +15 -20
  143. nautobot/core/ui/object_detail.py +143 -55
  144. nautobot/core/ui/titles.py +3 -6
  145. nautobot/core/urls.py +20 -9
  146. nautobot/core/utils/cache.py +2 -1
  147. nautobot/core/utils/filtering.py +28 -18
  148. nautobot/core/utils/lookup.py +49 -8
  149. nautobot/core/utils/module_loading.py +21 -0
  150. nautobot/core/utils/patch_social_django.py +128 -0
  151. nautobot/core/views/__init__.py +38 -1
  152. nautobot/core/views/generic.py +3 -3
  153. nautobot/core/views/mixins.py +45 -22
  154. nautobot/core/views/renderers.py +4 -3
  155. nautobot/core/views/viewsets.py +2 -1
  156. nautobot/data_validation/apps.py +1 -5
  157. nautobot/data_validation/custom_validators.py +4 -4
  158. nautobot/data_validation/filters.py +1 -1
  159. nautobot/data_validation/forms.py +40 -0
  160. nautobot/data_validation/migrations/0001_initial.py +0 -7
  161. nautobot/data_validation/migrations/0002_data_migration_from_app.py +3 -14
  162. nautobot/data_validation/models.py +16 -7
  163. nautobot/data_validation/navigation.py +8 -1
  164. nautobot/data_validation/tables.py +12 -5
  165. nautobot/data_validation/templates/data_validation/datacompliance_tab.html +1 -0
  166. nautobot/data_validation/templates/data_validation/device_constraints.html +61 -0
  167. nautobot/data_validation/tests/__init__.py +2 -2
  168. nautobot/data_validation/tests/migrations/test_migrations.py +83 -3
  169. nautobot/data_validation/tests/test_data_compliance_rules.py +12 -7
  170. nautobot/data_validation/tests/test_filters.py +8 -6
  171. nautobot/data_validation/tests/test_models.py +15 -0
  172. nautobot/data_validation/tests/test_views.py +190 -32
  173. nautobot/data_validation/urls.py +2 -5
  174. nautobot/data_validation/views.py +73 -40
  175. nautobot/dcim/api/serializers.py +3 -13
  176. nautobot/dcim/apps.py +4 -0
  177. nautobot/dcim/choices.py +65 -0
  178. nautobot/dcim/constants.py +7 -0
  179. nautobot/dcim/custom_validators.py +84 -0
  180. nautobot/dcim/factory.py +1 -1
  181. nautobot/dcim/filter_mixins.py +353 -4
  182. nautobot/dcim/{filters/__init__.py → filters.py} +15 -36
  183. nautobot/dcim/forms.py +90 -4
  184. nautobot/dcim/migrations/0075_interface_duplex_interface_speed_and_more.py +32 -0
  185. nautobot/dcim/migrations/{0075_add_deviceclusterassignment.py → 0076_add_deviceclusterassignment.py} +1 -1
  186. nautobot/dcim/migrations/{0076_device_cluster_to_clusters_data_migration.py → 0077_device_cluster_to_clusters_data_migration.py} +1 -1
  187. nautobot/dcim/migrations/{0077_remove_device_cluster.py → 0078_remove_device_cluster.py} +1 -1
  188. nautobot/dcim/migrations/0079_remove_device_location_tenant_name_uniqueness.py +16 -0
  189. nautobot/dcim/migrations/0080_device_name_data_migration.py +59 -0
  190. nautobot/dcim/migrations/0081_alter_device_device_redundancy_group_priority_and_more.py +25 -0
  191. nautobot/dcim/models/device_component_templates.py +33 -1
  192. nautobot/dcim/models/device_components.py +98 -64
  193. nautobot/dcim/models/devices.py +30 -20
  194. nautobot/dcim/navigation.py +7 -6
  195. nautobot/dcim/tables/devices.py +18 -0
  196. nautobot/dcim/tables/devicetypes.py +8 -1
  197. nautobot/dcim/tables/racks.py +0 -2
  198. nautobot/dcim/tables/template_code.py +15 -15
  199. nautobot/dcim/templates/dcim/cable_connect.html +28 -112
  200. nautobot/dcim/templates/dcim/cable_trace.html +0 -4
  201. nautobot/dcim/templates/dcim/{cable_edit.html → cable_update.html} +1 -1
  202. nautobot/dcim/templates/dcim/consoleport.html +7 -6
  203. nautobot/dcim/templates/dcim/consoleserverport.html +7 -6
  204. nautobot/dcim/templates/dcim/device/config.html +2 -2
  205. nautobot/dcim/templates/dcim/device/lldp_neighbors.html +1 -1
  206. nautobot/dcim/templates/dcim/device/status.html +8 -8
  207. nautobot/dcim/templates/dcim/device.html +1 -1
  208. nautobot/dcim/templates/dcim/device_component_add.html +2 -2
  209. nautobot/dcim/templates/dcim/device_create.html +5 -3
  210. nautobot/dcim/templates/dcim/device_interface_delete.html +1 -1
  211. nautobot/dcim/templates/dcim/device_list.html +73 -10
  212. nautobot/dcim/templates/dcim/devicebay.html +1 -1
  213. nautobot/dcim/templates/dcim/devicebay_populate.html +2 -2
  214. nautobot/dcim/templates/dcim/devicetype_component_add.html +2 -2
  215. nautobot/dcim/templates/dcim/footer_convert_to_contact_or_team_record.html +14 -0
  216. nautobot/dcim/templates/dcim/frontport.html +10 -9
  217. nautobot/dcim/templates/dcim/inc/devicetype_component_table.html +1 -1
  218. nautobot/dcim/templates/dcim/inc/edit_form_softwareversion_js.html +2 -2
  219. nautobot/dcim/templates/dcim/inc/moduletype_component_table.html +1 -1
  220. nautobot/dcim/templates/dcim/inc/rack_elevation.html +1 -1
  221. nautobot/dcim/templates/dcim/interface.html +35 -7
  222. nautobot/dcim/templates/dcim/interface_bulk_delete.html +1 -1
  223. nautobot/dcim/templates/dcim/interface_edit.html +2 -0
  224. nautobot/dcim/templates/dcim/inventoryitem.html +1 -1
  225. nautobot/dcim/templates/dcim/inventoryitem_add.html +3 -1
  226. nautobot/dcim/templates/dcim/inventoryitem_bulk_delete.html +1 -1
  227. nautobot/dcim/templates/dcim/inventoryitem_edit.html +3 -1
  228. nautobot/dcim/templates/dcim/module/base.html +49 -9
  229. nautobot/dcim/templates/dcim/module_consoleports.html +1 -1
  230. nautobot/dcim/templates/dcim/module_consoleserverports.html +1 -1
  231. nautobot/dcim/templates/dcim/module_frontports.html +1 -1
  232. nautobot/dcim/templates/dcim/module_interfaces.html +1 -1
  233. nautobot/dcim/templates/dcim/module_list.html +57 -8
  234. nautobot/dcim/templates/dcim/module_modulebays.html +1 -1
  235. nautobot/dcim/templates/dcim/module_poweroutlets.html +1 -1
  236. nautobot/dcim/templates/dcim/module_powerports.html +1 -1
  237. nautobot/dcim/templates/dcim/module_rearports.html +1 -1
  238. nautobot/dcim/templates/dcim/modulefamily_retrieve.html +1 -1
  239. nautobot/dcim/templates/dcim/moduletype_list.html +2 -2
  240. nautobot/dcim/templates/dcim/moduletype_retrieve.html +49 -9
  241. nautobot/dcim/templates/dcim/platform_create.html +1 -1
  242. nautobot/dcim/templates/dcim/poweroutlet.html +1 -1
  243. nautobot/dcim/templates/dcim/powerport.html +6 -5
  244. nautobot/dcim/templates/dcim/rack_elevation_list.html +17 -5
  245. nautobot/dcim/templates/dcim/rack_retrieve.html +22 -15
  246. nautobot/dcim/templates/dcim/rearport.html +8 -7
  247. nautobot/dcim/templates/dcim/trace/cable.html +1 -1
  248. nautobot/dcim/templates/dcim/virtualchassis_add_member.html +16 -14
  249. nautobot/dcim/templates/dcim/virtualchassis_update.html +15 -7
  250. nautobot/dcim/tests/integration/test_controller.py +4 -6
  251. nautobot/dcim/tests/integration/test_controller_managed_device_group.py +1 -5
  252. nautobot/dcim/tests/integration/test_create_device.py +0 -2
  253. nautobot/dcim/tests/integration/test_device_bulk_operations.py +1 -3
  254. nautobot/dcim/tests/integration/test_fileinputpicker.py +6 -10
  255. nautobot/dcim/tests/integration/test_location_bulk_operations.py +0 -2
  256. nautobot/dcim/tests/integration/test_module_bay_position.py +3 -4
  257. nautobot/dcim/tests/test_api.py +194 -6
  258. nautobot/dcim/tests/test_custom_validators.py +229 -0
  259. nautobot/dcim/tests/test_filters.py +55 -7
  260. nautobot/dcim/tests/test_forms.py +110 -8
  261. nautobot/dcim/tests/test_graphql.py +44 -1
  262. nautobot/dcim/tests/test_models.py +328 -4
  263. nautobot/dcim/tests/test_tables.py +160 -0
  264. nautobot/dcim/tests/test_views.py +132 -29
  265. nautobot/dcim/urls.py +64 -21
  266. nautobot/dcim/utils.py +3 -3
  267. nautobot/dcim/views.py +777 -397
  268. nautobot/extras/api/views.py +60 -45
  269. nautobot/extras/choices.py +2 -13
  270. nautobot/extras/datasources/git.py +3 -1
  271. nautobot/extras/{filters/mixins.py → filter_mixins.py} +1 -1
  272. nautobot/extras/{filters/customfields.py → filter_mixins_customfields.py} +42 -6
  273. nautobot/extras/{filters/__init__.py → filters.py} +33 -48
  274. nautobot/extras/forms/forms.py +14 -15
  275. nautobot/extras/forms/mixins.py +0 -41
  276. nautobot/extras/jobs.py +2 -0
  277. nautobot/extras/jobs_ui.py +4 -3
  278. nautobot/extras/management/__init__.py +11 -0
  279. nautobot/extras/management/commands/refresh_dynamic_group_member_caches.py +4 -1
  280. nautobot/extras/migrations/0127_approval_workflow_models.py +6 -6
  281. nautobot/extras/migrations/0129_jobresult_debug_log_count_jobresult_error_log_count_and_more.py +37 -0
  282. nautobot/extras/migrations/0130_jobresult_generate_log_entry_counts.py +42 -0
  283. nautobot/extras/migrations/0131_configcontext_device_families.py +18 -0
  284. nautobot/extras/models/__init__.py +1 -2
  285. nautobot/extras/models/approvals.py +33 -14
  286. nautobot/extras/models/change_logging.py +4 -0
  287. nautobot/extras/models/contacts.py +2 -0
  288. nautobot/extras/models/groups.py +44 -5
  289. nautobot/extras/models/jobs.py +60 -4
  290. nautobot/extras/models/mixins.py +28 -0
  291. nautobot/extras/models/models.py +23 -2
  292. nautobot/extras/models/secrets.py +1 -0
  293. nautobot/extras/models/statuses.py +0 -15
  294. nautobot/extras/navigation.py +13 -9
  295. nautobot/extras/plugins/__init__.py +33 -55
  296. nautobot/extras/plugins/marketplace_manifest.yml +49 -1
  297. nautobot/extras/plugins/tables.py +3 -3
  298. nautobot/extras/plugins/urls.py +2 -21
  299. nautobot/extras/plugins/utils.py +1 -33
  300. nautobot/extras/plugins/views.py +0 -9
  301. nautobot/extras/querysets.py +8 -0
  302. nautobot/extras/signals.py +20 -19
  303. nautobot/extras/tables.py +64 -68
  304. nautobot/extras/templates/django_ajax_tables/ajax_wrapper.html +2 -0
  305. nautobot/extras/templates/extras/approval_dashboard.html +7 -5
  306. nautobot/extras/templates/extras/approvalworkflowdefinition_update.html +4 -2
  307. nautobot/extras/templates/extras/approvalworkflowstage_retrieve.html +20 -12
  308. nautobot/extras/templates/extras/configcontext_update.html +1 -0
  309. nautobot/extras/templates/extras/configcontextschema_validation.html +2 -2
  310. nautobot/extras/templates/extras/dynamicgroup_retrieve.html +11 -5
  311. nautobot/extras/templates/extras/dynamicgroup_update.html +1 -1
  312. nautobot/extras/templates/extras/gitrepository_result.html +0 -2
  313. nautobot/extras/templates/extras/inc/approval_buttons_column.html +20 -6
  314. nautobot/extras/templates/extras/inc/bulk_edit_overridable_field.html +8 -7
  315. nautobot/extras/templates/extras/inc/configcontext_format.html +10 -3
  316. nautobot/extras/templates/extras/inc/graphqlquery_execute.html +71 -0
  317. nautobot/extras/templates/extras/inc/job_tiles.html +15 -3
  318. nautobot/extras/templates/extras/inc/json_format.html +10 -3
  319. nautobot/extras/templates/extras/inc/overridable_field.html +13 -12
  320. nautobot/extras/templates/extras/job.html +29 -12
  321. nautobot/extras/templates/extras/job_bulk_edit.html +18 -0
  322. nautobot/extras/templates/extras/job_edit.html +52 -46
  323. nautobot/extras/templates/extras/job_list.html +29 -25
  324. nautobot/extras/templates/extras/marketplace.html +5 -9
  325. nautobot/extras/templates/extras/object_configcontext.html +1 -1
  326. nautobot/extras/templates/extras/object_dynamicgroups.html +2 -2
  327. nautobot/extras/templates/extras/objectchange_retrieve.html +19 -39
  328. nautobot/extras/templates/extras/plugin_detail.html +29 -24
  329. nautobot/extras/templates/extras/plugins_list.html +16 -26
  330. nautobot/extras/templates/extras/role_retrieve.html +64 -0
  331. nautobot/extras/templates/extras/scheduledjob.html +4 -2
  332. nautobot/extras/templates/extras/secret_create.html +1 -1
  333. nautobot/extras/templatetags/custom_links.py +12 -12
  334. nautobot/extras/templatetags/job_buttons.py +14 -12
  335. nautobot/extras/test_jobs/invalid_import.py +9 -0
  336. nautobot/extras/test_jobs/log_counts_by_level.py +23 -0
  337. nautobot/extras/test_jobs/missing_import.py +11 -0
  338. nautobot/extras/tests/integration/test_computedfields.py +8 -9
  339. nautobot/extras/tests/integration/test_configcontextschema.py +27 -26
  340. nautobot/extras/tests/integration/test_customfields.py +9 -10
  341. nautobot/extras/tests/integration/test_dynamicgroups.py +12 -9
  342. nautobot/extras/tests/integration/test_plugin_banner.py +3 -0
  343. nautobot/extras/tests/integration/test_plugins.py +18 -6
  344. nautobot/extras/tests/integration/test_relationships.py +0 -2
  345. nautobot/extras/tests/test_api.py +90 -18
  346. nautobot/extras/tests/test_approvals.py +38 -38
  347. nautobot/extras/tests/test_changelog.py +59 -5
  348. nautobot/extras/tests/test_customfields.py +22 -13
  349. nautobot/extras/tests/test_customfields_filters.py +479 -0
  350. nautobot/extras/tests/test_dynamicgroups.py +39 -1
  351. nautobot/extras/tests/test_filters.py +57 -22
  352. nautobot/extras/tests/test_forms.py +18 -21
  353. nautobot/extras/tests/test_jobs.py +25 -4
  354. nautobot/extras/tests/test_migrations.py +1 -0
  355. nautobot/extras/tests/test_models.py +51 -33
  356. nautobot/extras/tests/test_plugins.py +36 -10
  357. nautobot/extras/tests/test_utils.py +3 -4
  358. nautobot/extras/tests/test_views.py +52 -112
  359. nautobot/extras/urls.py +0 -14
  360. nautobot/extras/views.py +164 -71
  361. nautobot/ipam/factory.py +7 -0
  362. nautobot/ipam/filter_mixins.py +38 -0
  363. nautobot/ipam/filters.py +53 -38
  364. nautobot/ipam/formfields.py +1 -1
  365. nautobot/ipam/forms.py +6 -3
  366. nautobot/ipam/migrations/0030_ipam__namespaces.py +13 -0
  367. nautobot/ipam/migrations/0031_ipam___data_migrations.py +4 -1
  368. nautobot/ipam/migrations/0054_namespace_tenant.py +25 -0
  369. nautobot/ipam/models.py +29 -2
  370. nautobot/ipam/navigation.py +3 -2
  371. nautobot/ipam/signals.py +71 -0
  372. nautobot/ipam/tables.py +19 -6
  373. nautobot/ipam/templates/ipam/inc/toggle_available.html +10 -10
  374. nautobot/ipam/templates/ipam/inc/vlangroup_header.html +1 -0
  375. nautobot/ipam/templates/ipam/ipaddress.html +14 -0
  376. nautobot/ipam/templates/ipam/ipaddress_merge.html +3 -3
  377. nautobot/ipam/templates/ipam/ipaddresstointerface_retrieve.html +1 -0
  378. nautobot/ipam/templates/ipam/namespace_ip_addresses.html +1 -1
  379. nautobot/ipam/templates/ipam/namespace_prefixes.html +1 -1
  380. nautobot/ipam/templates/ipam/namespace_update.html +15 -0
  381. nautobot/ipam/templates/ipam/namespace_vrfs.html +1 -1
  382. nautobot/ipam/templates/ipam/prefix_delete.html +1 -1
  383. nautobot/ipam/templates/ipam/prefix_list.html +14 -13
  384. nautobot/ipam/templates/ipam/vlan_interfaces.html +1 -1
  385. nautobot/ipam/templates/ipam/vlan_vminterfaces.html +1 -1
  386. nautobot/ipam/tests/migration/test_migrations.py +89 -0
  387. nautobot/ipam/tests/test_api.py +13 -6
  388. nautobot/ipam/tests/test_filters.py +36 -1
  389. nautobot/ipam/tests/test_forms.py +1 -1
  390. nautobot/ipam/tests/test_models.py +44 -2
  391. nautobot/ipam/tests/test_tables.py +1 -2
  392. nautobot/ipam/tests/test_utils.py +1 -1
  393. nautobot/ipam/tests/test_views.py +13 -14
  394. nautobot/ipam/ui.py +0 -17
  395. nautobot/ipam/utils/migrations.py +16 -2
  396. nautobot/ipam/utils/testing.py +9 -3
  397. nautobot/ipam/views.py +53 -11
  398. nautobot/load_balancers/__init__.py +0 -0
  399. nautobot/load_balancers/api/__init__.py +1 -0
  400. nautobot/load_balancers/api/serializers.py +75 -0
  401. nautobot/load_balancers/api/urls.py +23 -0
  402. nautobot/load_balancers/api/views.py +61 -0
  403. nautobot/load_balancers/apps.py +17 -0
  404. nautobot/load_balancers/choices.py +167 -0
  405. nautobot/load_balancers/filters.py +225 -0
  406. nautobot/load_balancers/forms.py +532 -0
  407. nautobot/load_balancers/management/commands/__init__.py +0 -0
  408. nautobot/load_balancers/management/commands/generate_load_balancer_models_test_data.py +38 -0
  409. nautobot/load_balancers/migrations/0001_initial.py +465 -0
  410. nautobot/load_balancers/migrations/0002_create_default_statuses_pool_members.py +31 -0
  411. nautobot/load_balancers/migrations/__init__.py +0 -0
  412. nautobot/load_balancers/models.py +423 -0
  413. nautobot/load_balancers/navigation.py +80 -0
  414. nautobot/load_balancers/tables.py +255 -0
  415. nautobot/load_balancers/tests/__init__.py +474 -0
  416. nautobot/load_balancers/tests/test_api.py +353 -0
  417. nautobot/load_balancers/tests/test_filters.py +134 -0
  418. nautobot/load_balancers/tests/test_forms.py +266 -0
  419. nautobot/load_balancers/tests/test_models.py +195 -0
  420. nautobot/load_balancers/tests/test_views.py +229 -0
  421. nautobot/load_balancers/urls.py +17 -0
  422. nautobot/load_balancers/views.py +248 -0
  423. nautobot/project-static/dist/css/github-dark.min.css +10 -0
  424. nautobot/project-static/dist/css/github.min.css +10 -0
  425. nautobot/project-static/dist/css/nautobot.css +1 -11
  426. nautobot/project-static/dist/css/nautobot.css.map +1 -1
  427. nautobot/project-static/dist/js/libraries.js +1 -1
  428. nautobot/project-static/dist/js/libraries.js.map +1 -1
  429. nautobot/project-static/dist/js/nautobot.js +1 -1
  430. nautobot/project-static/dist/js/nautobot.js.map +1 -1
  431. nautobot/project-static/js/cabletrace.js +1 -1
  432. nautobot/project-static/js/forms.js +13 -0
  433. nautobot/project-static/js/interface_filtering.js +20 -16
  434. nautobot/project-static/nautobot-icons/battery-3.svg +3 -0
  435. nautobot/project-static/nautobot-icons/bus-globe.svg +3 -0
  436. nautobot/project-static/nautobot-icons/bus-shield-check.svg +3 -0
  437. nautobot/project-static/nautobot-icons/bus-shield.svg +3 -0
  438. nautobot/project-static/nautobot-icons/cloud.svg +1 -1
  439. nautobot/project-static/nautobot-icons/control-panel.svg +1 -1
  440. nautobot/project-static/nautobot-icons/device-lifecycle.svg +1 -1
  441. nautobot/project-static/nautobot-icons/elements.svg +1 -1
  442. nautobot/project-static/nautobot-icons/extensibility.svg +3 -0
  443. nautobot/project-static/nautobot-icons/hammer.svg +1 -1
  444. nautobot/project-static/nautobot-icons/organization.svg +3 -0
  445. nautobot/project-static/nautobot-icons/secrets.svg +1 -1
  446. nautobot/project-static/nautobot-icons/security.svg +3 -0
  447. nautobot/project-static/nautobot-icons/server.svg +1 -1
  448. nautobot/project-static/nautobot-icons/star-filled.svg +1 -1
  449. nautobot/project-static/nautobot-icons/star.svg +1 -1
  450. nautobot/tenancy/api/serializers.py +1 -0
  451. nautobot/tenancy/api/views.py +2 -1
  452. nautobot/tenancy/{filters/__init__.py → filters.py} +2 -10
  453. nautobot/tenancy/navigation.py +3 -1
  454. nautobot/tenancy/tests/test_filters.py +0 -2
  455. nautobot/tenancy/views.py +2 -1
  456. nautobot/ui/package-lock.json +87 -4
  457. nautobot/ui/package.json +2 -1
  458. nautobot/ui/src/js/collapse.js +3 -3
  459. nautobot/ui/src/js/nautobot.js +16 -1
  460. nautobot/ui/src/js/select2.js +53 -2
  461. nautobot/ui/src/scss/colors.scss +1 -1
  462. nautobot/ui/src/scss/nautobot.scss +112 -30
  463. nautobot/ui/webpack.config.js +13 -0
  464. nautobot/users/templates/users/preferences.html +11 -2
  465. nautobot/users/templates/users/profile.html +45 -12
  466. nautobot/users/templates/users/sessionkey_delete.html +1 -1
  467. nautobot/users/tests/test_api.py +4 -0
  468. nautobot/users/views.py +4 -2
  469. nautobot/virtualization/filters.py +6 -1
  470. nautobot/virtualization/models.py +1 -68
  471. nautobot/virtualization/navigation.py +3 -2
  472. nautobot/virtualization/templates/virtualization/virtual_machine_vminterface_delete.html +1 -1
  473. nautobot/virtualization/templates/virtualization/virtualmachine_list.html +2 -2
  474. nautobot/virtualization/templates/virtualization/virtualmachine_update.html +3 -1
  475. nautobot/virtualization/tests/test_api.py +3 -0
  476. nautobot/virtualization/tests/test_filters.py +10 -1
  477. nautobot/virtualization/tests/test_models.py +45 -4
  478. nautobot/virtualization/views.py +4 -1
  479. nautobot/vpn/__init__.py +0 -0
  480. nautobot/vpn/api/serializers.py +113 -0
  481. nautobot/vpn/api/urls.py +19 -0
  482. nautobot/vpn/api/views.py +70 -0
  483. nautobot/vpn/apps.py +8 -0
  484. nautobot/vpn/choices.py +171 -0
  485. nautobot/vpn/factory.py +219 -0
  486. nautobot/vpn/filters.py +234 -0
  487. nautobot/vpn/forms.py +487 -0
  488. nautobot/vpn/homepage.py +19 -0
  489. nautobot/vpn/migrations/0001_initial.py +541 -0
  490. nautobot/vpn/migrations/0002_populate_defaults.py +199 -0
  491. nautobot/vpn/migrations/__init__.py +0 -0
  492. nautobot/vpn/models.py +535 -0
  493. nautobot/vpn/navigation.py +98 -0
  494. nautobot/vpn/tables.py +383 -0
  495. nautobot/vpn/templates/vpn/vpnprofile_create.html +150 -0
  496. nautobot/vpn/tests/__init__.py +0 -0
  497. nautobot/vpn/tests/test_api.py +336 -0
  498. nautobot/vpn/tests/test_filters.py +139 -0
  499. nautobot/vpn/tests/test_forms.py +293 -0
  500. nautobot/vpn/tests/test_models.py +147 -0
  501. nautobot/vpn/tests/test_views.py +300 -0
  502. nautobot/vpn/urls.py +16 -0
  503. nautobot/vpn/views.py +495 -0
  504. nautobot/wireless/navigation.py +3 -2
  505. nautobot/wireless/tests/integration/test_radio_profile.py +1 -5
  506. nautobot/wireless/tests/test_api.py +1 -1
  507. {nautobot-3.0.0a2.dist-info → nautobot-3.0.0rc1.dist-info}/METADATA +15 -15
  508. {nautobot-3.0.0a2.dist-info → nautobot-3.0.0rc1.dist-info}/RECORD +514 -572
  509. {nautobot-3.0.0a2.dist-info → nautobot-3.0.0rc1.dist-info}/entry_points.txt +1 -0
  510. nautobot/circuits/templates/circuits/circuit.html +0 -2
  511. nautobot/circuits/templates/circuits/circuit_edit.html +0 -2
  512. nautobot/circuits/templates/circuits/circuit_retrieve.html +0 -2
  513. nautobot/circuits/templates/circuits/circuit_update.html +0 -1
  514. nautobot/circuits/templates/circuits/circuittermination.html +0 -2
  515. nautobot/circuits/templates/circuits/circuittermination_edit.html +0 -2
  516. nautobot/circuits/templates/circuits/circuittermination_retrieve.html +0 -2
  517. nautobot/circuits/templates/circuits/circuittermination_update.html +0 -1
  518. nautobot/circuits/templates/circuits/circuittype.html +0 -2
  519. nautobot/circuits/templates/circuits/circuittype_retrieve.html +0 -2
  520. nautobot/circuits/templates/circuits/inc/circuit_termination.html +0 -85
  521. nautobot/circuits/templates/circuits/provider.html +0 -2
  522. nautobot/circuits/templates/circuits/provider_edit.html +0 -2
  523. nautobot/circuits/templates/circuits/provider_retrieve.html +0 -1
  524. nautobot/circuits/templates/circuits/provider_update.html +0 -1
  525. nautobot/circuits/templates/circuits/providernetwork.html +0 -2
  526. nautobot/circuits/templates/circuits/providernetwork_retrieve.html +0 -2
  527. nautobot/cloud/templates/cloud/cloudaccount_retrieve.html +0 -2
  528. nautobot/cloud/templates/cloud/cloudnetwork_retrieve.html +0 -2
  529. nautobot/cloud/templates/cloud/cloudresourcetype_retrieve.html +0 -2
  530. nautobot/cloud/templates/cloud/cloudservice_retrieve.html +0 -2
  531. nautobot/core/templates/buttons/import.html +0 -9
  532. nautobot/data_validation/template_content.py +0 -42
  533. nautobot/data_validation/templates/data_validation/datacompliance_retrieve.html +0 -1
  534. nautobot/dcim/filters/mixins.py +0 -354
  535. nautobot/dcim/templates/dcim/controller/base.html +0 -2
  536. nautobot/dcim/templates/dcim/controller_retrieve.html +0 -2
  537. nautobot/dcim/templates/dcim/controller_wirelessnetworks.html +0 -2
  538. nautobot/dcim/templates/dcim/controllermanageddevicegroup_retrieve.html +0 -2
  539. nautobot/dcim/templates/dcim/device/base.html +0 -2
  540. nautobot/dcim/templates/dcim/device/consoleports.html +0 -2
  541. nautobot/dcim/templates/dcim/device/consoleserverports.html +0 -2
  542. nautobot/dcim/templates/dcim/device/devicebays.html +0 -2
  543. nautobot/dcim/templates/dcim/device/frontports.html +0 -2
  544. nautobot/dcim/templates/dcim/device/interfaces.html +0 -2
  545. nautobot/dcim/templates/dcim/device/inventory.html +0 -2
  546. nautobot/dcim/templates/dcim/device/modulebays.html +0 -2
  547. nautobot/dcim/templates/dcim/device/poweroutlets.html +0 -2
  548. nautobot/dcim/templates/dcim/device/powerports.html +0 -2
  549. nautobot/dcim/templates/dcim/device/rearports.html +0 -2
  550. nautobot/dcim/templates/dcim/device/wireless.html +0 -2
  551. nautobot/dcim/templates/dcim/device_component.html +0 -2
  552. nautobot/dcim/templates/dcim/device_edit.html +0 -2
  553. nautobot/dcim/templates/dcim/devicefamily_retrieve.html +0 -2
  554. nautobot/dcim/templates/dcim/deviceredundancygroup_retrieve.html +0 -2
  555. nautobot/dcim/templates/dcim/devicetype.html +0 -2
  556. nautobot/dcim/templates/dcim/devicetype_edit.html +0 -2
  557. nautobot/dcim/templates/dcim/devicetype_retrieve.html +0 -2
  558. nautobot/dcim/templates/dcim/inc/device_napalm_tabs.html +0 -1
  559. nautobot/dcim/templates/dcim/interfaceredundancygroup_retrieve.html +0 -2
  560. nautobot/dcim/templates/dcim/location.html +0 -2
  561. nautobot/dcim/templates/dcim/location_edit.html +0 -2
  562. nautobot/dcim/templates/dcim/location_retrieve.html +0 -243
  563. nautobot/dcim/templates/dcim/locationtype.html +0 -2
  564. nautobot/dcim/templates/dcim/locationtype_retrieve.html +0 -2
  565. nautobot/dcim/templates/dcim/manufacturer.html +0 -2
  566. nautobot/dcim/templates/dcim/modulebay_retrieve.html +0 -1
  567. nautobot/dcim/templates/dcim/platform.html +0 -2
  568. nautobot/dcim/templates/dcim/powerfeed.html +0 -2
  569. nautobot/dcim/templates/dcim/powerfeed_retrieve.html +0 -2
  570. nautobot/dcim/templates/dcim/powerpanel.html +0 -2
  571. nautobot/dcim/templates/dcim/powerpanel_edit.html +0 -2
  572. nautobot/dcim/templates/dcim/powerpanel_retrieve.html +0 -2
  573. nautobot/dcim/templates/dcim/rack.html +0 -2
  574. nautobot/dcim/templates/dcim/rack_edit.html +0 -2
  575. nautobot/dcim/templates/dcim/rackgroup.html +0 -2
  576. nautobot/dcim/templates/dcim/rackreservation.html +0 -2
  577. nautobot/dcim/templates/dcim/softwareimagefile_retrieve.html +0 -2
  578. nautobot/dcim/templates/dcim/softwareversion_retrieve.html +0 -2
  579. nautobot/dcim/templates/dcim/virtualchassis.html +0 -2
  580. nautobot/dcim/templates/dcim/virtualchassis_add.html +0 -2
  581. nautobot/dcim/templates/dcim/virtualchassis_edit.html +0 -2
  582. nautobot/dcim/templates/dcim/virtualchassis_retrieve.html +0 -2
  583. nautobot/dcim/templates/dcim/virtualdevicecontext_retrieve.html +0 -2
  584. nautobot/dcim/ui.py +0 -29
  585. nautobot/extras/templates/extras/computedfield.html +0 -2
  586. nautobot/extras/templates/extras/computedfield_retrieve.html +0 -2
  587. nautobot/extras/templates/extras/configcontext.html +0 -2
  588. nautobot/extras/templates/extras/configcontext_edit.html +0 -2
  589. nautobot/extras/templates/extras/configcontext_retrieve.html +0 -2
  590. nautobot/extras/templates/extras/configcontextschema.html +0 -2
  591. nautobot/extras/templates/extras/configcontextschema_edit.html +0 -2
  592. nautobot/extras/templates/extras/contact_retrieve.html +0 -2
  593. nautobot/extras/templates/extras/customfield.html +0 -2
  594. nautobot/extras/templates/extras/customfield_edit.html +0 -2
  595. nautobot/extras/templates/extras/customfield_retrieve.html +0 -2
  596. nautobot/extras/templates/extras/customlink.html +0 -2
  597. nautobot/extras/templates/extras/dynamicgroup.html +0 -2
  598. nautobot/extras/templates/extras/dynamicgroup_edit.html +0 -2
  599. nautobot/extras/templates/extras/exporttemplate.html +0 -2
  600. nautobot/extras/templates/extras/gitrepository.html +0 -2
  601. nautobot/extras/templates/extras/gitrepository_object_edit.html +0 -2
  602. nautobot/extras/templates/extras/graphqlquery.html +0 -2
  603. nautobot/extras/templates/extras/graphqlquery_list.html +0 -1
  604. nautobot/extras/templates/extras/graphqlquery_retrieve.html +0 -97
  605. nautobot/extras/templates/extras/job_detail.html +0 -2
  606. nautobot/extras/templates/extras/jobbutton_retrieve.html +0 -2
  607. nautobot/extras/templates/extras/jobhook.html +0 -2
  608. nautobot/extras/templates/extras/jobqueue_retrieve.html +0 -2
  609. nautobot/extras/templates/extras/jobresult.html +0 -2
  610. nautobot/extras/templates/extras/metadatatype_retrieve.html +0 -2
  611. nautobot/extras/templates/extras/note.html +0 -2
  612. nautobot/extras/templates/extras/note_retrieve.html +0 -1
  613. nautobot/extras/templates/extras/object_changelog.html +0 -2
  614. nautobot/extras/templates/extras/object_notes.html +0 -2
  615. nautobot/extras/templates/extras/objectchange.html +0 -2
  616. nautobot/extras/templates/extras/objectchange_list.html +0 -3
  617. nautobot/extras/templates/extras/relationship.html +0 -1
  618. nautobot/extras/templates/extras/secret.html +0 -1
  619. nautobot/extras/templates/extras/secret_edit.html +0 -1
  620. nautobot/extras/templates/extras/secretsgroup.html +0 -2
  621. nautobot/extras/templates/extras/secretsgroup_edit.html +0 -2
  622. nautobot/extras/templates/extras/secretsgroup_retrieve.html +0 -2
  623. nautobot/extras/templates/extras/status.html +0 -2
  624. nautobot/extras/templates/extras/tag.html +0 -2
  625. nautobot/extras/templates/extras/tag_edit.html +0 -2
  626. nautobot/extras/templates/extras/tag_retrieve.html +0 -2
  627. nautobot/extras/templates/extras/team_retrieve.html +0 -2
  628. nautobot/ipam/templates/ipam/inc/prefix_header_extra_content_table.html +0 -4
  629. nautobot/ipam/templates/ipam/namespace_retrieve.html +0 -1
  630. nautobot/ipam/templates/ipam/prefix.html +0 -2
  631. nautobot/ipam/templates/ipam/prefix_edit.html +0 -1
  632. nautobot/ipam/templates/ipam/prefix_retrieve.html +0 -2
  633. nautobot/ipam/templates/ipam/rir.html +0 -2
  634. nautobot/ipam/templates/ipam/routetarget.html +0 -1
  635. nautobot/ipam/templates/ipam/service.html +0 -2
  636. nautobot/ipam/templates/ipam/service_edit.html +0 -2
  637. nautobot/ipam/templates/ipam/service_retrieve.html +0 -2
  638. nautobot/ipam/templates/ipam/vlan.html +0 -2
  639. nautobot/ipam/templates/ipam/vlan_edit.html +0 -2
  640. nautobot/ipam/templates/ipam/vlan_retrieve.html +0 -2
  641. nautobot/ipam/templates/ipam/vlangroup.html +0 -2
  642. nautobot/ipam/templates/ipam/vrf.html +0 -1
  643. nautobot/tenancy/templates/tenancy/tenant.html +0 -2
  644. nautobot/tenancy/templates/tenancy/tenant_edit.html +0 -2
  645. nautobot/tenancy/templates/tenancy/tenantgroup.html +0 -2
  646. nautobot/tenancy/templates/tenancy/tenantgroup_retrieve.html +0 -1
  647. nautobot/virtualization/templates/virtualization/clustergroup.html +0 -2
  648. nautobot/virtualization/templates/virtualization/clustertype.html +0 -2
  649. nautobot/virtualization/templates/virtualization/virtualmachine.html +0 -2
  650. nautobot/virtualization/templates/virtualization/virtualmachine_edit.html +0 -2
  651. nautobot/virtualization/templates/virtualization/virtualmachine_retrieve.html +0 -2
  652. nautobot/wireless/templates/wireless/radioprofile_retrieve.html +0 -2
  653. nautobot/wireless/templates/wireless/supporteddatarate_retrieve.html +0 -2
  654. nautobot/wireless/templates/wireless/wirelessnetwork_retrieve.html +0 -2
  655. /nautobot/dcim/templates/dcim/{cable.html → cable_retrieve.html} +0 -0
  656. /nautobot/tenancy/{filters/mixins.py → filter_mixins.py} +0 -0
  657. {nautobot-3.0.0a2.dist-info → nautobot-3.0.0rc1.dist-info}/LICENSE.txt +0 -0
  658. {nautobot-3.0.0a2.dist-info → nautobot-3.0.0rc1.dist-info}/NOTICE +0 -0
  659. {nautobot-3.0.0a2.dist-info → nautobot-3.0.0rc1.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(
@@ -378,6 +379,11 @@ class PrefixTable(StatusTableMixin, RoleTableMixin, BaseTable):
378
379
  cloud_networks_count = LinkedCountColumn(
379
380
  viewname="cloud:cloudnetwork_list", url_params={"prefixes": "pk"}, verbose_name="Cloud Networks"
380
381
  )
382
+ tunnel_endpoints_count = LinkedCountColumn(
383
+ viewname="vpn:vpntunnelendpoint_list",
384
+ url_params={"protected_prefixes": "pk"},
385
+ verbose_name="VPN Tunnel Endpoints",
386
+ )
381
387
  actions = ButtonsColumn(Prefix)
382
388
 
383
389
  class Meta(BaseTable.Meta):
@@ -393,6 +399,7 @@ class PrefixTable(StatusTableMixin, RoleTableMixin, BaseTable):
393
399
  "tenant",
394
400
  "location_count",
395
401
  "cloud_networks_count",
402
+ "tunnel_endpoints_count",
396
403
  "vlan",
397
404
  "role",
398
405
  "rir",
@@ -415,7 +422,7 @@ class PrefixTable(StatusTableMixin, RoleTableMixin, BaseTable):
415
422
  "actions",
416
423
  )
417
424
  row_attrs = {
418
- "class": lambda record: "success" if not record.present_in_database else "",
425
+ "class": lambda record: "table-success" if not record.present_in_database else "",
419
426
  }
420
427
 
421
428
 
@@ -449,6 +456,7 @@ class PrefixDetailTable(PrefixTable):
449
456
  "role",
450
457
  "description",
451
458
  "tags",
459
+ "actions",
452
460
  )
453
461
  default_columns = (
454
462
  "pk",
@@ -463,6 +471,7 @@ class PrefixDetailTable(PrefixTable):
463
471
  "vlan",
464
472
  "role",
465
473
  "description",
474
+ "actions",
466
475
  )
467
476
 
468
477
 
@@ -499,6 +508,7 @@ class IPAddressTable(StatusTableMixin, RoleTableMixin, BaseTable):
499
508
  distinct=True,
500
509
  verbose_name="Virtual Machines",
501
510
  )
511
+ actions = ButtonsColumn(Prefix)
502
512
 
503
513
  class Meta(BaseTable.Meta):
504
514
  model = IPAddress
@@ -516,9 +526,10 @@ class IPAddressTable(StatusTableMixin, RoleTableMixin, BaseTable):
516
526
  "interface_parent_count",
517
527
  "vm_interface_count",
518
528
  "vm_interface_parent_count",
529
+ "actions",
519
530
  )
520
531
  row_attrs = {
521
- "class": lambda record: "success" if not isinstance(record, IPAddress) else "",
532
+ "class": lambda record: "table-success" if not isinstance(record, IPAddress) else "",
522
533
  }
523
534
 
524
535
 
@@ -545,6 +556,7 @@ class IPAddressDetailTable(IPAddressTable):
545
556
  "dns_name",
546
557
  "description",
547
558
  "tags",
559
+ "actions",
548
560
  )
549
561
  default_columns = (
550
562
  "pk",
@@ -557,6 +569,7 @@ class IPAddressDetailTable(IPAddressTable):
557
569
  "assigned",
558
570
  "dns_name",
559
571
  "description",
572
+ "actions",
560
573
  )
561
574
 
562
575
 
@@ -651,7 +664,7 @@ class IPAddressInterfaceTable(InterfaceTable):
651
664
  "connection",
652
665
  ]
653
666
  row_attrs = {
654
- "style": cable_status_color_css,
667
+ "class": cable_status_color_css,
655
668
  "data-name": lambda record: record.name,
656
669
  }
657
670
 
@@ -744,7 +757,7 @@ class VLANTable(StatusTableMixin, RoleTableMixin, BaseTable):
744
757
  "description",
745
758
  )
746
759
  row_attrs = {
747
- "class": lambda record: "success" if not isinstance(record, VLAN) else "",
760
+ "class": lambda record: "table-success" if not isinstance(record, VLAN) else "",
748
761
  }
749
762
 
750
763
 
@@ -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
 
@@ -1,4 +1,4 @@
1
- {% extends 'ipam/namespace_retrieve.html' %}
1
+ {% extends 'generic/object_retrieve.html' %}
2
2
 
3
3
  {% block title %}{{ block.super }} - IP Addresses{% endblock %}
4
4
 
@@ -1,4 +1,4 @@
1
- {% extends 'ipam/namespace_retrieve.html' %}
1
+ {% extends 'generic/object_retrieve.html' %}
2
2
 
3
3
  {% block title %}{{ block.super }} - Prefixes{% endblock %}
4
4
 
@@ -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 'ipam/namespace_retrieve.html' %}
1
+ {% extends 'generic/object_retrieve.html' %}
2
2
 
3
3
  {% block title %}{{ block.super }} - VRFs{% endblock %}
4
4
 
@@ -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,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"))