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
@@ -13,7 +13,9 @@ from nautobot.core.testing import APITestCase, APIViewTestCases
13
13
  from nautobot.core.testing.utils import generate_random_device_asset_tag_of_specified_size, get_deletable_objects
14
14
  from nautobot.dcim.choices import (
15
15
  ConsolePortTypeChoices,
16
+ InterfaceDuplexChoices,
16
17
  InterfaceModeChoices,
18
+ InterfaceSpeedChoices,
17
19
  InterfaceTypeChoices,
18
20
  PortTypeChoices,
19
21
  PowerFeedBreakerPoleChoices,
@@ -1177,6 +1179,7 @@ class PowerOutletTemplateTest(Mixins.ModularDeviceComponentTemplateMixin, Mixins
1177
1179
 
1178
1180
  class InterfaceTemplateTest(Mixins.ModularDeviceComponentTemplateMixin, Mixins.BasePortTemplateTestMixin):
1179
1181
  model = InterfaceTemplate
1182
+ choices_fields = ["duplex", "type"]
1180
1183
  modular_component_create_data = {"type": InterfaceTypeChoices.TYPE_1GE_FIXED}
1181
1184
 
1182
1185
  @classmethod
@@ -1200,6 +1203,62 @@ class InterfaceTemplateTest(Mixins.ModularDeviceComponentTemplateMixin, Mixins.B
1200
1203
  },
1201
1204
  ]
1202
1205
 
1206
+ def test_create_base_t_with_speed_and_duplex(self):
1207
+ self.add_permissions("dcim.add_interfacetemplate", "dcim.view_interfacetemplate", "dcim.view_devicetype")
1208
+ url = self._get_list_url()
1209
+ payload = {
1210
+ "device_type": self.device_type.pk,
1211
+ "name": "Eth1",
1212
+ "type": InterfaceTypeChoices.TYPE_1GE_FIXED,
1213
+ "mgmt_only": False,
1214
+ "speed": InterfaceSpeedChoices.SPEED_1G,
1215
+ "duplex": InterfaceDuplexChoices.DUPLEX_FULL,
1216
+ }
1217
+ response = self.client.post(url, data=payload, format="json", **self.header)
1218
+ self.assertHttpStatus(response, status.HTTP_201_CREATED)
1219
+ obj = InterfaceTemplate.objects.get(pk=response.data["id"]) # type: ignore[index]
1220
+ self.assertEqual(obj.speed, InterfaceSpeedChoices.SPEED_1G)
1221
+ self.assertEqual(obj.duplex, InterfaceDuplexChoices.DUPLEX_FULL)
1222
+
1223
+ def test_create_sfp_with_duplex_rejected(self):
1224
+ self.add_permissions("dcim.add_interfacetemplate", "dcim.view_interfacetemplate", "dcim.view_devicetype")
1225
+ url = self._get_list_url()
1226
+ payload = {
1227
+ "device_type": self.device_type.pk,
1228
+ "name": "SFP1",
1229
+ "type": InterfaceTypeChoices.TYPE_1GE_SFP,
1230
+ "duplex": InterfaceDuplexChoices.DUPLEX_FULL,
1231
+ }
1232
+ response = self.client.post(url, data=payload, format="json", **self.header)
1233
+ self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
1234
+ self.assertIn("duplex", response.data)
1235
+
1236
+ def test_create_lag_with_speed_rejected(self):
1237
+ self.add_permissions("dcim.add_interfacetemplate", "dcim.view_interfacetemplate", "dcim.view_devicetype")
1238
+ url = self._get_list_url()
1239
+ payload = {
1240
+ "device_type": self.device_type.pk,
1241
+ "name": "Port-Channel1",
1242
+ "type": InterfaceTypeChoices.TYPE_LAG,
1243
+ "speed": InterfaceSpeedChoices.SPEED_1G,
1244
+ }
1245
+ response = self.client.post(url, data=payload, format="json", **self.header)
1246
+ self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
1247
+ self.assertIn("speed", response.data)
1248
+
1249
+ def test_create_virtual_with_speed_rejected(self):
1250
+ self.add_permissions("dcim.add_interfacetemplate", "dcim.view_interfacetemplate", "dcim.view_devicetype")
1251
+ url = self._get_list_url()
1252
+ payload = {
1253
+ "device_type": self.device_type.pk,
1254
+ "name": "V0",
1255
+ "type": InterfaceTypeChoices.TYPE_VIRTUAL,
1256
+ "speed": InterfaceSpeedChoices.SPEED_1G,
1257
+ }
1258
+ response = self.client.post(url, data=payload, format="json", **self.header)
1259
+ self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
1260
+ self.assertIn("speed", response.data)
1261
+
1203
1262
 
1204
1263
  class FrontPortTemplateTest(Mixins.BasePortTemplateTestMixin):
1205
1264
  model = FrontPortTemplate
@@ -1471,6 +1530,9 @@ class PlatformTest(APIViewTestCases.APIViewTestCase):
1471
1530
  class DeviceTest(APIViewTestCases.APIViewTestCase):
1472
1531
  model = Device
1473
1532
  choices_fields = ["face"]
1533
+ validation_excluded_fields = [
1534
+ "software_image_files", # M2M field, excluded by default
1535
+ ]
1474
1536
 
1475
1537
  @classmethod
1476
1538
  def setUpTestData(cls):
@@ -2167,7 +2229,10 @@ class PowerOutletTest(Mixins.ModularDeviceComponentMixin, Mixins.BasePortTestMix
2167
2229
  class InterfaceTest(Mixins.ModularDeviceComponentMixin, Mixins.BasePortTestMixin):
2168
2230
  model = Interface
2169
2231
  peer_termination_type = Interface
2170
- choices_fields = ["mode", "type"]
2232
+ choices_fields = ["duplex", "mode", "type"]
2233
+ validation_excluded_fields = [
2234
+ "tagged_vlans", # M2M field, excluded by default
2235
+ ]
2171
2236
 
2172
2237
  @classmethod
2173
2238
  def setUpTestData(cls):
@@ -2208,14 +2273,14 @@ class InterfaceTest(Mixins.ModularDeviceComponentMixin, Mixins.BasePortTestMixin
2208
2273
  Interface.objects.create(
2209
2274
  device=cls.devices[0],
2210
2275
  name="Test Interface 1",
2211
- type="1000base-t",
2276
+ type=InterfaceTypeChoices.TYPE_1GE_FIXED,
2212
2277
  status=non_default_status,
2213
2278
  role=intf_role,
2214
2279
  ),
2215
2280
  Interface.objects.create(
2216
2281
  device=cls.devices[0],
2217
2282
  name="Test Interface 2",
2218
- type="1000base-t",
2283
+ type=InterfaceTypeChoices.TYPE_1GE_FIXED,
2219
2284
  status=non_default_status,
2220
2285
  ),
2221
2286
  Interface.objects.create(
@@ -2266,7 +2331,7 @@ class InterfaceTest(Mixins.ModularDeviceComponentMixin, Mixins.BasePortTestMixin
2266
2331
  {
2267
2332
  "device": cls.devices[0].pk,
2268
2333
  "name": "Test Interface 8",
2269
- "type": "1000base-t",
2334
+ "type": InterfaceTypeChoices.TYPE_1GE_FIXED,
2270
2335
  "status": interface_status.pk,
2271
2336
  "role": intf_role.pk,
2272
2337
  "mode": InterfaceModeChoices.MODE_TAGGED,
@@ -2277,7 +2342,7 @@ class InterfaceTest(Mixins.ModularDeviceComponentMixin, Mixins.BasePortTestMixin
2277
2342
  {
2278
2343
  "device": cls.devices[0].pk,
2279
2344
  "name": "Test Interface 9",
2280
- "type": "1000base-t",
2345
+ "type": InterfaceTypeChoices.TYPE_1GE_FIXED,
2281
2346
  "status": interface_status.pk,
2282
2347
  "role": intf_role.pk,
2283
2348
  "mode": InterfaceModeChoices.MODE_TAGGED,
@@ -2289,13 +2354,35 @@ class InterfaceTest(Mixins.ModularDeviceComponentMixin, Mixins.BasePortTestMixin
2289
2354
  {
2290
2355
  "device": cls.devices[0].pk,
2291
2356
  "name": "Test Interface 10",
2292
- "type": "virtual",
2357
+ "type": InterfaceTypeChoices.TYPE_VIRTUAL,
2293
2358
  "status": interface_status.pk,
2294
2359
  "mode": InterfaceModeChoices.MODE_TAGGED,
2295
2360
  "parent_interface": cls.interfaces[1].pk,
2296
2361
  "tagged_vlans": [cls.vlans[0].pk, cls.vlans[1].pk],
2297
2362
  "untagged_vlan": cls.vlans[2].pk,
2298
2363
  },
2364
+ {
2365
+ "device": cls.devices[0].pk,
2366
+ "name": "Test Interface 11",
2367
+ "type": InterfaceTypeChoices.TYPE_1GE_FIXED,
2368
+ "status": interface_status.pk,
2369
+ "speed": InterfaceSpeedChoices.SPEED_1G,
2370
+ },
2371
+ {
2372
+ "device": cls.devices[0].pk,
2373
+ "name": "Test Interface 12",
2374
+ "type": InterfaceTypeChoices.TYPE_1GE_FIXED,
2375
+ "status": interface_status.pk,
2376
+ "duplex": InterfaceDuplexChoices.DUPLEX_FULL,
2377
+ },
2378
+ {
2379
+ "device": cls.devices[0].pk,
2380
+ "name": "Test Interface 13",
2381
+ "type": InterfaceTypeChoices.TYPE_1GE_FIXED,
2382
+ "status": interface_status.pk,
2383
+ "speed": InterfaceSpeedChoices.SPEED_1G,
2384
+ "duplex": InterfaceDuplexChoices.DUPLEX_FULL,
2385
+ },
2299
2386
  ]
2300
2387
 
2301
2388
  cls.untagged_vlan_data = {
@@ -2504,6 +2591,105 @@ class InterfaceTest(Mixins.ModularDeviceComponentMixin, Mixins.BasePortTestMixin
2504
2591
  response = self.client.patch(self._get_detail_url(interface), data=payload, format="json", **self.header)
2505
2592
  self.assertHttpStatus(response, status.HTTP_200_OK)
2506
2593
 
2594
+ def test_speed_duplex_invalid_by_type(self):
2595
+ """Test that API rejects speed/duplex for disallowed interface types."""
2596
+ self.add_permissions("dcim.add_interface", "dcim.view_interface", "dcim.view_device", "extras.view_status")
2597
+
2598
+ # LAG disallows speed/duplex
2599
+ for field, value in (("speed", InterfaceSpeedChoices.SPEED_1G), ("duplex", InterfaceDuplexChoices.DUPLEX_FULL)):
2600
+ with self.subTest(if_type=InterfaceTypeChoices.TYPE_LAG, field=field):
2601
+ payload = {
2602
+ "device": self.devices[0].pk,
2603
+ "name": f"if-lag-{field}",
2604
+ "type": InterfaceTypeChoices.TYPE_LAG,
2605
+ "status": Status.objects.get_for_model(Interface).first().pk,
2606
+ field: value,
2607
+ }
2608
+ response = self.client.post(self._get_list_url(), data=payload, format="json", **self.header)
2609
+ self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
2610
+ self.assertIn(field, response.data)
2611
+
2612
+ # Virtual/wireless disallow speed/duplex
2613
+ for if_type in (InterfaceTypeChoices.TYPE_VIRTUAL, InterfaceTypeChoices.TYPE_80211AC):
2614
+ for field, value in (
2615
+ ("speed", InterfaceSpeedChoices.SPEED_1G),
2616
+ ("duplex", InterfaceDuplexChoices.DUPLEX_FULL),
2617
+ ):
2618
+ with self.subTest(if_type=if_type, field=field):
2619
+ payload = {
2620
+ "device": self.devices[0].pk,
2621
+ "name": f"if-{if_type}-{field}",
2622
+ "type": if_type,
2623
+ "status": Status.objects.get_for_model(Interface).first().pk,
2624
+ field: value,
2625
+ }
2626
+ response = self.client.post(self._get_list_url(), data=payload, format="json", **self.header)
2627
+ self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
2628
+ self.assertIn(field, response.data)
2629
+
2630
+ # Optical disallows duplex
2631
+ with self.subTest(if_type=InterfaceTypeChoices.TYPE_10GE_SFP_PLUS, field="duplex"):
2632
+ payload = {
2633
+ "device": self.devices[0].pk,
2634
+ "name": "if-opt-duplex",
2635
+ "type": InterfaceTypeChoices.TYPE_10GE_SFP_PLUS,
2636
+ "status": Status.objects.get_for_model(Interface).first().pk,
2637
+ "duplex": InterfaceDuplexChoices.DUPLEX_FULL,
2638
+ }
2639
+ response = self.client.post(self._get_list_url(), data=payload, format="json", **self.header)
2640
+ self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
2641
+ self.assertIn("duplex", response.data)
2642
+
2643
+ def test_update_type_to_optical_fails_when_duplex_set(self):
2644
+ """Test that changing a copper interface with duplex set to an optical type fails."""
2645
+ self.add_permissions("dcim.change_interface")
2646
+ interface = self.interfaces[0] # 1000base-t
2647
+
2648
+ # Ensure duplex is set on copper via API
2649
+ response = self.client.patch(
2650
+ self._get_detail_url(interface),
2651
+ data={"duplex": InterfaceDuplexChoices.DUPLEX_FULL},
2652
+ format="json",
2653
+ **self.header,
2654
+ )
2655
+ self.assertHttpStatus(response, status.HTTP_200_OK)
2656
+ self.assertEqual(response.data["duplex"]["value"], InterfaceDuplexChoices.DUPLEX_FULL)
2657
+
2658
+ # Attempt to change type to optical while duplex remains set
2659
+ response = self.client.patch(
2660
+ self._get_detail_url(interface),
2661
+ data={"type": InterfaceTypeChoices.TYPE_10GE_SFP_PLUS},
2662
+ format="json",
2663
+ **self.header,
2664
+ )
2665
+ self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
2666
+ self.assertIn("duplex", response.data)
2667
+
2668
+ def test_update_type_to_optical_succeeds_when_unsetting_duplex(self):
2669
+ """Test that changing type with duplex set to optical while unsetting duplex in the same request succeeds."""
2670
+ self.add_permissions("dcim.change_interface")
2671
+ interface = self.interfaces[1] # 1000base-t
2672
+
2673
+ # Ensure duplex is set on copper first
2674
+ response = self.client.patch(
2675
+ self._get_detail_url(interface),
2676
+ data={"duplex": InterfaceDuplexChoices.DUPLEX_FULL},
2677
+ format="json",
2678
+ **self.header,
2679
+ )
2680
+ self.assertHttpStatus(response, status.HTTP_200_OK)
2681
+ self.assertEqual(response.data["duplex"]["value"], InterfaceDuplexChoices.DUPLEX_FULL)
2682
+
2683
+ # Change to optical and unset duplex in same call
2684
+ response = self.client.patch(
2685
+ self._get_detail_url(interface),
2686
+ data={"type": InterfaceTypeChoices.TYPE_10GE_SFP_PLUS, "duplex": ""},
2687
+ format="json",
2688
+ **self.header,
2689
+ )
2690
+ self.assertHttpStatus(response, status.HTTP_200_OK)
2691
+ self.assertIsNone(response.data["duplex"])
2692
+
2507
2693
 
2508
2694
  class FrontPortTest(Mixins.BasePortTestMixin):
2509
2695
  model = FrontPort
@@ -3588,6 +3774,7 @@ class DeviceTypeToSoftwareImageFileTestCase(
3588
3774
 
3589
3775
  class ControllerTestCase(APIViewTestCases.APIViewTestCase):
3590
3776
  model = Controller
3777
+ choices_fields = ("capabilities",)
3591
3778
 
3592
3779
  def get_deletable_object(self):
3593
3780
  # This method is used in `test_recreate_object_csv`,
@@ -3647,6 +3834,7 @@ class ControllerTestCase(APIViewTestCases.APIViewTestCase):
3647
3834
 
3648
3835
  class ControllerManagedDeviceGroupTestCase(APIViewTestCases.APIViewTestCase):
3649
3836
  model = ControllerManagedDeviceGroup
3837
+ choices_fields = ("capabilities",)
3650
3838
 
3651
3839
  def get_deletable_object(self):
3652
3840
  # This method is used in `test_recreate_object_csv`,
@@ -0,0 +1,229 @@
1
+ from django.contrib.contenttypes.models import ContentType
2
+ from django.core.exceptions import ValidationError
3
+ from django.test import override_settings, TestCase
4
+
5
+ from nautobot.core.testing.mixins import NautobotTestCaseMixin
6
+ from nautobot.data_validation.models import RequiredValidationRule
7
+ from nautobot.dcim.choices import DeviceUniquenessChoices
8
+ from nautobot.dcim.models import Device, DeviceType, Location
9
+ from nautobot.extras.models import Role, Status
10
+ from nautobot.tenancy.models import Tenant
11
+
12
+
13
+ class DeviceUniquenessValidatorTest(NautobotTestCaseMixin, TestCase):
14
+ """Tests for the DeviceUniquenessValidator custom validator."""
15
+
16
+ def setUp(self):
17
+ super().setUp()
18
+ self.device_status = Status.objects.get_for_model(Device).first()
19
+ self.device_type = DeviceType.objects.first()
20
+ self.device_role = Role.objects.get_for_model(Device).first()
21
+ self.location = Location.objects.first()
22
+ self.tenant = Tenant.objects.create(name="Tenant")
23
+ self.device_name = "Device"
24
+ self.device = Device.objects.create(
25
+ name=self.device_name,
26
+ device_type=self.device_type,
27
+ role=self.device_role,
28
+ location=self.location,
29
+ status=self.device_status,
30
+ tenant=self.tenant,
31
+ )
32
+
33
+ @override_settings(DEVICE_UNIQUENESS=DeviceUniquenessChoices.LOCATION_TENANT_NAME)
34
+ def test_duplicate_same_location_tenant_name_fails(self):
35
+ """Same name, tenant, and location should raise ValidationError."""
36
+ dup_device = Device(
37
+ name=self.device_name,
38
+ device_type=self.device_type,
39
+ role=self.device_role,
40
+ location=self.location,
41
+ status=self.device_status,
42
+ tenant=self.tenant,
43
+ )
44
+ with self.assertRaises(ValidationError) as contextmanager:
45
+ dup_device.full_clean()
46
+ self.assertIn(
47
+ f"A device named '{self.device_name}' already exists in this location: {self.location} and tenant: {self.tenant}. ",
48
+ str(contextmanager.exception),
49
+ )
50
+
51
+ @override_settings(DEVICE_UNIQUENESS=DeviceUniquenessChoices.LOCATION_TENANT_NAME)
52
+ def test_different_tenant_allows_duplicate_name(self):
53
+ """Same name and location, different tenant should be allowed."""
54
+ tenant = Tenant.objects.create(name="Tenant2")
55
+ non_dup_device = Device(
56
+ name=self.device_name,
57
+ device_type=self.device_type,
58
+ role=self.device_role,
59
+ location=self.location,
60
+ status=self.device_status,
61
+ tenant=tenant,
62
+ )
63
+ non_dup_device.full_clean() # should not raise
64
+
65
+ @override_settings(DEVICE_UNIQUENESS=DeviceUniquenessChoices.LOCATION_TENANT_NAME)
66
+ def test_different_location_allows_duplicate_name(self):
67
+ """Same name and tenant, different location should be allowed."""
68
+ location = Location.objects.last()
69
+ non_dup_device = Device(
70
+ name=self.device_name,
71
+ device_type=self.device_type,
72
+ role=self.device_role,
73
+ location=location,
74
+ status=self.device_status,
75
+ tenant=self.tenant,
76
+ )
77
+ non_dup_device.full_clean() # should not raise
78
+
79
+ @override_settings(DEVICE_UNIQUENESS=DeviceUniquenessChoices.LOCATION_TENANT_NAME)
80
+ def test_duplicate_name_with_null_tenant_fails(self):
81
+ """Duplicate name with tenant=None should raise ValidationError."""
82
+ Device.objects.create(
83
+ name="Device-2",
84
+ location=self.location,
85
+ tenant=None,
86
+ device_type=self.device_type,
87
+ role=self.device_role,
88
+ status=self.device_status,
89
+ )
90
+ dup = Device(
91
+ name="Device-2",
92
+ location=self.location,
93
+ tenant=None,
94
+ device_type=self.device_type,
95
+ role=self.device_role,
96
+ status=self.device_status,
97
+ )
98
+ with self.assertRaises(ValidationError) as contextmanager:
99
+ dup.full_clean()
100
+ self.assertIn(
101
+ f"A device named '{dup.name}' with no tenant already exists in this location: {self.location}. ",
102
+ str(contextmanager.exception),
103
+ )
104
+
105
+ @override_settings(DEVICE_UNIQUENESS=DeviceUniquenessChoices.NAME)
106
+ def test_duplicate_name_globally_fails(self):
107
+ """Duplicate name should raise ValidationError."""
108
+ tenant = Tenant.objects.create(name="Tenant2")
109
+ location = Location.objects.last()
110
+ dup_device = Device(
111
+ name=self.device_name,
112
+ device_type=self.device_type,
113
+ role=self.device_role,
114
+ location=location,
115
+ status=self.device_status,
116
+ tenant=tenant,
117
+ )
118
+ with self.assertRaises(ValidationError) as contextmanager:
119
+ dup_device.full_clean()
120
+ self.assertIn(
121
+ f"At least one other device named '{dup_device.name}' already exists. ", str(contextmanager.exception)
122
+ )
123
+
124
+ @override_settings(DEVICE_UNIQUENESS=DeviceUniquenessChoices.NAME)
125
+ def test_different_name_succeeds(self):
126
+ """Different name should be allowed globally."""
127
+ non_dup_device = Device(
128
+ name="Device-2",
129
+ device_type=self.device_type,
130
+ role=self.device_role,
131
+ location=self.location,
132
+ status=self.device_status,
133
+ tenant=self.tenant,
134
+ )
135
+ non_dup_device.full_clean() # should not raise
136
+
137
+ @override_settings(DEVICE_UNIQUENESS=DeviceUniquenessChoices.NAME)
138
+ def test_unnamed_device_allowed_if_name_not_required(self):
139
+ """Unnamed device allowed if DEVICE_NAME_REQUIRED is False."""
140
+ Device.objects.create(
141
+ name=None,
142
+ location=self.location,
143
+ tenant=self.tenant,
144
+ device_type=self.device_type,
145
+ role=self.device_role,
146
+ status=self.device_status,
147
+ )
148
+ unnamed2 = Device(
149
+ name=None,
150
+ location=self.location,
151
+ tenant=self.tenant,
152
+ device_type=self.device_type,
153
+ role=self.device_role,
154
+ status=self.device_status,
155
+ )
156
+ self.assertFalse(
157
+ RequiredValidationRule.objects.filter(
158
+ content_type=ContentType.objects.get_for_model(Device), field="name"
159
+ ).exists()
160
+ )
161
+ unnamed2.full_clean() # should not raise
162
+
163
+ def test_unnamed_device_fails_if_name_is_required(self):
164
+ """Unnamed device should raise a ValidationError if DEVICE_NAME_REQUIRED is True."""
165
+ unnamed = Device(
166
+ name=None,
167
+ location=self.location,
168
+ tenant=self.tenant,
169
+ device_type=self.device_type,
170
+ role=self.device_role,
171
+ status=self.device_status,
172
+ )
173
+ RequiredValidationRule.objects.create(content_type=ContentType.objects.get_for_model(Device), field="name")
174
+ with self.assertRaises(ValidationError) as contextmanager:
175
+ unnamed.full_clean()
176
+ # This error is from RequiredValidationRule
177
+ self.assertIn("{'name': ['This field cannot be blank.']}", str(contextmanager.exception))
178
+
179
+ def test_empty_device_fails_if_name_is_required(self):
180
+ """Empty name device should raise a ValidationError if DEVICE_NAME_REQUIRED is True."""
181
+ unnamed = Device(
182
+ name="",
183
+ location=self.location,
184
+ tenant=self.tenant,
185
+ device_type=self.device_type,
186
+ role=self.device_role,
187
+ status=self.device_status,
188
+ )
189
+ RequiredValidationRule.objects.create(content_type=ContentType.objects.get_for_model(Device), field="name")
190
+ with self.assertRaises(ValidationError) as contextmanager:
191
+ unnamed.full_clean()
192
+ # This error is from RequiredValidationRule
193
+ self.assertIn("{'name': ['This field cannot be blank.']}", str(contextmanager.exception))
194
+
195
+ @override_settings(DEVICE_UNIQUENESS=DeviceUniquenessChoices.NONE)
196
+ def test_no_uniqueness_enforced(self):
197
+ """Devices should not trigger validation errors when uniqueness is disabled."""
198
+ dup_device = Device(
199
+ name=self.device.name,
200
+ location=self.location,
201
+ tenant=self.tenant,
202
+ role=self.device_role,
203
+ device_type=self.device_type,
204
+ status=self.device_status,
205
+ )
206
+
207
+ # Should NOT raise any error since uniqueness enforcement is off
208
+ dup_device.full_clean()
209
+
210
+ @override_settings(DEVICE_UNIQUENESS=DeviceUniquenessChoices.NONE)
211
+ def test_allow_duplicate_devices_with_empty_name_when_uniqueness_is_none(self):
212
+ """Allow duplicate devices with empty name when DEVICE_UNIQUENESS="none"."""
213
+ Device.objects.create(
214
+ name="",
215
+ location=self.location,
216
+ tenant=self.tenant,
217
+ device_type=self.device_type,
218
+ role=self.device_role,
219
+ status=self.device_status,
220
+ )
221
+ empty_name = Device(
222
+ name="",
223
+ location=self.location,
224
+ tenant=self.tenant,
225
+ device_type=self.device_type,
226
+ role=self.device_role,
227
+ status=self.device_status,
228
+ )
229
+ empty_name.full_clean()
@@ -11,7 +11,9 @@ from nautobot.dcim.choices import (
11
11
  CableLengthUnitChoices,
12
12
  CableTypeChoices,
13
13
  DeviceFaceChoices,
14
+ InterfaceDuplexChoices,
14
15
  InterfaceModeChoices,
16
+ InterfaceSpeedChoices,
15
17
  InterfaceTypeChoices,
16
18
  PortTypeChoices,
17
19
  PowerFeedBreakerPoleChoices,
@@ -124,9 +126,10 @@ from nautobot.dcim.models import (
124
126
  VirtualChassis,
125
127
  VirtualDeviceContext,
126
128
  )
127
- from nautobot.extras.filters.mixins import RoleFilter, StatusFilter
129
+ from nautobot.extras.filter_mixins import RoleFilter, StatusFilter
128
130
  from nautobot.extras.models import ExternalIntegration, Role, SecretsGroup, Status, Tag
129
- from nautobot.ipam.models import IPAddress, Namespace, Prefix, Service, VLAN, VLANGroup
131
+ from nautobot.extras.tests.test_customfields_filters import CustomFieldsFilters
132
+ from nautobot.ipam.models import IPAddress, Namespace, Prefix, Service, VLAN, VLANGroup, VRF, VRFDeviceAssignment
130
133
  from nautobot.tenancy.models import Tenant
131
134
  from nautobot.virtualization.models import Cluster, ClusterType, VirtualMachine
132
135
  from nautobot.wireless.models import RadioProfile, WirelessNetwork
@@ -1039,7 +1042,9 @@ class PathEndpointModelTestMixin:
1039
1042
  )
1040
1043
 
1041
1044
 
1042
- class LocationTypeFilterSetTestCase(FilterTestCases.FilterTestCase):
1045
+ class LocationTypeFilterSetTestCase(
1046
+ FilterTestCases.FilterTestCase, CustomFieldsFilters.CustomFieldsFilterSetTestCaseMixin
1047
+ ):
1043
1048
  queryset = LocationType.objects.all()
1044
1049
  filterset = LocationTypeFilterSet
1045
1050
  generic_filter_tests = [
@@ -1076,7 +1081,10 @@ class LocationTypeFilterSetTestCase(FilterTestCases.FilterTestCase):
1076
1081
  )
1077
1082
 
1078
1083
 
1079
- class LocationFilterSetTestCase(FilterTestCases.FilterTestCase, FilterTestCases.TenancyFilterTestCaseMixin):
1084
+ class LocationFilterSetTestCase(
1085
+ FilterTestCases.FilterTestCase,
1086
+ FilterTestCases.TenancyFilterTestCaseMixin,
1087
+ ):
1080
1088
  queryset = Location.objects.all()
1081
1089
  filterset = LocationFilterSet
1082
1090
  tenancy_related_name = "locations"
@@ -1149,7 +1157,7 @@ class LocationFilterSetTestCase(FilterTestCases.FilterTestCase, FilterTestCases.
1149
1157
  )
1150
1158
 
1151
1159
 
1152
- class RackGroupTestCase(FilterTestCases.FilterTestCase):
1160
+ class RackGroupTestCase(FilterTestCases.FilterTestCase, CustomFieldsFilters.CustomFieldsFilterSetTestCaseMixin):
1153
1161
  queryset = RackGroup.objects.all()
1154
1162
  filterset = RackGroupFilterSet
1155
1163
  generic_filter_tests = [
@@ -1357,7 +1365,7 @@ class RackReservationTestCase(FilterTestCases.FilterTestCase, FilterTestCases.Te
1357
1365
  common_test_data(cls)
1358
1366
 
1359
1367
 
1360
- class ManufacturerTestCase(FilterTestCases.FilterTestCase):
1368
+ class ManufacturerTestCase(FilterTestCases.FilterTestCase, CustomFieldsFilters.CustomFieldsFilterSetTestCaseMixin):
1361
1369
  queryset = Manufacturer.objects.all()
1362
1370
  filterset = ManufacturerFilterSet
1363
1371
  generic_filter_tests = [
@@ -1393,7 +1401,7 @@ class DeviceFamilyTestCase(FilterTestCases.FilterTestCase):
1393
1401
  ]
1394
1402
 
1395
1403
 
1396
- class DeviceTypeTestCase(FilterTestCases.FilterTestCase):
1404
+ class DeviceTypeTestCase(FilterTestCases.FilterTestCase, CustomFieldsFilters.CustomFieldsFilterSetTestCaseMixin):
1397
1405
  queryset = DeviceType.objects.all()
1398
1406
  filterset = DeviceTypeFilterSet
1399
1407
  generic_filter_tests = [
@@ -1822,6 +1830,8 @@ class DeviceTestCase(
1822
1830
  ("vc_priority",),
1823
1831
  ("virtual_chassis", "virtual_chassis__id"),
1824
1832
  ("virtual_chassis", "virtual_chassis__name"),
1833
+ ("vrfs", "vrfs__id"),
1834
+ ("vrfs", "vrfs__rd"),
1825
1835
  ("wireless_networks", "controller_managed_device_group__wireless_networks__id"),
1826
1836
  ("wireless_networks", "controller_managed_device_group__wireless_networks__name"),
1827
1837
  ]
@@ -1928,6 +1938,14 @@ class DeviceTestCase(
1928
1938
  virtual_chassis_2 = VirtualChassis.objects.create(name="vc2", master=devices[2])
1929
1939
  Device.objects.filter(pk=devices[2].pk).update(virtual_chassis=virtual_chassis_2, vc_position=1, vc_priority=1)
1930
1940
 
1941
+ # VRF assignment for filtering
1942
+ vrfs = (
1943
+ VRF.objects.create(name="VRF 1", rd="1:1"),
1944
+ VRF.objects.create(name="VRF 2", rd="1:2"),
1945
+ )
1946
+ VRFDeviceAssignment.objects.create(device=devices[0], vrf=vrfs[0])
1947
+ VRFDeviceAssignment.objects.create(device=devices[1], vrf=vrfs[1])
1948
+
1931
1949
  def test_special_filters(self):
1932
1950
  # TODO: Not a generic_filter_test because this is a single-value filter
1933
1951
  with self.subTest("face"):
@@ -2247,6 +2265,8 @@ class InterfaceTestCase(PathEndpointModelTestMixin, ModularDeviceComponentTestMi
2247
2265
  ("name",),
2248
2266
  ("parent_interface", "parent_interface__id"),
2249
2267
  ("parent_interface", "parent_interface__name"),
2268
+ ("speed",),
2269
+ ("duplex",),
2250
2270
  ("role", "role__id"),
2251
2271
  ("role", "role__name"),
2252
2272
  ("status", "status__id"),
@@ -2326,6 +2346,8 @@ class InterfaceTestCase(PathEndpointModelTestMixin, ModularDeviceComponentTestMi
2326
2346
  mtu=100,
2327
2347
  status=interface_statuses[0],
2328
2348
  untagged_vlan=vlans[0],
2349
+ speed=InterfaceSpeedChoices.SPEED_1G,
2350
+ duplex=InterfaceDuplexChoices.DUPLEX_FULL,
2329
2351
  )
2330
2352
 
2331
2353
  Interface.objects.filter(pk=cabled_interfaces[1].pk).update(
@@ -2335,6 +2357,8 @@ class InterfaceTestCase(PathEndpointModelTestMixin, ModularDeviceComponentTestMi
2335
2357
  mtu=200,
2336
2358
  status=interface_statuses[3],
2337
2359
  untagged_vlan=vlans[1],
2360
+ speed=InterfaceSpeedChoices.SPEED_10G,
2361
+ duplex=InterfaceDuplexChoices.DUPLEX_HALF,
2338
2362
  )
2339
2363
 
2340
2364
  Interface.objects.filter(pk=cabled_interfaces[2].pk).update(
@@ -2348,6 +2372,16 @@ class InterfaceTestCase(PathEndpointModelTestMixin, ModularDeviceComponentTestMi
2348
2372
  for interface in cabled_interfaces:
2349
2373
  interface.refresh_from_db()
2350
2374
 
2375
+ # Additional optical interface for speed filtering (no duplex)
2376
+ Interface.objects.create(
2377
+ device=devices[2],
2378
+ name="Filter Optical IF",
2379
+ type=InterfaceTypeChoices.TYPE_10GE_SFP_PLUS,
2380
+ status=interface_statuses[0],
2381
+ speed=InterfaceSpeedChoices.SPEED_10G,
2382
+ duplex="",
2383
+ )
2384
+
2351
2385
  cable_statuses = Status.objects.get_for_model(Cable)
2352
2386
  connected_status = cable_statuses.get(name="Connected")
2353
2387
 
@@ -2543,6 +2577,20 @@ class InterfaceTestCase(PathEndpointModelTestMixin, ModularDeviceComponentTestMi
2543
2577
  params = {"mode": [InterfaceModeChoices.MODE_ACCESS]}
2544
2578
  self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
2545
2579
 
2580
+ def test_speed_multi(self):
2581
+ params = {"speed": [InterfaceSpeedChoices.SPEED_1G, InterfaceSpeedChoices.SPEED_10G]}
2582
+ self.assertQuerysetEqualAndNotEmpty(
2583
+ self.filterset(params, self.queryset).qs,
2584
+ self.queryset.filter(speed__in=params["speed"]),
2585
+ )
2586
+
2587
+ def test_speed_and_duplex(self):
2588
+ params = {"speed": [InterfaceSpeedChoices.SPEED_10G], "duplex": [InterfaceDuplexChoices.DUPLEX_HALF]}
2589
+ self.assertQuerysetEqualAndNotEmpty(
2590
+ self.filterset(params, self.queryset).qs,
2591
+ self.queryset.filter(speed__in=params["speed"], duplex__in=params["duplex"]),
2592
+ )
2593
+
2546
2594
  def test_device_with_common_vc(self):
2547
2595
  """Assert only interfaces belonging to devices with common VC are returned"""
2548
2596
  device_type = DeviceType.objects.first()