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
@@ -1,8 +1,17 @@
1
+ from constance.test import override_config
1
2
  from django.test import TestCase
2
3
 
3
4
  from nautobot.core.testing.forms import FormTestCases
4
5
  from nautobot.core.testing.mixins import NautobotTestCaseMixin
5
- from nautobot.dcim.choices import DeviceFaceChoices, InterfaceModeChoices, InterfaceTypeChoices, RackWidthChoices
6
+ from nautobot.dcim.choices import (
7
+ DeviceFaceChoices,
8
+ InterfaceDuplexChoices,
9
+ InterfaceModeChoices,
10
+ InterfaceSpeedChoices,
11
+ InterfaceTypeChoices,
12
+ RackWidthChoices,
13
+ )
14
+ from nautobot.dcim.constants import RACK_U_HEIGHT_DEFAULT
6
15
  from nautobot.dcim.forms import (
7
16
  DeviceFilterForm,
8
17
  DeviceForm,
@@ -327,24 +336,56 @@ class RackTestCase(TestCase):
327
336
  form = RackForm(data=data, instance=racks[0])
328
337
  self.assertTrue(form.is_valid())
329
338
 
339
+ def test_rack_form_initial_u_height_default(self):
340
+ """Test that RackForm sets initial u_height from default Constance config (42)."""
341
+ # Create a new form (not bound to an instance)
342
+ form = RackForm()
343
+
344
+ # The initial value should be 42 (default Constance config)
345
+ self.assertEqual(form.fields["u_height"].initial, RACK_U_HEIGHT_DEFAULT)
346
+
347
+ @override_config(RACK_DEFAULT_U_HEIGHT=48)
348
+ def test_rack_form_initial_u_height_custom(self):
349
+ """Test that RackForm sets initial u_height from custom Constance config."""
350
+ # Create a new form (not bound to an instance)
351
+ form = RackForm()
352
+
353
+ # The initial value should be 48 (from Constance config)
354
+ self.assertEqual(form.fields["u_height"].initial, 48)
355
+
356
+ def test_rack_form_initial_u_height_not_set_on_edit(self):
357
+ """Test that RackForm does NOT override u_height when editing an existing rack."""
358
+ location = Location.objects.filter(location_type=LocationType.objects.get(name="Campus")).first()
359
+ status = Status.objects.get(name="Active")
360
+
361
+ # Create a rack with u_height of 24
362
+ rack = Rack.objects.create(name="Test Rack", location=location, status=status, u_height=24)
363
+
364
+ # Create a form bound to the existing rack
365
+ form = RackForm(instance=rack)
366
+
367
+ # The initial value should NOT be overridden - it should use the rack's actual value
368
+ # (The form will show the model instance's value, not the Constance config)
369
+ self.assertEqual(form.initial["u_height"], 24)
370
+
330
371
 
331
372
  class InterfaceTestCase(NautobotTestCaseMixin, TestCase):
332
373
  @classmethod
333
374
  def setUpTestData(cls):
334
375
  cls.device = Device.objects.first()
335
- status = Status.objects.get_for_model(Interface).first()
376
+ cls.status = Status.objects.get_for_model(Interface).first()
336
377
  cls.interface = Interface.objects.create(
337
378
  device=cls.device,
338
379
  name="test interface form 0.0",
339
380
  type=InterfaceTypeChoices.TYPE_2GFC_SFP,
340
- status=status,
381
+ status=cls.status,
341
382
  )
342
383
  cls.vlan = VLAN.objects.first()
343
384
  cls.data = {
344
385
  "device": cls.device.pk,
345
386
  "name": "test interface form 0.0",
346
387
  "type": InterfaceTypeChoices.TYPE_2GFC_SFP,
347
- "status": status.pk,
388
+ "status": cls.status.pk,
348
389
  "mode": InterfaceModeChoices.MODE_TAGGED,
349
390
  "tagged_vlans": [cls.vlan.pk],
350
391
  }
@@ -394,7 +435,6 @@ class InterfaceTestCase(NautobotTestCaseMixin, TestCase):
394
435
  Assert that untagged_vlans field dropdown are populated correctly in InterfaceForm and InterfaceBulkEditForm,
395
436
  and that the queryset is the same for both forms.
396
437
  """
397
- status = Status.objects.get_for_model(Interface).first()
398
438
  location = Location.objects.filter(location_type=LocationType.objects.get(name="Campus")).first()
399
439
  devices = Device.objects.all()[:3]
400
440
  for device in devices:
@@ -405,19 +445,19 @@ class InterfaceTestCase(NautobotTestCaseMixin, TestCase):
405
445
  device=devices[0],
406
446
  name="Test Interface 1",
407
447
  type=InterfaceTypeChoices.TYPE_2GFC_SFP,
408
- status=status,
448
+ status=self.status,
409
449
  ),
410
450
  Interface.objects.create(
411
451
  device=devices[1],
412
452
  name="Test Interface 2",
413
453
  type=InterfaceTypeChoices.TYPE_LAG,
414
- status=status,
454
+ status=self.status,
415
455
  ),
416
456
  Interface.objects.create(
417
457
  device=devices[2],
418
458
  name="Test Interface 3",
419
459
  type=InterfaceTypeChoices.TYPE_100ME_FIXED,
420
- status=status,
460
+ status=self.status,
421
461
  ),
422
462
  )
423
463
  edit_form = InterfaceForm(data=self.data, instance=interfaces[0])
@@ -429,3 +469,65 @@ class InterfaceTestCase(NautobotTestCaseMixin, TestCase):
429
469
  edit_form.fields["untagged_vlan"].queryset,
430
470
  bulk_edit_form.fields["untagged_vlan"].queryset,
431
471
  )
472
+
473
+ def test_interface_form_fields_and_blank(self):
474
+ data = {
475
+ "device": self.device.pk,
476
+ "name": self.interface.name,
477
+ "type": InterfaceTypeChoices.TYPE_1GE_FIXED,
478
+ "status": self.status.pk,
479
+ "speed": "", # blank should coerce to None
480
+ "duplex": "", # blank allowed
481
+ }
482
+ form = InterfaceForm(data=data, instance=self.interface)
483
+ self.assertIn("speed", form.fields)
484
+ self.assertIn("duplex", form.fields)
485
+ self.assertTrue(form.is_valid())
486
+ self.assertIsNone(form.cleaned_data["speed"]) # TypedChoiceField(empty->None)
487
+ self.assertEqual(form.cleaned_data["duplex"], "")
488
+
489
+ def test_interface_form_speed_choice_coerces_int(self):
490
+ speed_choice = InterfaceSpeedChoices.SPEED_10G
491
+ data = {
492
+ "device": self.device.pk,
493
+ "name": self.interface.name,
494
+ "type": InterfaceTypeChoices.TYPE_1GE_FIXED,
495
+ "status": self.status.pk,
496
+ # Posted value is a string; TypedChoiceField should coerce to int
497
+ "speed": str(speed_choice),
498
+ "duplex": InterfaceDuplexChoices.DUPLEX_FULL,
499
+ }
500
+ form = InterfaceForm(data=data, instance=self.interface)
501
+ self.assertTrue(form.is_valid())
502
+ self.assertIsInstance(form.cleaned_data["speed"], int)
503
+ self.assertEqual(form.cleaned_data["speed"], speed_choice)
504
+ self.assertEqual(form.cleaned_data["duplex"], InterfaceDuplexChoices.DUPLEX_FULL)
505
+
506
+ def test_interface_create_form_blank_and_choice(self):
507
+ # Blank speed
508
+ data_blank = {
509
+ "device": self.device.pk,
510
+ "name_pattern": "eth1",
511
+ "status": self.status.pk,
512
+ "type": InterfaceTypeChoices.TYPE_1GE_FIXED,
513
+ "speed": "",
514
+ "duplex": "",
515
+ }
516
+ form_blank = InterfaceCreateForm(data_blank)
517
+ self.assertTrue(form_blank.is_valid())
518
+ self.assertIsNone(form_blank.cleaned_data["speed"]) # TypedChoiceField(empty->None)
519
+
520
+ # With a specific choice
521
+ speed_choice = InterfaceSpeedChoices.SPEED_1G
522
+ data_choice = {
523
+ "device": self.device.pk,
524
+ "name_pattern": "eth2",
525
+ "status": self.status.pk,
526
+ "type": InterfaceTypeChoices.TYPE_1GE_FIXED,
527
+ "speed": str(speed_choice),
528
+ "duplex": InterfaceDuplexChoices.DUPLEX_AUTO,
529
+ }
530
+ form_choice = InterfaceCreateForm(data_choice)
531
+ self.assertTrue(form_choice.is_valid())
532
+ self.assertEqual(form_choice.cleaned_data["speed"], speed_choice)
533
+ self.assertEqual(form_choice.cleaned_data["duplex"], InterfaceDuplexChoices.DUPLEX_AUTO)
@@ -3,7 +3,7 @@ from django.test import override_settings
3
3
 
4
4
  from nautobot.core.graphql import execute_query
5
5
  from nautobot.core.testing import create_test_user, TestCase
6
- from nautobot.dcim.choices import InterfaceTypeChoices
6
+ from nautobot.dcim.choices import InterfaceDuplexChoices, InterfaceSpeedChoices, InterfaceTypeChoices
7
7
  from nautobot.dcim.models import (
8
8
  Controller,
9
9
  Device,
@@ -52,6 +52,22 @@ class GraphQLTestCase(TestCase):
52
52
  type=InterfaceTypeChoices.TYPE_VIRTUAL,
53
53
  mac_address=None,
54
54
  ),
55
+ Interface.objects.create(
56
+ device=self.device,
57
+ name="eth2",
58
+ status=interface_status,
59
+ type=InterfaceTypeChoices.TYPE_1GE_FIXED,
60
+ speed=InterfaceSpeedChoices.SPEED_1G,
61
+ duplex=InterfaceDuplexChoices.DUPLEX_FULL,
62
+ ),
63
+ Interface.objects.create(
64
+ device=self.device,
65
+ name="eth3",
66
+ status=interface_status,
67
+ type=InterfaceTypeChoices.TYPE_10GE_SFP_PLUS,
68
+ speed=InterfaceSpeedChoices.SPEED_10G,
69
+ duplex="",
70
+ ),
55
71
  )
56
72
  for interface in self.interfaces:
57
73
  interface.validated_save()
@@ -131,3 +147,30 @@ class GraphQLTestCase(TestCase):
131
147
  self.assertIsNone(resp.errors)
132
148
  for device in resp.data["devices"]:
133
149
  self.assertNotEqual(device["serial"], "")
150
+
151
+ with self.subTest("interface speed/duplex fields on device query"):
152
+ query = "query { devices { name interfaces { name speed duplex } } }"
153
+ resp = execute_query(query, user=self.user)
154
+ self.assertFalse(resp.errors)
155
+ interfaces = [i for d in resp.data["devices"] if d["name"] == self.device.name for i in d["interfaces"]]
156
+ eth2 = next(i for i in interfaces if i["name"] == "eth2")
157
+ eth3 = next(i for i in interfaces if i["name"] == "eth3")
158
+ self.assertEqual(eth2["speed"], InterfaceSpeedChoices.SPEED_1G)
159
+ self.assertEqual(eth2["duplex"].lower(), InterfaceDuplexChoices.DUPLEX_FULL)
160
+ self.assertEqual(eth3["speed"], InterfaceSpeedChoices.SPEED_10G)
161
+ self.assertEqual(eth3["duplex"], None)
162
+
163
+ with self.subTest("interfaces root filter by speed and duplex"):
164
+ query = f"query {{ interfaces(speed: {InterfaceSpeedChoices.SPEED_1G}) {{ name }} }}"
165
+ resp = execute_query(query, user=self.user)
166
+ self.assertFalse(resp.errors)
167
+ names = {i["name"] for i in resp.data["interfaces"]}
168
+ self.assertIn("eth2", names)
169
+ self.assertNotIn("eth3", names)
170
+
171
+ query = 'query { interfaces(duplex: ["full"]) { name } }'
172
+ resp = execute_query(query, user=self.user)
173
+ self.assertFalse(resp.errors)
174
+ names = {i["name"] for i in resp.data["interfaces"]}
175
+ self.assertIn("eth2", names)
176
+ self.assertNotIn("eth3", names)
@@ -2,6 +2,7 @@ from decimal import Decimal
2
2
 
3
3
  from constance.test import override_config
4
4
  from django.contrib.contenttypes.models import ContentType
5
+ from django.core.cache import caches
5
6
  from django.core.exceptions import ValidationError
6
7
  from django.db import IntegrityError
7
8
  from django.db.models import Model
@@ -16,7 +17,10 @@ from nautobot.dcim.choices import (
16
17
  CableTypeChoices,
17
18
  ConsolePortTypeChoices,
18
19
  DeviceFaceChoices,
20
+ DeviceUniquenessChoices,
21
+ InterfaceDuplexChoices,
19
22
  InterfaceModeChoices,
23
+ InterfaceSpeedChoices,
20
24
  InterfaceTypeChoices,
21
25
  PortTypeChoices,
22
26
  PowerFeedBreakerPoleChoices,
@@ -725,6 +729,101 @@ class InterfaceTemplateTestCase(ModularDeviceComponentTemplateTestCaseMixin, Tes
725
729
  first_status = Status.objects.get_for_model(Interface).first()
726
730
  self.assertIsNotNone(device_2.interfaces.get(name="Test_Template_1").status, first_status)
727
731
 
732
+ def test_speed_disallowed_for_lag_virtual_wireless(self):
733
+ """speed must be None for LAG, virtual, and wireless templates."""
734
+ manufacturer = Manufacturer.objects.first()
735
+ device_type = DeviceType.objects.create(manufacturer=manufacturer, model="SpeedGuard 1000")
736
+
737
+ for if_type in (
738
+ InterfaceTypeChoices.TYPE_LAG,
739
+ InterfaceTypeChoices.TYPE_VIRTUAL,
740
+ InterfaceTypeChoices.TYPE_80211N,
741
+ ):
742
+ with self.subTest(if_type=if_type):
743
+ with self.assertRaises(ValidationError) as cm:
744
+ InterfaceTemplate(
745
+ device_type=device_type,
746
+ name=f"bad-{if_type}",
747
+ type=if_type,
748
+ speed=InterfaceSpeedChoices.SPEED_1G,
749
+ ).full_clean()
750
+ self.assertIn("Speed is not applicable to this interface type.", str(cm.exception))
751
+
752
+ def test_duplex_disallowed_for_lag_virtual_wireless(self):
753
+ """duplex must be blank for LAG, virtual, and wireless templates."""
754
+ manufacturer = Manufacturer.objects.first()
755
+ device_type = DeviceType.objects.create(manufacturer=manufacturer, model="DuplexGuard 1000")
756
+
757
+ for itype in (
758
+ InterfaceTypeChoices.TYPE_LAG,
759
+ InterfaceTypeChoices.TYPE_VIRTUAL,
760
+ InterfaceTypeChoices.TYPE_80211N,
761
+ ):
762
+ with self.assertRaises(ValidationError):
763
+ InterfaceTemplate(
764
+ device_type=device_type,
765
+ name=f"bad-{itype}",
766
+ type=itype,
767
+ duplex=InterfaceDuplexChoices.DUPLEX_FULL,
768
+ ).full_clean()
769
+
770
+ def test_duplex_disallowed_for_non_base_t(self):
771
+ """duplex must be blank for non-BASE-T physical types (e.g., SFP)."""
772
+ manufacturer = Manufacturer.objects.first()
773
+ device_type = DeviceType.objects.create(manufacturer=manufacturer, model="SfpGuard 1000")
774
+
775
+ with self.assertRaises(ValidationError) as cm:
776
+ InterfaceTemplate(
777
+ device_type=device_type,
778
+ name="sfp0",
779
+ type=InterfaceTypeChoices.TYPE_1GE_SFP,
780
+ duplex=InterfaceDuplexChoices.DUPLEX_FULL,
781
+ ).full_clean()
782
+ self.assertIn("Duplex is only applicable to copper twisted-pair interfaces.", str(cm.exception))
783
+
784
+ def test_duplex_and_speed_allowed_for_base_t(self):
785
+ """BASE-T physical types accept duplex and speed values."""
786
+ manufacturer = Manufacturer.objects.first()
787
+ device_type = DeviceType.objects.create(manufacturer=manufacturer, model="CopperOK 1000")
788
+
789
+ tmpl = InterfaceTemplate(
790
+ device_type=device_type,
791
+ name="eth0",
792
+ type=InterfaceTypeChoices.TYPE_1GE_FIXED,
793
+ speed=InterfaceSpeedChoices.SPEED_1G,
794
+ duplex=InterfaceDuplexChoices.DUPLEX_FULL,
795
+ )
796
+ tmpl.full_clean() # should not raise
797
+
798
+ def test_instantiation_propagates_speed_and_duplex(self):
799
+ """Interface created from template inherits speed and duplex."""
800
+ statuses = Status.objects.get_for_model(Device)
801
+ location = Location.objects.filter(location_type=LocationType.objects.get(name="Campus")).first()
802
+ manufacturer = Manufacturer.objects.first()
803
+ device_role = Role.objects.get_for_model(Device).first()
804
+ device_type = DeviceType.objects.create(manufacturer=manufacturer, model="Propagate 2000")
805
+
806
+ InterfaceTemplate.objects.create(
807
+ device_type=device_type,
808
+ name="EthX",
809
+ type=InterfaceTypeChoices.TYPE_1GE_FIXED,
810
+ mgmt_only=False,
811
+ speed=InterfaceSpeedChoices.SPEED_1G,
812
+ duplex=InterfaceDuplexChoices.DUPLEX_FULL,
813
+ )
814
+
815
+ device = Device.objects.create(
816
+ device_type=device_type,
817
+ role=device_role,
818
+ status=statuses[0],
819
+ name="Device-Prop",
820
+ location=location,
821
+ )
822
+
823
+ iface = device.interfaces.get(name="EthX")
824
+ self.assertEqual(iface.speed, InterfaceSpeedChoices.SPEED_1G)
825
+ self.assertEqual(iface.duplex, InterfaceDuplexChoices.DUPLEX_FULL)
826
+
728
827
 
729
828
  class InterfaceRedundancyGroupTestCase(ModelTestCases.BaseModelTestCase):
730
829
  model = InterfaceRedundancyGroup
@@ -1423,6 +1522,10 @@ class DeviceTestCase(ModelTestCases.BaseModelTestCase):
1423
1522
  model = Device
1424
1523
 
1425
1524
  def setUp(self):
1525
+ # clear Constance cache
1526
+ cache = caches[settings.CONSTANCE_DATABASE_CACHE_BACKEND]
1527
+ cache.clear()
1528
+
1426
1529
  manufacturer = Manufacturer.objects.first()
1427
1530
  self.device_type = DeviceType.objects.create(
1428
1531
  manufacturer=manufacturer,
@@ -1532,12 +1635,25 @@ class DeviceTestCase(ModelTestCases.BaseModelTestCase):
1532
1635
 
1533
1636
  def test_natural_key_overrides(self):
1534
1637
  """Ensure that the natural-key for Device is affected by settings/Constance."""
1535
- with override_config(DEVICE_NAME_AS_NATURAL_KEY=True):
1638
+ with override_config(DEVICE_UNIQUENESS=DeviceUniquenessChoices.NAME):
1536
1639
  self.assertEqual([self.device.name], self.device.natural_key())
1537
1640
  # self.assertEqual(construct_composite_key([self.device.name]), self.device.composite_key) # TODO: Revist this if we reintroduce composite keys
1538
1641
  self.assertEqual(self.device, Device.objects.get_by_natural_key([self.device.name]))
1539
1642
  # self.assertEqual(self.device, Device.objects.get(composite_key=self.device.composite_key)) # TODO: Revist this if we reintroduce composite keys
1540
1643
 
1644
+ with override_config(DEVICE_UNIQUENESS=DeviceUniquenessChoices.LOCATION_TENANT_NAME):
1645
+ self.assertEqual(
1646
+ [self.device.name, self.device.tenant, self.device.location.name], self.device.natural_key()
1647
+ )
1648
+ self.assertEqual(
1649
+ self.device,
1650
+ Device.objects.get_by_natural_key([self.device.name, self.device.tenant, self.device.location]),
1651
+ )
1652
+
1653
+ with override_config(DEVICE_UNIQUENESS=DeviceUniquenessChoices.NONE):
1654
+ self.assertEqual([str(self.device.pk)], self.device.natural_key())
1655
+ self.assertEqual(self.device, Device.objects.get_by_natural_key([self.device.pk]))
1656
+
1541
1657
  with override_config(LOCATION_NAME_AS_NATURAL_KEY=True):
1542
1658
  self.assertEqual([self.device.name, None, self.device.location.name], self.device.natural_key())
1543
1659
  # self.assertEqual(
@@ -2760,6 +2876,7 @@ class InterfaceTestCase(ModularDeviceComponentTestCaseMixin, ModelTestCases.Base
2760
2876
  name="VLAN 1", vid=100, location=location, status=vlan_status, vlan_group=vlan_group
2761
2877
  )
2762
2878
  status = Status.objects.get_for_model(Device).first()
2879
+ cls.intf_status = Status.objects.get_for_model(Interface).first()
2763
2880
  cls.device = Device.objects.create(
2764
2881
  name="Device 1",
2765
2882
  device_type=devicetype,
@@ -2899,6 +3016,11 @@ class InterfaceTestCase(ModularDeviceComponentTestCaseMixin, ModelTestCases.Base
2899
3016
  self.assertEqual(count, 1)
2900
3017
  self.assertEqual(IPAddressToInterface.objects.filter(ip_address=ips[-1], interface=interface).count(), 1)
2901
3018
 
3019
+ # add a single instance which is already there
3020
+ count = interface.add_ip_addresses(ips[-1])
3021
+ self.assertEqual(count, 0)
3022
+ self.assertEqual(IPAddressToInterface.objects.filter(ip_address=ips[-1], interface=interface).count(), 1)
3023
+
2902
3024
  # add multiple instances
2903
3025
  count = interface.add_ip_addresses(ips[:5])
2904
3026
  self.assertEqual(count, 5)
@@ -2906,6 +3028,20 @@ class InterfaceTestCase(ModularDeviceComponentTestCaseMixin, ModelTestCases.Base
2906
3028
  for ip in ips[:5]:
2907
3029
  self.assertEqual(IPAddressToInterface.objects.filter(ip_address=ip, interface=interface).count(), 1)
2908
3030
 
3031
+ # add multiple instances all of which are already there
3032
+ count = interface.add_ip_addresses(ips[:5])
3033
+ self.assertEqual(count, 0)
3034
+ self.assertEqual(IPAddressToInterface.objects.filter(interface=interface).count(), 6)
3035
+ for ip in ips[:5]:
3036
+ self.assertEqual(IPAddressToInterface.objects.filter(ip_address=ip, interface=interface).count(), 1)
3037
+
3038
+ # add multiple IPs some of which are there
3039
+ count = interface.add_ip_addresses(ips[3:7])
3040
+ self.assertEqual(count, 2)
3041
+ self.assertEqual(IPAddressToInterface.objects.filter(interface=interface).count(), 8)
3042
+ for ip in ips[3:7]:
3043
+ self.assertEqual(IPAddressToInterface.objects.filter(ip_address=ip, interface=interface).count(), 1)
3044
+
2909
3045
  def test_remove_ip_addresses(self):
2910
3046
  """Test the `remove_ip_addresses` helper method on `Interface`"""
2911
3047
  interface = Interface.objects.create(
@@ -2928,13 +3064,28 @@ class InterfaceTestCase(ModularDeviceComponentTestCaseMixin, ModelTestCases.Base
2928
3064
  self.assertEqual(count, 1)
2929
3065
  self.assertEqual(IPAddressToInterface.objects.filter(interface=interface).count(), 9)
2930
3066
 
3067
+ # remove a single instance which has already been removed
3068
+ count = interface.remove_ip_addresses(ips[-1])
3069
+ self.assertEqual(count, 0)
3070
+ self.assertEqual(IPAddressToInterface.objects.filter(interface=interface).count(), 9)
3071
+
2931
3072
  # remove multiple instances
2932
3073
  count = interface.remove_ip_addresses(ips[:5])
2933
3074
  self.assertEqual(count, 5)
2934
3075
  self.assertEqual(IPAddressToInterface.objects.filter(interface=interface).count(), 4)
2935
3076
 
3077
+ # remove multiple instances all which have already been removed
3078
+ count = interface.remove_ip_addresses(ips[:5])
3079
+ self.assertEqual(count, 0)
3080
+ self.assertEqual(IPAddressToInterface.objects.filter(interface=interface).count(), 4)
3081
+
3082
+ # remove multiple instances some of which have already been removed
3083
+ count = interface.remove_ip_addresses(ips[3:7])
3084
+ self.assertEqual(count, 2)
3085
+ self.assertEqual(IPAddressToInterface.objects.filter(interface=interface).count(), 2)
3086
+
2936
3087
  count = interface.remove_ip_addresses(ips)
2937
- self.assertEqual(count, 4)
3088
+ self.assertEqual(count, 2)
2938
3089
  self.assertEqual(IPAddressToInterface.objects.filter(interface=interface).count(), 0)
2939
3090
 
2940
3091
  # Test the pre_delete signal for IPAddressToInterface instances
@@ -2942,13 +3093,186 @@ class InterfaceTestCase(ModularDeviceComponentTestCaseMixin, ModelTestCases.Base
2942
3093
  self.device.primary_ip4 = interface.ip_addresses.all().filter(ip_version=4).first()
2943
3094
  self.device.primary_ip6 = interface.ip_addresses.all().filter(ip_version=6).first()
2944
3095
  self.device.save()
2945
- interface.remove_ip_addresses(self.device.primary_ip4)
3096
+
3097
+ count = interface.remove_ip_addresses(self.device.primary_ip4)
3098
+ self.assertEqual(count, 1)
2946
3099
  self.device.refresh_from_db()
2947
3100
  self.assertEqual(self.device.primary_ip4, None)
2948
- interface.remove_ip_addresses(self.device.primary_ip6)
3101
+ # NOTE: This effectively tests what happens when you pass remove_ip_addresses None; it
3102
+ # NOTE: does not remove a v6 address, because there are no v6 IPs created in this test
3103
+ # NOTE: class.
3104
+ count = interface.remove_ip_addresses(self.device.primary_ip6)
3105
+ self.assertEqual(count, 0)
2949
3106
  self.device.refresh_from_db()
2950
3107
  self.assertEqual(self.device.primary_ip6, None)
2951
3108
 
3109
+ def _assert_invalid_speed_duplex(self, if_type, speed=None, duplex="", expected_error=""):
3110
+ iface = Interface(
3111
+ device=self.device,
3112
+ name=f"test-{if_type}",
3113
+ type=if_type,
3114
+ status=self.intf_status,
3115
+ speed=speed,
3116
+ duplex=duplex,
3117
+ )
3118
+ with self.assertRaises(ValidationError) as cm:
3119
+ iface.full_clean()
3120
+ self.assertIn(expected_error, str(cm.exception))
3121
+
3122
+ def test_disallowed_speed_and_duplex_matrix(self):
3123
+ """Test that interface types with no speed or duplex disallow those settings."""
3124
+ test_cases = [
3125
+ # LAG
3126
+ (
3127
+ InterfaceTypeChoices.TYPE_LAG,
3128
+ InterfaceSpeedChoices.SPEED_1M,
3129
+ None,
3130
+ "Speed is not applicable to this interface type.",
3131
+ ),
3132
+ (
3133
+ InterfaceTypeChoices.TYPE_LAG,
3134
+ None,
3135
+ InterfaceDuplexChoices.DUPLEX_FULL,
3136
+ "Duplex is not applicable to this interface type.",
3137
+ ),
3138
+ # Virtual
3139
+ (
3140
+ InterfaceTypeChoices.TYPE_VIRTUAL,
3141
+ InterfaceSpeedChoices.SPEED_1M,
3142
+ None,
3143
+ "Speed is not applicable to this interface type.",
3144
+ ),
3145
+ (
3146
+ InterfaceTypeChoices.TYPE_VIRTUAL,
3147
+ None,
3148
+ InterfaceDuplexChoices.DUPLEX_FULL,
3149
+ "Duplex is not applicable to this interface type.",
3150
+ ),
3151
+ # Wireless
3152
+ (
3153
+ InterfaceTypeChoices.TYPE_80211AC,
3154
+ InterfaceSpeedChoices.SPEED_1M,
3155
+ None,
3156
+ "Speed is not applicable to this interface type.",
3157
+ ),
3158
+ (
3159
+ InterfaceTypeChoices.TYPE_80211AC,
3160
+ None,
3161
+ InterfaceDuplexChoices.DUPLEX_FULL,
3162
+ "Duplex is not applicable to this interface type.",
3163
+ ),
3164
+ # Copper (negative speed is invalid)
3165
+ (InterfaceTypeChoices.TYPE_1GE_FIXED, -100, None, "Ensure this value is greater than or equal to 0."),
3166
+ # Copper (speed as a string is invalid)
3167
+ (InterfaceTypeChoices.TYPE_1GE_FIXED, "100 Mbps", None, "value must be an integer."),
3168
+ # Copper (invalid duplex is invalid)
3169
+ (
3170
+ InterfaceTypeChoices.TYPE_1GE_FIXED,
3171
+ InterfaceSpeedChoices.SPEED_1M,
3172
+ "invalid",
3173
+ "Value 'invalid' is not a valid choice.",
3174
+ ),
3175
+ # Optical (no duplex allowed)
3176
+ (
3177
+ InterfaceTypeChoices.TYPE_10GE_SFP_PLUS,
3178
+ InterfaceSpeedChoices.SPEED_1M,
3179
+ InterfaceDuplexChoices.DUPLEX_FULL,
3180
+ "Duplex is only applicable to copper twisted-pair interfaces.",
3181
+ ),
3182
+ ]
3183
+ for if_type, speed, duplex, expected_error in test_cases:
3184
+ with self.subTest(f"{if_type} with speed={speed} and duplex={duplex}"):
3185
+ self._assert_invalid_speed_duplex(if_type, speed, duplex, expected_error)
3186
+
3187
+ def test_copper_allows_duplex_and_non_negative_speed(self):
3188
+ """Test that copper interfaces allow duplex and non-negative speed."""
3189
+ iface = Interface(
3190
+ device=self.device,
3191
+ name="eth1",
3192
+ type=InterfaceTypeChoices.TYPE_1GE_FIXED, # 1000BASE-T
3193
+ status=self.intf_status,
3194
+ speed=InterfaceSpeedChoices.SPEED_1G,
3195
+ duplex=InterfaceDuplexChoices.DUPLEX_FULL,
3196
+ )
3197
+ # Should not raise
3198
+ iface.full_clean()
3199
+
3200
+ iface.speed = 0
3201
+ iface.full_clean()
3202
+
3203
+ def test_lag_allows_no_speed_or_duplex(self):
3204
+ """Test that LAG interfaces pass validation when speed and duplex are not set."""
3205
+ iface = Interface(
3206
+ device=self.device,
3207
+ name="Port-Channel1",
3208
+ type=InterfaceTypeChoices.TYPE_LAG,
3209
+ status=self.intf_status,
3210
+ )
3211
+ # Should not raise when speed and duplex are not set
3212
+ iface.full_clean()
3213
+
3214
+ def test_optical_disallows_duplex_allows_speed(self):
3215
+ """Test that optical interfaces do not allow duplex and allow positive speed."""
3216
+ # Duplex set should error
3217
+ iface_bad = Interface(
3218
+ device=self.device,
3219
+ name="xe0",
3220
+ type=InterfaceTypeChoices.TYPE_10GE_SFP_PLUS,
3221
+ status=self.intf_status,
3222
+ duplex=InterfaceDuplexChoices.DUPLEX_FULL,
3223
+ )
3224
+ with self.assertRaises(ValidationError) as cm:
3225
+ iface_bad.full_clean()
3226
+ self.assertIn("Duplex is only applicable to copper twisted-pair interfaces.", str(cm.exception))
3227
+
3228
+ # Speed positive should pass
3229
+ iface_ok = Interface(
3230
+ device=self.device,
3231
+ name="xe1",
3232
+ type=InterfaceTypeChoices.TYPE_10GE_SFP_PLUS,
3233
+ status=self.intf_status,
3234
+ speed=InterfaceSpeedChoices.SPEED_10G,
3235
+ )
3236
+ iface_ok.full_clean()
3237
+
3238
+ def test_changing_copper_interface_with_speed_and_duplex_to_optical_fails(self):
3239
+ """Test that changing a copper interface with speed and duplex to an optical interface fails."""
3240
+
3241
+ with self.subTest("speed"):
3242
+ iface = Interface(
3243
+ device=self.device,
3244
+ name="eth3",
3245
+ type=InterfaceTypeChoices.TYPE_1GE_FIXED,
3246
+ status=self.intf_status,
3247
+ speed=InterfaceSpeedChoices.SPEED_1G,
3248
+ )
3249
+ iface.full_clean()
3250
+
3251
+ iface.type = InterfaceTypeChoices.TYPE_LAG
3252
+ with self.assertRaises(ValidationError) as cm:
3253
+ iface.full_clean()
3254
+ self.assertIn("Speed is not applicable to this interface type.", str(cm.exception))
3255
+
3256
+ with self.subTest("duplex"):
3257
+ iface = Interface(
3258
+ device=self.device,
3259
+ name="eth3",
3260
+ type=InterfaceTypeChoices.TYPE_1GE_FIXED,
3261
+ status=self.intf_status,
3262
+ duplex=InterfaceDuplexChoices.DUPLEX_FULL,
3263
+ )
3264
+ iface.full_clean()
3265
+
3266
+ iface.type = InterfaceTypeChoices.TYPE_10GE_SFP_PLUS
3267
+ with self.assertRaises(ValidationError) as cm:
3268
+ iface.full_clean()
3269
+ self.assertIn("Duplex is only applicable to copper twisted-pair interfaces.", str(cm.exception))
3270
+
3271
+ iface.type = InterfaceTypeChoices.TYPE_10GE_SFP_PLUS
3272
+ with self.assertRaises(ValidationError) as cm:
3273
+ iface.full_clean()
3274
+ self.assertIn("Duplex is only applicable to copper twisted-pair interfaces.", str(cm.exception))
3275
+
2952
3276
 
2953
3277
  class SoftwareImageFileTestCase(ModelTestCases.BaseModelTestCase):
2954
3278
  model = SoftwareImageFile