nautobot 2.2.9__py3-none-any.whl → 2.3.0b1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of nautobot might be problematic. Click here for more details.

Files changed (697) hide show
  1. nautobot/apps/forms.py +4 -0
  2. nautobot/apps/models.py +10 -1
  3. nautobot/circuits/__init__.py +0 -1
  4. nautobot/circuits/apps.py +1 -0
  5. nautobot/circuits/factory.py +15 -3
  6. nautobot/circuits/filters.py +13 -0
  7. nautobot/circuits/forms.py +13 -0
  8. nautobot/circuits/migrations/0021_alter_circuit_status_alter_circuittermination__path.py +32 -0
  9. nautobot/circuits/migrations/0022_circuittermination_cloud_network.py +25 -0
  10. nautobot/circuits/models.py +16 -3
  11. nautobot/circuits/tables.py +16 -2
  12. nautobot/circuits/templates/circuits/circuittermination_create.html +10 -2
  13. nautobot/circuits/templates/circuits/circuittermination_retrieve.html +6 -0
  14. nautobot/circuits/templates/circuits/inc/circuit_termination.html +6 -1
  15. nautobot/circuits/tests/test_api.py +7 -5
  16. nautobot/circuits/tests/test_filters.py +12 -5
  17. nautobot/circuits/tests/test_models.py +33 -2
  18. nautobot/circuits/views.py +2 -3
  19. nautobot/cloud/__init__.py +0 -0
  20. nautobot/cloud/api/__init__.py +0 -0
  21. nautobot/cloud/api/serializers.py +54 -0
  22. nautobot/cloud/api/urls.py +16 -0
  23. nautobot/cloud/api/views.py +48 -0
  24. nautobot/cloud/apps.py +13 -0
  25. nautobot/cloud/factory.py +111 -0
  26. nautobot/cloud/filters.py +184 -0
  27. nautobot/cloud/forms.py +333 -0
  28. nautobot/cloud/homepage.py +43 -0
  29. nautobot/cloud/migrations/0001_initial.py +304 -0
  30. nautobot/cloud/migrations/__init__.py +0 -0
  31. nautobot/cloud/models.py +247 -0
  32. nautobot/cloud/navigation.py +85 -0
  33. nautobot/cloud/tables.py +173 -0
  34. nautobot/cloud/templates/cloud/cloudaccount_retrieve.html +43 -0
  35. nautobot/cloud/templates/cloud/cloudnetwork_retrieve.html +128 -0
  36. nautobot/cloud/templates/cloud/cloudnetwork_update.html +33 -0
  37. nautobot/cloud/templates/cloud/cloudresourcetype_retrieve.html +100 -0
  38. nautobot/cloud/templates/cloud/cloudservice_retrieve.html +65 -0
  39. nautobot/cloud/templates/cloud/cloudservice_update.html +25 -0
  40. nautobot/cloud/tests/__init__.py +0 -0
  41. nautobot/cloud/tests/test_api.py +248 -0
  42. nautobot/cloud/tests/test_filters.py +113 -0
  43. nautobot/cloud/tests/test_models.py +43 -0
  44. nautobot/cloud/tests/test_views.py +153 -0
  45. nautobot/cloud/urls.py +14 -0
  46. nautobot/cloud/views.py +181 -0
  47. nautobot/core/__init__.py +0 -3
  48. nautobot/core/api/metadata.py +1 -0
  49. nautobot/core/api/parsers.py +7 -1
  50. nautobot/core/api/urls.py +1 -0
  51. nautobot/core/api/utils.py +1 -0
  52. nautobot/core/api/views.py +4 -0
  53. nautobot/core/apps/__init__.py +6 -3
  54. nautobot/core/constants.py +8 -0
  55. nautobot/core/factory.py +32 -1
  56. nautobot/core/filters.py +96 -28
  57. nautobot/core/forms/fields.py +10 -4
  58. nautobot/core/forms/forms.py +1 -1
  59. nautobot/core/forms/widgets.py +18 -1
  60. nautobot/core/graphql/generators.py +2 -2
  61. nautobot/core/graphql/schema.py +34 -4
  62. nautobot/core/jobs/__init__.py +17 -6
  63. nautobot/core/jobs/cleanup.py +100 -0
  64. nautobot/core/jobs/groups.py +38 -0
  65. nautobot/core/management/commands/generate_test_data.py +116 -3
  66. nautobot/core/models/__init__.py +34 -9
  67. nautobot/core/models/generics.py +19 -3
  68. nautobot/core/models/name_color_content_types.py +7 -28
  69. nautobot/core/models/querysets.py +4 -3
  70. nautobot/core/models/tree_queries.py +1 -1
  71. nautobot/core/models/utils.py +21 -5
  72. nautobot/core/settings.py +4 -30
  73. nautobot/core/settings.yaml +34 -27
  74. nautobot/core/settings_funcs.py +103 -0
  75. nautobot/core/tables.py +127 -56
  76. nautobot/core/templates/admin/search_form.html +1 -1
  77. nautobot/core/templates/buttons/add.html +11 -3
  78. nautobot/core/templates/buttons/consolidated_bulk_action_buttons.html +13 -0
  79. nautobot/core/templates/buttons/consolidated_detail_view_action_buttons.html +13 -0
  80. nautobot/core/templates/buttons/export.html +101 -53
  81. nautobot/core/templates/buttons/job_import.html +11 -3
  82. nautobot/core/templates/generic/object_bulk_destroy.html +3 -1
  83. nautobot/core/templates/generic/object_bulk_update.html +3 -1
  84. nautobot/core/templates/generic/object_changelog.html +0 -9
  85. nautobot/core/templates/generic/object_list.html +156 -17
  86. nautobot/core/templates/generic/object_retrieve.html +80 -16
  87. nautobot/core/templates/inc/extras_features_edit_form_fields.html +8 -0
  88. nautobot/core/templates/inc/javascript.html +2 -0
  89. nautobot/core/templates/inc/media.html +2 -2
  90. nautobot/core/templates/inc/nav_menu.html +1 -0
  91. nautobot/core/templates/inc/paginator.html +7 -7
  92. nautobot/core/templates/inc/search_panel.html +2 -2
  93. nautobot/core/templates/inc/table.html +2 -2
  94. nautobot/core/templates/nautobot_config.py.j2 +13 -23
  95. nautobot/core/templates/utilities/templatetags/dynamic_group_assignment_modal.html +37 -0
  96. nautobot/core/templates/utilities/templatetags/filter_form_modal.html +2 -2
  97. nautobot/core/templates/utilities/templatetags/saved_view_modal.html +38 -0
  98. nautobot/core/templates/utilities/theme_preview.html +25 -8
  99. nautobot/core/templates/utilities/worker_status.html +152 -0
  100. nautobot/core/templatetags/buttons.py +335 -38
  101. nautobot/core/templatetags/form_helpers.py +1 -1
  102. nautobot/core/templatetags/helpers.py +181 -11
  103. nautobot/core/testing/api.py +5 -4
  104. nautobot/core/testing/filters.py +51 -13
  105. nautobot/core/testing/mixins.py +46 -0
  106. nautobot/core/testing/models.py +22 -0
  107. nautobot/core/testing/schema.py +4 -8
  108. nautobot/core/testing/views.py +31 -14
  109. nautobot/core/tests/integration/test_general_functionality.py +1 -1
  110. nautobot/core/tests/integration/test_import_objects_ui.py +1 -0
  111. nautobot/core/tests/integration/test_swagger.py +1 -1
  112. nautobot/core/tests/nautobot_config.py +0 -1
  113. nautobot/core/tests/runner.py +2 -2
  114. nautobot/core/tests/test_api.py +1 -0
  115. nautobot/core/tests/test_authentication.py +7 -2
  116. nautobot/core/tests/test_filters.py +11 -9
  117. nautobot/core/tests/test_forms.py +9 -0
  118. nautobot/core/tests/test_graphql.py +27 -16
  119. nautobot/core/tests/test_jobs.py +123 -74
  120. nautobot/core/tests/test_tables.py +3 -1
  121. nautobot/core/tests/test_templatetags_helpers.py +12 -5
  122. nautobot/core/tests/test_utils.py +31 -20
  123. nautobot/core/tests/test_views.py +6 -6
  124. nautobot/core/urls.py +8 -3
  125. nautobot/core/utils/deprecation.py +29 -0
  126. nautobot/core/utils/filtering.py +12 -9
  127. nautobot/core/utils/lookup.py +37 -2
  128. nautobot/core/utils/requests.py +4 -1
  129. nautobot/core/views/__init__.py +137 -24
  130. nautobot/core/views/generic.py +118 -66
  131. nautobot/core/views/mixins.py +104 -35
  132. nautobot/core/views/paginator.py +9 -3
  133. nautobot/core/views/renderers.py +121 -56
  134. nautobot/core/views/utils.py +79 -1
  135. nautobot/dcim/__init__.py +0 -1
  136. nautobot/dcim/api/serializers.py +180 -44
  137. nautobot/dcim/api/urls.py +7 -3
  138. nautobot/dcim/api/views.py +53 -7
  139. nautobot/dcim/apps.py +3 -0
  140. nautobot/dcim/choices.py +25 -0
  141. nautobot/dcim/constants.py +7 -0
  142. nautobot/dcim/factory.py +249 -18
  143. nautobot/dcim/filters/__init__.py +369 -193
  144. nautobot/dcim/filters/mixins.py +274 -1
  145. nautobot/dcim/forms.py +817 -109
  146. nautobot/dcim/graphql/types.py +2 -2
  147. nautobot/dcim/homepage.py +1 -1
  148. nautobot/dcim/migrations/0059_add_role_field_to_interface_models.py +27 -0
  149. nautobot/dcim/migrations/0060_alter_cable_status_alter_consoleport__path_and_more.py +303 -0
  150. nautobot/dcim/migrations/0061_module_models.py +861 -0
  151. nautobot/dcim/migrations/0062_module_data_migration.py +25 -0
  152. nautobot/dcim/models/__init__.py +8 -0
  153. nautobot/dcim/models/cables.py +15 -0
  154. nautobot/dcim/models/device_component_templates.py +207 -53
  155. nautobot/dcim/models/device_components.py +275 -106
  156. nautobot/dcim/models/devices.py +466 -13
  157. nautobot/dcim/navigation.py +47 -0
  158. nautobot/dcim/signals.py +3 -3
  159. nautobot/dcim/tables/__init__.py +35 -23
  160. nautobot/dcim/tables/devices.py +231 -59
  161. nautobot/dcim/tables/devicetypes.py +65 -9
  162. nautobot/dcim/tables/racks.py +5 -1
  163. nautobot/dcim/tables/template_code.py +46 -26
  164. nautobot/dcim/templates/dcim/cable_connect.html +76 -3
  165. nautobot/dcim/templates/dcim/console_port_connection_list.html +7 -5
  166. nautobot/dcim/templates/dcim/device/base.html +15 -7
  167. nautobot/dcim/templates/dcim/device/consoleports.html +2 -3
  168. nautobot/dcim/templates/dcim/device/consoleserverports.html +2 -3
  169. nautobot/dcim/templates/dcim/device/devicebays.html +6 -7
  170. nautobot/dcim/templates/dcim/device/frontports.html +2 -3
  171. nautobot/dcim/templates/dcim/device/interfaces.html +2 -3
  172. nautobot/dcim/templates/dcim/device/inventory.html +2 -3
  173. nautobot/dcim/templates/dcim/device/modulebays.html +49 -0
  174. nautobot/dcim/templates/dcim/device/poweroutlets.html +2 -3
  175. nautobot/dcim/templates/dcim/device/powerports.html +2 -3
  176. nautobot/dcim/templates/dcim/device/rearports.html +2 -3
  177. nautobot/dcim/templates/dcim/device.html +45 -1
  178. nautobot/dcim/templates/dcim/device_component.html +13 -5
  179. nautobot/dcim/templates/dcim/device_list.html +2 -1
  180. nautobot/dcim/templates/dcim/deviceredundancygroup_retrieve.html +0 -6
  181. nautobot/dcim/templates/dcim/devicetype.html +99 -98
  182. nautobot/dcim/templates/dcim/devicetype_list.html +8 -16
  183. nautobot/dcim/templates/dcim/inc/devicetype_component_table.html +1 -1
  184. nautobot/dcim/templates/dcim/inc/moduletype_component_table.html +39 -0
  185. nautobot/dcim/templates/dcim/interface.html +17 -2
  186. nautobot/dcim/templates/dcim/interface_connection_list.html +7 -5
  187. nautobot/dcim/templates/dcim/interface_edit.html +1 -0
  188. nautobot/dcim/templates/dcim/manufacturer.html +24 -0
  189. nautobot/dcim/templates/dcim/module/base.html +97 -0
  190. nautobot/dcim/templates/dcim/module_bulk_destroy.html +5 -0
  191. nautobot/dcim/templates/dcim/module_consoleports.html +53 -0
  192. nautobot/dcim/templates/dcim/module_consoleserverports.html +53 -0
  193. nautobot/dcim/templates/dcim/module_destroy.html +5 -0
  194. nautobot/dcim/templates/dcim/module_frontports.html +53 -0
  195. nautobot/dcim/templates/dcim/module_interfaces.html +57 -0
  196. nautobot/dcim/templates/dcim/module_list.html +20 -0
  197. nautobot/dcim/templates/dcim/module_modulebays.html +49 -0
  198. nautobot/dcim/templates/dcim/module_poweroutlets.html +53 -0
  199. nautobot/dcim/templates/dcim/module_powerports.html +53 -0
  200. nautobot/dcim/templates/dcim/module_rearports.html +53 -0
  201. nautobot/dcim/templates/dcim/module_retrieve.html +63 -0
  202. nautobot/dcim/templates/dcim/module_update.html +71 -0
  203. nautobot/dcim/templates/dcim/modulebay_bulk_destroy.html +5 -0
  204. nautobot/dcim/templates/dcim/modulebay_destroy.html +8 -0
  205. nautobot/dcim/templates/dcim/modulebay_retrieve.html +101 -0
  206. nautobot/dcim/templates/dcim/moduletype_list.html +11 -0
  207. nautobot/dcim/templates/dcim/moduletype_retrieve.html +142 -0
  208. nautobot/dcim/templates/dcim/power_port_connection_list.html +7 -5
  209. nautobot/dcim/templates/dcim/softwareimagefile_retrieve.html +65 -19
  210. nautobot/dcim/tests/integration/test_cable_connect_form.py +4 -4
  211. nautobot/dcim/tests/test_api.py +691 -208
  212. nautobot/dcim/tests/test_filters.py +836 -217
  213. nautobot/dcim/tests/test_models.py +1072 -39
  214. nautobot/dcim/tests/test_views.py +1488 -358
  215. nautobot/dcim/urls.py +17 -2
  216. nautobot/dcim/utils.py +2 -3
  217. nautobot/dcim/views.py +1107 -120
  218. nautobot/extras/__init__.py +0 -1
  219. nautobot/extras/api/serializers.py +115 -3
  220. nautobot/extras/api/urls.py +12 -0
  221. nautobot/extras/api/views.py +125 -7
  222. nautobot/extras/apps.py +2 -2
  223. nautobot/extras/choices.py +43 -0
  224. nautobot/extras/context_managers.py +13 -8
  225. nautobot/extras/datasources/git.py +2 -0
  226. nautobot/extras/factory.py +422 -9
  227. nautobot/extras/filters/__init__.py +174 -3
  228. nautobot/extras/filters/mixins.py +46 -43
  229. nautobot/extras/forms/base.py +17 -4
  230. nautobot/extras/forms/forms.py +227 -8
  231. nautobot/extras/forms/mixins.py +93 -0
  232. nautobot/extras/graphql/types.py +23 -10
  233. nautobot/extras/homepage.py +16 -13
  234. nautobot/extras/jobs.py +2 -2
  235. nautobot/extras/management/__init__.py +1 -0
  236. nautobot/extras/management/commands/refresh_dynamic_group_member_caches.py +1 -16
  237. nautobot/extras/migrations/0021_customfield_changelog_data.py +1 -0
  238. nautobot/extras/migrations/0109_dynamicgroup_group_type_dynamicgroup_tags_and_more.py +108 -0
  239. nautobot/extras/migrations/0110_alter_configcontext_cluster_groups_and_more.py +111 -0
  240. nautobot/extras/migrations/0111_metadata.py +162 -0
  241. nautobot/extras/migrations/0112_dynamic_group_group_type_data_migration.py +28 -0
  242. nautobot/extras/migrations/0113_saved_views.py +77 -0
  243. nautobot/extras/models/__init__.py +15 -1
  244. nautobot/extras/models/change_logging.py +3 -3
  245. nautobot/extras/models/contacts.py +4 -0
  246. nautobot/extras/models/customfields.py +18 -3
  247. nautobot/extras/models/groups.py +389 -225
  248. nautobot/extras/models/jobs.py +4 -84
  249. nautobot/extras/models/metadata.py +441 -0
  250. nautobot/extras/models/mixins.py +72 -62
  251. nautobot/extras/models/models.py +116 -9
  252. nautobot/extras/models/relationships.py +9 -2
  253. nautobot/extras/models/tags.py +13 -2
  254. nautobot/extras/navigation.py +57 -0
  255. nautobot/extras/plugins/__init__.py +3 -1
  256. nautobot/extras/querysets.py +30 -66
  257. nautobot/extras/signals.py +96 -114
  258. nautobot/extras/tables.py +171 -47
  259. nautobot/extras/templates/extras/dynamicgroup.html +44 -15
  260. nautobot/extras/templates/extras/dynamicgroup_edit.html +2 -0
  261. nautobot/extras/templates/extras/job.html +1 -1
  262. nautobot/extras/templates/extras/job_detail.html +0 -11
  263. nautobot/extras/templates/extras/jobresult.html +61 -74
  264. nautobot/extras/templates/extras/metadatatype_create.html +89 -0
  265. nautobot/extras/templates/extras/metadatatype_retrieve.html +67 -0
  266. nautobot/extras/templates/extras/object_dynamicgroups.html +7 -0
  267. nautobot/extras/templates/extras/objectchange_list.html +0 -12
  268. nautobot/extras/templates/extras/plugins_list.html +1 -3
  269. nautobot/extras/templates/extras/role_retrieve.html +48 -0
  270. nautobot/extras/templates/extras/staticgroupassociation_retrieve.html +20 -0
  271. nautobot/extras/tests/integration/test_customfields.py +1 -0
  272. nautobot/extras/tests/test_api.py +501 -22
  273. nautobot/extras/tests/test_changelog.py +20 -9
  274. nautobot/extras/tests/test_context_managers.py +22 -15
  275. nautobot/extras/tests/test_datasources.py +13 -1
  276. nautobot/extras/tests/test_dynamicgroups.py +201 -171
  277. nautobot/extras/tests/test_filters.py +211 -12
  278. nautobot/extras/tests/test_jobs.py +4 -4
  279. nautobot/extras/tests/test_models.py +499 -4
  280. nautobot/extras/tests/test_relationships.py +1 -0
  281. nautobot/extras/tests/test_views.py +565 -28
  282. nautobot/extras/tests/test_webhooks.py +1 -1
  283. nautobot/extras/urls.py +5 -0
  284. nautobot/extras/utils.py +56 -45
  285. nautobot/extras/views.py +585 -96
  286. nautobot/ipam/__init__.py +0 -1
  287. nautobot/ipam/apps.py +1 -0
  288. nautobot/ipam/factory.py +17 -19
  289. nautobot/ipam/filters.py +14 -1
  290. nautobot/ipam/forms.py +9 -5
  291. nautobot/ipam/graphql/types.py +2 -2
  292. nautobot/ipam/migrations/0047_alter_ipaddress_role_alter_ipaddress_status_and_more.py +59 -0
  293. nautobot/ipam/models.py +23 -9
  294. nautobot/ipam/querysets.py +1 -1
  295. nautobot/ipam/signals.py +4 -2
  296. nautobot/ipam/tables.py +1 -0
  297. nautobot/ipam/templates/ipam/ipaddress_interfaces.html +1 -1
  298. nautobot/ipam/templates/ipam/ipaddress_vm_interfaces.html +1 -1
  299. nautobot/ipam/templates/ipam/prefix.html +1 -0
  300. nautobot/ipam/tests/test_api.py +37 -18
  301. nautobot/ipam/tests/test_filters.py +26 -2
  302. nautobot/ipam/tests/test_models.py +8 -3
  303. nautobot/ipam/tests/test_querysets.py +1 -1
  304. nautobot/ipam/tests/test_views.py +3 -2
  305. nautobot/ipam/urls.py +2 -2
  306. nautobot/ipam/views.py +25 -28
  307. nautobot/project-static/css/base.css +20 -1
  308. nautobot/project-static/css/dark.css +11 -0
  309. nautobot/project-static/docs/404.html +884 -80
  310. nautobot/project-static/docs/apps/index.html +884 -80
  311. nautobot/project-static/docs/apps/nautobot-apps.html +884 -80
  312. nautobot/project-static/docs/assets/_mkdocstrings.css +5 -0
  313. nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +911 -112
  314. nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +896 -93
  315. nautobot/project-static/docs/code-reference/nautobot/apps/api.html +1457 -790
  316. nautobot/project-static/docs/code-reference/nautobot/apps/change_logging.html +927 -136
  317. nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +969 -180
  318. nautobot/project-static/docs/code-reference/nautobot/apps/config.html +893 -91
  319. nautobot/project-static/docs/code-reference/nautobot/apps/constants.html +889 -85
  320. nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +983 -185
  321. nautobot/project-static/docs/code-reference/nautobot/apps/exceptions.html +938 -143
  322. nautobot/project-static/docs/code-reference/nautobot/apps/factory.html +1064 -274
  323. nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +1190 -346
  324. nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +1663 -865
  325. nautobot/project-static/docs/code-reference/nautobot/apps/graphql.html +1156 -373
  326. nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +2200 -1502
  327. nautobot/project-static/docs/code-reference/nautobot/apps/models.html +2229 -1421
  328. nautobot/project-static/docs/code-reference/nautobot/apps/querysets.html +904 -103
  329. nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +955 -155
  330. nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +1002 -215
  331. nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +1911 -1275
  332. nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +1835 -1091
  333. nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +896 -93
  334. nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +2323 -1693
  335. nautobot/project-static/docs/code-reference/nautobot/apps/views.html +1785 -1023
  336. nautobot/project-static/docs/development/apps/api/configuration-view.html +884 -80
  337. nautobot/project-static/docs/development/apps/api/database-backend-config.html +884 -80
  338. nautobot/project-static/docs/development/apps/api/models/django-admin.html +884 -80
  339. nautobot/project-static/docs/development/apps/api/models/global-search.html +884 -80
  340. nautobot/project-static/docs/development/apps/api/models/graphql.html +884 -80
  341. nautobot/project-static/docs/development/apps/api/models/index.html +922 -81
  342. nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +884 -80
  343. nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +884 -80
  344. nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +884 -80
  345. nautobot/project-static/docs/development/apps/api/platform-features/git-repository-content.html +884 -80
  346. nautobot/project-static/docs/development/apps/api/platform-features/index.html +884 -80
  347. nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +884 -80
  348. nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +884 -80
  349. nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +884 -80
  350. nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +884 -80
  351. nautobot/project-static/docs/development/apps/api/platform-features/uniquely-identify-objects.html +884 -80
  352. nautobot/project-static/docs/development/apps/api/prometheus.html +884 -80
  353. nautobot/project-static/docs/development/apps/api/setup.html +884 -80
  354. nautobot/project-static/docs/development/apps/api/testing.html +884 -80
  355. nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +884 -80
  356. nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +884 -80
  357. nautobot/project-static/docs/development/apps/api/ui-extensions/index.html +884 -80
  358. nautobot/project-static/docs/development/apps/api/ui-extensions/navigation.html +884 -80
  359. nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +884 -80
  360. nautobot/project-static/docs/development/apps/api/views/base-template.html +884 -80
  361. nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +884 -80
  362. nautobot/project-static/docs/development/apps/api/views/django-generic-views.html +884 -80
  363. nautobot/project-static/docs/development/apps/api/views/help-documentation.html +884 -80
  364. nautobot/project-static/docs/development/apps/api/views/index.html +884 -80
  365. nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +884 -80
  366. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +884 -80
  367. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +884 -80
  368. nautobot/project-static/docs/development/apps/api/views/notes.html +884 -80
  369. nautobot/project-static/docs/development/apps/api/views/rest-api.html +884 -80
  370. nautobot/project-static/docs/development/apps/api/views/urls.html +884 -80
  371. nautobot/project-static/docs/development/apps/index.html +884 -80
  372. nautobot/project-static/docs/development/apps/migration/code-updates.html +884 -80
  373. nautobot/project-static/docs/development/apps/migration/dependency-updates.html +884 -80
  374. nautobot/project-static/docs/development/apps/migration/from-v1.html +884 -80
  375. nautobot/project-static/docs/development/apps/migration/model-updates/dcim.html +884 -80
  376. nautobot/project-static/docs/development/apps/migration/model-updates/extras.html +884 -80
  377. nautobot/project-static/docs/development/apps/migration/model-updates/global.html +884 -80
  378. nautobot/project-static/docs/development/apps/migration/model-updates/ipam.html +884 -80
  379. nautobot/project-static/docs/development/apps/porting-from-netbox.html +884 -80
  380. nautobot/project-static/docs/development/core/application-registry.html +884 -80
  381. nautobot/project-static/docs/development/core/best-practices.html +885 -80
  382. nautobot/project-static/docs/development/core/bootstrap-ui.html +884 -80
  383. nautobot/project-static/docs/development/core/caching.html +884 -80
  384. nautobot/project-static/docs/development/core/controllers.html +884 -80
  385. nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +884 -80
  386. nautobot/project-static/docs/development/core/generic-views.html +884 -80
  387. nautobot/project-static/docs/development/core/getting-started.html +884 -80
  388. nautobot/project-static/docs/development/core/homepage.html +884 -80
  389. nautobot/project-static/docs/development/core/index.html +884 -91
  390. nautobot/project-static/docs/development/core/model-checklist.html +887 -81
  391. nautobot/project-static/docs/development/core/model-features.html +884 -80
  392. nautobot/project-static/docs/development/core/natural-keys.html +884 -80
  393. nautobot/project-static/docs/development/core/navigation-menu.html +884 -80
  394. nautobot/project-static/docs/development/core/release-checklist.html +887 -83
  395. nautobot/project-static/docs/development/core/role-internals.html +884 -80
  396. nautobot/project-static/docs/development/core/settings.html +884 -80
  397. nautobot/project-static/docs/development/core/style-guide.html +885 -81
  398. nautobot/project-static/docs/development/core/templates.html +896 -81
  399. nautobot/project-static/docs/development/core/testing.html +884 -80
  400. nautobot/project-static/docs/development/core/user-preferences.html +884 -80
  401. nautobot/project-static/docs/development/index.html +884 -80
  402. nautobot/project-static/docs/development/jobs/index.html +1247 -457
  403. nautobot/project-static/docs/development/jobs/migration/from-v1.html +884 -80
  404. nautobot/project-static/docs/index.html +13 -8228
  405. nautobot/project-static/docs/media/models/cloud_aws_direct_connect_dark.png +0 -0
  406. nautobot/project-static/docs/media/models/cloud_aws_direct_connect_light.png +0 -0
  407. nautobot/project-static/docs/models/cloud/cloudaccount.html +15 -0
  408. nautobot/project-static/docs/models/cloud/cloudnetwork.html +15 -0
  409. nautobot/project-static/docs/models/cloud/cloudnetworkprefixassignment.html +15 -0
  410. nautobot/project-static/docs/models/cloud/cloudresourcetype.html +15 -0
  411. nautobot/project-static/docs/models/cloud/cloudservice.html +15 -0
  412. nautobot/project-static/docs/models/cloud/cloudservicenetworkassignment.html +15 -0
  413. nautobot/project-static/docs/models/dcim/module.html +15 -0
  414. nautobot/project-static/docs/models/dcim/modulebay.html +15 -0
  415. nautobot/project-static/docs/models/dcim/modulebaytemplate.html +15 -0
  416. nautobot/project-static/docs/models/dcim/moduletype.html +15 -0
  417. nautobot/project-static/docs/models/extras/metadatachoice.html +15 -0
  418. nautobot/project-static/docs/models/extras/metadatatype.html +15 -0
  419. nautobot/project-static/docs/models/extras/objectmetadata.html +15 -0
  420. nautobot/project-static/docs/models/extras/role.html +15 -0
  421. nautobot/project-static/docs/models/extras/savedview.html +15 -0
  422. nautobot/project-static/docs/models/extras/staticgroupassociation.html +15 -0
  423. nautobot/project-static/docs/models/extras/status.html +15 -0
  424. nautobot/project-static/docs/objects.inv +0 -0
  425. nautobot/project-static/docs/overview/application_stack.html +892 -81
  426. nautobot/project-static/docs/overview/design_philosophy.html +886 -82
  427. nautobot/project-static/docs/overview/index.html +9032 -13
  428. nautobot/project-static/docs/release-notes/index.html +887 -83
  429. nautobot/project-static/docs/release-notes/version-1.0.html +884 -80
  430. nautobot/project-static/docs/release-notes/version-1.1.html +884 -80
  431. nautobot/project-static/docs/release-notes/version-1.2.html +884 -80
  432. nautobot/project-static/docs/release-notes/version-1.3.html +884 -80
  433. nautobot/project-static/docs/release-notes/version-1.4.html +884 -80
  434. nautobot/project-static/docs/release-notes/version-1.5.html +885 -81
  435. nautobot/project-static/docs/release-notes/version-1.6.html +885 -81
  436. nautobot/project-static/docs/release-notes/version-2.0.html +884 -80
  437. nautobot/project-static/docs/release-notes/version-2.1.html +884 -80
  438. nautobot/project-static/docs/release-notes/version-2.2.html +990 -323
  439. nautobot/project-static/docs/release-notes/version-2.3.html +9524 -0
  440. nautobot/project-static/docs/requirements.txt +4 -4
  441. nautobot/project-static/docs/search/search_index.json +1 -1
  442. nautobot/project-static/docs/sitemap.xml +335 -260
  443. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  444. nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +884 -80
  445. nautobot/project-static/docs/user-guide/administration/configuration/authentication/remote.html +884 -80
  446. nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +884 -80
  447. nautobot/project-static/docs/user-guide/administration/configuration/index.html +884 -80
  448. nautobot/project-static/docs/user-guide/administration/configuration/optional-settings.html +983 -197
  449. nautobot/project-static/docs/user-guide/administration/configuration/required-settings.html +884 -80
  450. nautobot/project-static/docs/user-guide/administration/configuration/time-zones.html +884 -80
  451. nautobot/project-static/docs/user-guide/administration/guides/caching.html +884 -80
  452. nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +888 -84
  453. nautobot/project-static/docs/user-guide/administration/guides/healthcheck.html +884 -80
  454. nautobot/project-static/docs/user-guide/administration/guides/permissions.html +884 -80
  455. nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +884 -80
  456. nautobot/project-static/docs/user-guide/administration/guides/replicating-nautobot.html +884 -80
  457. nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +884 -80
  458. nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +884 -80
  459. nautobot/project-static/docs/user-guide/administration/installation/app-install.html +884 -80
  460. nautobot/project-static/docs/user-guide/administration/installation/external-authentication.html +884 -80
  461. nautobot/project-static/docs/user-guide/administration/installation/http-server.html +884 -80
  462. nautobot/project-static/docs/user-guide/administration/installation/index.html +888 -80
  463. nautobot/project-static/docs/user-guide/administration/installation/install_system.html +884 -80
  464. nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +884 -80
  465. nautobot/project-static/docs/user-guide/administration/installation/services.html +888 -80
  466. nautobot/project-static/docs/user-guide/administration/installation-extras/docker.html +900 -91
  467. nautobot/project-static/docs/user-guide/administration/installation-extras/health-checks.html +884 -80
  468. nautobot/project-static/docs/user-guide/administration/installation-extras/selinux-troubleshooting.html +884 -80
  469. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-netbox.html +884 -80
  470. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +884 -80
  471. nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +915 -163
  472. nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +884 -80
  473. nautobot/project-static/docs/user-guide/administration/upgrading/database-backup.html +884 -80
  474. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/after-you-upgrade.html +884 -80
  475. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/before-you-upgrade.html +884 -80
  476. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/for-developers.html +884 -80
  477. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/index.html +884 -80
  478. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/whats-changed.html +884 -80
  479. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/region-and-site-data-migration-guide.html +884 -80
  480. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +884 -80
  481. nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +884 -80
  482. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuit.html +884 -80
  483. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittermination.html +885 -81
  484. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittype.html +884 -80
  485. nautobot/project-static/docs/user-guide/core-data-model/circuits/provider.html +888 -80
  486. nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +887 -83
  487. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloud.html +8984 -0
  488. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudaccount.html +8828 -0
  489. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetwork.html +8829 -0
  490. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetworkprefixassignment.html +8828 -0
  491. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudresourcetype.html +8829 -0
  492. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservice.html +8833 -0
  493. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservicenetworkassignment.html +8828 -0
  494. nautobot/project-static/docs/user-guide/core-data-model/dcim/cable.html +898 -94
  495. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +915 -97
  496. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +915 -97
  497. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +910 -92
  498. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +915 -97
  499. nautobot/project-static/docs/user-guide/core-data-model/dcim/controller.html +898 -94
  500. nautobot/project-static/docs/user-guide/core-data-model/dcim/controllermanageddevicegroup.html +898 -94
  501. nautobot/project-static/docs/user-guide/core-data-model/dcim/device.html +905 -97
  502. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +912 -108
  503. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +913 -109
  504. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicefamily.html +910 -106
  505. nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +898 -94
  506. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +906 -97
  507. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +918 -100
  508. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +928 -110
  509. nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +920 -98
  510. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +898 -94
  511. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +929 -111
  512. nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +920 -102
  513. nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +910 -106
  514. nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +913 -109
  515. nautobot/project-static/docs/user-guide/core-data-model/dcim/manufacturer.html +914 -106
  516. nautobot/project-static/docs/user-guide/core-data-model/dcim/module.html +8828 -0
  517. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebay.html +8846 -0
  518. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebaytemplate.html +8843 -0
  519. nautobot/project-static/docs/user-guide/core-data-model/dcim/moduletype.html +8823 -0
  520. nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +908 -104
  521. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerfeed.html +898 -94
  522. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +932 -75
  523. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +916 -98
  524. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerpanel.html +898 -94
  525. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +935 -78
  526. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +913 -95
  527. nautobot/project-static/docs/user-guide/core-data-model/dcim/rack.html +921 -117
  528. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackgroup.html +910 -106
  529. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackreservation.html +898 -94
  530. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +914 -96
  531. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +916 -98
  532. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +898 -94
  533. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareversion.html +898 -94
  534. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualchassis.html +898 -94
  535. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +884 -80
  536. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +884 -80
  537. nautobot/project-static/docs/user-guide/core-data-model/extras/contact.html +889 -81
  538. nautobot/project-static/docs/user-guide/core-data-model/extras/team.html +889 -81
  539. nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +884 -80
  540. nautobot/project-static/docs/user-guide/core-data-model/ipam/namespace.html +884 -80
  541. nautobot/project-static/docs/user-guide/core-data-model/ipam/prefix.html +884 -80
  542. nautobot/project-static/docs/user-guide/core-data-model/ipam/rir.html +884 -80
  543. nautobot/project-static/docs/user-guide/core-data-model/ipam/routetarget.html +884 -80
  544. nautobot/project-static/docs/user-guide/core-data-model/ipam/service.html +884 -80
  545. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +884 -80
  546. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlangroup.html +884 -80
  547. nautobot/project-static/docs/user-guide/core-data-model/ipam/vrf.html +884 -80
  548. nautobot/project-static/docs/user-guide/core-data-model/overview/introduction.html +884 -80
  549. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenant.html +893 -88
  550. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenantgroup.html +884 -80
  551. nautobot/project-static/docs/user-guide/core-data-model/virtualization/cluster.html +884 -80
  552. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustergroup.html +884 -80
  553. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustertype.html +884 -80
  554. nautobot/project-static/docs/user-guide/core-data-model/virtualization/virtualmachine.html +884 -80
  555. nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +889 -81
  556. nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +884 -80
  557. nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +884 -80
  558. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +884 -80
  559. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +884 -80
  560. nautobot/project-static/docs/user-guide/feature-guides/getting-started/index.html +884 -80
  561. nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +884 -80
  562. nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +884 -80
  563. nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +884 -80
  564. nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +884 -80
  565. nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +884 -80
  566. nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +884 -80
  567. nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +884 -80
  568. nautobot/project-static/docs/user-guide/feature-guides/graphql.html +884 -80
  569. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/clear-view-button.png +0 -0
  570. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/cleared-view.png +0 -0
  571. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/config-table-columns-to-locations.png +0 -0
  572. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/configure-button.png +0 -0
  573. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/create-saved-view-success.png +0 -0
  574. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/current-saved-view-drop-down-menu.png +0 -0
  575. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/default-location-list-view.png +0 -0
  576. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/dropdown-button-after-new-saved-view.png +0 -0
  577. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/filter-application-to-locations.png +0 -0
  578. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/filter-button.png +0 -0
  579. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/global-default-location-list-view.png +0 -0
  580. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/location-list-view-with-saved-views.png +0 -0
  581. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/navigation-menu.png +0 -0
  582. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/save-as-new-view-drop-down.png +0 -0
  583. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/save-view-modal.png +0 -0
  584. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/saved-view-admin-edit-buttons.png +0 -0
  585. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/saved-view-admin-edit-success.png +0 -0
  586. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/saved-view-admin-edit-view-unchecked.png +0 -0
  587. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/saved-view-admin-edit-view.png +0 -0
  588. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/saved-view-different-user.png +0 -0
  589. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/saved-view-modal-unchecked.png +0 -0
  590. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/set-as-my-default-button.png +0 -0
  591. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/set-as-my-default-success.png +0 -0
  592. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/unsaved-saved-view.png +0 -0
  593. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/updated-saved-view.png +0 -0
  594. nautobot/project-static/docs/user-guide/feature-guides/ip-address-merge-tool.html +884 -80
  595. nautobot/project-static/docs/user-guide/feature-guides/relationships.html +884 -80
  596. nautobot/project-static/docs/user-guide/feature-guides/software-image-files-and-versions.html +884 -80
  597. nautobot/project-static/docs/user-guide/index.html +884 -80
  598. nautobot/project-static/docs/user-guide/platform-functionality/change-logging.html +884 -80
  599. nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +884 -80
  600. nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +884 -80
  601. nautobot/project-static/docs/user-guide/platform-functionality/customlink.html +884 -80
  602. nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +1250 -777
  603. nautobot/project-static/docs/user-guide/platform-functionality/exporttemplate.html +887 -83
  604. nautobot/project-static/docs/user-guide/platform-functionality/externalintegration.html +884 -80
  605. nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +884 -80
  606. nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +884 -80
  607. nautobot/project-static/docs/user-guide/platform-functionality/graphqlquery.html +884 -80
  608. nautobot/project-static/docs/user-guide/platform-functionality/imageattachment.html +884 -80
  609. nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +884 -80
  610. nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +887 -83
  611. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +884 -80
  612. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +884 -80
  613. nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +888 -80
  614. nautobot/project-static/docs/user-guide/platform-functionality/metadata.html +8948 -0
  615. nautobot/project-static/docs/user-guide/platform-functionality/napalm.html +887 -83
  616. nautobot/project-static/docs/user-guide/platform-functionality/note.html +884 -80
  617. nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +884 -80
  618. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +884 -80
  619. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +884 -80
  620. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +884 -80
  621. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/ui-related-endpoints.html +884 -80
  622. nautobot/project-static/docs/user-guide/platform-functionality/role.html +887 -83
  623. nautobot/project-static/docs/user-guide/platform-functionality/savedview.html +9137 -0
  624. nautobot/project-static/docs/user-guide/platform-functionality/secret.html +887 -83
  625. nautobot/project-static/docs/user-guide/platform-functionality/staticgroupassociation.html +8933 -0
  626. nautobot/project-static/docs/user-guide/platform-functionality/status.html +884 -80
  627. nautobot/project-static/docs/user-guide/platform-functionality/tag.html +884 -80
  628. nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +942 -113
  629. nautobot/project-static/docs/user-guide/platform-functionality/users/objectpermission.html +884 -80
  630. nautobot/project-static/docs/user-guide/platform-functionality/users/token.html +884 -80
  631. nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +884 -80
  632. nautobot/project-static/js/forms.js +71 -0
  633. nautobot/project-static/js/table_sorting_indicator.js +46 -0
  634. nautobot/project-static/js/tableconfig.js +6 -1
  635. nautobot/project-static/materialdesignicons-7.4.47/css/materialdesignicons.min.css +3 -0
  636. nautobot/project-static/{materialdesignicons-6.5.95 → materialdesignicons-7.4.47}/fonts/materialdesignicons-webfont.eot +0 -0
  637. nautobot/project-static/{materialdesignicons-6.5.95 → materialdesignicons-7.4.47}/fonts/materialdesignicons-webfont.ttf +0 -0
  638. nautobot/project-static/materialdesignicons-7.4.47/fonts/materialdesignicons-webfont.woff +0 -0
  639. nautobot/project-static/materialdesignicons-7.4.47/fonts/materialdesignicons-webfont.woff2 +0 -0
  640. nautobot/tenancy/__init__.py +0 -1
  641. nautobot/tenancy/apps.py +1 -0
  642. nautobot/tenancy/factory.py +3 -2
  643. nautobot/tenancy/filters/__init__.py +1 -0
  644. nautobot/tenancy/forms.py +1 -1
  645. nautobot/tenancy/templates/tenancy/tenant.html +22 -18
  646. nautobot/tenancy/views.py +11 -10
  647. nautobot/users/__init__.py +0 -1
  648. nautobot/users/api/serializers.py +1 -1
  649. nautobot/users/api/views.py +4 -2
  650. nautobot/users/apps.py +3 -2
  651. nautobot/users/factory.py +3 -3
  652. nautobot/users/migrations/0010_user_default_saved_views.py +20 -0
  653. nautobot/users/models.py +12 -0
  654. nautobot/users/tests/test_filters.py +6 -3
  655. nautobot/users/urls.py +8 -0
  656. nautobot/virtualization/__init__.py +0 -1
  657. nautobot/virtualization/apps.py +1 -0
  658. nautobot/virtualization/filters.py +6 -1
  659. nautobot/virtualization/forms.py +11 -3
  660. nautobot/virtualization/graphql/types.py +2 -2
  661. nautobot/virtualization/migrations/0029_add_role_field_to_interface_models.py +27 -0
  662. nautobot/virtualization/migrations/0030_alter_virtualmachine_local_config_context_data_owner_content_type_and_more.py +67 -0
  663. nautobot/virtualization/tables.py +15 -5
  664. nautobot/virtualization/templates/virtualization/virtualmachine.html +1 -1
  665. nautobot/virtualization/templates/virtualization/vminterface.html +7 -1
  666. nautobot/virtualization/templates/virtualization/vminterface_edit.html +1 -0
  667. nautobot/virtualization/tests/test_api.py +9 -4
  668. nautobot/virtualization/tests/test_filters.py +22 -0
  669. nautobot/virtualization/tests/test_models.py +7 -3
  670. nautobot/virtualization/tests/test_views.py +19 -3
  671. nautobot/virtualization/urls.py +2 -2
  672. nautobot/virtualization/views.py +10 -32
  673. {nautobot-2.2.9.dist-info → nautobot-2.3.0b1.dist-info}/METADATA +21 -19
  674. {nautobot-2.2.9.dist-info → nautobot-2.3.0b1.dist-info}/RECORD +679 -559
  675. nautobot/project-static/materialdesignicons-6.5.95/.github/ISSUE_TEMPLATE.md +0 -3
  676. nautobot/project-static/materialdesignicons-6.5.95/README.md +0 -25
  677. nautobot/project-static/materialdesignicons-6.5.95/css/materialdesignicons.css +0 -26654
  678. nautobot/project-static/materialdesignicons-6.5.95/css/materialdesignicons.css.map +0 -16
  679. nautobot/project-static/materialdesignicons-6.5.95/css/materialdesignicons.min.css +0 -3
  680. nautobot/project-static/materialdesignicons-6.5.95/css/materialdesignicons.min.css.map +0 -16
  681. nautobot/project-static/materialdesignicons-6.5.95/fonts/materialdesignicons-webfont.woff +0 -0
  682. nautobot/project-static/materialdesignicons-6.5.95/fonts/materialdesignicons-webfont.woff2 +0 -0
  683. nautobot/project-static/materialdesignicons-6.5.95/package.json +0 -28
  684. nautobot/project-static/materialdesignicons-6.5.95/preview.html +0 -717
  685. nautobot/project-static/materialdesignicons-6.5.95/scss/_animated.scss +0 -27
  686. nautobot/project-static/materialdesignicons-6.5.95/scss/_core.scss +0 -10
  687. nautobot/project-static/materialdesignicons-6.5.95/scss/_extras.scss +0 -65
  688. nautobot/project-static/materialdesignicons-6.5.95/scss/_functions.scss +0 -20
  689. nautobot/project-static/materialdesignicons-6.5.95/scss/_icons.scss +0 -10
  690. nautobot/project-static/materialdesignicons-6.5.95/scss/_path.scss +0 -10
  691. nautobot/project-static/materialdesignicons-6.5.95/scss/_variables.scss +0 -6606
  692. nautobot/project-static/materialdesignicons-6.5.95/scss/materialdesignicons.scss +0 -8
  693. /nautobot/project-static/{materialdesignicons-6.5.95 → materialdesignicons-7.4.47}/LICENSE +0 -0
  694. {nautobot-2.2.9.dist-info → nautobot-2.3.0b1.dist-info}/LICENSE.txt +0 -0
  695. {nautobot-2.2.9.dist-info → nautobot-2.3.0b1.dist-info}/NOTICE +0 -0
  696. {nautobot-2.2.9.dist-info → nautobot-2.3.0b1.dist-info}/WHEEL +0 -0
  697. {nautobot-2.2.9.dist-info → nautobot-2.3.0b1.dist-info}/entry_points.txt +0 -0
@@ -12,10 +12,13 @@ from rest_framework import status
12
12
  from nautobot.core.testing import APITestCase, APIViewTestCases
13
13
  from nautobot.core.testing.utils import generate_random_device_asset_tag_of_specified_size
14
14
  from nautobot.dcim.choices import (
15
+ ConsolePortTypeChoices,
15
16
  InterfaceModeChoices,
16
17
  InterfaceTypeChoices,
17
18
  PortTypeChoices,
18
19
  PowerFeedTypeChoices,
20
+ PowerOutletTypeChoices,
21
+ PowerPortTypeChoices,
19
22
  SoftwareImageFileHashingAlgorithmChoices,
20
23
  SubdeviceRoleChoices,
21
24
  )
@@ -43,6 +46,10 @@ from nautobot.dcim.models import (
43
46
  Location,
44
47
  LocationType,
45
48
  Manufacturer,
49
+ Module,
50
+ ModuleBay,
51
+ ModuleBayTemplate,
52
+ ModuleType,
46
53
  Platform,
47
54
  PowerFeed,
48
55
  PowerOutlet,
@@ -130,7 +137,7 @@ class Mixins:
130
137
  @classmethod
131
138
  def setUpTestData(cls):
132
139
  super().setUpTestData()
133
- cls.device_type = DeviceType.objects.exclude(manufacturer__isnull=True).first()
140
+ cls.device_type = DeviceType.objects.first()
134
141
  cls.manufacturer = cls.device_type.manufacturer
135
142
  cls.location = Location.objects.filter(location_type=LocationType.objects.get(name="Campus")).first()
136
143
  cls.device_role = Role.objects.get_for_model(Device).first()
@@ -142,6 +149,8 @@ class Mixins:
142
149
  location=cls.location,
143
150
  status=cls.device_status,
144
151
  )
152
+ cls.module = Module.objects.first()
153
+ cls.module_type = cls.module.module_type
145
154
 
146
155
  class BasePortTestMixin(ComponentTraceMixin, BaseComponentTestMixin):
147
156
  """Mixin class for all `FooPort` tests."""
@@ -167,6 +176,166 @@ class Mixins:
167
176
  ]
168
177
  )
169
178
 
179
+ class ModularDeviceComponentMixin:
180
+ modular_component_create_data = {}
181
+ device_field = "device" # field name for the parent device
182
+ module_field = "module" # field name for the parent module
183
+ update_data = {"label": "updated label", "description": "updated description"}
184
+
185
+ def test_module_device_validation(self):
186
+ """Assert that a modular component can have a module or a device but not both."""
187
+
188
+ self.add_permissions(f"{self.model._meta.app_label}.add_{self.model._meta.model_name}")
189
+ data = {
190
+ self.module_field: self.module.pk,
191
+ self.device_field: self.device.pk,
192
+ "name": "test parent module validation",
193
+ **self.modular_component_create_data,
194
+ }
195
+ url = self._get_list_url()
196
+ response = self.client.post(url, data, format="json", **self.header)
197
+ self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
198
+ self.assertEqual(
199
+ response.json(),
200
+ {"non_field_errors": [f"Only one of {self.device_field} or {self.module_field} must be set"]},
201
+ )
202
+
203
+ data.pop(self.module_field)
204
+ self.assertHttpStatus(self.client.post(url, data, format="json", **self.header), status.HTTP_201_CREATED)
205
+
206
+ data.pop(self.device_field)
207
+ data[self.module_field] = self.module.pk
208
+ self.assertHttpStatus(self.client.post(url, data, format="json", **self.header), status.HTTP_201_CREATED)
209
+
210
+ data.pop(self.module_field)
211
+ response = self.client.post(url, data, format="json", **self.header)
212
+ self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
213
+ self.assertEqual(
214
+ response.json(),
215
+ {"__all__": [f"Either {self.device_field} or {self.module_field} must be set"]},
216
+ )
217
+
218
+ def test_module_device_name_unique_validation(self):
219
+ """Assert uniqueness constraint is enforced for (device,name) and (module,name) fields."""
220
+
221
+ self.add_permissions(f"{self.model._meta.app_label}.add_{self.model._meta.model_name}")
222
+ modules = Module.objects.all()[:2]
223
+ data = {
224
+ self.module_field: modules[0].pk,
225
+ "name": "test modular device component parent validation",
226
+ **self.modular_component_create_data,
227
+ }
228
+ url = self._get_list_url()
229
+ self.assertHttpStatus(self.client.post(url, data, format="json", **self.header), status.HTTP_201_CREATED)
230
+ response = self.client.post(url, data, format="json", **self.header)
231
+ self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
232
+ self.assertEqual(
233
+ response.json(),
234
+ {"non_field_errors": [f"The fields {self.module_field}, name must make a unique set."]},
235
+ )
236
+
237
+ # same name, different module works
238
+ data[self.module_field] = modules[1].pk
239
+ self.assertHttpStatus(self.client.post(url, data, format="json", **self.header), status.HTTP_201_CREATED)
240
+
241
+ devices = Device.objects.all()[:2]
242
+ data = {
243
+ self.device_field: devices[0].pk,
244
+ "name": "test modular device component parent validation",
245
+ **self.modular_component_create_data,
246
+ }
247
+ url = self._get_list_url()
248
+ self.assertHttpStatus(self.client.post(url, data, format="json", **self.header), status.HTTP_201_CREATED)
249
+ response = self.client.post(url, data, format="json", **self.header)
250
+ self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
251
+ self.assertEqual(
252
+ response.json(),
253
+ {"non_field_errors": [f"The fields {self.device_field}, name must make a unique set."]},
254
+ )
255
+
256
+ # same name, different device works
257
+ data[self.device_field] = devices[1].pk
258
+ self.assertHttpStatus(self.client.post(url, data, format="json", **self.header), status.HTTP_201_CREATED)
259
+
260
+ class ModularDeviceComponentTemplateMixin:
261
+ modular_component_create_data = {}
262
+ update_data = {"label": "updated label", "description": "updated description"}
263
+
264
+ def test_module_type_device_type_validation(self):
265
+ """Assert that a modular component template can have a module_type or a device_type but not both."""
266
+
267
+ self.add_permissions(f"{self.model._meta.app_label}.add_{self.model._meta.model_name}")
268
+ data = {
269
+ "module_type": self.module_type.pk,
270
+ "device_type": self.device_type.pk,
271
+ "name": "test parent module_type validation",
272
+ **self.modular_component_create_data,
273
+ }
274
+ url = self._get_list_url()
275
+ response = self.client.post(url, data, format="json", **self.header)
276
+ self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
277
+ self.assertEqual(
278
+ response.json(),
279
+ {"non_field_errors": ["Only one of device_type or module_type must be set"]},
280
+ )
281
+
282
+ data.pop("module_type")
283
+ self.assertHttpStatus(self.client.post(url, data, format="json", **self.header), status.HTTP_201_CREATED)
284
+
285
+ data.pop("device_type")
286
+ data["module_type"] = self.module_type.pk
287
+ self.assertHttpStatus(self.client.post(url, data, format="json", **self.header), status.HTTP_201_CREATED)
288
+
289
+ data.pop("module_type")
290
+ response = self.client.post(url, data, format="json", **self.header)
291
+ self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
292
+ self.assertEqual(
293
+ response.json(),
294
+ {"__all__": ["Either device_type or module_type must be set"]},
295
+ )
296
+
297
+ def test_module_type_device_type_name_unique_validation(self):
298
+ """Assert uniqueness constraint is enforced for (device_type,name) and (module_type,name) fields."""
299
+
300
+ self.add_permissions(f"{self.model._meta.app_label}.add_{self.model._meta.model_name}")
301
+ module_types = ModuleType.objects.all()[:2]
302
+ data = {
303
+ "module_type": module_types[0].pk,
304
+ "name": "test modular device component template parent validation",
305
+ **self.modular_component_create_data,
306
+ }
307
+ url = self._get_list_url()
308
+ self.assertHttpStatus(self.client.post(url, data, format="json", **self.header), status.HTTP_201_CREATED)
309
+ response = self.client.post(url, data, format="json", **self.header)
310
+ self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
311
+ self.assertEqual(
312
+ response.json(),
313
+ {"non_field_errors": ["The fields module_type, name must make a unique set."]},
314
+ )
315
+
316
+ # same name, different module_type works
317
+ data["module_type"] = module_types[1].pk
318
+ self.assertHttpStatus(self.client.post(url, data, format="json", **self.header), status.HTTP_201_CREATED)
319
+
320
+ device_types = DeviceType.objects.all()[:2]
321
+ data = {
322
+ "device_type": device_types[0].pk,
323
+ "name": "test modular device component template parent validation",
324
+ **self.modular_component_create_data,
325
+ }
326
+ url = self._get_list_url()
327
+ self.assertHttpStatus(self.client.post(url, data, format="json", **self.header), status.HTTP_201_CREATED)
328
+ response = self.client.post(url, data, format="json", **self.header)
329
+ self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
330
+ self.assertEqual(
331
+ response.json(),
332
+ {"non_field_errors": ["The fields device_type, name must make a unique set."]},
333
+ )
334
+
335
+ # same name, different device_type works
336
+ data["device_type"] = device_types[1].pk
337
+ self.assertHttpStatus(self.client.post(url, data, format="json", **self.header), status.HTTP_201_CREATED)
338
+
170
339
 
171
340
  class LocationTypeTest(APIViewTestCases.APIViewTestCase, APIViewTestCases.TreeModelAPIViewTestCaseMixin):
172
341
  model = LocationType
@@ -302,7 +471,7 @@ class LocationTest(APIViewTestCases.APIViewTestCase, APIViewTestCases.TreeModelA
302
471
  # Attempt to create new location with blank time_zone attr.
303
472
  response = self.client.post(url, **self.header, data=location, format="json")
304
473
  self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
305
- self.assertEqual(response.json()["time_zone"], ["A valid timezone is required."])
474
+ self.assertEqual(response.json()["time_zone"], ["This field may not be blank."])
306
475
 
307
476
  def test_time_zone_field_post_valid(self):
308
477
  """
@@ -802,15 +971,17 @@ class ManufacturerTest(APIViewTestCases.APIViewTestCase):
802
971
  "description": "New description",
803
972
  }
804
973
 
805
- @classmethod
806
- def setUpTestData(cls):
807
- # FIXME: This has to be replaced with# `get_deletable_object` and
808
- # `get_deletable_object_pks` but this is a workaround just so all of these objects are
809
- # deletable for now.
810
- Controller.objects.filter(controller_device__isnull=False).delete()
811
- Device.objects.all().delete()
812
- DeviceType.objects.all().delete()
813
- Platform.objects.all().delete()
974
+ def get_deletable_object(self):
975
+ mf = Manufacturer.objects.create(name="Deletable Manufacturer")
976
+ return mf
977
+
978
+ def get_deletable_object_pks(self):
979
+ mfs = [
980
+ Manufacturer.objects.create(name="Deletable Manufacturer 1"),
981
+ Manufacturer.objects.create(name="Deletable Manufacturer 2"),
982
+ Manufacturer.objects.create(name="Deletable Manufacturer 3"),
983
+ ]
984
+ return [mf.pk for mf in mfs]
814
985
 
815
986
 
816
987
  class DeviceTypeTest(Mixins.SoftwareImageFileRelatedModelMixin, APIViewTestCases.APIViewTestCase):
@@ -847,24 +1018,52 @@ class DeviceTypeTest(Mixins.SoftwareImageFileRelatedModelMixin, APIViewTestCases
847
1018
  ]
848
1019
 
849
1020
 
850
- class ConsolePortTemplateTest(Mixins.BasePortTemplateTestMixin):
1021
+ class ModuleTypeTest(APIViewTestCases.APIViewTestCase):
1022
+ model = ModuleType
1023
+ bulk_update_data = {
1024
+ "part_number": "ABC123",
1025
+ }
1026
+
1027
+ @classmethod
1028
+ def setUpTestData(cls):
1029
+ manufacturer_id = Manufacturer.objects.first().pk
1030
+
1031
+ cls.create_data = [
1032
+ {
1033
+ "manufacturer": manufacturer_id,
1034
+ "model": "Module Type 1",
1035
+ "part_number": "123456",
1036
+ },
1037
+ {
1038
+ "manufacturer": manufacturer_id,
1039
+ "model": "Module Type 2",
1040
+ },
1041
+ {
1042
+ "manufacturer": manufacturer_id,
1043
+ "model": "Module Type 3",
1044
+ },
1045
+ {
1046
+ "manufacturer": manufacturer_id,
1047
+ "model": "Module Type 4",
1048
+ },
1049
+ ]
1050
+
1051
+
1052
+ class ConsolePortTemplateTest(Mixins.ModularDeviceComponentTemplateMixin, Mixins.BasePortTemplateTestMixin):
851
1053
  model = ConsolePortTemplate
1054
+ modular_component_create_data = {"type": ConsolePortTypeChoices.TYPE_RJ45}
852
1055
 
853
1056
  @classmethod
854
1057
  def setUpTestData(cls):
855
1058
  super().setUpTestData()
856
1059
 
857
- ConsolePortTemplate.objects.create(device_type=cls.device_type, name="Console Port Template 1")
858
- ConsolePortTemplate.objects.create(device_type=cls.device_type, name="Console Port Template 2")
859
- ConsolePortTemplate.objects.create(device_type=cls.device_type, name="Console Port Template 3")
860
-
861
1060
  cls.create_data = [
862
1061
  {
863
1062
  "device_type": cls.device_type.pk,
864
1063
  "name": "Console Port Template 4",
865
1064
  },
866
1065
  {
867
- "device_type": cls.device_type.pk,
1066
+ "module_type": cls.module_type.pk,
868
1067
  "name": "Console Port Template 5",
869
1068
  },
870
1069
  {
@@ -874,24 +1073,21 @@ class ConsolePortTemplateTest(Mixins.BasePortTemplateTestMixin):
874
1073
  ]
875
1074
 
876
1075
 
877
- class ConsoleServerPortTemplateTest(Mixins.BasePortTemplateTestMixin):
1076
+ class ConsoleServerPortTemplateTest(Mixins.ModularDeviceComponentTemplateMixin, Mixins.BasePortTemplateTestMixin):
878
1077
  model = ConsoleServerPortTemplate
1078
+ modular_component_create_data = {"type": ConsolePortTypeChoices.TYPE_RJ45}
879
1079
 
880
1080
  @classmethod
881
1081
  def setUpTestData(cls):
882
1082
  super().setUpTestData()
883
1083
 
884
- ConsoleServerPortTemplate.objects.create(device_type=cls.device_type, name="Console Server Port Template 1")
885
- ConsoleServerPortTemplate.objects.create(device_type=cls.device_type, name="Console Server Port Template 2")
886
- ConsoleServerPortTemplate.objects.create(device_type=cls.device_type, name="Console Server Port Template 3")
887
-
888
1084
  cls.create_data = [
889
1085
  {
890
1086
  "device_type": cls.device_type.pk,
891
1087
  "name": "Console Server Port Template 4",
892
1088
  },
893
1089
  {
894
- "device_type": cls.device_type.pk,
1090
+ "module_type": cls.module_type.pk,
895
1091
  "name": "Console Server Port Template 5",
896
1092
  },
897
1093
  {
@@ -901,24 +1097,21 @@ class ConsoleServerPortTemplateTest(Mixins.BasePortTemplateTestMixin):
901
1097
  ]
902
1098
 
903
1099
 
904
- class PowerPortTemplateTest(Mixins.BasePortTemplateTestMixin):
1100
+ class PowerPortTemplateTest(Mixins.ModularDeviceComponentTemplateMixin, Mixins.BasePortTemplateTestMixin):
905
1101
  model = PowerPortTemplate
1102
+ modular_component_create_data = {"type": PowerPortTypeChoices.TYPE_NEMA_1030P}
906
1103
 
907
1104
  @classmethod
908
1105
  def setUpTestData(cls):
909
1106
  super().setUpTestData()
910
1107
 
911
- PowerPortTemplate.objects.create(device_type=cls.device_type, name="Power Port Template 1")
912
- PowerPortTemplate.objects.create(device_type=cls.device_type, name="Power Port Template 2")
913
- PowerPortTemplate.objects.create(device_type=cls.device_type, name="Power Port Template 3")
914
-
915
1108
  cls.create_data = [
916
1109
  {
917
1110
  "device_type": cls.device_type.pk,
918
1111
  "name": "Power Port Template 4",
919
1112
  },
920
1113
  {
921
- "device_type": cls.device_type.pk,
1114
+ "module_type": cls.module_type.pk,
922
1115
  "name": "Power Port Template 5",
923
1116
  },
924
1117
  {
@@ -928,25 +1121,22 @@ class PowerPortTemplateTest(Mixins.BasePortTemplateTestMixin):
928
1121
  ]
929
1122
 
930
1123
 
931
- class PowerOutletTemplateTest(Mixins.BasePortTemplateTestMixin):
1124
+ class PowerOutletTemplateTest(Mixins.ModularDeviceComponentTemplateMixin, Mixins.BasePortTemplateTestMixin):
932
1125
  model = PowerOutletTemplate
933
1126
  choices_fields = ["feed_leg", "type"]
1127
+ modular_component_create_data = {"type": PowerOutletTypeChoices.TYPE_IEC_C13}
934
1128
 
935
1129
  @classmethod
936
1130
  def setUpTestData(cls):
937
1131
  super().setUpTestData()
938
1132
 
939
- PowerOutletTemplate.objects.create(device_type=cls.device_type, name="Power Outlet Template 1")
940
- PowerOutletTemplate.objects.create(device_type=cls.device_type, name="Power Outlet Template 2")
941
- PowerOutletTemplate.objects.create(device_type=cls.device_type, name="Power Outlet Template 3")
942
-
943
1133
  cls.create_data = [
944
1134
  {
945
1135
  "device_type": cls.device_type.pk,
946
1136
  "name": "Power Outlet Template 4",
947
1137
  },
948
1138
  {
949
- "device_type": cls.device_type.pk,
1139
+ "module_type": cls.module_type.pk,
950
1140
  "name": "Power Outlet Template 5",
951
1141
  },
952
1142
  {
@@ -956,17 +1146,13 @@ class PowerOutletTemplateTest(Mixins.BasePortTemplateTestMixin):
956
1146
  ]
957
1147
 
958
1148
 
959
- class InterfaceTemplateTest(Mixins.BasePortTemplateTestMixin):
1149
+ class InterfaceTemplateTest(Mixins.ModularDeviceComponentTemplateMixin, Mixins.BasePortTemplateTestMixin):
960
1150
  model = InterfaceTemplate
1151
+ modular_component_create_data = {"type": InterfaceTypeChoices.TYPE_1GE_FIXED}
961
1152
 
962
1153
  @classmethod
963
1154
  def setUpTestData(cls):
964
1155
  super().setUpTestData()
965
-
966
- InterfaceTemplate.objects.create(device_type=cls.device_type, name="Interface Template 1", type="1000base-t")
967
- InterfaceTemplate.objects.create(device_type=cls.device_type, name="Interface Template 2", type="1000base-t")
968
- InterfaceTemplate.objects.create(device_type=cls.device_type, name="Interface Template 3", type="1000base-t")
969
-
970
1156
  cls.create_data = [
971
1157
  {
972
1158
  "device_type": cls.device_type.pk,
@@ -974,7 +1160,7 @@ class InterfaceTemplateTest(Mixins.BasePortTemplateTestMixin):
974
1160
  "type": "1000base-t",
975
1161
  },
976
1162
  {
977
- "device_type": cls.device_type.pk,
1163
+ "module_type": cls.module_type.pk,
978
1164
  "name": "Interface Template 5",
979
1165
  "type": "1000base-t",
980
1166
  },
@@ -988,61 +1174,21 @@ class InterfaceTemplateTest(Mixins.BasePortTemplateTestMixin):
988
1174
 
989
1175
  class FrontPortTemplateTest(Mixins.BasePortTemplateTestMixin):
990
1176
  model = FrontPortTemplate
1177
+ update_data = {"label": "updated label", "description": "updated description"}
991
1178
 
992
1179
  @classmethod
993
1180
  def setUpTestData(cls):
994
1181
  super().setUpTestData()
995
1182
 
996
- rear_port_templates = (
997
- RearPortTemplate.objects.create(
998
- device_type=cls.device_type,
999
- name="Rear Port Template 1",
1000
- type=PortTypeChoices.TYPE_8P8C,
1001
- ),
1002
- RearPortTemplate.objects.create(
1003
- device_type=cls.device_type,
1004
- name="Rear Port Template 2",
1005
- type=PortTypeChoices.TYPE_8P8C,
1006
- ),
1007
- RearPortTemplate.objects.create(
1008
- device_type=cls.device_type,
1009
- name="Rear Port Template 3",
1010
- type=PortTypeChoices.TYPE_8P8C,
1011
- ),
1012
- RearPortTemplate.objects.create(
1013
- device_type=cls.device_type,
1014
- name="Rear Port Template 4",
1015
- type=PortTypeChoices.TYPE_8P8C,
1016
- ),
1017
- RearPortTemplate.objects.create(
1018
- device_type=cls.device_type,
1019
- name="Rear Port Template 5",
1020
- type=PortTypeChoices.TYPE_8P8C,
1021
- ),
1022
- RearPortTemplate.objects.create(
1023
- device_type=cls.device_type,
1024
- name="Rear Port Template 6",
1025
- type=PortTypeChoices.TYPE_8P8C,
1026
- ),
1183
+ cls.module_type = ModuleType.objects.first()
1184
+ cls.module_rear_port_templates = (
1185
+ RearPortTemplate.objects.create(module_type=cls.module_type, name="Test FrontPort RP1", positions=100),
1186
+ RearPortTemplate.objects.create(module_type=cls.module_type, name="Test FrontPort RP2", positions=100),
1027
1187
  )
1028
-
1029
- FrontPortTemplate.objects.create(
1030
- device_type=cls.device_type,
1031
- name="Front Port Template 1",
1032
- type=PortTypeChoices.TYPE_8P8C,
1033
- rear_port_template=rear_port_templates[0],
1034
- )
1035
- FrontPortTemplate.objects.create(
1036
- device_type=cls.device_type,
1037
- name="Front Port Template 2",
1038
- type=PortTypeChoices.TYPE_8P8C,
1039
- rear_port_template=rear_port_templates[1],
1040
- )
1041
- FrontPortTemplate.objects.create(
1042
- device_type=cls.device_type,
1043
- name="Front Port Template 3",
1044
- type=PortTypeChoices.TYPE_8P8C,
1045
- rear_port_template=rear_port_templates[2],
1188
+ cls.device_type = DeviceType.objects.first()
1189
+ cls.device_rear_port_templates = (
1190
+ RearPortTemplate.objects.create(device_type=cls.device_type, name="Test FrontPort RP3", positions=100),
1191
+ RearPortTemplate.objects.create(device_type=cls.device_type, name="Test FrontPort RP4", positions=100),
1046
1192
  )
1047
1193
 
1048
1194
  cls.create_data = [
@@ -1050,49 +1196,114 @@ class FrontPortTemplateTest(Mixins.BasePortTemplateTestMixin):
1050
1196
  "device_type": cls.device_type.pk,
1051
1197
  "name": "Front Port Template 4",
1052
1198
  "type": PortTypeChoices.TYPE_8P8C,
1053
- "rear_port_template": rear_port_templates[3].pk,
1199
+ "rear_port_template": cls.device_rear_port_templates[0].pk,
1054
1200
  "rear_port_position": 1,
1055
1201
  },
1056
1202
  {
1057
1203
  "device_type": cls.device_type.pk,
1058
1204
  "name": "Front Port Template 5",
1059
1205
  "type": PortTypeChoices.TYPE_8P8C,
1060
- "rear_port_template": rear_port_templates[4].pk,
1206
+ "rear_port_template": cls.device_rear_port_templates[1].pk,
1061
1207
  "rear_port_position": 1,
1062
1208
  },
1063
1209
  {
1064
- "device_type": cls.device_type.pk,
1210
+ "module_type": cls.module_type.pk,
1065
1211
  "name": "Front Port Template 6",
1066
1212
  "type": PortTypeChoices.TYPE_8P8C,
1067
- "rear_port_template": rear_port_templates[5].pk,
1213
+ "rear_port_template": cls.module_rear_port_templates[0].pk,
1068
1214
  "rear_port_position": 1,
1069
1215
  },
1070
1216
  ]
1071
1217
 
1218
+ def test_module_type_device_type_validation(self):
1219
+ """Assert that a modular component template can have a module_type or a device_type but not both."""
1072
1220
 
1073
- class RearPortTemplateTest(Mixins.BasePortTemplateTestMixin):
1221
+ self.add_permissions("dcim.add_frontporttemplate")
1222
+ data = {
1223
+ "module_type": self.module_type.pk,
1224
+ "device_type": self.device_type.pk,
1225
+ "name": "test parent module_type validation",
1226
+ "type": PortTypeChoices.TYPE_8P8C,
1227
+ "rear_port_template": self.device_rear_port_templates[0].pk,
1228
+ "rear_port_position": 2,
1229
+ }
1230
+ url = self._get_list_url()
1231
+ response = self.client.post(url, data, format="json", **self.header)
1232
+ self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
1233
+ self.assertEqual(
1234
+ response.json(),
1235
+ {"non_field_errors": ["Only one of device_type or module_type must be set"]},
1236
+ )
1237
+
1238
+ data.pop("module_type")
1239
+ self.assertHttpStatus(self.client.post(url, data, format="json", **self.header), status.HTTP_201_CREATED)
1240
+
1241
+ data.pop("device_type")
1242
+ data["module_type"] = self.module_type.pk
1243
+ data["rear_port_template"] = self.module_rear_port_templates[0].pk
1244
+ self.assertHttpStatus(self.client.post(url, data, format="json", **self.header), status.HTTP_201_CREATED)
1245
+
1246
+ def test_module_type_device_type_name_unique_validation(self):
1247
+ """Assert uniqueness constraint is enforced for (device_type,name) and (module_type,name) fields."""
1248
+
1249
+ self.add_permissions("dcim.add_frontporttemplate")
1250
+ data = {
1251
+ "module_type": self.module_type.pk,
1252
+ "name": "test modular device_type component parent validation",
1253
+ "type": PortTypeChoices.TYPE_8P8C,
1254
+ "rear_port_template": self.module_rear_port_templates[0].pk,
1255
+ "rear_port_position": 2,
1256
+ }
1257
+ url = self._get_list_url()
1258
+ self.assertHttpStatus(self.client.post(url, data, format="json", **self.header), status.HTTP_201_CREATED)
1259
+
1260
+ data = {
1261
+ "module_type": self.module_type.pk,
1262
+ "name": "test modular device_type component parent validation",
1263
+ "type": PortTypeChoices.TYPE_8P8C,
1264
+ "rear_port_template": self.module_rear_port_templates[1].pk,
1265
+ "rear_port_position": 2,
1266
+ }
1267
+ response = self.client.post(url, data, format="json", **self.header)
1268
+ self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
1269
+ self.assertEqual(
1270
+ response.json(),
1271
+ {"non_field_errors": ["The fields module_type, name must make a unique set."]},
1272
+ )
1273
+
1274
+ data = {
1275
+ "device_type": self.device_type.pk,
1276
+ "name": "test modular device_type component parent validation",
1277
+ "type": PortTypeChoices.TYPE_8P8C,
1278
+ "rear_port_template": self.device_rear_port_templates[0].pk,
1279
+ "rear_port_position": 2,
1280
+ }
1281
+ url = self._get_list_url()
1282
+ self.assertHttpStatus(self.client.post(url, data, format="json", **self.header), status.HTTP_201_CREATED)
1283
+
1284
+ data = {
1285
+ "device_type": self.device_type.pk,
1286
+ "name": "test modular device_type component parent validation",
1287
+ "type": PortTypeChoices.TYPE_8P8C,
1288
+ "rear_port_template": self.device_rear_port_templates[1].pk,
1289
+ "rear_port_position": 2,
1290
+ }
1291
+ response = self.client.post(url, data, format="json", **self.header)
1292
+ self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
1293
+ self.assertEqual(
1294
+ response.json(),
1295
+ {"non_field_errors": ["The fields device_type, name must make a unique set."]},
1296
+ )
1297
+
1298
+
1299
+ class RearPortTemplateTest(Mixins.ModularDeviceComponentTemplateMixin, Mixins.BasePortTemplateTestMixin):
1074
1300
  model = RearPortTemplate
1301
+ modular_component_create_data = {"type": PortTypeChoices.TYPE_8P8C}
1075
1302
 
1076
1303
  @classmethod
1077
1304
  def setUpTestData(cls):
1078
1305
  super().setUpTestData()
1079
1306
 
1080
- RearPortTemplate.objects.create(
1081
- device_type=cls.device_type,
1082
- name="Rear Port Template 1",
1083
- type=PortTypeChoices.TYPE_8P8C,
1084
- )
1085
- RearPortTemplate.objects.create(
1086
- device_type=cls.device_type,
1087
- name="Rear Port Template 2",
1088
- type=PortTypeChoices.TYPE_8P8C,
1089
- )
1090
- RearPortTemplate.objects.create(
1091
- device_type=cls.device_type,
1092
- name="Rear Port Template 3",
1093
- type=PortTypeChoices.TYPE_8P8C,
1094
- )
1095
-
1096
1307
  cls.create_data = [
1097
1308
  {
1098
1309
  "device_type": cls.device_type.pk,
@@ -1100,12 +1311,12 @@ class RearPortTemplateTest(Mixins.BasePortTemplateTestMixin):
1100
1311
  "type": PortTypeChoices.TYPE_8P8C,
1101
1312
  },
1102
1313
  {
1103
- "device_type": cls.device_type.pk,
1314
+ "module_type": cls.module_type.pk,
1104
1315
  "name": "Rear Port Template 5",
1105
1316
  "type": PortTypeChoices.TYPE_8P8C,
1106
1317
  },
1107
1318
  {
1108
- "device_type": cls.device_type.pk,
1319
+ "module_type": cls.module_type.pk,
1109
1320
  "name": "Rear Port Template 6",
1110
1321
  "type": PortTypeChoices.TYPE_8P8C,
1111
1322
  },
@@ -1141,6 +1352,30 @@ class DeviceBayTemplateTest(Mixins.BasePortTemplateTestMixin):
1141
1352
  ]
1142
1353
 
1143
1354
 
1355
+ class ModuleBayTemplateTest(Mixins.ModularDeviceComponentTemplateMixin, Mixins.BaseComponentTestMixin):
1356
+ model = ModuleBayTemplate
1357
+ choices_fields = []
1358
+
1359
+ @classmethod
1360
+ def setUpTestData(cls):
1361
+ super().setUpTestData()
1362
+
1363
+ cls.create_data = [
1364
+ {
1365
+ "device_type": cls.device_type.pk,
1366
+ "name": "Test1",
1367
+ },
1368
+ {
1369
+ "module_type": cls.module_type.pk,
1370
+ "name": "Test2",
1371
+ },
1372
+ {
1373
+ "device_type": cls.device_type.pk,
1374
+ "name": "Test3",
1375
+ },
1376
+ ]
1377
+
1378
+
1144
1379
  class PlatformTest(APIViewTestCases.APIViewTestCase):
1145
1380
  model = Platform
1146
1381
  create_data = [
@@ -1462,120 +1697,238 @@ class DeviceTest(APIViewTestCases.APIViewTestCase):
1462
1697
  self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
1463
1698
 
1464
1699
 
1465
- class ConsolePortTest(Mixins.BasePortTestMixin):
1700
+ class ModuleTestCase(APIViewTestCases.APIViewTestCase):
1701
+ model = Module
1702
+
1703
+ @classmethod
1704
+ def setUpTestData(cls):
1705
+ cls.module_type = ModuleType.objects.first()
1706
+ cls.module_bay = ModuleBay.objects.filter(installed_module__isnull=True).first()
1707
+ cls.module_status = Status.objects.get_for_model(Module).first()
1708
+ cls.location = Location.objects.get_for_model(Module).first()
1709
+ cls.create_data = [
1710
+ {
1711
+ "module_type": cls.module_type.pk,
1712
+ "parent_module_bay": cls.module_bay.pk,
1713
+ "status": cls.module_status.pk,
1714
+ },
1715
+ {
1716
+ "module_type": cls.module_type.pk,
1717
+ "location": cls.location.pk,
1718
+ "status": cls.module_status.pk,
1719
+ },
1720
+ {
1721
+ "module_type": cls.module_type.pk,
1722
+ "location": cls.location.pk,
1723
+ "serial": "test module serial xyz",
1724
+ "asset_tag": "test module 2",
1725
+ "status": cls.module_status.pk,
1726
+ },
1727
+ {
1728
+ "module_type": cls.module_type.pk,
1729
+ "location": cls.location.pk,
1730
+ "asset_tag": "Test Module 3",
1731
+ "status": cls.module_status.pk,
1732
+ },
1733
+ {
1734
+ "module_type": cls.module_type.pk,
1735
+ "location": cls.location.pk,
1736
+ "serial": "test module serial abc",
1737
+ "status": cls.module_status.pk,
1738
+ },
1739
+ ]
1740
+ cls.bulk_update_data = {
1741
+ "tenant": Tenant.objects.first().pk,
1742
+ }
1743
+
1744
+ cls.update_data = {
1745
+ "serial": "new serial 789",
1746
+ "asset_tag": "new asset tag 789",
1747
+ "status": Status.objects.get_for_model(Module).last().pk,
1748
+ }
1749
+
1750
+ def get_deletable_object_pks(self):
1751
+ # Since Modules and ModuleBays are nestable, we need to delete Modules that don't have any child Modules
1752
+ return Module.objects.exclude(module_bays__installed_module__isnull=False).values_list("pk", flat=True)[:3]
1753
+
1754
+ def test_parent_module_bay_location_validation(self):
1755
+ """Assert that a module can have a parent_module_bay or a location but not both."""
1756
+
1757
+ self.add_permissions("dcim.add_module")
1758
+ data = {
1759
+ "module_type": self.module_type.pk,
1760
+ "location": self.location.pk,
1761
+ "parent_module_bay": self.module_bay.pk,
1762
+ "status": self.module_status.pk,
1763
+ }
1764
+ url = self._get_list_url()
1765
+ response = self.client.post(url, data, format="json", **self.header)
1766
+ self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
1767
+ self.assertEqual(
1768
+ response.json(),
1769
+ {"non_field_errors": ["Only one of parent_module_bay or location must be set"]},
1770
+ )
1771
+
1772
+ data.pop("parent_module_bay")
1773
+ self.assertHttpStatus(self.client.post(url, data, format="json", **self.header), status.HTTP_201_CREATED)
1774
+
1775
+ data.pop("location")
1776
+ data["parent_module_bay"] = self.module_bay.pk
1777
+ self.assertHttpStatus(self.client.post(url, data, format="json", **self.header), status.HTTP_201_CREATED)
1778
+
1779
+ data.pop("parent_module_bay")
1780
+ response = self.client.post(url, data, format="json", **self.header)
1781
+ self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
1782
+ self.assertEqual(
1783
+ response.json(),
1784
+ {"__all__": ["One of location or parent_module_bay must be set"]},
1785
+ )
1786
+
1787
+ def test_serial_module_type_unique_validation(self):
1788
+ self.add_permissions("dcim.add_module")
1789
+ data = {
1790
+ "module_type": self.module_type.pk,
1791
+ "location": self.location.pk,
1792
+ "status": self.module_status.pk,
1793
+ }
1794
+ url = self._get_list_url()
1795
+ # create multiple instances with null serial
1796
+ self.assertHttpStatus(self.client.post(url, data, format="json", **self.header), status.HTTP_201_CREATED)
1797
+ self.assertHttpStatus(self.client.post(url, data, format="json", **self.header), status.HTTP_201_CREATED)
1798
+ data["serial"] = ""
1799
+ self.assertHttpStatus(self.client.post(url, data, format="json", **self.header), status.HTTP_201_CREATED)
1800
+ data["serial"] = None
1801
+ self.assertHttpStatus(self.client.post(url, data, format="json", **self.header), status.HTTP_201_CREATED)
1802
+
1803
+ data["serial"] = "xyz"
1804
+ self.assertHttpStatus(self.client.post(url, data, format="json", **self.header), status.HTTP_201_CREATED)
1805
+ response = self.client.post(url, data, format="json", **self.header)
1806
+ self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
1807
+ self.assertEqual(
1808
+ response.json(),
1809
+ {"non_field_errors": ["The fields module_type, serial must make a unique set."]},
1810
+ )
1811
+
1812
+ def test_asset_tag_unique_validation(self):
1813
+ self.add_permissions("dcim.add_module")
1814
+ data = {
1815
+ "module_type": self.module_type.pk,
1816
+ "location": self.location.pk,
1817
+ "status": self.module_status.pk,
1818
+ "asset_tag": "xyz123",
1819
+ }
1820
+ url = self._get_list_url()
1821
+ self.assertHttpStatus(self.client.post(url, data, format="json", **self.header), status.HTTP_201_CREATED)
1822
+ response = self.client.post(url, data, format="json", **self.header)
1823
+ self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
1824
+ self.assertEqual(
1825
+ response.json(),
1826
+ {"asset_tag": ["module with this Asset tag already exists."]},
1827
+ )
1828
+
1829
+
1830
+ class ConsolePortTest(Mixins.ModularDeviceComponentMixin, Mixins.BasePortTestMixin):
1466
1831
  model = ConsolePort
1467
1832
  peer_termination_type = ConsoleServerPort
1833
+ modular_component_create_data = {"type": ConsolePortTypeChoices.TYPE_RJ45}
1468
1834
 
1469
1835
  @classmethod
1470
1836
  def setUpTestData(cls):
1471
1837
  super().setUpTestData()
1472
1838
 
1473
- ConsolePort.objects.create(device=cls.device, name="Console Port 1")
1474
- ConsolePort.objects.create(device=cls.device, name="Console Port 2")
1475
- ConsolePort.objects.create(device=cls.device, name="Console Port 3")
1476
-
1477
1839
  cls.create_data = [
1478
1840
  {
1479
1841
  "device": cls.device.pk,
1480
- "name": "Console Port 4",
1842
+ "name": "Console Port 1",
1481
1843
  },
1482
1844
  {
1483
- "device": cls.device.pk,
1484
- "name": "Console Port 5",
1845
+ "module": cls.module.pk,
1846
+ "name": "Console Port 2",
1485
1847
  },
1486
1848
  {
1487
1849
  "device": cls.device.pk,
1488
- "name": "Console Port 6",
1850
+ "name": "Console Port 3",
1489
1851
  },
1490
1852
  ]
1491
1853
 
1492
1854
 
1493
- class ConsoleServerPortTest(Mixins.BasePortTestMixin):
1855
+ class ConsoleServerPortTest(Mixins.ModularDeviceComponentMixin, Mixins.BasePortTestMixin):
1494
1856
  model = ConsoleServerPort
1495
1857
  peer_termination_type = ConsolePort
1858
+ modular_component_create_data = {"type": ConsolePortTypeChoices.TYPE_RJ45}
1496
1859
 
1497
1860
  @classmethod
1498
1861
  def setUpTestData(cls):
1499
1862
  super().setUpTestData()
1500
1863
 
1501
- ConsoleServerPort.objects.create(device=cls.device, name="Console Server Port 1")
1502
- ConsoleServerPort.objects.create(device=cls.device, name="Console Server Port 2")
1503
- ConsoleServerPort.objects.create(device=cls.device, name="Console Server Port 3")
1504
-
1505
1864
  cls.create_data = [
1506
1865
  {
1507
1866
  "device": cls.device.pk,
1508
- "name": "Console Server Port 4",
1867
+ "name": "Console Server Port 1",
1509
1868
  },
1510
1869
  {
1511
- "device": cls.device.pk,
1512
- "name": "Console Server Port 5",
1870
+ "module": cls.module.pk,
1871
+ "name": "Console Server Port 2",
1513
1872
  },
1514
1873
  {
1515
1874
  "device": cls.device.pk,
1516
- "name": "Console Server Port 6",
1875
+ "name": "Console Server Port 3",
1517
1876
  },
1518
1877
  ]
1519
1878
 
1520
1879
 
1521
- class PowerPortTest(Mixins.BasePortTestMixin):
1880
+ class PowerPortTest(Mixins.ModularDeviceComponentMixin, Mixins.BasePortTestMixin):
1522
1881
  model = PowerPort
1523
1882
  peer_termination_type = PowerOutlet
1883
+ modular_component_create_data = {"type": PowerPortTypeChoices.TYPE_NEMA_1030P}
1524
1884
 
1525
1885
  @classmethod
1526
1886
  def setUpTestData(cls):
1527
1887
  super().setUpTestData()
1528
1888
 
1529
- PowerPort.objects.create(device=cls.device, name="Power Port 1")
1530
- PowerPort.objects.create(device=cls.device, name="Power Port 2")
1531
- PowerPort.objects.create(device=cls.device, name="Power Port 3")
1532
-
1533
1889
  cls.create_data = [
1534
1890
  {
1535
1891
  "device": cls.device.pk,
1536
- "name": "Power Port 4",
1892
+ "name": "Power Port 1",
1537
1893
  },
1538
1894
  {
1539
- "device": cls.device.pk,
1540
- "name": "Power Port 5",
1895
+ "module": cls.module.pk,
1896
+ "name": "Power Port 2",
1541
1897
  },
1542
1898
  {
1543
1899
  "device": cls.device.pk,
1544
- "name": "Power Port 6",
1900
+ "name": "Power Port 3",
1545
1901
  },
1546
1902
  ]
1547
1903
 
1548
1904
 
1549
- class PowerOutletTest(Mixins.BasePortTestMixin):
1905
+ class PowerOutletTest(Mixins.ModularDeviceComponentMixin, Mixins.BasePortTestMixin):
1550
1906
  model = PowerOutlet
1551
1907
  peer_termination_type = PowerPort
1552
1908
  choices_fields = ["feed_leg", "type"]
1909
+ modular_component_create_data = {"type": PowerOutletTypeChoices.TYPE_IEC_C13}
1553
1910
 
1554
1911
  @classmethod
1555
1912
  def setUpTestData(cls):
1556
1913
  super().setUpTestData()
1557
1914
 
1558
- PowerOutlet.objects.create(device=cls.device, name="Power Outlet 1")
1559
- PowerOutlet.objects.create(device=cls.device, name="Power Outlet 2")
1560
- PowerOutlet.objects.create(device=cls.device, name="Power Outlet 3")
1561
-
1562
1915
  cls.create_data = [
1563
1916
  {
1564
1917
  "device": cls.device.pk,
1565
- "name": "Power Outlet 4",
1918
+ "name": "Power Outlet 1",
1566
1919
  },
1567
1920
  {
1568
- "device": cls.device.pk,
1569
- "name": "Power Outlet 5",
1921
+ "module": cls.module.pk,
1922
+ "name": "Power Outlet 2",
1570
1923
  },
1571
1924
  {
1572
1925
  "device": cls.device.pk,
1573
- "name": "Power Outlet 6",
1926
+ "name": "Power Outlet 3",
1574
1927
  },
1575
1928
  ]
1576
1929
 
1577
1930
 
1578
- class InterfaceTest(Mixins.BasePortTestMixin):
1931
+ class InterfaceTest(Mixins.ModularDeviceComponentMixin, Mixins.BasePortTestMixin):
1579
1932
  model = Interface
1580
1933
  peer_termination_type = Interface
1581
1934
  choices_fields = ["mode", "type"]
@@ -1584,7 +1937,10 @@ class InterfaceTest(Mixins.BasePortTestMixin):
1584
1937
  def setUpTestData(cls):
1585
1938
  super().setUpTestData()
1586
1939
  interface_status = Status.objects.get_for_model(Interface).first()
1587
-
1940
+ cls.modular_component_create_data = {
1941
+ "type": InterfaceTypeChoices.TYPE_1GE_FIXED,
1942
+ "status": interface_status.pk,
1943
+ }
1588
1944
  cls.devices = (
1589
1945
  cls.device,
1590
1946
  Device.objects.create(
@@ -1611,48 +1967,54 @@ class InterfaceTest(Mixins.BasePortTestMixin):
1611
1967
 
1612
1968
  # Interfaces have special handling around the "Active" status so let's set our interfaces to something else.
1613
1969
  non_default_status = Status.objects.get_for_model(Interface).exclude(name="Active").first()
1970
+ intf_role = Role.objects.get_for_model(Interface).first()
1614
1971
  cls.interfaces = (
1615
1972
  Interface.objects.create(
1616
1973
  device=cls.devices[0],
1617
- name="Interface 1",
1974
+ name="Test Interface 1",
1618
1975
  type="1000base-t",
1619
1976
  status=non_default_status,
1977
+ role=intf_role,
1620
1978
  ),
1621
1979
  Interface.objects.create(
1622
1980
  device=cls.devices[0],
1623
- name="Interface 2",
1981
+ name="Test Interface 2",
1624
1982
  type="1000base-t",
1625
1983
  status=non_default_status,
1626
1984
  ),
1627
1985
  Interface.objects.create(
1628
1986
  device=cls.devices[0],
1629
- name="Interface 3",
1987
+ name="Test Interface 3",
1630
1988
  type=InterfaceTypeChoices.TYPE_BRIDGE,
1631
1989
  status=non_default_status,
1990
+ role=intf_role,
1632
1991
  ),
1633
1992
  Interface.objects.create(
1634
1993
  device=cls.devices[1],
1635
- name="Interface 4",
1994
+ name="Test Interface 4",
1636
1995
  type=InterfaceTypeChoices.TYPE_1GE_GBIC,
1637
1996
  status=non_default_status,
1997
+ role=intf_role,
1638
1998
  ),
1639
1999
  Interface.objects.create(
1640
2000
  device=cls.devices[1],
1641
- name="Interface 5",
2001
+ name="Test Interface 5",
1642
2002
  type=InterfaceTypeChoices.TYPE_LAG,
1643
2003
  status=non_default_status,
1644
2004
  ),
1645
2005
  Interface.objects.create(
1646
2006
  device=cls.devices[2],
1647
- name="Interface 6",
2007
+ name="Test Interface 6",
1648
2008
  type=InterfaceTypeChoices.TYPE_LAG,
1649
2009
  status=non_default_status,
2010
+ role=intf_role,
1650
2011
  ),
1651
2012
  Interface.objects.create(
1652
2013
  device=cls.devices[2],
1653
- name="Interface 7",
2014
+ name="Test Interface 7",
1654
2015
  type=InterfaceTypeChoices.TYPE_1GE_GBIC,
1655
2016
  status=non_default_status,
2017
+ role=intf_role,
1656
2018
  ),
1657
2019
  )
1658
2020
 
@@ -1670,6 +2032,7 @@ class InterfaceTest(Mixins.BasePortTestMixin):
1670
2032
  "name": "Interface 8",
1671
2033
  "type": "1000base-t",
1672
2034
  "status": interface_status.pk,
2035
+ "role": intf_role.pk,
1673
2036
  "mode": InterfaceModeChoices.MODE_TAGGED,
1674
2037
  "tagged_vlans": [cls.vlans[0].pk, cls.vlans[1].pk],
1675
2038
  "untagged_vlan": cls.vlans[2].pk,
@@ -1680,6 +2043,7 @@ class InterfaceTest(Mixins.BasePortTestMixin):
1680
2043
  "name": "Interface 9",
1681
2044
  "type": "1000base-t",
1682
2045
  "status": interface_status.pk,
2046
+ "role": intf_role.pk,
1683
2047
  "mode": InterfaceModeChoices.MODE_TAGGED,
1684
2048
  "bridge": cls.interfaces[3].pk,
1685
2049
  "tagged_vlans": [cls.vlans[0].pk, cls.vlans[1].pk],
@@ -1730,6 +2094,7 @@ class InterfaceTest(Mixins.BasePortTestMixin):
1730
2094
  {
1731
2095
  "device": cls.devices[0].pk,
1732
2096
  "name": "interface test 1",
2097
+ "role": intf_role.pk,
1733
2098
  "type": InterfaceTypeChoices.TYPE_VIRTUAL,
1734
2099
  "status": interface_status.pk,
1735
2100
  "parent_interface": cls.interfaces[6].id, # do not belong to same device or vc
@@ -1741,6 +2106,7 @@ class InterfaceTest(Mixins.BasePortTestMixin):
1741
2106
  "device": cls.devices[0].pk,
1742
2107
  "name": "interface test 2",
1743
2108
  "type": InterfaceTypeChoices.TYPE_1GE_GBIC,
2109
+ "role": intf_role.pk,
1744
2110
  "status": interface_status.pk,
1745
2111
  "bridge": cls.interfaces[6].id, # does not belong to same device or vc
1746
2112
  },
@@ -1840,6 +2206,7 @@ class InterfaceTest(Mixins.BasePortTestMixin):
1840
2206
  mode=InterfaceModeChoices.MODE_TAGGED,
1841
2207
  type=InterfaceTypeChoices.TYPE_VIRTUAL,
1842
2208
  status=Status.objects.get_for_model(Interface).first(),
2209
+ role=Role.objects.get_for_model(Interface).first(),
1843
2210
  )
1844
2211
  interface.tagged_vlans.add(self.vlans[0])
1845
2212
  payload = {"mode": None, "tagged_vlans": [self.vlans[2].pk]}
@@ -1871,6 +2238,7 @@ class InterfaceTest(Mixins.BasePortTestMixin):
1871
2238
  class FrontPortTest(Mixins.BasePortTestMixin):
1872
2239
  model = FrontPort
1873
2240
  peer_termination_type = Interface
2241
+ update_data = {"label": "updated label", "description": "updated description"}
1874
2242
 
1875
2243
  def test_trace(self):
1876
2244
  """FrontPorts don't support trace."""
@@ -1879,62 +2247,135 @@ class FrontPortTest(Mixins.BasePortTestMixin):
1879
2247
  def setUpTestData(cls):
1880
2248
  super().setUpTestData()
1881
2249
 
1882
- rear_ports = (
1883
- RearPort.objects.create(device=cls.device, name="Rear Port 1", type=PortTypeChoices.TYPE_8P8C),
1884
- RearPort.objects.create(device=cls.device, name="Rear Port 2", type=PortTypeChoices.TYPE_8P8C),
1885
- RearPort.objects.create(device=cls.device, name="Rear Port 3", type=PortTypeChoices.TYPE_8P8C),
1886
- RearPort.objects.create(device=cls.device, name="Rear Port 4", type=PortTypeChoices.TYPE_8P8C),
1887
- RearPort.objects.create(device=cls.device, name="Rear Port 5", type=PortTypeChoices.TYPE_8P8C),
1888
- RearPort.objects.create(device=cls.device, name="Rear Port 6", type=PortTypeChoices.TYPE_8P8C),
2250
+ cls.module = Module.objects.first()
2251
+ cls.module_rear_ports = (
2252
+ RearPort.objects.create(module=cls.module, name="Test FrontPort RP1", positions=100),
2253
+ RearPort.objects.create(module=cls.module, name="Test FrontPort RP2", positions=100),
1889
2254
  )
1890
-
1891
- FrontPort.objects.create(
1892
- device=cls.device,
1893
- name="Front Port 1",
1894
- type=PortTypeChoices.TYPE_8P8C,
1895
- rear_port=rear_ports[0],
1896
- )
1897
- FrontPort.objects.create(
1898
- device=cls.device,
1899
- name="Front Port 2",
1900
- type=PortTypeChoices.TYPE_8P8C,
1901
- rear_port=rear_ports[1],
1902
- )
1903
- FrontPort.objects.create(
1904
- device=cls.device,
1905
- name="Front Port 3",
1906
- type=PortTypeChoices.TYPE_8P8C,
1907
- rear_port=rear_ports[2],
2255
+ cls.device = Device.objects.first()
2256
+ cls.device_rear_ports = (
2257
+ RearPort.objects.create(device=cls.device, name="Test FrontPort RP3", positions=100),
2258
+ RearPort.objects.create(device=cls.device, name="Test FrontPort RP4", positions=100),
1908
2259
  )
1909
2260
 
1910
2261
  cls.create_data = [
1911
2262
  {
1912
2263
  "device": cls.device.pk,
1913
- "name": "Front Port 4",
2264
+ "name": "Front Port 1",
1914
2265
  "type": PortTypeChoices.TYPE_8P8C,
1915
- "rear_port": rear_ports[3].pk,
2266
+ "rear_port": cls.device_rear_ports[0].pk,
1916
2267
  "rear_port_position": 1,
1917
2268
  },
1918
2269
  {
1919
2270
  "device": cls.device.pk,
1920
- "name": "Front Port 5",
2271
+ "name": "Front Port 2",
1921
2272
  "type": PortTypeChoices.TYPE_8P8C,
1922
- "rear_port": rear_ports[4].pk,
2273
+ "rear_port": cls.device_rear_ports[1].pk,
1923
2274
  "rear_port_position": 1,
1924
2275
  },
1925
2276
  {
1926
- "device": cls.device.pk,
1927
- "name": "Front Port 6",
2277
+ "module": cls.module.pk,
2278
+ "name": "Front Port 3",
1928
2279
  "type": PortTypeChoices.TYPE_8P8C,
1929
- "rear_port": rear_ports[5].pk,
2280
+ "rear_port": cls.module_rear_ports[0].pk,
1930
2281
  "rear_port_position": 1,
1931
2282
  },
1932
2283
  ]
1933
2284
 
2285
+ def test_module_device_validation(self):
2286
+ """Assert that a modular component can have a module or a device but not both."""
2287
+
2288
+ self.add_permissions("dcim.add_frontport")
2289
+ data = {
2290
+ "module": self.module.pk,
2291
+ "device": self.device.pk,
2292
+ "name": "test parent module validation",
2293
+ "type": PortTypeChoices.TYPE_8P8C,
2294
+ "rear_port": self.device_rear_ports[0].pk,
2295
+ "rear_port_position": 2,
2296
+ }
2297
+ url = self._get_list_url()
2298
+ response = self.client.post(url, data, format="json", **self.header)
2299
+ self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
2300
+ self.assertEqual(
2301
+ response.json(),
2302
+ {"non_field_errors": ["Only one of device or module must be set"]},
2303
+ )
2304
+
2305
+ data.pop("module")
2306
+ self.assertHttpStatus(self.client.post(url, data, format="json", **self.header), status.HTTP_201_CREATED)
2307
+
2308
+ data.pop("device")
2309
+ data["module"] = self.module.pk
2310
+ data["rear_port"] = self.module_rear_ports[0].pk
2311
+ self.assertHttpStatus(self.client.post(url, data, format="json", **self.header), status.HTTP_201_CREATED)
2312
+
2313
+ data.pop("module")
2314
+ data["rear_port_position"] = 3
2315
+ response = self.client.post(url, data, format="json", **self.header)
2316
+ self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
2317
+ self.assertEqual(
2318
+ response.json(),
2319
+ {"__all__": ["Either device or module must be set"]},
2320
+ )
2321
+
2322
+ def test_module_device_name_unique_validation(self):
2323
+ """Assert uniqueness constraint is enforced for (device,name) and (module,name) fields."""
2324
+
2325
+ self.add_permissions("dcim.add_frontport")
2326
+ data = {
2327
+ "module": self.module.pk,
2328
+ "name": "test modular device component parent validation",
2329
+ "type": PortTypeChoices.TYPE_8P8C,
2330
+ "rear_port": self.module_rear_ports[0].pk,
2331
+ "rear_port_position": 2,
2332
+ }
2333
+ url = self._get_list_url()
2334
+ self.assertHttpStatus(self.client.post(url, data, format="json", **self.header), status.HTTP_201_CREATED)
2335
+
2336
+ data = {
2337
+ "module": self.module.pk,
2338
+ "name": "test modular device component parent validation",
2339
+ "type": PortTypeChoices.TYPE_8P8C,
2340
+ "rear_port": self.module_rear_ports[1].pk,
2341
+ "rear_port_position": 2,
2342
+ }
2343
+ response = self.client.post(url, data, format="json", **self.header)
2344
+ self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
2345
+ self.assertEqual(
2346
+ response.json(),
2347
+ {"non_field_errors": ["The fields module, name must make a unique set."]},
2348
+ )
2349
+
2350
+ data = {
2351
+ "device": self.device.pk,
2352
+ "name": "test modular device component parent validation",
2353
+ "type": PortTypeChoices.TYPE_8P8C,
2354
+ "rear_port": self.device_rear_ports[0].pk,
2355
+ "rear_port_position": 2,
2356
+ }
2357
+ url = self._get_list_url()
2358
+ self.assertHttpStatus(self.client.post(url, data, format="json", **self.header), status.HTTP_201_CREATED)
1934
2359
 
1935
- class RearPortTest(Mixins.BasePortTestMixin):
2360
+ data = {
2361
+ "device": self.device.pk,
2362
+ "name": "test modular device component parent validation",
2363
+ "type": PortTypeChoices.TYPE_8P8C,
2364
+ "rear_port": self.device_rear_ports[1].pk,
2365
+ "rear_port_position": 2,
2366
+ }
2367
+ response = self.client.post(url, data, format="json", **self.header)
2368
+ self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
2369
+ self.assertEqual(
2370
+ response.json(),
2371
+ {"non_field_errors": ["The fields device, name must make a unique set."]},
2372
+ )
2373
+
2374
+
2375
+ class RearPortTest(Mixins.ModularDeviceComponentMixin, Mixins.BasePortTestMixin):
1936
2376
  model = RearPort
1937
2377
  peer_termination_type = Interface
2378
+ modular_component_create_data = {"type": PortTypeChoices.TYPE_8P8C}
1938
2379
 
1939
2380
  def test_trace(self):
1940
2381
  """RearPorts don't support trace."""
@@ -1943,24 +2384,20 @@ class RearPortTest(Mixins.BasePortTestMixin):
1943
2384
  def setUpTestData(cls):
1944
2385
  super().setUpTestData()
1945
2386
 
1946
- RearPort.objects.create(device=cls.device, name="Rear Port 1", type=PortTypeChoices.TYPE_8P8C)
1947
- RearPort.objects.create(device=cls.device, name="Rear Port 2", type=PortTypeChoices.TYPE_8P8C)
1948
- RearPort.objects.create(device=cls.device, name="Rear Port 3", type=PortTypeChoices.TYPE_8P8C)
1949
-
1950
2387
  cls.create_data = [
1951
2388
  {
1952
2389
  "device": cls.device.pk,
1953
- "name": "Rear Port 4",
2390
+ "name": "Rear Port 1",
1954
2391
  "type": PortTypeChoices.TYPE_8P8C,
1955
2392
  },
1956
2393
  {
1957
- "device": cls.device.pk,
1958
- "name": "Rear Port 5",
2394
+ "module": cls.module.pk,
2395
+ "name": "Rear Port 2",
1959
2396
  "type": PortTypeChoices.TYPE_8P8C,
1960
2397
  },
1961
2398
  {
1962
2399
  "device": cls.device.pk,
1963
- "name": "Rear Port 6",
2400
+ "name": "Rear Port 3",
1964
2401
  "type": PortTypeChoices.TYPE_8P8C,
1965
2402
  },
1966
2403
  ]
@@ -2078,6 +2515,36 @@ class InventoryItemTest(Mixins.BaseComponentTestMixin, APIViewTestCases.TreeMode
2078
2515
  pass
2079
2516
 
2080
2517
 
2518
+ class ModuleBayTest(Mixins.ModularDeviceComponentMixin, Mixins.BaseComponentTestMixin):
2519
+ model = ModuleBay
2520
+ choices_fields = []
2521
+ device_field = "parent_device"
2522
+ module_field = "parent_module"
2523
+
2524
+ @classmethod
2525
+ def setUpTestData(cls):
2526
+ super().setUpTestData()
2527
+
2528
+ cls.create_data = [
2529
+ {
2530
+ "parent_device": cls.device.pk,
2531
+ "name": "Test1",
2532
+ },
2533
+ {
2534
+ "parent_module": cls.module.pk,
2535
+ "name": "Test2",
2536
+ },
2537
+ {
2538
+ "parent_device": cls.device.pk,
2539
+ "name": "Test3",
2540
+ },
2541
+ ]
2542
+
2543
+ def get_deletable_object_pks(self):
2544
+ # Since Modules and ModuleBays are nestable, we need to delete ModuleBays that don't have any child ModuleBays
2545
+ return ModuleBay.objects.filter(installed_module__isnull=True).values_list("pk", flat=True)[:3]
2546
+
2547
+
2081
2548
  class CableTest(Mixins.BaseComponentTestMixin):
2082
2549
  model = Cable
2083
2550
  bulk_update_data = {
@@ -2111,6 +2578,7 @@ class CableTest(Mixins.BaseComponentTestMixin):
2111
2578
 
2112
2579
  interfaces = []
2113
2580
  interface_status = Status.objects.get_for_model(Interface).first()
2581
+ interface_role = Role.objects.get_for_model(Interface).first()
2114
2582
  for device in devices:
2115
2583
  for i in range(0, 10):
2116
2584
  interfaces.append(
@@ -2119,6 +2587,7 @@ class CableTest(Mixins.BaseComponentTestMixin):
2119
2587
  type=InterfaceTypeChoices.TYPE_1GE_FIXED,
2120
2588
  name=f"eth{i}",
2121
2589
  status=interface_status,
2590
+ role=interface_role,
2122
2591
  )
2123
2592
  )
2124
2593
 
@@ -2197,8 +2666,16 @@ class ConnectedDeviceTest(APITestCase):
2197
2666
  location=location,
2198
2667
  )
2199
2668
  interface_status = Status.objects.get_for_model(Interface).first()
2200
- interface1 = Interface.objects.create(device=self.device1, name="eth0", status=interface_status)
2201
- interface2 = Interface.objects.create(device=device2, name="eth0", status=interface_status)
2669
+ interface1 = Interface.objects.create(
2670
+ device=self.device1,
2671
+ name="eth0",
2672
+ status=interface_status,
2673
+ )
2674
+ interface2 = Interface.objects.create(
2675
+ device=device2,
2676
+ name="eth0",
2677
+ status=interface_status,
2678
+ )
2202
2679
 
2203
2680
  cable = Cable(termination_a=interface1, termination_b=interface2, status=cable_status)
2204
2681
  cable.validated_save()
@@ -2313,6 +2790,7 @@ class VirtualChassisTest(APIViewTestCases.APIViewTestCase):
2313
2790
 
2314
2791
  # Create 12 interfaces per device
2315
2792
  interface_status = Status.objects.get_for_model(Interface).first()
2793
+ interface_role = Role.objects.get_for_model(Interface).first()
2316
2794
  interfaces = []
2317
2795
  for i, device in enumerate(devices):
2318
2796
  for j in range(0, 13):
@@ -2323,6 +2801,7 @@ class VirtualChassisTest(APIViewTestCases.APIViewTestCase):
2323
2801
  name=f"{i%3+1}/{j}",
2324
2802
  type=InterfaceTypeChoices.TYPE_1GE_FIXED,
2325
2803
  status=interface_status,
2804
+ role=interface_role,
2326
2805
  )
2327
2806
  )
2328
2807
 
@@ -2635,7 +3114,7 @@ class InterfaceRedundancyGroupTestCase(APIViewTestCases.APIViewTestCase):
2635
3114
 
2636
3115
  interface_redundancy_groups = (
2637
3116
  InterfaceRedundancyGroup(
2638
- name="Interface Redundancy Group 1",
3117
+ name="Test Interface Redundancy Group 1",
2639
3118
  protocol="hsrp",
2640
3119
  status=statuses[0],
2641
3120
  virtual_ip=None,
@@ -2643,7 +3122,7 @@ class InterfaceRedundancyGroupTestCase(APIViewTestCases.APIViewTestCase):
2643
3122
  secrets_group=secrets_groups[0],
2644
3123
  ),
2645
3124
  InterfaceRedundancyGroup(
2646
- name="Interface Redundancy Group 2",
3125
+ name="Test Interface Redundancy Group 2",
2647
3126
  protocol="carp",
2648
3127
  status=statuses[1],
2649
3128
  virtual_ip=ips[1],
@@ -2651,7 +3130,7 @@ class InterfaceRedundancyGroupTestCase(APIViewTestCases.APIViewTestCase):
2651
3130
  secrets_group=secrets_groups[1],
2652
3131
  ),
2653
3132
  InterfaceRedundancyGroup(
2654
- name="Interface Redundancy Group 3",
3133
+ name="Test Interface Redundancy Group 3",
2655
3134
  protocol="vrrp",
2656
3135
  status=statuses[2],
2657
3136
  virtual_ip=ips[2],
@@ -2678,19 +3157,19 @@ class InterfaceRedundancyGroupTestCase(APIViewTestCases.APIViewTestCase):
2678
3157
  cls.interfaces = (
2679
3158
  Interface.objects.create(
2680
3159
  device=cls.device,
2681
- name="Interface 1",
3160
+ name="Test Interface 1",
2682
3161
  type="1000base-t",
2683
3162
  status=non_default_status,
2684
3163
  ),
2685
3164
  Interface.objects.create(
2686
3165
  device=cls.device,
2687
- name="Interface 2",
3166
+ name="Test Interface 2",
2688
3167
  type="1000base-t",
2689
3168
  status=non_default_status,
2690
3169
  ),
2691
3170
  Interface.objects.create(
2692
3171
  device=cls.device,
2693
- name="Interface 3",
3172
+ name="Test Interface 3",
2694
3173
  type=InterfaceTypeChoices.TYPE_BRIDGE,
2695
3174
  status=non_default_status,
2696
3175
  ),
@@ -2878,6 +3357,10 @@ class ControllerManagedDeviceGroupTestCase(APIViewTestCases.APIViewTestCase):
2878
3357
  "weight": 200,
2879
3358
  },
2880
3359
  ]
3360
+ # changing controller is error-prone since a child group must have the same controller as its parent
3361
+ cls.update_data = {
3362
+ "weight": 300,
3363
+ }
2881
3364
  cls.bulk_update_data = {
2882
3365
  "weight": 300,
2883
3366
  }