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