nautobot 3.0.0a3__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 (388) hide show
  1. nautobot/apps/choices.py +4 -0
  2. nautobot/apps/ui.py +4 -0
  3. nautobot/apps/utils.py +8 -0
  4. nautobot/circuits/tests/integration/test_circuits_bulk_operations.py +0 -3
  5. nautobot/circuits/views.py +6 -2
  6. nautobot/core/api/serializers.py +1 -1
  7. nautobot/core/api/urls.py +1 -0
  8. nautobot/core/api/views.py +4 -0
  9. nautobot/core/choices.py +1 -1
  10. nautobot/core/cli/bootstrap_v3_to_v5.py +36 -13
  11. nautobot/core/cli/migrate_deprecated_templates.py +36 -9
  12. nautobot/core/filters.py +4 -0
  13. nautobot/core/forms/__init__.py +2 -0
  14. nautobot/core/forms/widgets.py +21 -2
  15. nautobot/core/jobs/__init__.py +56 -0
  16. nautobot/core/management/commands/generate_test_data.py +3 -3
  17. nautobot/core/models/__init__.py +11 -0
  18. nautobot/core/models/utils.py +1 -1
  19. nautobot/core/settings.py +17 -7
  20. nautobot/core/settings.yaml +4 -26
  21. nautobot/core/templates/admin/base.html +1 -2
  22. nautobot/core/templates/admin/change_list.html +9 -12
  23. nautobot/core/templates/base_django.html +1 -2
  24. nautobot/core/templates/components/panel/header_extra_content_table.html +1 -1
  25. nautobot/core/templates/components/tab/content_wrapper.html +4 -4
  26. nautobot/core/templates/echarts/echarts.html +21 -8
  27. nautobot/core/templates/generic/object_bulk_create.html +2 -2
  28. nautobot/core/templates/generic/object_bulk_delete.html +1 -1
  29. nautobot/core/templates/generic/object_bulk_edit.html +1 -1
  30. nautobot/core/templates/generic/object_bulk_import.html +1 -1
  31. nautobot/core/templates/generic/object_delete.html +1 -1
  32. nautobot/core/templates/generic/object_detail.html +1 -1
  33. nautobot/core/templates/generic/object_edit.html +1 -1
  34. nautobot/core/templates/generic/object_retrieve.html +2 -2
  35. nautobot/core/templates/graphene/graphiql.html +0 -1
  36. nautobot/core/templates/inc/footer.html +3 -1
  37. nautobot/core/templates/inc/header.html +10 -0
  38. nautobot/core/templates/inc/media.html +14 -0
  39. nautobot/core/templates/inc/nav_menu.html +1 -8
  40. nautobot/core/templates/inc/object_details_advanced_panel.html +2 -2
  41. nautobot/core/templates/nautobot_config.py.j2 +0 -6
  42. nautobot/core/templates/rest_framework/api.html +103 -2
  43. nautobot/core/templates/utilities/templatetags/filter_form_drawer.html +33 -0
  44. nautobot/core/templates/utilities/theme_preview.html +3 -0
  45. nautobot/core/templates/widgets/number_input_with_choices.html +44 -0
  46. nautobot/core/templatetags/helpers.py +24 -12
  47. nautobot/core/testing/integration.py +24 -13
  48. nautobot/core/testing/utils.py +18 -4
  49. nautobot/core/testing/views.py +104 -17
  50. nautobot/core/tests/integration/test_filters.py +48 -11
  51. nautobot/core/tests/integration/test_theme.py +22 -21
  52. nautobot/core/tests/nautobot_config.py +3 -0
  53. nautobot/core/tests/runner.py +1 -2
  54. nautobot/core/tests/test_breadcrumbs.py +21 -21
  55. nautobot/core/tests/test_jobs.py +73 -6
  56. nautobot/core/tests/test_renderers.py +59 -0
  57. nautobot/core/tests/test_settings_schema.py +1 -0
  58. nautobot/core/tests/test_templatetags_helpers.py +9 -0
  59. nautobot/core/tests/test_titles.py +0 -16
  60. nautobot/core/tests/test_ui.py +122 -3
  61. nautobot/core/tests/test_utils.py +41 -1
  62. nautobot/core/ui/breadcrumbs.py +68 -17
  63. nautobot/core/ui/bulk_buttons.py +1 -1
  64. nautobot/core/ui/choices.py +49 -65
  65. nautobot/core/ui/echarts.py +15 -20
  66. nautobot/core/ui/object_detail.py +54 -46
  67. nautobot/core/ui/titles.py +3 -6
  68. nautobot/core/urls.py +8 -8
  69. nautobot/core/utils/filtering.py +11 -1
  70. nautobot/core/utils/lookup.py +46 -0
  71. nautobot/core/views/mixins.py +31 -20
  72. nautobot/core/views/renderers.py +2 -3
  73. nautobot/data_validation/migrations/0002_data_migration_from_app.py +3 -2
  74. nautobot/dcim/api/serializers.py +3 -0
  75. nautobot/dcim/choices.py +49 -0
  76. nautobot/dcim/constants.py +7 -0
  77. nautobot/dcim/factory.py +1 -1
  78. nautobot/dcim/filters.py +13 -1
  79. nautobot/dcim/forms.py +89 -3
  80. nautobot/dcim/migrations/0075_interface_duplex_interface_speed_and_more.py +32 -0
  81. nautobot/dcim/migrations/{0075_add_deviceclusterassignment.py → 0076_add_deviceclusterassignment.py} +1 -1
  82. nautobot/dcim/migrations/{0076_device_cluster_to_clusters_data_migration.py → 0077_device_cluster_to_clusters_data_migration.py} +1 -1
  83. nautobot/dcim/migrations/{0077_remove_device_cluster.py → 0078_remove_device_cluster.py} +1 -1
  84. nautobot/dcim/migrations/{0078_remove_device_location_tenant_name_uniqueness.py → 0079_remove_device_location_tenant_name_uniqueness.py} +1 -1
  85. nautobot/dcim/migrations/{0079_device_name_data_migration.py → 0080_device_name_data_migration.py} +1 -1
  86. nautobot/dcim/migrations/0081_alter_device_device_redundancy_group_priority_and_more.py +25 -0
  87. nautobot/dcim/models/device_component_templates.py +33 -1
  88. nautobot/dcim/models/device_components.py +22 -1
  89. nautobot/dcim/models/devices.py +17 -4
  90. nautobot/dcim/tables/devices.py +15 -0
  91. nautobot/dcim/tables/devicetypes.py +8 -1
  92. nautobot/dcim/tables/racks.py +0 -2
  93. nautobot/dcim/tables/template_code.py +1 -1
  94. nautobot/dcim/templates/dcim/cable_trace.html +0 -2
  95. nautobot/dcim/templates/dcim/consoleport.html +1 -1
  96. nautobot/dcim/templates/dcim/consoleserverport.html +1 -1
  97. nautobot/dcim/templates/dcim/devicebay.html +1 -1
  98. nautobot/dcim/templates/dcim/frontport.html +1 -1
  99. nautobot/dcim/templates/dcim/inc/devicetype_component_table.html +1 -1
  100. nautobot/dcim/templates/dcim/inc/moduletype_component_table.html +1 -1
  101. nautobot/dcim/templates/dcim/inc/rack_elevation.html +1 -1
  102. nautobot/dcim/templates/dcim/interface.html +9 -1
  103. nautobot/dcim/templates/dcim/interface_edit.html +2 -0
  104. nautobot/dcim/templates/dcim/inventoryitem.html +1 -1
  105. nautobot/dcim/templates/dcim/module_consoleports.html +1 -1
  106. nautobot/dcim/templates/dcim/module_consoleserverports.html +1 -1
  107. nautobot/dcim/templates/dcim/module_frontports.html +1 -1
  108. nautobot/dcim/templates/dcim/module_interfaces.html +1 -1
  109. nautobot/dcim/templates/dcim/module_modulebays.html +1 -1
  110. nautobot/dcim/templates/dcim/module_poweroutlets.html +1 -1
  111. nautobot/dcim/templates/dcim/module_powerports.html +1 -1
  112. nautobot/dcim/templates/dcim/module_rearports.html +1 -1
  113. nautobot/dcim/templates/dcim/moduletype_list.html +2 -2
  114. nautobot/dcim/templates/dcim/poweroutlet.html +1 -1
  115. nautobot/dcim/templates/dcim/powerport.html +1 -1
  116. nautobot/dcim/templates/dcim/rack_elevation_list.html +1 -1
  117. nautobot/dcim/templates/dcim/rack_retrieve.html +0 -11
  118. nautobot/dcim/templates/dcim/rearport.html +1 -1
  119. nautobot/dcim/templates/dcim/trace/cable.html +1 -1
  120. nautobot/dcim/templates/dcim/virtualchassis_update.html +1 -1
  121. nautobot/dcim/tests/integration/test_controller.py +3 -6
  122. nautobot/dcim/tests/integration/test_controller_managed_device_group.py +1 -5
  123. nautobot/dcim/tests/integration/test_create_device.py +0 -2
  124. nautobot/dcim/tests/integration/test_device_bulk_operations.py +1 -3
  125. nautobot/dcim/tests/integration/test_fileinputpicker.py +6 -10
  126. nautobot/dcim/tests/integration/test_location_bulk_operations.py +0 -2
  127. nautobot/dcim/tests/integration/test_module_bay_position.py +3 -4
  128. nautobot/dcim/tests/test_api.py +186 -6
  129. nautobot/dcim/tests/test_filters.py +43 -1
  130. nautobot/dcim/tests/test_forms.py +110 -8
  131. nautobot/dcim/tests/test_graphql.py +44 -1
  132. nautobot/dcim/tests/test_models.py +265 -0
  133. nautobot/dcim/tests/test_tables.py +160 -0
  134. nautobot/dcim/tests/test_views.py +69 -7
  135. nautobot/dcim/views.py +232 -126
  136. nautobot/extras/api/views.py +51 -44
  137. nautobot/extras/datasources/git.py +3 -1
  138. nautobot/extras/filters.py +19 -2
  139. nautobot/extras/forms/forms.py +9 -2
  140. nautobot/extras/jobs.py +2 -0
  141. nautobot/extras/jobs_ui.py +4 -3
  142. nautobot/extras/management/__init__.py +2 -0
  143. nautobot/extras/management/commands/refresh_dynamic_group_member_caches.py +4 -1
  144. nautobot/extras/migrations/0131_configcontext_device_families.py +18 -0
  145. nautobot/extras/models/approvals.py +11 -1
  146. nautobot/extras/models/change_logging.py +4 -0
  147. nautobot/extras/models/jobs.py +1 -3
  148. nautobot/extras/models/models.py +10 -2
  149. nautobot/extras/plugins/marketplace_manifest.yml +49 -1
  150. nautobot/extras/plugins/views.py +0 -5
  151. nautobot/extras/querysets.py +8 -0
  152. nautobot/extras/tables.py +12 -0
  153. nautobot/extras/templates/django_ajax_tables/ajax_wrapper.html +2 -0
  154. nautobot/extras/templates/extras/configcontext_update.html +1 -0
  155. nautobot/extras/templates/extras/dynamicgroup_update.html +1 -1
  156. nautobot/extras/templates/extras/objectchange_retrieve.html +0 -2
  157. nautobot/extras/templates/extras/plugin_detail.html +3 -3
  158. nautobot/extras/templates/extras/secret_create.html +1 -1
  159. nautobot/extras/tests/integration/test_computedfields.py +8 -9
  160. nautobot/extras/tests/integration/test_customfields.py +1 -3
  161. nautobot/extras/tests/integration/test_dynamicgroups.py +7 -8
  162. nautobot/extras/tests/integration/test_relationships.py +0 -2
  163. nautobot/extras/tests/test_api.py +63 -0
  164. nautobot/extras/tests/test_changelog.py +24 -2
  165. nautobot/extras/tests/test_filters.py +36 -3
  166. nautobot/extras/tests/test_models.py +38 -2
  167. nautobot/extras/tests/test_utils.py +3 -4
  168. nautobot/extras/tests/test_views.py +22 -83
  169. nautobot/extras/urls.py +0 -14
  170. nautobot/extras/views.py +83 -52
  171. nautobot/ipam/filters.py +26 -0
  172. nautobot/ipam/tables.py +6 -0
  173. nautobot/ipam/templates/ipam/namespace_ip_addresses.html +1 -1
  174. nautobot/ipam/templates/ipam/namespace_prefixes.html +1 -1
  175. nautobot/ipam/templates/ipam/namespace_vrfs.html +1 -1
  176. nautobot/ipam/tests/test_filters.py +26 -1
  177. nautobot/ipam/tests/test_models.py +1 -1
  178. nautobot/ipam/views.py +9 -7
  179. nautobot/load_balancers/__init__.py +0 -0
  180. nautobot/load_balancers/api/__init__.py +1 -0
  181. nautobot/load_balancers/api/serializers.py +75 -0
  182. nautobot/load_balancers/api/urls.py +23 -0
  183. nautobot/load_balancers/api/views.py +61 -0
  184. nautobot/load_balancers/apps.py +17 -0
  185. nautobot/load_balancers/choices.py +167 -0
  186. nautobot/load_balancers/filters.py +225 -0
  187. nautobot/load_balancers/forms.py +532 -0
  188. nautobot/load_balancers/management/commands/__init__.py +0 -0
  189. nautobot/load_balancers/management/commands/generate_load_balancer_models_test_data.py +38 -0
  190. nautobot/load_balancers/migrations/0001_initial.py +465 -0
  191. nautobot/load_balancers/migrations/0002_create_default_statuses_pool_members.py +31 -0
  192. nautobot/load_balancers/migrations/__init__.py +0 -0
  193. nautobot/load_balancers/models.py +423 -0
  194. nautobot/load_balancers/navigation.py +80 -0
  195. nautobot/load_balancers/tables.py +255 -0
  196. nautobot/load_balancers/tests/__init__.py +474 -0
  197. nautobot/load_balancers/tests/test_api.py +353 -0
  198. nautobot/load_balancers/tests/test_filters.py +134 -0
  199. nautobot/load_balancers/tests/test_forms.py +266 -0
  200. nautobot/load_balancers/tests/test_models.py +195 -0
  201. nautobot/load_balancers/tests/test_views.py +229 -0
  202. nautobot/load_balancers/urls.py +17 -0
  203. nautobot/load_balancers/views.py +248 -0
  204. nautobot/project-static/dist/css/github-dark.min.css +10 -0
  205. nautobot/project-static/dist/css/github.min.css +10 -0
  206. nautobot/project-static/dist/css/nautobot.css +1 -11
  207. nautobot/project-static/dist/css/nautobot.css.map +1 -1
  208. nautobot/project-static/dist/js/libraries.js +1 -1
  209. nautobot/project-static/dist/js/libraries.js.map +1 -1
  210. nautobot/project-static/dist/js/nautobot.js +1 -1
  211. nautobot/project-static/dist/js/nautobot.js.map +1 -1
  212. nautobot/project-static/js/forms.js +13 -0
  213. nautobot/project-static/nautobot-icons/bus-globe.svg +3 -0
  214. nautobot/project-static/nautobot-icons/bus-shield-check.svg +3 -0
  215. nautobot/project-static/nautobot-icons/bus-shield.svg +3 -0
  216. nautobot/ui/package-lock.json +87 -4
  217. nautobot/ui/package.json +2 -1
  218. nautobot/ui/src/js/nautobot.js +0 -1
  219. nautobot/ui/src/js/select2.js +53 -2
  220. nautobot/ui/src/scss/nautobot.scss +51 -2
  221. nautobot/ui/webpack.config.js +13 -0
  222. nautobot/users/templates/users/preferences.html +11 -2
  223. nautobot/virtualization/filters.py +6 -1
  224. nautobot/virtualization/tests/test_filters.py +10 -1
  225. nautobot/virtualization/tests/test_models.py +1 -0
  226. nautobot/virtualization/views.py +4 -1
  227. nautobot/vpn/factory.py +25 -15
  228. nautobot/vpn/filters.py +1 -0
  229. nautobot/vpn/forms.py +1 -0
  230. nautobot/vpn/migrations/0001_initial.py +1 -1
  231. nautobot/vpn/models.py +16 -8
  232. nautobot/vpn/tables.py +5 -2
  233. nautobot/vpn/tests/test_api.py +0 -5
  234. nautobot/vpn/tests/test_forms.py +1 -2
  235. nautobot/vpn/tests/test_models.py +57 -7
  236. nautobot/vpn/tests/test_views.py +22 -3
  237. nautobot/vpn/views.py +78 -20
  238. {nautobot-3.0.0a3.dist-info → nautobot-3.0.0rc1.dist-info}/METADATA +4 -4
  239. {nautobot-3.0.0a3.dist-info → nautobot-3.0.0rc1.dist-info}/RECORD +243 -352
  240. nautobot/circuits/templates/circuits/circuit.html +0 -2
  241. nautobot/circuits/templates/circuits/circuit_edit.html +0 -2
  242. nautobot/circuits/templates/circuits/circuit_retrieve.html +0 -2
  243. nautobot/circuits/templates/circuits/circuit_update.html +0 -1
  244. nautobot/circuits/templates/circuits/circuittermination.html +0 -2
  245. nautobot/circuits/templates/circuits/circuittermination_edit.html +0 -2
  246. nautobot/circuits/templates/circuits/circuittermination_retrieve.html +0 -2
  247. nautobot/circuits/templates/circuits/circuittermination_update.html +0 -1
  248. nautobot/circuits/templates/circuits/circuittype.html +0 -2
  249. nautobot/circuits/templates/circuits/circuittype_retrieve.html +0 -2
  250. nautobot/circuits/templates/circuits/inc/circuit_termination.html +0 -85
  251. nautobot/circuits/templates/circuits/provider.html +0 -2
  252. nautobot/circuits/templates/circuits/provider_edit.html +0 -2
  253. nautobot/circuits/templates/circuits/provider_retrieve.html +0 -1
  254. nautobot/circuits/templates/circuits/provider_update.html +0 -1
  255. nautobot/circuits/templates/circuits/providernetwork.html +0 -2
  256. nautobot/circuits/templates/circuits/providernetwork_retrieve.html +0 -2
  257. nautobot/cloud/templates/cloud/cloudaccount_retrieve.html +0 -2
  258. nautobot/cloud/templates/cloud/cloudnetwork_retrieve.html +0 -2
  259. nautobot/cloud/templates/cloud/cloudresourcetype_retrieve.html +0 -2
  260. nautobot/cloud/templates/cloud/cloudservice_retrieve.html +0 -2
  261. nautobot/core/templates/buttons/import.html +0 -9
  262. nautobot/data_validation/templates/data_validation/datacompliance_retrieve.html +0 -1
  263. nautobot/dcim/templates/dcim/cable.html +0 -2
  264. nautobot/dcim/templates/dcim/cable_edit.html +0 -2
  265. nautobot/dcim/templates/dcim/controller/base.html +0 -2
  266. nautobot/dcim/templates/dcim/controller_retrieve.html +0 -2
  267. nautobot/dcim/templates/dcim/controller_wirelessnetworks.html +0 -2
  268. nautobot/dcim/templates/dcim/controllermanageddevicegroup_retrieve.html +0 -2
  269. nautobot/dcim/templates/dcim/device/base.html +0 -2
  270. nautobot/dcim/templates/dcim/device/consoleports.html +0 -2
  271. nautobot/dcim/templates/dcim/device/consoleserverports.html +0 -2
  272. nautobot/dcim/templates/dcim/device/devicebays.html +0 -2
  273. nautobot/dcim/templates/dcim/device/frontports.html +0 -2
  274. nautobot/dcim/templates/dcim/device/interfaces.html +0 -2
  275. nautobot/dcim/templates/dcim/device/inventory.html +0 -2
  276. nautobot/dcim/templates/dcim/device/modulebays.html +0 -2
  277. nautobot/dcim/templates/dcim/device/poweroutlets.html +0 -2
  278. nautobot/dcim/templates/dcim/device/powerports.html +0 -2
  279. nautobot/dcim/templates/dcim/device/rearports.html +0 -2
  280. nautobot/dcim/templates/dcim/device/wireless.html +0 -2
  281. nautobot/dcim/templates/dcim/device_component.html +0 -2
  282. nautobot/dcim/templates/dcim/device_edit.html +0 -2
  283. nautobot/dcim/templates/dcim/devicefamily_retrieve.html +0 -2
  284. nautobot/dcim/templates/dcim/deviceredundancygroup_retrieve.html +0 -2
  285. nautobot/dcim/templates/dcim/devicetype.html +0 -2
  286. nautobot/dcim/templates/dcim/devicetype_edit.html +0 -2
  287. nautobot/dcim/templates/dcim/devicetype_retrieve.html +0 -2
  288. nautobot/dcim/templates/dcim/inc/device_napalm_tabs.html +0 -1
  289. nautobot/dcim/templates/dcim/interfaceredundancygroup_retrieve.html +0 -2
  290. nautobot/dcim/templates/dcim/location.html +0 -2
  291. nautobot/dcim/templates/dcim/location_edit.html +0 -2
  292. nautobot/dcim/templates/dcim/location_retrieve.html +0 -2
  293. nautobot/dcim/templates/dcim/locationtype.html +0 -2
  294. nautobot/dcim/templates/dcim/locationtype_retrieve.html +0 -2
  295. nautobot/dcim/templates/dcim/manufacturer.html +0 -2
  296. nautobot/dcim/templates/dcim/modulebay_retrieve.html +0 -1
  297. nautobot/dcim/templates/dcim/platform.html +0 -2
  298. nautobot/dcim/templates/dcim/powerfeed.html +0 -2
  299. nautobot/dcim/templates/dcim/powerfeed_retrieve.html +0 -2
  300. nautobot/dcim/templates/dcim/powerpanel.html +0 -2
  301. nautobot/dcim/templates/dcim/powerpanel_edit.html +0 -2
  302. nautobot/dcim/templates/dcim/powerpanel_retrieve.html +0 -2
  303. nautobot/dcim/templates/dcim/rack.html +0 -2
  304. nautobot/dcim/templates/dcim/rack_edit.html +0 -2
  305. nautobot/dcim/templates/dcim/rackgroup.html +0 -2
  306. nautobot/dcim/templates/dcim/rackreservation.html +0 -2
  307. nautobot/dcim/templates/dcim/softwareimagefile_retrieve.html +0 -2
  308. nautobot/dcim/templates/dcim/softwareversion_retrieve.html +0 -2
  309. nautobot/dcim/templates/dcim/virtualchassis.html +0 -2
  310. nautobot/dcim/templates/dcim/virtualchassis_add.html +0 -2
  311. nautobot/dcim/templates/dcim/virtualchassis_edit.html +0 -2
  312. nautobot/dcim/templates/dcim/virtualchassis_retrieve.html +0 -2
  313. nautobot/dcim/templates/dcim/virtualdevicecontext_retrieve.html +0 -2
  314. nautobot/dcim/ui.py +0 -29
  315. nautobot/extras/templates/extras/computedfield.html +0 -2
  316. nautobot/extras/templates/extras/computedfield_retrieve.html +0 -2
  317. nautobot/extras/templates/extras/configcontext.html +0 -2
  318. nautobot/extras/templates/extras/configcontext_edit.html +0 -2
  319. nautobot/extras/templates/extras/configcontext_retrieve.html +0 -2
  320. nautobot/extras/templates/extras/configcontextschema.html +0 -2
  321. nautobot/extras/templates/extras/configcontextschema_edit.html +0 -2
  322. nautobot/extras/templates/extras/contact_retrieve.html +0 -2
  323. nautobot/extras/templates/extras/customfield.html +0 -2
  324. nautobot/extras/templates/extras/customfield_edit.html +0 -2
  325. nautobot/extras/templates/extras/customfield_retrieve.html +0 -2
  326. nautobot/extras/templates/extras/customlink.html +0 -2
  327. nautobot/extras/templates/extras/dynamicgroup.html +0 -2
  328. nautobot/extras/templates/extras/dynamicgroup_edit.html +0 -2
  329. nautobot/extras/templates/extras/exporttemplate.html +0 -2
  330. nautobot/extras/templates/extras/gitrepository.html +0 -2
  331. nautobot/extras/templates/extras/gitrepository_object_edit.html +0 -2
  332. nautobot/extras/templates/extras/graphqlquery.html +0 -2
  333. nautobot/extras/templates/extras/graphqlquery_list.html +0 -1
  334. nautobot/extras/templates/extras/graphqlquery_retrieve.html +0 -2
  335. nautobot/extras/templates/extras/job_detail.html +0 -2
  336. nautobot/extras/templates/extras/jobbutton_retrieve.html +0 -2
  337. nautobot/extras/templates/extras/jobhook.html +0 -2
  338. nautobot/extras/templates/extras/jobqueue_retrieve.html +0 -2
  339. nautobot/extras/templates/extras/jobresult.html +0 -2
  340. nautobot/extras/templates/extras/metadatatype_retrieve.html +0 -2
  341. nautobot/extras/templates/extras/note.html +0 -2
  342. nautobot/extras/templates/extras/note_retrieve.html +0 -1
  343. nautobot/extras/templates/extras/object_changelog.html +0 -2
  344. nautobot/extras/templates/extras/object_notes.html +0 -2
  345. nautobot/extras/templates/extras/objectchange.html +0 -2
  346. nautobot/extras/templates/extras/objectchange_list.html +0 -3
  347. nautobot/extras/templates/extras/relationship.html +0 -1
  348. nautobot/extras/templates/extras/secret.html +0 -1
  349. nautobot/extras/templates/extras/secret_edit.html +0 -1
  350. nautobot/extras/templates/extras/secretsgroup.html +0 -2
  351. nautobot/extras/templates/extras/secretsgroup_edit.html +0 -2
  352. nautobot/extras/templates/extras/secretsgroup_retrieve.html +0 -2
  353. nautobot/extras/templates/extras/status.html +0 -2
  354. nautobot/extras/templates/extras/tag.html +0 -2
  355. nautobot/extras/templates/extras/tag_edit.html +0 -2
  356. nautobot/extras/templates/extras/tag_retrieve.html +0 -2
  357. nautobot/extras/templates/extras/team_retrieve.html +0 -2
  358. nautobot/ipam/templates/ipam/namespace_retrieve.html +0 -1
  359. nautobot/ipam/templates/ipam/prefix.html +0 -2
  360. nautobot/ipam/templates/ipam/prefix_edit.html +0 -1
  361. nautobot/ipam/templates/ipam/prefix_retrieve.html +0 -2
  362. nautobot/ipam/templates/ipam/rir.html +0 -2
  363. nautobot/ipam/templates/ipam/routetarget.html +0 -1
  364. nautobot/ipam/templates/ipam/service.html +0 -2
  365. nautobot/ipam/templates/ipam/service_edit.html +0 -2
  366. nautobot/ipam/templates/ipam/service_retrieve.html +0 -2
  367. nautobot/ipam/templates/ipam/vlan.html +0 -2
  368. nautobot/ipam/templates/ipam/vlan_edit.html +0 -2
  369. nautobot/ipam/templates/ipam/vlan_retrieve.html +0 -2
  370. nautobot/ipam/templates/ipam/vlangroup.html +0 -2
  371. nautobot/ipam/templates/ipam/vrf.html +0 -1
  372. nautobot/tenancy/templates/tenancy/tenant.html +0 -2
  373. nautobot/tenancy/templates/tenancy/tenant_edit.html +0 -2
  374. nautobot/tenancy/templates/tenancy/tenantgroup.html +0 -2
  375. nautobot/tenancy/templates/tenancy/tenantgroup_retrieve.html +0 -1
  376. nautobot/virtualization/templates/virtualization/clustergroup.html +0 -2
  377. nautobot/virtualization/templates/virtualization/clustertype.html +0 -2
  378. nautobot/virtualization/templates/virtualization/virtualmachine.html +0 -2
  379. nautobot/virtualization/templates/virtualization/virtualmachine_edit.html +0 -2
  380. nautobot/virtualization/templates/virtualization/virtualmachine_retrieve.html +0 -2
  381. nautobot/vpn/templates/vpn/vpnprofile.html +0 -2
  382. nautobot/wireless/templates/wireless/radioprofile_retrieve.html +0 -2
  383. nautobot/wireless/templates/wireless/supporteddatarate_retrieve.html +0 -2
  384. nautobot/wireless/templates/wireless/wirelessnetwork_retrieve.html +0 -2
  385. {nautobot-3.0.0a3.dist-info → nautobot-3.0.0rc1.dist-info}/LICENSE.txt +0 -0
  386. {nautobot-3.0.0a3.dist-info → nautobot-3.0.0rc1.dist-info}/NOTICE +0 -0
  387. {nautobot-3.0.0a3.dist-info → nautobot-3.0.0rc1.dist-info}/WHEEL +0 -0
  388. {nautobot-3.0.0a3.dist-info → nautobot-3.0.0rc1.dist-info}/entry_points.txt +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
@@ -2170,7 +2229,7 @@ class PowerOutletTest(Mixins.ModularDeviceComponentMixin, Mixins.BasePortTestMix
2170
2229
  class InterfaceTest(Mixins.ModularDeviceComponentMixin, Mixins.BasePortTestMixin):
2171
2230
  model = Interface
2172
2231
  peer_termination_type = Interface
2173
- choices_fields = ["mode", "type"]
2232
+ choices_fields = ["duplex", "mode", "type"]
2174
2233
  validation_excluded_fields = [
2175
2234
  "tagged_vlans", # M2M field, excluded by default
2176
2235
  ]
@@ -2214,14 +2273,14 @@ class InterfaceTest(Mixins.ModularDeviceComponentMixin, Mixins.BasePortTestMixin
2214
2273
  Interface.objects.create(
2215
2274
  device=cls.devices[0],
2216
2275
  name="Test Interface 1",
2217
- type="1000base-t",
2276
+ type=InterfaceTypeChoices.TYPE_1GE_FIXED,
2218
2277
  status=non_default_status,
2219
2278
  role=intf_role,
2220
2279
  ),
2221
2280
  Interface.objects.create(
2222
2281
  device=cls.devices[0],
2223
2282
  name="Test Interface 2",
2224
- type="1000base-t",
2283
+ type=InterfaceTypeChoices.TYPE_1GE_FIXED,
2225
2284
  status=non_default_status,
2226
2285
  ),
2227
2286
  Interface.objects.create(
@@ -2272,7 +2331,7 @@ class InterfaceTest(Mixins.ModularDeviceComponentMixin, Mixins.BasePortTestMixin
2272
2331
  {
2273
2332
  "device": cls.devices[0].pk,
2274
2333
  "name": "Test Interface 8",
2275
- "type": "1000base-t",
2334
+ "type": InterfaceTypeChoices.TYPE_1GE_FIXED,
2276
2335
  "status": interface_status.pk,
2277
2336
  "role": intf_role.pk,
2278
2337
  "mode": InterfaceModeChoices.MODE_TAGGED,
@@ -2283,7 +2342,7 @@ class InterfaceTest(Mixins.ModularDeviceComponentMixin, Mixins.BasePortTestMixin
2283
2342
  {
2284
2343
  "device": cls.devices[0].pk,
2285
2344
  "name": "Test Interface 9",
2286
- "type": "1000base-t",
2345
+ "type": InterfaceTypeChoices.TYPE_1GE_FIXED,
2287
2346
  "status": interface_status.pk,
2288
2347
  "role": intf_role.pk,
2289
2348
  "mode": InterfaceModeChoices.MODE_TAGGED,
@@ -2295,13 +2354,35 @@ class InterfaceTest(Mixins.ModularDeviceComponentMixin, Mixins.BasePortTestMixin
2295
2354
  {
2296
2355
  "device": cls.devices[0].pk,
2297
2356
  "name": "Test Interface 10",
2298
- "type": "virtual",
2357
+ "type": InterfaceTypeChoices.TYPE_VIRTUAL,
2299
2358
  "status": interface_status.pk,
2300
2359
  "mode": InterfaceModeChoices.MODE_TAGGED,
2301
2360
  "parent_interface": cls.interfaces[1].pk,
2302
2361
  "tagged_vlans": [cls.vlans[0].pk, cls.vlans[1].pk],
2303
2362
  "untagged_vlan": cls.vlans[2].pk,
2304
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
+ },
2305
2386
  ]
2306
2387
 
2307
2388
  cls.untagged_vlan_data = {
@@ -2510,6 +2591,105 @@ class InterfaceTest(Mixins.ModularDeviceComponentMixin, Mixins.BasePortTestMixin
2510
2591
  response = self.client.patch(self._get_detail_url(interface), data=payload, format="json", **self.header)
2511
2592
  self.assertHttpStatus(response, status.HTTP_200_OK)
2512
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
+
2513
2693
 
2514
2694
  class FrontPortTest(Mixins.BasePortTestMixin):
2515
2695
  model = FrontPort
@@ -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,
@@ -127,7 +129,7 @@ from nautobot.dcim.models import (
127
129
  from nautobot.extras.filter_mixins import RoleFilter, StatusFilter
128
130
  from nautobot.extras.models import ExternalIntegration, Role, SecretsGroup, Status, Tag
129
131
  from nautobot.extras.tests.test_customfields_filters import CustomFieldsFilters
130
- from nautobot.ipam.models import IPAddress, Namespace, Prefix, Service, VLAN, VLANGroup
132
+ from nautobot.ipam.models import IPAddress, Namespace, Prefix, Service, VLAN, VLANGroup, VRF, VRFDeviceAssignment
131
133
  from nautobot.tenancy.models import Tenant
132
134
  from nautobot.virtualization.models import Cluster, ClusterType, VirtualMachine
133
135
  from nautobot.wireless.models import RadioProfile, WirelessNetwork
@@ -1828,6 +1830,8 @@ class DeviceTestCase(
1828
1830
  ("vc_priority",),
1829
1831
  ("virtual_chassis", "virtual_chassis__id"),
1830
1832
  ("virtual_chassis", "virtual_chassis__name"),
1833
+ ("vrfs", "vrfs__id"),
1834
+ ("vrfs", "vrfs__rd"),
1831
1835
  ("wireless_networks", "controller_managed_device_group__wireless_networks__id"),
1832
1836
  ("wireless_networks", "controller_managed_device_group__wireless_networks__name"),
1833
1837
  ]
@@ -1934,6 +1938,14 @@ class DeviceTestCase(
1934
1938
  virtual_chassis_2 = VirtualChassis.objects.create(name="vc2", master=devices[2])
1935
1939
  Device.objects.filter(pk=devices[2].pk).update(virtual_chassis=virtual_chassis_2, vc_position=1, vc_priority=1)
1936
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
+
1937
1949
  def test_special_filters(self):
1938
1950
  # TODO: Not a generic_filter_test because this is a single-value filter
1939
1951
  with self.subTest("face"):
@@ -2253,6 +2265,8 @@ class InterfaceTestCase(PathEndpointModelTestMixin, ModularDeviceComponentTestMi
2253
2265
  ("name",),
2254
2266
  ("parent_interface", "parent_interface__id"),
2255
2267
  ("parent_interface", "parent_interface__name"),
2268
+ ("speed",),
2269
+ ("duplex",),
2256
2270
  ("role", "role__id"),
2257
2271
  ("role", "role__name"),
2258
2272
  ("status", "status__id"),
@@ -2332,6 +2346,8 @@ class InterfaceTestCase(PathEndpointModelTestMixin, ModularDeviceComponentTestMi
2332
2346
  mtu=100,
2333
2347
  status=interface_statuses[0],
2334
2348
  untagged_vlan=vlans[0],
2349
+ speed=InterfaceSpeedChoices.SPEED_1G,
2350
+ duplex=InterfaceDuplexChoices.DUPLEX_FULL,
2335
2351
  )
2336
2352
 
2337
2353
  Interface.objects.filter(pk=cabled_interfaces[1].pk).update(
@@ -2341,6 +2357,8 @@ class InterfaceTestCase(PathEndpointModelTestMixin, ModularDeviceComponentTestMi
2341
2357
  mtu=200,
2342
2358
  status=interface_statuses[3],
2343
2359
  untagged_vlan=vlans[1],
2360
+ speed=InterfaceSpeedChoices.SPEED_10G,
2361
+ duplex=InterfaceDuplexChoices.DUPLEX_HALF,
2344
2362
  )
2345
2363
 
2346
2364
  Interface.objects.filter(pk=cabled_interfaces[2].pk).update(
@@ -2354,6 +2372,16 @@ class InterfaceTestCase(PathEndpointModelTestMixin, ModularDeviceComponentTestMi
2354
2372
  for interface in cabled_interfaces:
2355
2373
  interface.refresh_from_db()
2356
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
+
2357
2385
  cable_statuses = Status.objects.get_for_model(Cable)
2358
2386
  connected_status = cable_statuses.get(name="Connected")
2359
2387
 
@@ -2549,6 +2577,20 @@ class InterfaceTestCase(PathEndpointModelTestMixin, ModularDeviceComponentTestMi
2549
2577
  params = {"mode": [InterfaceModeChoices.MODE_ACCESS]}
2550
2578
  self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
2551
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
+
2552
2594
  def test_device_with_common_vc(self):
2553
2595
  """Assert only interfaces belonging to devices with common VC are returned"""
2554
2596
  device_type = DeviceType.objects.first()
@@ -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)