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

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

Potentially problematic release.


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

Files changed (697) hide show
  1. nautobot/apps/forms.py +4 -0
  2. nautobot/apps/models.py +10 -1
  3. nautobot/circuits/__init__.py +0 -1
  4. nautobot/circuits/apps.py +1 -0
  5. nautobot/circuits/factory.py +15 -3
  6. nautobot/circuits/filters.py +13 -0
  7. nautobot/circuits/forms.py +13 -0
  8. nautobot/circuits/migrations/0021_alter_circuit_status_alter_circuittermination__path.py +32 -0
  9. nautobot/circuits/migrations/0022_circuittermination_cloud_network.py +25 -0
  10. nautobot/circuits/models.py +16 -3
  11. nautobot/circuits/tables.py +16 -2
  12. nautobot/circuits/templates/circuits/circuittermination_create.html +10 -2
  13. nautobot/circuits/templates/circuits/circuittermination_retrieve.html +6 -0
  14. nautobot/circuits/templates/circuits/inc/circuit_termination.html +6 -1
  15. nautobot/circuits/tests/test_api.py +7 -5
  16. nautobot/circuits/tests/test_filters.py +12 -5
  17. nautobot/circuits/tests/test_models.py +33 -2
  18. nautobot/circuits/views.py +2 -3
  19. nautobot/cloud/__init__.py +0 -0
  20. nautobot/cloud/api/__init__.py +0 -0
  21. nautobot/cloud/api/serializers.py +54 -0
  22. nautobot/cloud/api/urls.py +16 -0
  23. nautobot/cloud/api/views.py +48 -0
  24. nautobot/cloud/apps.py +13 -0
  25. nautobot/cloud/factory.py +111 -0
  26. nautobot/cloud/filters.py +184 -0
  27. nautobot/cloud/forms.py +333 -0
  28. nautobot/cloud/homepage.py +43 -0
  29. nautobot/cloud/migrations/0001_initial.py +304 -0
  30. nautobot/cloud/migrations/__init__.py +0 -0
  31. nautobot/cloud/models.py +247 -0
  32. nautobot/cloud/navigation.py +85 -0
  33. nautobot/cloud/tables.py +173 -0
  34. nautobot/cloud/templates/cloud/cloudaccount_retrieve.html +43 -0
  35. nautobot/cloud/templates/cloud/cloudnetwork_retrieve.html +128 -0
  36. nautobot/cloud/templates/cloud/cloudnetwork_update.html +33 -0
  37. nautobot/cloud/templates/cloud/cloudresourcetype_retrieve.html +100 -0
  38. nautobot/cloud/templates/cloud/cloudservice_retrieve.html +65 -0
  39. nautobot/cloud/templates/cloud/cloudservice_update.html +25 -0
  40. nautobot/cloud/tests/__init__.py +0 -0
  41. nautobot/cloud/tests/test_api.py +248 -0
  42. nautobot/cloud/tests/test_filters.py +113 -0
  43. nautobot/cloud/tests/test_models.py +43 -0
  44. nautobot/cloud/tests/test_views.py +153 -0
  45. nautobot/cloud/urls.py +14 -0
  46. nautobot/cloud/views.py +181 -0
  47. nautobot/core/__init__.py +0 -3
  48. nautobot/core/api/metadata.py +1 -0
  49. nautobot/core/api/parsers.py +7 -1
  50. nautobot/core/api/urls.py +1 -0
  51. nautobot/core/api/utils.py +1 -0
  52. nautobot/core/api/views.py +4 -0
  53. nautobot/core/apps/__init__.py +6 -3
  54. nautobot/core/constants.py +8 -0
  55. nautobot/core/factory.py +32 -1
  56. nautobot/core/filters.py +96 -28
  57. nautobot/core/forms/fields.py +10 -4
  58. nautobot/core/forms/forms.py +1 -1
  59. nautobot/core/forms/widgets.py +18 -1
  60. nautobot/core/graphql/generators.py +2 -2
  61. nautobot/core/graphql/schema.py +34 -4
  62. nautobot/core/jobs/__init__.py +17 -6
  63. nautobot/core/jobs/cleanup.py +100 -0
  64. nautobot/core/jobs/groups.py +38 -0
  65. nautobot/core/management/commands/generate_test_data.py +116 -3
  66. nautobot/core/models/__init__.py +34 -9
  67. nautobot/core/models/generics.py +19 -3
  68. nautobot/core/models/name_color_content_types.py +7 -28
  69. nautobot/core/models/querysets.py +4 -3
  70. nautobot/core/models/tree_queries.py +1 -1
  71. nautobot/core/models/utils.py +21 -5
  72. nautobot/core/settings.py +4 -30
  73. nautobot/core/settings.yaml +34 -27
  74. nautobot/core/settings_funcs.py +103 -0
  75. nautobot/core/tables.py +127 -56
  76. nautobot/core/templates/admin/search_form.html +1 -1
  77. nautobot/core/templates/buttons/add.html +11 -3
  78. nautobot/core/templates/buttons/consolidated_bulk_action_buttons.html +13 -0
  79. nautobot/core/templates/buttons/consolidated_detail_view_action_buttons.html +13 -0
  80. nautobot/core/templates/buttons/export.html +101 -53
  81. nautobot/core/templates/buttons/job_import.html +11 -3
  82. nautobot/core/templates/generic/object_bulk_destroy.html +3 -1
  83. nautobot/core/templates/generic/object_bulk_update.html +3 -1
  84. nautobot/core/templates/generic/object_changelog.html +0 -9
  85. nautobot/core/templates/generic/object_list.html +156 -17
  86. nautobot/core/templates/generic/object_retrieve.html +80 -16
  87. nautobot/core/templates/inc/extras_features_edit_form_fields.html +8 -0
  88. nautobot/core/templates/inc/javascript.html +2 -0
  89. nautobot/core/templates/inc/media.html +2 -2
  90. nautobot/core/templates/inc/nav_menu.html +1 -0
  91. nautobot/core/templates/inc/paginator.html +7 -7
  92. nautobot/core/templates/inc/search_panel.html +2 -2
  93. nautobot/core/templates/inc/table.html +2 -2
  94. nautobot/core/templates/nautobot_config.py.j2 +13 -23
  95. nautobot/core/templates/utilities/templatetags/dynamic_group_assignment_modal.html +37 -0
  96. nautobot/core/templates/utilities/templatetags/filter_form_modal.html +2 -2
  97. nautobot/core/templates/utilities/templatetags/saved_view_modal.html +38 -0
  98. nautobot/core/templates/utilities/theme_preview.html +25 -8
  99. nautobot/core/templates/utilities/worker_status.html +152 -0
  100. nautobot/core/templatetags/buttons.py +335 -38
  101. nautobot/core/templatetags/form_helpers.py +1 -1
  102. nautobot/core/templatetags/helpers.py +181 -11
  103. nautobot/core/testing/api.py +5 -4
  104. nautobot/core/testing/filters.py +51 -13
  105. nautobot/core/testing/mixins.py +46 -0
  106. nautobot/core/testing/models.py +22 -0
  107. nautobot/core/testing/schema.py +4 -8
  108. nautobot/core/testing/views.py +31 -14
  109. nautobot/core/tests/integration/test_general_functionality.py +1 -1
  110. nautobot/core/tests/integration/test_import_objects_ui.py +1 -0
  111. nautobot/core/tests/integration/test_swagger.py +1 -1
  112. nautobot/core/tests/nautobot_config.py +0 -1
  113. nautobot/core/tests/runner.py +2 -2
  114. nautobot/core/tests/test_api.py +1 -0
  115. nautobot/core/tests/test_authentication.py +7 -2
  116. nautobot/core/tests/test_filters.py +11 -9
  117. nautobot/core/tests/test_forms.py +9 -0
  118. nautobot/core/tests/test_graphql.py +27 -16
  119. nautobot/core/tests/test_jobs.py +123 -74
  120. nautobot/core/tests/test_tables.py +3 -1
  121. nautobot/core/tests/test_templatetags_helpers.py +12 -5
  122. nautobot/core/tests/test_utils.py +31 -20
  123. nautobot/core/tests/test_views.py +6 -6
  124. nautobot/core/urls.py +8 -3
  125. nautobot/core/utils/deprecation.py +29 -0
  126. nautobot/core/utils/filtering.py +12 -9
  127. nautobot/core/utils/lookup.py +37 -2
  128. nautobot/core/utils/requests.py +4 -1
  129. nautobot/core/views/__init__.py +137 -24
  130. nautobot/core/views/generic.py +118 -66
  131. nautobot/core/views/mixins.py +104 -35
  132. nautobot/core/views/paginator.py +9 -3
  133. nautobot/core/views/renderers.py +121 -56
  134. nautobot/core/views/utils.py +79 -1
  135. nautobot/dcim/__init__.py +0 -1
  136. nautobot/dcim/api/serializers.py +180 -44
  137. nautobot/dcim/api/urls.py +7 -3
  138. nautobot/dcim/api/views.py +53 -7
  139. nautobot/dcim/apps.py +3 -0
  140. nautobot/dcim/choices.py +25 -0
  141. nautobot/dcim/constants.py +7 -0
  142. nautobot/dcim/factory.py +249 -18
  143. nautobot/dcim/filters/__init__.py +369 -193
  144. nautobot/dcim/filters/mixins.py +274 -1
  145. nautobot/dcim/forms.py +817 -109
  146. nautobot/dcim/graphql/types.py +2 -2
  147. nautobot/dcim/homepage.py +1 -1
  148. nautobot/dcim/migrations/0059_add_role_field_to_interface_models.py +27 -0
  149. nautobot/dcim/migrations/0060_alter_cable_status_alter_consoleport__path_and_more.py +303 -0
  150. nautobot/dcim/migrations/0061_module_models.py +861 -0
  151. nautobot/dcim/migrations/0062_module_data_migration.py +25 -0
  152. nautobot/dcim/models/__init__.py +8 -0
  153. nautobot/dcim/models/cables.py +15 -0
  154. nautobot/dcim/models/device_component_templates.py +207 -53
  155. nautobot/dcim/models/device_components.py +275 -106
  156. nautobot/dcim/models/devices.py +466 -13
  157. nautobot/dcim/navigation.py +47 -0
  158. nautobot/dcim/signals.py +3 -3
  159. nautobot/dcim/tables/__init__.py +35 -23
  160. nautobot/dcim/tables/devices.py +231 -59
  161. nautobot/dcim/tables/devicetypes.py +65 -9
  162. nautobot/dcim/tables/racks.py +5 -1
  163. nautobot/dcim/tables/template_code.py +46 -26
  164. nautobot/dcim/templates/dcim/cable_connect.html +76 -3
  165. nautobot/dcim/templates/dcim/console_port_connection_list.html +7 -5
  166. nautobot/dcim/templates/dcim/device/base.html +15 -7
  167. nautobot/dcim/templates/dcim/device/consoleports.html +2 -3
  168. nautobot/dcim/templates/dcim/device/consoleserverports.html +2 -3
  169. nautobot/dcim/templates/dcim/device/devicebays.html +6 -7
  170. nautobot/dcim/templates/dcim/device/frontports.html +2 -3
  171. nautobot/dcim/templates/dcim/device/interfaces.html +2 -3
  172. nautobot/dcim/templates/dcim/device/inventory.html +2 -3
  173. nautobot/dcim/templates/dcim/device/modulebays.html +49 -0
  174. nautobot/dcim/templates/dcim/device/poweroutlets.html +2 -3
  175. nautobot/dcim/templates/dcim/device/powerports.html +2 -3
  176. nautobot/dcim/templates/dcim/device/rearports.html +2 -3
  177. nautobot/dcim/templates/dcim/device.html +45 -1
  178. nautobot/dcim/templates/dcim/device_component.html +13 -5
  179. nautobot/dcim/templates/dcim/device_list.html +2 -1
  180. nautobot/dcim/templates/dcim/deviceredundancygroup_retrieve.html +0 -6
  181. nautobot/dcim/templates/dcim/devicetype.html +99 -98
  182. nautobot/dcim/templates/dcim/devicetype_list.html +8 -16
  183. nautobot/dcim/templates/dcim/inc/devicetype_component_table.html +1 -1
  184. nautobot/dcim/templates/dcim/inc/moduletype_component_table.html +39 -0
  185. nautobot/dcim/templates/dcim/interface.html +17 -2
  186. nautobot/dcim/templates/dcim/interface_connection_list.html +7 -5
  187. nautobot/dcim/templates/dcim/interface_edit.html +1 -0
  188. nautobot/dcim/templates/dcim/manufacturer.html +24 -0
  189. nautobot/dcim/templates/dcim/module/base.html +97 -0
  190. nautobot/dcim/templates/dcim/module_bulk_destroy.html +5 -0
  191. nautobot/dcim/templates/dcim/module_consoleports.html +53 -0
  192. nautobot/dcim/templates/dcim/module_consoleserverports.html +53 -0
  193. nautobot/dcim/templates/dcim/module_destroy.html +5 -0
  194. nautobot/dcim/templates/dcim/module_frontports.html +53 -0
  195. nautobot/dcim/templates/dcim/module_interfaces.html +57 -0
  196. nautobot/dcim/templates/dcim/module_list.html +20 -0
  197. nautobot/dcim/templates/dcim/module_modulebays.html +49 -0
  198. nautobot/dcim/templates/dcim/module_poweroutlets.html +53 -0
  199. nautobot/dcim/templates/dcim/module_powerports.html +53 -0
  200. nautobot/dcim/templates/dcim/module_rearports.html +53 -0
  201. nautobot/dcim/templates/dcim/module_retrieve.html +63 -0
  202. nautobot/dcim/templates/dcim/module_update.html +71 -0
  203. nautobot/dcim/templates/dcim/modulebay_bulk_destroy.html +5 -0
  204. nautobot/dcim/templates/dcim/modulebay_destroy.html +8 -0
  205. nautobot/dcim/templates/dcim/modulebay_retrieve.html +101 -0
  206. nautobot/dcim/templates/dcim/moduletype_list.html +11 -0
  207. nautobot/dcim/templates/dcim/moduletype_retrieve.html +142 -0
  208. nautobot/dcim/templates/dcim/power_port_connection_list.html +7 -5
  209. nautobot/dcim/templates/dcim/softwareimagefile_retrieve.html +65 -19
  210. nautobot/dcim/tests/integration/test_cable_connect_form.py +4 -4
  211. nautobot/dcim/tests/test_api.py +691 -208
  212. nautobot/dcim/tests/test_filters.py +836 -217
  213. nautobot/dcim/tests/test_models.py +1072 -39
  214. nautobot/dcim/tests/test_views.py +1488 -358
  215. nautobot/dcim/urls.py +17 -2
  216. nautobot/dcim/utils.py +2 -3
  217. nautobot/dcim/views.py +1107 -120
  218. nautobot/extras/__init__.py +0 -1
  219. nautobot/extras/api/serializers.py +115 -3
  220. nautobot/extras/api/urls.py +12 -0
  221. nautobot/extras/api/views.py +125 -7
  222. nautobot/extras/apps.py +2 -2
  223. nautobot/extras/choices.py +43 -0
  224. nautobot/extras/context_managers.py +13 -8
  225. nautobot/extras/datasources/git.py +2 -0
  226. nautobot/extras/factory.py +422 -9
  227. nautobot/extras/filters/__init__.py +174 -3
  228. nautobot/extras/filters/mixins.py +46 -43
  229. nautobot/extras/forms/base.py +17 -4
  230. nautobot/extras/forms/forms.py +227 -8
  231. nautobot/extras/forms/mixins.py +93 -0
  232. nautobot/extras/graphql/types.py +23 -10
  233. nautobot/extras/homepage.py +16 -13
  234. nautobot/extras/jobs.py +2 -2
  235. nautobot/extras/management/__init__.py +1 -0
  236. nautobot/extras/management/commands/refresh_dynamic_group_member_caches.py +1 -16
  237. nautobot/extras/migrations/0021_customfield_changelog_data.py +1 -0
  238. nautobot/extras/migrations/0109_dynamicgroup_group_type_dynamicgroup_tags_and_more.py +108 -0
  239. nautobot/extras/migrations/0110_alter_configcontext_cluster_groups_and_more.py +111 -0
  240. nautobot/extras/migrations/0111_metadata.py +162 -0
  241. nautobot/extras/migrations/0112_dynamic_group_group_type_data_migration.py +28 -0
  242. nautobot/extras/migrations/0113_saved_views.py +77 -0
  243. nautobot/extras/models/__init__.py +15 -1
  244. nautobot/extras/models/change_logging.py +3 -3
  245. nautobot/extras/models/contacts.py +4 -0
  246. nautobot/extras/models/customfields.py +18 -3
  247. nautobot/extras/models/groups.py +389 -225
  248. nautobot/extras/models/jobs.py +4 -84
  249. nautobot/extras/models/metadata.py +441 -0
  250. nautobot/extras/models/mixins.py +72 -62
  251. nautobot/extras/models/models.py +116 -9
  252. nautobot/extras/models/relationships.py +9 -2
  253. nautobot/extras/models/tags.py +13 -2
  254. nautobot/extras/navigation.py +57 -0
  255. nautobot/extras/plugins/__init__.py +3 -1
  256. nautobot/extras/querysets.py +30 -66
  257. nautobot/extras/signals.py +96 -114
  258. nautobot/extras/tables.py +171 -47
  259. nautobot/extras/templates/extras/dynamicgroup.html +44 -15
  260. nautobot/extras/templates/extras/dynamicgroup_edit.html +2 -0
  261. nautobot/extras/templates/extras/job.html +1 -1
  262. nautobot/extras/templates/extras/job_detail.html +0 -11
  263. nautobot/extras/templates/extras/jobresult.html +61 -74
  264. nautobot/extras/templates/extras/metadatatype_create.html +89 -0
  265. nautobot/extras/templates/extras/metadatatype_retrieve.html +67 -0
  266. nautobot/extras/templates/extras/object_dynamicgroups.html +7 -0
  267. nautobot/extras/templates/extras/objectchange_list.html +0 -12
  268. nautobot/extras/templates/extras/plugins_list.html +1 -3
  269. nautobot/extras/templates/extras/role_retrieve.html +48 -0
  270. nautobot/extras/templates/extras/staticgroupassociation_retrieve.html +20 -0
  271. nautobot/extras/tests/integration/test_customfields.py +1 -0
  272. nautobot/extras/tests/test_api.py +501 -22
  273. nautobot/extras/tests/test_changelog.py +20 -9
  274. nautobot/extras/tests/test_context_managers.py +22 -15
  275. nautobot/extras/tests/test_datasources.py +13 -1
  276. nautobot/extras/tests/test_dynamicgroups.py +201 -171
  277. nautobot/extras/tests/test_filters.py +211 -12
  278. nautobot/extras/tests/test_jobs.py +4 -4
  279. nautobot/extras/tests/test_models.py +499 -4
  280. nautobot/extras/tests/test_relationships.py +1 -0
  281. nautobot/extras/tests/test_views.py +565 -28
  282. nautobot/extras/tests/test_webhooks.py +1 -1
  283. nautobot/extras/urls.py +5 -0
  284. nautobot/extras/utils.py +56 -45
  285. nautobot/extras/views.py +585 -96
  286. nautobot/ipam/__init__.py +0 -1
  287. nautobot/ipam/apps.py +1 -0
  288. nautobot/ipam/factory.py +17 -19
  289. nautobot/ipam/filters.py +14 -1
  290. nautobot/ipam/forms.py +9 -5
  291. nautobot/ipam/graphql/types.py +2 -2
  292. nautobot/ipam/migrations/0047_alter_ipaddress_role_alter_ipaddress_status_and_more.py +59 -0
  293. nautobot/ipam/models.py +23 -9
  294. nautobot/ipam/querysets.py +1 -1
  295. nautobot/ipam/signals.py +4 -2
  296. nautobot/ipam/tables.py +1 -0
  297. nautobot/ipam/templates/ipam/ipaddress_interfaces.html +1 -1
  298. nautobot/ipam/templates/ipam/ipaddress_vm_interfaces.html +1 -1
  299. nautobot/ipam/templates/ipam/prefix.html +1 -0
  300. nautobot/ipam/tests/test_api.py +37 -18
  301. nautobot/ipam/tests/test_filters.py +26 -2
  302. nautobot/ipam/tests/test_models.py +8 -3
  303. nautobot/ipam/tests/test_querysets.py +1 -1
  304. nautobot/ipam/tests/test_views.py +3 -2
  305. nautobot/ipam/urls.py +2 -2
  306. nautobot/ipam/views.py +25 -28
  307. nautobot/project-static/css/base.css +20 -1
  308. nautobot/project-static/css/dark.css +11 -0
  309. nautobot/project-static/docs/404.html +884 -80
  310. nautobot/project-static/docs/apps/index.html +884 -80
  311. nautobot/project-static/docs/apps/nautobot-apps.html +884 -80
  312. nautobot/project-static/docs/assets/_mkdocstrings.css +5 -0
  313. nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +911 -112
  314. nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +896 -93
  315. nautobot/project-static/docs/code-reference/nautobot/apps/api.html +1457 -790
  316. nautobot/project-static/docs/code-reference/nautobot/apps/change_logging.html +927 -136
  317. nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +969 -180
  318. nautobot/project-static/docs/code-reference/nautobot/apps/config.html +893 -91
  319. nautobot/project-static/docs/code-reference/nautobot/apps/constants.html +889 -85
  320. nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +983 -185
  321. nautobot/project-static/docs/code-reference/nautobot/apps/exceptions.html +938 -143
  322. nautobot/project-static/docs/code-reference/nautobot/apps/factory.html +1064 -274
  323. nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +1190 -346
  324. nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +1663 -865
  325. nautobot/project-static/docs/code-reference/nautobot/apps/graphql.html +1156 -373
  326. nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +2200 -1502
  327. nautobot/project-static/docs/code-reference/nautobot/apps/models.html +2229 -1421
  328. nautobot/project-static/docs/code-reference/nautobot/apps/querysets.html +904 -103
  329. nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +955 -155
  330. nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +1002 -215
  331. nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +1911 -1275
  332. nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +1835 -1091
  333. nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +896 -93
  334. nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +2323 -1693
  335. nautobot/project-static/docs/code-reference/nautobot/apps/views.html +1785 -1023
  336. nautobot/project-static/docs/development/apps/api/configuration-view.html +884 -80
  337. nautobot/project-static/docs/development/apps/api/database-backend-config.html +884 -80
  338. nautobot/project-static/docs/development/apps/api/models/django-admin.html +884 -80
  339. nautobot/project-static/docs/development/apps/api/models/global-search.html +884 -80
  340. nautobot/project-static/docs/development/apps/api/models/graphql.html +884 -80
  341. nautobot/project-static/docs/development/apps/api/models/index.html +922 -81
  342. nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +884 -80
  343. nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +884 -80
  344. nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +884 -80
  345. nautobot/project-static/docs/development/apps/api/platform-features/git-repository-content.html +884 -80
  346. nautobot/project-static/docs/development/apps/api/platform-features/index.html +884 -80
  347. nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +884 -80
  348. nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +884 -80
  349. nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +884 -80
  350. nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +884 -80
  351. nautobot/project-static/docs/development/apps/api/platform-features/uniquely-identify-objects.html +884 -80
  352. nautobot/project-static/docs/development/apps/api/prometheus.html +884 -80
  353. nautobot/project-static/docs/development/apps/api/setup.html +884 -80
  354. nautobot/project-static/docs/development/apps/api/testing.html +884 -80
  355. nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +884 -80
  356. nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +884 -80
  357. nautobot/project-static/docs/development/apps/api/ui-extensions/index.html +884 -80
  358. nautobot/project-static/docs/development/apps/api/ui-extensions/navigation.html +884 -80
  359. nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +884 -80
  360. nautobot/project-static/docs/development/apps/api/views/base-template.html +884 -80
  361. nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +884 -80
  362. nautobot/project-static/docs/development/apps/api/views/django-generic-views.html +884 -80
  363. nautobot/project-static/docs/development/apps/api/views/help-documentation.html +884 -80
  364. nautobot/project-static/docs/development/apps/api/views/index.html +884 -80
  365. nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +884 -80
  366. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +884 -80
  367. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +884 -80
  368. nautobot/project-static/docs/development/apps/api/views/notes.html +884 -80
  369. nautobot/project-static/docs/development/apps/api/views/rest-api.html +884 -80
  370. nautobot/project-static/docs/development/apps/api/views/urls.html +884 -80
  371. nautobot/project-static/docs/development/apps/index.html +884 -80
  372. nautobot/project-static/docs/development/apps/migration/code-updates.html +884 -80
  373. nautobot/project-static/docs/development/apps/migration/dependency-updates.html +884 -80
  374. nautobot/project-static/docs/development/apps/migration/from-v1.html +884 -80
  375. nautobot/project-static/docs/development/apps/migration/model-updates/dcim.html +884 -80
  376. nautobot/project-static/docs/development/apps/migration/model-updates/extras.html +884 -80
  377. nautobot/project-static/docs/development/apps/migration/model-updates/global.html +884 -80
  378. nautobot/project-static/docs/development/apps/migration/model-updates/ipam.html +884 -80
  379. nautobot/project-static/docs/development/apps/porting-from-netbox.html +884 -80
  380. nautobot/project-static/docs/development/core/application-registry.html +884 -80
  381. nautobot/project-static/docs/development/core/best-practices.html +885 -80
  382. nautobot/project-static/docs/development/core/bootstrap-ui.html +884 -80
  383. nautobot/project-static/docs/development/core/caching.html +884 -80
  384. nautobot/project-static/docs/development/core/controllers.html +884 -80
  385. nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +884 -80
  386. nautobot/project-static/docs/development/core/generic-views.html +884 -80
  387. nautobot/project-static/docs/development/core/getting-started.html +884 -80
  388. nautobot/project-static/docs/development/core/homepage.html +884 -80
  389. nautobot/project-static/docs/development/core/index.html +884 -91
  390. nautobot/project-static/docs/development/core/model-checklist.html +887 -81
  391. nautobot/project-static/docs/development/core/model-features.html +884 -80
  392. nautobot/project-static/docs/development/core/natural-keys.html +884 -80
  393. nautobot/project-static/docs/development/core/navigation-menu.html +884 -80
  394. nautobot/project-static/docs/development/core/release-checklist.html +887 -83
  395. nautobot/project-static/docs/development/core/role-internals.html +884 -80
  396. nautobot/project-static/docs/development/core/settings.html +884 -80
  397. nautobot/project-static/docs/development/core/style-guide.html +885 -81
  398. nautobot/project-static/docs/development/core/templates.html +896 -81
  399. nautobot/project-static/docs/development/core/testing.html +884 -80
  400. nautobot/project-static/docs/development/core/user-preferences.html +884 -80
  401. nautobot/project-static/docs/development/index.html +884 -80
  402. nautobot/project-static/docs/development/jobs/index.html +1247 -457
  403. nautobot/project-static/docs/development/jobs/migration/from-v1.html +884 -80
  404. nautobot/project-static/docs/index.html +13 -8228
  405. nautobot/project-static/docs/media/models/cloud_aws_direct_connect_dark.png +0 -0
  406. nautobot/project-static/docs/media/models/cloud_aws_direct_connect_light.png +0 -0
  407. nautobot/project-static/docs/models/cloud/cloudaccount.html +15 -0
  408. nautobot/project-static/docs/models/cloud/cloudnetwork.html +15 -0
  409. nautobot/project-static/docs/models/cloud/cloudnetworkprefixassignment.html +15 -0
  410. nautobot/project-static/docs/models/cloud/cloudresourcetype.html +15 -0
  411. nautobot/project-static/docs/models/cloud/cloudservice.html +15 -0
  412. nautobot/project-static/docs/models/cloud/cloudservicenetworkassignment.html +15 -0
  413. nautobot/project-static/docs/models/dcim/module.html +15 -0
  414. nautobot/project-static/docs/models/dcim/modulebay.html +15 -0
  415. nautobot/project-static/docs/models/dcim/modulebaytemplate.html +15 -0
  416. nautobot/project-static/docs/models/dcim/moduletype.html +15 -0
  417. nautobot/project-static/docs/models/extras/metadatachoice.html +15 -0
  418. nautobot/project-static/docs/models/extras/metadatatype.html +15 -0
  419. nautobot/project-static/docs/models/extras/objectmetadata.html +15 -0
  420. nautobot/project-static/docs/models/extras/role.html +15 -0
  421. nautobot/project-static/docs/models/extras/savedview.html +15 -0
  422. nautobot/project-static/docs/models/extras/staticgroupassociation.html +15 -0
  423. nautobot/project-static/docs/models/extras/status.html +15 -0
  424. nautobot/project-static/docs/objects.inv +0 -0
  425. nautobot/project-static/docs/overview/application_stack.html +892 -81
  426. nautobot/project-static/docs/overview/design_philosophy.html +886 -82
  427. nautobot/project-static/docs/overview/index.html +9032 -13
  428. nautobot/project-static/docs/release-notes/index.html +887 -83
  429. nautobot/project-static/docs/release-notes/version-1.0.html +884 -80
  430. nautobot/project-static/docs/release-notes/version-1.1.html +884 -80
  431. nautobot/project-static/docs/release-notes/version-1.2.html +884 -80
  432. nautobot/project-static/docs/release-notes/version-1.3.html +884 -80
  433. nautobot/project-static/docs/release-notes/version-1.4.html +884 -80
  434. nautobot/project-static/docs/release-notes/version-1.5.html +885 -81
  435. nautobot/project-static/docs/release-notes/version-1.6.html +885 -81
  436. nautobot/project-static/docs/release-notes/version-2.0.html +884 -80
  437. nautobot/project-static/docs/release-notes/version-2.1.html +884 -80
  438. nautobot/project-static/docs/release-notes/version-2.2.html +990 -323
  439. nautobot/project-static/docs/release-notes/version-2.3.html +9524 -0
  440. nautobot/project-static/docs/requirements.txt +4 -4
  441. nautobot/project-static/docs/search/search_index.json +1 -1
  442. nautobot/project-static/docs/sitemap.xml +335 -260
  443. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  444. nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +884 -80
  445. nautobot/project-static/docs/user-guide/administration/configuration/authentication/remote.html +884 -80
  446. nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +884 -80
  447. nautobot/project-static/docs/user-guide/administration/configuration/index.html +884 -80
  448. nautobot/project-static/docs/user-guide/administration/configuration/optional-settings.html +983 -197
  449. nautobot/project-static/docs/user-guide/administration/configuration/required-settings.html +884 -80
  450. nautobot/project-static/docs/user-guide/administration/configuration/time-zones.html +884 -80
  451. nautobot/project-static/docs/user-guide/administration/guides/caching.html +884 -80
  452. nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +888 -84
  453. nautobot/project-static/docs/user-guide/administration/guides/healthcheck.html +884 -80
  454. nautobot/project-static/docs/user-guide/administration/guides/permissions.html +884 -80
  455. nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +884 -80
  456. nautobot/project-static/docs/user-guide/administration/guides/replicating-nautobot.html +884 -80
  457. nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +884 -80
  458. nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +884 -80
  459. nautobot/project-static/docs/user-guide/administration/installation/app-install.html +884 -80
  460. nautobot/project-static/docs/user-guide/administration/installation/external-authentication.html +884 -80
  461. nautobot/project-static/docs/user-guide/administration/installation/http-server.html +884 -80
  462. nautobot/project-static/docs/user-guide/administration/installation/index.html +888 -80
  463. nautobot/project-static/docs/user-guide/administration/installation/install_system.html +884 -80
  464. nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +884 -80
  465. nautobot/project-static/docs/user-guide/administration/installation/services.html +888 -80
  466. nautobot/project-static/docs/user-guide/administration/installation-extras/docker.html +900 -91
  467. nautobot/project-static/docs/user-guide/administration/installation-extras/health-checks.html +884 -80
  468. nautobot/project-static/docs/user-guide/administration/installation-extras/selinux-troubleshooting.html +884 -80
  469. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-netbox.html +884 -80
  470. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +884 -80
  471. nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +915 -163
  472. nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +884 -80
  473. nautobot/project-static/docs/user-guide/administration/upgrading/database-backup.html +884 -80
  474. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/after-you-upgrade.html +884 -80
  475. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/before-you-upgrade.html +884 -80
  476. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/for-developers.html +884 -80
  477. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/index.html +884 -80
  478. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/whats-changed.html +884 -80
  479. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/region-and-site-data-migration-guide.html +884 -80
  480. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +884 -80
  481. nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +884 -80
  482. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuit.html +884 -80
  483. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittermination.html +885 -81
  484. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittype.html +884 -80
  485. nautobot/project-static/docs/user-guide/core-data-model/circuits/provider.html +888 -80
  486. nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +887 -83
  487. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloud.html +8984 -0
  488. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudaccount.html +8828 -0
  489. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetwork.html +8829 -0
  490. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetworkprefixassignment.html +8828 -0
  491. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudresourcetype.html +8829 -0
  492. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservice.html +8833 -0
  493. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservicenetworkassignment.html +8828 -0
  494. nautobot/project-static/docs/user-guide/core-data-model/dcim/cable.html +898 -94
  495. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +915 -97
  496. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +915 -97
  497. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +910 -92
  498. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +915 -97
  499. nautobot/project-static/docs/user-guide/core-data-model/dcim/controller.html +898 -94
  500. nautobot/project-static/docs/user-guide/core-data-model/dcim/controllermanageddevicegroup.html +898 -94
  501. nautobot/project-static/docs/user-guide/core-data-model/dcim/device.html +905 -97
  502. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +912 -108
  503. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +913 -109
  504. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicefamily.html +910 -106
  505. nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +898 -94
  506. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +906 -97
  507. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +918 -100
  508. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +928 -110
  509. nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +920 -98
  510. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +898 -94
  511. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +929 -111
  512. nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +920 -102
  513. nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +910 -106
  514. nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +913 -109
  515. nautobot/project-static/docs/user-guide/core-data-model/dcim/manufacturer.html +914 -106
  516. nautobot/project-static/docs/user-guide/core-data-model/dcim/module.html +8828 -0
  517. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebay.html +8846 -0
  518. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebaytemplate.html +8843 -0
  519. nautobot/project-static/docs/user-guide/core-data-model/dcim/moduletype.html +8823 -0
  520. nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +908 -104
  521. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerfeed.html +898 -94
  522. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +932 -75
  523. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +916 -98
  524. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerpanel.html +898 -94
  525. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +935 -78
  526. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +913 -95
  527. nautobot/project-static/docs/user-guide/core-data-model/dcim/rack.html +921 -117
  528. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackgroup.html +910 -106
  529. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackreservation.html +898 -94
  530. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +914 -96
  531. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +916 -98
  532. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +898 -94
  533. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareversion.html +898 -94
  534. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualchassis.html +898 -94
  535. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +884 -80
  536. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +884 -80
  537. nautobot/project-static/docs/user-guide/core-data-model/extras/contact.html +889 -81
  538. nautobot/project-static/docs/user-guide/core-data-model/extras/team.html +889 -81
  539. nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +884 -80
  540. nautobot/project-static/docs/user-guide/core-data-model/ipam/namespace.html +884 -80
  541. nautobot/project-static/docs/user-guide/core-data-model/ipam/prefix.html +884 -80
  542. nautobot/project-static/docs/user-guide/core-data-model/ipam/rir.html +884 -80
  543. nautobot/project-static/docs/user-guide/core-data-model/ipam/routetarget.html +884 -80
  544. nautobot/project-static/docs/user-guide/core-data-model/ipam/service.html +884 -80
  545. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +884 -80
  546. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlangroup.html +884 -80
  547. nautobot/project-static/docs/user-guide/core-data-model/ipam/vrf.html +884 -80
  548. nautobot/project-static/docs/user-guide/core-data-model/overview/introduction.html +884 -80
  549. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenant.html +893 -88
  550. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenantgroup.html +884 -80
  551. nautobot/project-static/docs/user-guide/core-data-model/virtualization/cluster.html +884 -80
  552. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustergroup.html +884 -80
  553. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustertype.html +884 -80
  554. nautobot/project-static/docs/user-guide/core-data-model/virtualization/virtualmachine.html +884 -80
  555. nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +889 -81
  556. nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +884 -80
  557. nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +884 -80
  558. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +884 -80
  559. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +884 -80
  560. nautobot/project-static/docs/user-guide/feature-guides/getting-started/index.html +884 -80
  561. nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +884 -80
  562. nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +884 -80
  563. nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +884 -80
  564. nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +884 -80
  565. nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +884 -80
  566. nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +884 -80
  567. nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +884 -80
  568. nautobot/project-static/docs/user-guide/feature-guides/graphql.html +884 -80
  569. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/clear-view-button.png +0 -0
  570. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/cleared-view.png +0 -0
  571. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/config-table-columns-to-locations.png +0 -0
  572. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/configure-button.png +0 -0
  573. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/create-saved-view-success.png +0 -0
  574. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/current-saved-view-drop-down-menu.png +0 -0
  575. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/default-location-list-view.png +0 -0
  576. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/dropdown-button-after-new-saved-view.png +0 -0
  577. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/filter-application-to-locations.png +0 -0
  578. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/filter-button.png +0 -0
  579. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/global-default-location-list-view.png +0 -0
  580. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/location-list-view-with-saved-views.png +0 -0
  581. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/navigation-menu.png +0 -0
  582. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/save-as-new-view-drop-down.png +0 -0
  583. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/save-view-modal.png +0 -0
  584. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/saved-view-admin-edit-buttons.png +0 -0
  585. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/saved-view-admin-edit-success.png +0 -0
  586. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/saved-view-admin-edit-view-unchecked.png +0 -0
  587. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/saved-view-admin-edit-view.png +0 -0
  588. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/saved-view-different-user.png +0 -0
  589. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/saved-view-modal-unchecked.png +0 -0
  590. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/set-as-my-default-button.png +0 -0
  591. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/set-as-my-default-success.png +0 -0
  592. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/unsaved-saved-view.png +0 -0
  593. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/updated-saved-view.png +0 -0
  594. nautobot/project-static/docs/user-guide/feature-guides/ip-address-merge-tool.html +884 -80
  595. nautobot/project-static/docs/user-guide/feature-guides/relationships.html +884 -80
  596. nautobot/project-static/docs/user-guide/feature-guides/software-image-files-and-versions.html +884 -80
  597. nautobot/project-static/docs/user-guide/index.html +884 -80
  598. nautobot/project-static/docs/user-guide/platform-functionality/change-logging.html +884 -80
  599. nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +884 -80
  600. nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +884 -80
  601. nautobot/project-static/docs/user-guide/platform-functionality/customlink.html +884 -80
  602. nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +1250 -777
  603. nautobot/project-static/docs/user-guide/platform-functionality/exporttemplate.html +887 -83
  604. nautobot/project-static/docs/user-guide/platform-functionality/externalintegration.html +884 -80
  605. nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +884 -80
  606. nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +884 -80
  607. nautobot/project-static/docs/user-guide/platform-functionality/graphqlquery.html +884 -80
  608. nautobot/project-static/docs/user-guide/platform-functionality/imageattachment.html +884 -80
  609. nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +884 -80
  610. nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +887 -83
  611. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +884 -80
  612. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +884 -80
  613. nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +888 -80
  614. nautobot/project-static/docs/user-guide/platform-functionality/metadata.html +8948 -0
  615. nautobot/project-static/docs/user-guide/platform-functionality/napalm.html +887 -83
  616. nautobot/project-static/docs/user-guide/platform-functionality/note.html +884 -80
  617. nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +884 -80
  618. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +884 -80
  619. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +884 -80
  620. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +884 -80
  621. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/ui-related-endpoints.html +884 -80
  622. nautobot/project-static/docs/user-guide/platform-functionality/role.html +887 -83
  623. nautobot/project-static/docs/user-guide/platform-functionality/savedview.html +9137 -0
  624. nautobot/project-static/docs/user-guide/platform-functionality/secret.html +887 -83
  625. nautobot/project-static/docs/user-guide/platform-functionality/staticgroupassociation.html +8933 -0
  626. nautobot/project-static/docs/user-guide/platform-functionality/status.html +884 -80
  627. nautobot/project-static/docs/user-guide/platform-functionality/tag.html +884 -80
  628. nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +942 -113
  629. nautobot/project-static/docs/user-guide/platform-functionality/users/objectpermission.html +884 -80
  630. nautobot/project-static/docs/user-guide/platform-functionality/users/token.html +884 -80
  631. nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +884 -80
  632. nautobot/project-static/js/forms.js +71 -0
  633. nautobot/project-static/js/table_sorting_indicator.js +46 -0
  634. nautobot/project-static/js/tableconfig.js +6 -1
  635. nautobot/project-static/materialdesignicons-7.4.47/css/materialdesignicons.min.css +3 -0
  636. nautobot/project-static/{materialdesignicons-6.5.95 → materialdesignicons-7.4.47}/fonts/materialdesignicons-webfont.eot +0 -0
  637. nautobot/project-static/{materialdesignicons-6.5.95 → materialdesignicons-7.4.47}/fonts/materialdesignicons-webfont.ttf +0 -0
  638. nautobot/project-static/materialdesignicons-7.4.47/fonts/materialdesignicons-webfont.woff +0 -0
  639. nautobot/project-static/materialdesignicons-7.4.47/fonts/materialdesignicons-webfont.woff2 +0 -0
  640. nautobot/tenancy/__init__.py +0 -1
  641. nautobot/tenancy/apps.py +1 -0
  642. nautobot/tenancy/factory.py +3 -2
  643. nautobot/tenancy/filters/__init__.py +1 -0
  644. nautobot/tenancy/forms.py +1 -1
  645. nautobot/tenancy/templates/tenancy/tenant.html +22 -18
  646. nautobot/tenancy/views.py +11 -10
  647. nautobot/users/__init__.py +0 -1
  648. nautobot/users/api/serializers.py +1 -1
  649. nautobot/users/api/views.py +4 -2
  650. nautobot/users/apps.py +3 -2
  651. nautobot/users/factory.py +3 -3
  652. nautobot/users/migrations/0010_user_default_saved_views.py +20 -0
  653. nautobot/users/models.py +12 -0
  654. nautobot/users/tests/test_filters.py +6 -3
  655. nautobot/users/urls.py +8 -0
  656. nautobot/virtualization/__init__.py +0 -1
  657. nautobot/virtualization/apps.py +1 -0
  658. nautobot/virtualization/filters.py +6 -1
  659. nautobot/virtualization/forms.py +11 -3
  660. nautobot/virtualization/graphql/types.py +2 -2
  661. nautobot/virtualization/migrations/0029_add_role_field_to_interface_models.py +27 -0
  662. nautobot/virtualization/migrations/0030_alter_virtualmachine_local_config_context_data_owner_content_type_and_more.py +67 -0
  663. nautobot/virtualization/tables.py +15 -5
  664. nautobot/virtualization/templates/virtualization/virtualmachine.html +1 -1
  665. nautobot/virtualization/templates/virtualization/vminterface.html +7 -1
  666. nautobot/virtualization/templates/virtualization/vminterface_edit.html +1 -0
  667. nautobot/virtualization/tests/test_api.py +9 -4
  668. nautobot/virtualization/tests/test_filters.py +22 -0
  669. nautobot/virtualization/tests/test_models.py +7 -3
  670. nautobot/virtualization/tests/test_views.py +19 -3
  671. nautobot/virtualization/urls.py +2 -2
  672. nautobot/virtualization/views.py +10 -32
  673. {nautobot-2.2.9.dist-info → nautobot-2.3.0b1.dist-info}/METADATA +21 -19
  674. {nautobot-2.2.9.dist-info → nautobot-2.3.0b1.dist-info}/RECORD +679 -559
  675. nautobot/project-static/materialdesignicons-6.5.95/.github/ISSUE_TEMPLATE.md +0 -3
  676. nautobot/project-static/materialdesignicons-6.5.95/README.md +0 -25
  677. nautobot/project-static/materialdesignicons-6.5.95/css/materialdesignicons.css +0 -26654
  678. nautobot/project-static/materialdesignicons-6.5.95/css/materialdesignicons.css.map +0 -16
  679. nautobot/project-static/materialdesignicons-6.5.95/css/materialdesignicons.min.css +0 -3
  680. nautobot/project-static/materialdesignicons-6.5.95/css/materialdesignicons.min.css.map +0 -16
  681. nautobot/project-static/materialdesignicons-6.5.95/fonts/materialdesignicons-webfont.woff +0 -0
  682. nautobot/project-static/materialdesignicons-6.5.95/fonts/materialdesignicons-webfont.woff2 +0 -0
  683. nautobot/project-static/materialdesignicons-6.5.95/package.json +0 -28
  684. nautobot/project-static/materialdesignicons-6.5.95/preview.html +0 -717
  685. nautobot/project-static/materialdesignicons-6.5.95/scss/_animated.scss +0 -27
  686. nautobot/project-static/materialdesignicons-6.5.95/scss/_core.scss +0 -10
  687. nautobot/project-static/materialdesignicons-6.5.95/scss/_extras.scss +0 -65
  688. nautobot/project-static/materialdesignicons-6.5.95/scss/_functions.scss +0 -20
  689. nautobot/project-static/materialdesignicons-6.5.95/scss/_icons.scss +0 -10
  690. nautobot/project-static/materialdesignicons-6.5.95/scss/_path.scss +0 -10
  691. nautobot/project-static/materialdesignicons-6.5.95/scss/_variables.scss +0 -6606
  692. nautobot/project-static/materialdesignicons-6.5.95/scss/materialdesignicons.scss +0 -8
  693. /nautobot/project-static/{materialdesignicons-6.5.95 → materialdesignicons-7.4.47}/LICENSE +0 -0
  694. {nautobot-2.2.9.dist-info → nautobot-2.3.0b1.dist-info}/LICENSE.txt +0 -0
  695. {nautobot-2.2.9.dist-info → nautobot-2.3.0b1.dist-info}/NOTICE +0 -0
  696. {nautobot-2.2.9.dist-info → nautobot-2.3.0b1.dist-info}/WHEEL +0 -0
  697. {nautobot-2.2.9.dist-info → nautobot-2.3.0b1.dist-info}/entry_points.txt +0 -0
@@ -8,9 +8,13 @@ from django.db.models import Q
8
8
  from django.test import override_settings
9
9
  from django.urls import reverse
10
10
  from netaddr import EUI
11
- import pytz
12
11
  import yaml
13
12
 
13
+ try:
14
+ import zoneinfo
15
+ except ImportError: # python 3.8
16
+ from backports import zoneinfo
17
+
14
18
  from nautobot.circuits.choices import CircuitTerminationSideChoices
15
19
  from nautobot.circuits.models import Circuit, CircuitTermination, CircuitType, Provider
16
20
  from nautobot.core.templatetags.buttons import job_export_url, job_import_url
@@ -81,6 +85,10 @@ from nautobot.dcim.models import (
81
85
  Location,
82
86
  LocationType,
83
87
  Manufacturer,
88
+ Module,
89
+ ModuleBay,
90
+ ModuleBayTemplate,
91
+ ModuleType,
84
92
  Platform,
85
93
  PowerFeed,
86
94
  PowerOutlet,
@@ -167,7 +175,6 @@ class LocationTypeTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
167
175
  lt3 = LocationType.objects.get(name="Building")
168
176
  lt4 = LocationType.objects.get(name="Floor")
169
177
  for lt in [lt1, lt2, lt3, lt4]:
170
- lt.validated_save()
171
178
  lt.content_types.add(ContentType.objects.get_for_model(RackGroup))
172
179
  # Deletable Location Types
173
180
  LocationType.objects.create(name="Delete Me 1")
@@ -178,7 +185,7 @@ class LocationTypeTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
178
185
  # so we need to make sure we're not trying to introduce a reference loop to the LocationType tree...
179
186
  cls.form_data = {
180
187
  "name": "Intermediate 2",
181
- # "parent": lt1.pk, # TODO: Either overload how EditObjectViewTestCase finds an editable object or write a specific test case for this.
188
+ "parent": lt1.pk,
182
189
  "description": "Another intermediate type",
183
190
  "content_types": [
184
191
  ContentType.objects.get_for_model(Rack).pk,
@@ -188,7 +195,7 @@ class LocationTypeTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
188
195
  }
189
196
 
190
197
  def _get_queryset(self):
191
- return super()._get_queryset().order_by("last_updated")
198
+ return super()._get_queryset().order_by("-last_updated")
192
199
 
193
200
 
194
201
  class LocationTestCase(ViewTestCases.PrimaryObjectViewTestCase):
@@ -201,8 +208,6 @@ class LocationTestCase(ViewTestCases.PrimaryObjectViewTestCase):
201
208
  lt1 = LocationType.objects.get(name="Campus")
202
209
  lt2 = LocationType.objects.get(name="Building")
203
210
  lt3 = LocationType.objects.get(name="Floor")
204
- for lt in [lt1, lt2, lt3]:
205
- lt.validated_save()
206
211
 
207
212
  status = Status.objects.get_for_model(Location).first()
208
213
  tenant = Tenant.objects.first()
@@ -228,7 +233,7 @@ class LocationTestCase(ViewTestCases.PrimaryObjectViewTestCase):
228
233
  "tenant": tenant.pk,
229
234
  "facility": "Facility X",
230
235
  "asn": 65001,
231
- "time_zone": pytz.UTC,
236
+ "time_zone": zoneinfo.ZoneInfo("UTC"),
232
237
  "physical_address": "742 Evergreen Terrace, Springfield, USA",
233
238
  "shipping_address": "742 Evergreen Terrace, Springfield, USA",
234
239
  "latitude": Decimal("35.780000"),
@@ -248,9 +253,12 @@ class LocationTestCase(ViewTestCases.PrimaryObjectViewTestCase):
248
253
  "tenant": tenant.pk,
249
254
  "status": Status.objects.get_for_model(Location).last().pk,
250
255
  "asn": 65009,
251
- "time_zone": pytz.timezone("US/Eastern"),
256
+ "time_zone": zoneinfo.ZoneInfo("US/Eastern"),
252
257
  }
253
258
 
259
+ def _get_queryset(self):
260
+ return super()._get_queryset().filter(location_type__name="Campus")
261
+
254
262
  @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
255
263
  def test_create_child_location_under_a_non_globally_unique_named_parent_location(
256
264
  self,
@@ -751,19 +759,23 @@ class ManufacturerTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
751
759
 
752
760
  @classmethod
753
761
  def setUpTestData(cls):
754
- # FIXME(jathan): This has to be replaced with# `get_deletable_object` and
755
- # `get_deletable_object_pks` but this is a workaround just so all of these objects are
756
- # deletable for now.
757
- Controller.objects.filter(controller_device__isnull=False).delete()
758
- Device.objects.all().delete()
759
- DeviceType.objects.all().delete()
760
- Platform.objects.all().delete()
761
-
762
762
  cls.form_data = {
763
763
  "name": "Manufacturer X",
764
764
  "description": "A new manufacturer",
765
765
  }
766
766
 
767
+ def get_deletable_object(self):
768
+ mf = Manufacturer.objects.create(name="Deletable Manufacturer")
769
+ return mf
770
+
771
+ def get_deletable_object_pks(self):
772
+ mfs = [
773
+ Manufacturer.objects.create(name="Deletable Manufacturer 1"),
774
+ Manufacturer.objects.create(name="Deletable Manufacturer 2"),
775
+ Manufacturer.objects.create(name="Deletable Manufacturer 3"),
776
+ ]
777
+ return [mf.pk for mf in mfs]
778
+
767
779
 
768
780
  # TODO: Change base class to PrimaryObjectViewTestCase
769
781
  # Blocked by absence of bulk import view for DeviceTypes
@@ -803,8 +815,7 @@ class DeviceTypeTestCase(
803
815
  }
804
816
 
805
817
  cls.bulk_edit_data = {
806
- "manufacturer": manufacturers[1].pk,
807
- "u_height": 3,
818
+ "u_height": 0,
808
819
  "is_full_depth": False,
809
820
  }
810
821
 
@@ -817,29 +828,32 @@ class DeviceTypeTestCase(
817
828
 
818
829
  yaml_import_url = reverse("dcim:devicetype_import")
819
830
  csv_import_url = job_import_url(ContentType.objects.get_for_model(DeviceType))
820
- # Main import button links to YAML/JSON import
831
+ # Dropdown provides both YAML/JSON and CSV import as options
821
832
  self.assertInHTML(
822
- f'<a id="import-button" type="button" class="btn btn-info" href="{yaml_import_url}">'
823
- '<span class="mdi mdi-database-import" aria-hidden="true"></span> Import</a>',
833
+ f'<a href="{yaml_import_url}"><span class="mdi mdi-database-import text-muted" aria-hidden="true"></span> Import from JSON/YAML (single record)</a>',
834
+ content,
835
+ )
836
+ self.assertInHTML(
837
+ f'<a href="{csv_import_url}"><span class="mdi mdi-database-import text-muted" aria-hidden="true"></span> Import from CSV (multiple records)</a>',
824
838
  content,
825
839
  )
826
- # Dropdown provides both YAML/JSON and CSV import as options
827
- self.assertInHTML(f'<a href="{yaml_import_url}">JSON/YAML format (single record)</a>', content)
828
- self.assertInHTML(f'<a href="{csv_import_url}">CSV format (multiple records)</a>', content)
829
840
 
830
841
  export_url = job_export_url()
831
842
  # Export is a little trickier to check since it's done as a form submission rather than an <a> element.
832
843
  self.assertIn(f'<form action="{export_url}" method="post">', content)
833
844
  self.assertInHTML(
834
- f'<input type="hidden" name="content_type" value="{ContentType.objects.get_for_model(DeviceType).pk}">',
845
+ f'<input type="hidden" name="content_type" value="{ContentType.objects.get_for_model(self.model).pk}">',
835
846
  content,
836
847
  )
837
848
  self.assertInHTML('<input type="hidden" name="export_format" value="yaml">', content)
838
849
  self.assertInHTML(
839
- '<button type="submit" class="btn btn-link" form="export_default">YAML format</button>',
850
+ '<button type="submit"><span class="mdi mdi-database-export text-muted" aria-hidden="true"></span> Export as YAML</button>',
851
+ content,
852
+ )
853
+ self.assertInHTML(
854
+ '<button type="submit"><span class="mdi mdi-database-export text-muted" aria-hidden="true"></span> Export as CSV</button>',
840
855
  content,
841
856
  )
842
- self.assertInHTML('<button type="submit" class="btn btn-link">CSV format</button>', content)
843
857
 
844
858
  @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
845
859
  def test_import_objects(self):
@@ -920,6 +934,13 @@ device-bays:
920
934
  - name: Device Bay 1
921
935
  - name: Device Bay 2
922
936
  - name: Device Bay 3
937
+ module-bays:
938
+ - name: Module Bay 1
939
+ position: 1
940
+ - name: Module Bay 2
941
+ position: 2
942
+ - name: Module Bay 3
943
+ position: 3
923
944
  """
924
945
 
925
946
  # Add all required permissions to the test user
@@ -934,6 +955,7 @@ device-bays:
934
955
  "dcim.add_frontporttemplate",
935
956
  "dcim.add_rearporttemplate",
936
957
  "dcim.add_devicebaytemplate",
958
+ "dcim.add_modulebaytemplate",
937
959
  )
938
960
 
939
961
  form_data = {"data": IMPORT_DATA, "format": "yaml"}
@@ -944,47 +966,51 @@ device-bays:
944
966
 
945
967
  # Verify all of the components were created
946
968
  self.assertEqual(dt.console_port_templates.count(), 3)
947
- cp1 = ConsolePortTemplate.objects.first()
969
+ cp1 = dt.console_port_templates.first()
948
970
  self.assertEqual(cp1.name, "Console Port 1")
949
971
  self.assertEqual(cp1.type, ConsolePortTypeChoices.TYPE_DE9)
950
972
 
951
973
  self.assertEqual(dt.console_server_port_templates.count(), 3)
952
- csp1 = ConsoleServerPortTemplate.objects.first()
974
+ csp1 = dt.console_server_port_templates.first()
953
975
  self.assertEqual(csp1.name, "Console Server Port 1")
954
976
  self.assertEqual(csp1.type, ConsolePortTypeChoices.TYPE_RJ45)
955
977
 
956
978
  self.assertEqual(dt.power_port_templates.count(), 3)
957
- pp1 = PowerPortTemplate.objects.first()
979
+ pp1 = dt.power_port_templates.first()
958
980
  self.assertEqual(pp1.name, "Power Port 1")
959
981
  self.assertEqual(pp1.type, PowerPortTypeChoices.TYPE_IEC_C14)
960
982
 
961
983
  self.assertEqual(dt.power_outlet_templates.count(), 3)
962
- po1 = PowerOutletTemplate.objects.first()
984
+ po1 = dt.power_outlet_templates.first()
963
985
  self.assertEqual(po1.name, "Power Outlet 1")
964
986
  self.assertEqual(po1.type, PowerOutletTypeChoices.TYPE_IEC_C13)
965
987
  self.assertEqual(po1.power_port_template, pp1)
966
988
  self.assertEqual(po1.feed_leg, PowerOutletFeedLegChoices.FEED_LEG_A)
967
989
 
968
990
  self.assertEqual(dt.interface_templates.count(), 3)
969
- iface1 = InterfaceTemplate.objects.first()
991
+ iface1 = dt.interface_templates.first()
970
992
  self.assertEqual(iface1.name, "Interface 1")
971
993
  self.assertEqual(iface1.type, InterfaceTypeChoices.TYPE_1GE_FIXED)
972
994
  self.assertTrue(iface1.mgmt_only)
973
995
 
974
996
  self.assertEqual(dt.rear_port_templates.count(), 3)
975
- rp1 = RearPortTemplate.objects.first()
997
+ rp1 = dt.rear_port_templates.first()
976
998
  self.assertEqual(rp1.name, "Rear Port 1")
977
999
 
978
1000
  self.assertEqual(dt.front_port_templates.count(), 3)
979
- fp1 = FrontPortTemplate.objects.first()
1001
+ fp1 = dt.front_port_templates.first()
980
1002
  self.assertEqual(fp1.name, "Front Port 1")
981
1003
  self.assertEqual(fp1.rear_port_template, rp1)
982
1004
  self.assertEqual(fp1.rear_port_position, 1)
983
1005
 
984
1006
  self.assertEqual(dt.device_bay_templates.count(), 3)
985
- db1 = DeviceBayTemplate.objects.first()
1007
+ db1 = dt.device_bay_templates.first()
986
1008
  self.assertEqual(db1.name, "Device Bay 1")
987
1009
 
1010
+ self.assertEqual(dt.module_bay_templates.count(), 3)
1011
+ mb1 = dt.module_bay_templates.first()
1012
+ self.assertEqual(mb1.name, "Module Bay 1")
1013
+
988
1014
  def test_import_objects_unknown_type_enums(self):
989
1015
  """
990
1016
  YAML import of data with `type` values that we don't recognize should remap those to "other" rather than fail.
@@ -1023,6 +1049,13 @@ device-bays:
1023
1049
  - name: Device Bay of Uncertain Type
1024
1050
  type: unknown # should be ignored
1025
1051
  - name: Device Bay of Unspecified Type
1052
+ module-bays:
1053
+ - name: Module Bay 1
1054
+ position: 1
1055
+ - name: Module Bay 2
1056
+ position: 2
1057
+ - name: Module Bay 3
1058
+ position: 3
1026
1059
  """
1027
1060
  # Add all required permissions to the test user
1028
1061
  self.add_permissions(
@@ -1037,6 +1070,7 @@ device-bays:
1037
1070
  "dcim.add_frontporttemplate",
1038
1071
  "dcim.add_rearporttemplate",
1039
1072
  "dcim.add_devicebaytemplate",
1073
+ "dcim.add_modulebaytemplate",
1040
1074
  )
1041
1075
 
1042
1076
  form_data = {"data": IMPORT_DATA, "format": "yaml"}
@@ -1085,6 +1119,12 @@ device-bays:
1085
1119
  self.assertEqual(dt.device_bay_templates.count(), 2)
1086
1120
  # DeviceBayTemplate doesn't have a type field.
1087
1121
 
1122
+ self.assertEqual(dt.module_bay_templates.count(), 3)
1123
+ # ModuleBayTemplate doesn't have a type field.
1124
+ mbt = ModuleBayTemplate.objects.filter(device_type=dt).first()
1125
+ self.assertEqual(mbt.position, "1")
1126
+ self.assertEqual(mbt.name, "Module Bay 1")
1127
+
1088
1128
  def test_devicetype_export(self):
1089
1129
  url = reverse("dcim:devicetype_list")
1090
1130
  self.add_permissions("dcim.view_devicetype")
@@ -1135,128 +1175,501 @@ device-bays:
1135
1175
  self.assertIn("failed validation", response.content.decode(response.charset))
1136
1176
 
1137
1177
 
1138
- #
1139
- # DeviceType components
1140
- #
1141
-
1142
-
1143
- class ConsolePortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
1144
- model = ConsolePortTemplate
1178
+ class ModuleTypeTestCase(
1179
+ ViewTestCases.GetObjectViewTestCase,
1180
+ ViewTestCases.GetObjectChangelogViewTestCase,
1181
+ ViewTestCases.CreateObjectViewTestCase,
1182
+ ViewTestCases.EditObjectViewTestCase,
1183
+ ViewTestCases.DeleteObjectViewTestCase,
1184
+ ViewTestCases.ListObjectsViewTestCase,
1185
+ ViewTestCases.BulkEditObjectsViewTestCase,
1186
+ ViewTestCases.BulkDeleteObjectsViewTestCase,
1187
+ ):
1188
+ model = ModuleType
1145
1189
 
1146
1190
  @classmethod
1147
1191
  def setUpTestData(cls):
1148
- manufacturer = Manufacturer.objects.first()
1149
- devicetypes = (
1150
- DeviceType.objects.create(manufacturer=manufacturer, model="Device Type 1"),
1151
- DeviceType.objects.create(manufacturer=manufacturer, model="Device Type 2"),
1152
- )
1192
+ manufacturers = Manufacturer.objects.all()[:2]
1193
+ Module.objects.all().delete()
1194
+ ModuleType.objects.all().delete()
1153
1195
 
1154
- ConsolePortTemplate.objects.create(device_type=devicetypes[0], name="Console Port Template 1")
1155
- ConsolePortTemplate.objects.create(device_type=devicetypes[0], name="Console Port Template 2")
1156
- ConsolePortTemplate.objects.create(device_type=devicetypes[0], name="Console Port Template 3")
1196
+ ModuleType.objects.create(
1197
+ model="Test Module Type 1",
1198
+ manufacturer=manufacturers[0],
1199
+ )
1200
+ ModuleType.objects.create(
1201
+ model="Test Module Type 2",
1202
+ manufacturer=manufacturers[0],
1203
+ )
1204
+ ModuleType.objects.create(
1205
+ model="Test Module Type 3",
1206
+ manufacturer=manufacturers[0],
1207
+ )
1208
+ ModuleType.objects.create(
1209
+ model="Test Module Type 4",
1210
+ manufacturer=manufacturers[1],
1211
+ )
1157
1212
 
1158
1213
  cls.form_data = {
1159
- "device_type": devicetypes[1].pk,
1160
- "name": "Console Port Template X",
1161
- "type": ConsolePortTypeChoices.TYPE_RJ45,
1162
- }
1163
-
1164
- cls.bulk_create_data = {
1165
- "device_type": devicetypes[1].pk,
1166
- "name_pattern": "Console Port Template [4-6]",
1167
- "type": ConsolePortTypeChoices.TYPE_RJ45,
1214
+ "manufacturer": manufacturers[0].pk,
1215
+ "model": "Test Module Type X",
1216
+ "part_number": "123ABC",
1217
+ "tags": [t.pk for t in Tag.objects.get_for_model(ModuleType)],
1168
1218
  }
1169
1219
 
1170
1220
  cls.bulk_edit_data = {
1171
- "type": ConsolePortTypeChoices.TYPE_RJ45,
1221
+ "manufacturer": manufacturers[1].pk,
1172
1222
  }
1173
1223
 
1224
+ def test_list_has_correct_links(self):
1225
+ """Assert that the ModuleType list view has import/export buttons for both CSV and YAML/JSON formats."""
1226
+ self.add_permissions("dcim.add_moduletype", "dcim.view_moduletype")
1227
+ response = self.client.get(reverse("dcim:moduletype_list"))
1228
+ self.assertHttpStatus(response, 200)
1229
+ content = extract_page_body(response.content.decode(response.charset))
1174
1230
 
1175
- class ConsoleServerPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
1176
- model = ConsoleServerPortTemplate
1177
-
1178
- @classmethod
1179
- def setUpTestData(cls):
1180
- manufacturer = Manufacturer.objects.first()
1181
- devicetypes = (
1182
- DeviceType.objects.create(manufacturer=manufacturer, model="Device Type 1"),
1183
- DeviceType.objects.create(manufacturer=manufacturer, model="Device Type 2"),
1231
+ yaml_import_url = reverse("dcim:moduletype_import")
1232
+ csv_import_url = job_import_url(ContentType.objects.get_for_model(ModuleType))
1233
+ # Dropdown provides both YAML/JSON and CSV import as options
1234
+ self.assertInHTML(
1235
+ f'<a href="{yaml_import_url}"><span class="mdi mdi-database-import text-muted" aria-hidden="true"></span> Import from JSON/YAML (single record)</a>',
1236
+ content,
1237
+ )
1238
+ self.assertInHTML(
1239
+ f'<a href="{csv_import_url}"><span class="mdi mdi-database-import text-muted" aria-hidden="true"></span> Import from CSV (multiple records)</a>',
1240
+ content,
1184
1241
  )
1185
1242
 
1186
- ConsoleServerPortTemplate.objects.create(device_type=devicetypes[0], name="Console Server Port Template 1")
1187
- ConsoleServerPortTemplate.objects.create(device_type=devicetypes[0], name="Console Server Port Template 2")
1188
- ConsoleServerPortTemplate.objects.create(device_type=devicetypes[0], name="Console Server Port Template 3")
1189
-
1190
- cls.form_data = {
1191
- "device_type": devicetypes[1].pk,
1192
- "name": "Console Server Port Template X",
1193
- "type": ConsolePortTypeChoices.TYPE_RJ45,
1194
- }
1195
-
1196
- cls.bulk_create_data = {
1197
- "device_type": devicetypes[1].pk,
1198
- "name_pattern": "Console Server Port Template [4-6]",
1199
- "type": ConsolePortTypeChoices.TYPE_RJ45,
1200
- }
1201
-
1202
- cls.bulk_edit_data = {
1203
- "type": ConsolePortTypeChoices.TYPE_RJ45,
1204
- }
1205
-
1206
-
1207
- class PowerPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
1208
- model = PowerPortTemplate
1243
+ export_url = job_export_url()
1244
+ # Export is a little trickier to check since it's done as a form submission rather than an <a> element.
1245
+ self.assertIn(f'<form action="{export_url}" method="post">', content)
1246
+ self.assertInHTML(
1247
+ f'<input type="hidden" name="content_type" value="{ContentType.objects.get_for_model(self.model).pk}">',
1248
+ content,
1249
+ )
1250
+ self.assertInHTML('<input type="hidden" name="export_format" value="yaml">', content)
1251
+ self.assertInHTML(
1252
+ '<button type="submit"><span class="mdi mdi-database-export text-muted" aria-hidden="true"></span> Export as YAML</button>',
1253
+ content,
1254
+ )
1255
+ self.assertInHTML(
1256
+ '<button type="submit"><span class="mdi mdi-database-export text-muted" aria-hidden="true"></span> Export as CSV</button>',
1257
+ content,
1258
+ )
1209
1259
 
1210
- @classmethod
1211
- def setUpTestData(cls):
1260
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
1261
+ def test_import_objects(self):
1262
+ """
1263
+ Custom import test for YAML-based imports (versus CSV)
1264
+ """
1265
+ # Note use of "power-outlets.power_port" (not "power_port_template") and "front-ports.rear_port"
1266
+ # (not "rear_port_template"). Note also inclusion of "slug" even though we removed DeviceType.slug in 2.0.
1267
+ # This is intentional as we are testing backwards compatibility with the netbox/devicetype-library repository.
1212
1268
  manufacturer = Manufacturer.objects.first()
1213
- devicetypes = (
1214
- DeviceType.objects.create(manufacturer=manufacturer, model="Device Type 1"),
1215
- DeviceType.objects.create(manufacturer=manufacturer, model="Device Type 2"),
1269
+ IMPORT_DATA = f"""
1270
+ manufacturer: {manufacturer.name}
1271
+ model: TEST-1000
1272
+ slug: test-1000
1273
+ console-ports:
1274
+ - name: Console Port 1
1275
+ type: de-9
1276
+ - name: Console Port 2
1277
+ type: de-9
1278
+ - name: Console Port 3
1279
+ type: de-9
1280
+ console-server-ports:
1281
+ - name: Console Server Port 1
1282
+ type: rj-45
1283
+ - name: Console Server Port 2
1284
+ type: rj-45
1285
+ - name: Console Server Port 3
1286
+ type: rj-45
1287
+ power-ports:
1288
+ - name: Power Port 1
1289
+ type: iec-60320-c14
1290
+ - name: Power Port 2
1291
+ type: iec-60320-c14
1292
+ - name: Power Port 3
1293
+ type: iec-60320-c14
1294
+ power-outlets:
1295
+ - name: Power Outlet 1
1296
+ type: iec-60320-c13
1297
+ power_port: Power Port 1
1298
+ feed_leg: A
1299
+ - name: Power Outlet 2
1300
+ type: iec-60320-c13
1301
+ power_port: Power Port 1
1302
+ feed_leg: A
1303
+ - name: Power Outlet 3
1304
+ type: iec-60320-c13
1305
+ power_port: Power Port 1
1306
+ feed_leg: A
1307
+ interfaces:
1308
+ - name: Interface 1
1309
+ type: 1000base-t
1310
+ mgmt_only: true
1311
+ - name: Interface 2
1312
+ type: 1000base-t
1313
+ - name: Interface 3
1314
+ type: 1000base-t
1315
+ rear-ports:
1316
+ - name: Rear Port 1
1317
+ type: 8p8c
1318
+ - name: Rear Port 2
1319
+ type: 8p8c
1320
+ - name: Rear Port 3
1321
+ type: 8p8c
1322
+ front-ports:
1323
+ - name: Front Port 1
1324
+ type: 8p8c
1325
+ rear_port: Rear Port 1
1326
+ - name: Front Port 2
1327
+ type: 8p8c
1328
+ rear_port: Rear Port 2
1329
+ - name: Front Port 3
1330
+ type: 8p8c
1331
+ rear_port: Rear Port 3
1332
+ module-bays:
1333
+ - name: Module Bay 1
1334
+ position: 1
1335
+ - name: Module Bay 2
1336
+ position: 2
1337
+ - name: Module Bay 3
1338
+ position: 3
1339
+ """
1340
+
1341
+ # Add all required permissions to the test user
1342
+ self.add_permissions(
1343
+ "dcim.view_moduletype",
1344
+ "dcim.add_moduletype",
1345
+ "dcim.add_consoleporttemplate",
1346
+ "dcim.add_consoleserverporttemplate",
1347
+ "dcim.add_powerporttemplate",
1348
+ "dcim.add_poweroutlettemplate",
1349
+ "dcim.add_interfacetemplate",
1350
+ "dcim.add_frontporttemplate",
1351
+ "dcim.add_rearporttemplate",
1352
+ "dcim.add_modulebaytemplate",
1216
1353
  )
1217
1354
 
1218
- PowerPortTemplate.objects.create(device_type=devicetypes[0], name="Power Port Template 1")
1219
- PowerPortTemplate.objects.create(device_type=devicetypes[0], name="Power Port Template 2")
1220
- PowerPortTemplate.objects.create(device_type=devicetypes[0], name="Power Port Template 3")
1355
+ form_data = {"data": IMPORT_DATA, "format": "yaml"}
1356
+ response = self.client.post(reverse("dcim:moduletype_import"), data=form_data, follow=True)
1357
+ self.assertHttpStatus(response, 200)
1358
+ mt = ModuleType.objects.get(model="TEST-1000")
1221
1359
 
1222
- cls.form_data = {
1223
- "device_type": devicetypes[1].pk,
1224
- "name": "Power Port Template X",
1225
- "type": PowerPortTypeChoices.TYPE_IEC_C14,
1226
- "maximum_draw": 100,
1227
- "allocated_draw": 50,
1228
- }
1360
+ # Verify all of the components were created
1361
+ self.assertEqual(mt.console_port_templates.count(), 3)
1362
+ cp1 = mt.console_port_templates.first()
1363
+ self.assertEqual(cp1.name, "Console Port 1")
1364
+ self.assertEqual(cp1.type, ConsolePortTypeChoices.TYPE_DE9)
1229
1365
 
1230
- cls.bulk_create_data = {
1231
- "device_type": devicetypes[1].pk,
1232
- "name_pattern": "Power Port Template [4-6]",
1233
- "type": PowerPortTypeChoices.TYPE_IEC_C14,
1234
- "maximum_draw": 100,
1235
- "allocated_draw": 50,
1236
- }
1366
+ self.assertEqual(mt.console_server_port_templates.count(), 3)
1367
+ csp1 = mt.console_server_port_templates.first()
1368
+ self.assertEqual(csp1.name, "Console Server Port 1")
1369
+ self.assertEqual(csp1.type, ConsolePortTypeChoices.TYPE_RJ45)
1237
1370
 
1238
- cls.bulk_edit_data = {
1239
- "type": PowerPortTypeChoices.TYPE_IEC_C14,
1240
- "maximum_draw": 100,
1241
- "allocated_draw": 50,
1242
- }
1371
+ self.assertEqual(mt.power_port_templates.count(), 3)
1372
+ pp1 = mt.power_port_templates.first()
1373
+ self.assertEqual(pp1.name, "Power Port 1")
1374
+ self.assertEqual(pp1.type, PowerPortTypeChoices.TYPE_IEC_C14)
1243
1375
 
1376
+ self.assertEqual(mt.power_outlet_templates.count(), 3)
1377
+ po1 = mt.power_outlet_templates.first()
1378
+ self.assertEqual(po1.name, "Power Outlet 1")
1379
+ self.assertEqual(po1.type, PowerOutletTypeChoices.TYPE_IEC_C13)
1380
+ self.assertEqual(po1.power_port_template, pp1)
1381
+ self.assertEqual(po1.feed_leg, PowerOutletFeedLegChoices.FEED_LEG_A)
1244
1382
 
1245
- class PowerOutletTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
1246
- model = PowerOutletTemplate
1383
+ self.assertEqual(mt.interface_templates.count(), 3)
1384
+ iface1 = mt.interface_templates.first()
1385
+ self.assertEqual(iface1.name, "Interface 1")
1386
+ self.assertEqual(iface1.type, InterfaceTypeChoices.TYPE_1GE_FIXED)
1387
+ self.assertTrue(iface1.mgmt_only)
1247
1388
 
1248
- @classmethod
1249
- def setUpTestData(cls):
1250
- manufacturer = Manufacturer.objects.first()
1251
- devicetype = DeviceType.objects.create(manufacturer=manufacturer, model="Device Type 1")
1389
+ self.assertEqual(mt.rear_port_templates.count(), 3)
1390
+ rp1 = mt.rear_port_templates.first()
1391
+ self.assertEqual(rp1.name, "Rear Port 1")
1252
1392
 
1253
- PowerOutletTemplate.objects.create(device_type=devicetype, name="Power Outlet Template 1")
1254
- PowerOutletTemplate.objects.create(device_type=devicetype, name="Power Outlet Template 2")
1255
- PowerOutletTemplate.objects.create(device_type=devicetype, name="Power Outlet Template 3")
1393
+ self.assertEqual(mt.front_port_templates.count(), 3)
1394
+ fp1 = mt.front_port_templates.first()
1395
+ self.assertEqual(fp1.name, "Front Port 1")
1396
+ self.assertEqual(fp1.rear_port_template, rp1)
1397
+ self.assertEqual(fp1.rear_port_position, 1)
1256
1398
 
1257
- powerports = (PowerPortTemplate.objects.create(device_type=devicetype, name="Power Port Template 1"),)
1399
+ self.assertEqual(mt.module_bay_templates.count(), 3)
1400
+ mb1 = mt.module_bay_templates.first()
1401
+ self.assertEqual(mb1.name, "Module Bay 1")
1402
+ self.assertEqual(mb1.position, "1")
1258
1403
 
1259
- cls.form_data = {
1404
+ def test_import_objects_unknown_type_enums(self):
1405
+ """
1406
+ YAML import of data with `type` values that we don't recognize should remap those to "other" rather than fail.
1407
+ """
1408
+ manufacturer = Manufacturer.objects.first()
1409
+ IMPORT_DATA = f"""
1410
+ manufacturer: {manufacturer.name}
1411
+ model: TEST-2000
1412
+ console-ports:
1413
+ - name: Console Port Alpha-Beta
1414
+ type: alpha-beta
1415
+ console-server-ports:
1416
+ - name: Console Server Port Pineapple
1417
+ type: pineapple
1418
+ power-ports:
1419
+ - name: Power Port Fred
1420
+ type: frederick
1421
+ power-outlets:
1422
+ - name: Power Outlet Rick
1423
+ type: frederick
1424
+ power_port_template: Power Port Fred
1425
+ interfaces:
1426
+ - name: Interface North
1427
+ type: northern
1428
+ rear-ports:
1429
+ - name: Rear Port Foosball
1430
+ type: foosball
1431
+ front-ports:
1432
+ - name: Front Port Pickleball
1433
+ type: pickleball
1434
+ rear_port_template: Rear Port Foosball
1435
+ module-bays:
1436
+ - name: Module Bay 1
1437
+ position: 1
1438
+ - name: Module Bay 2
1439
+ position: 2
1440
+ - name: Module Bay 3
1441
+ position: 3
1442
+ """
1443
+ # Add all required permissions to the test user
1444
+ self.add_permissions(
1445
+ "dcim.view_moduletype",
1446
+ "dcim.view_manufacturer",
1447
+ "dcim.add_moduletype",
1448
+ "dcim.add_consoleporttemplate",
1449
+ "dcim.add_consoleserverporttemplate",
1450
+ "dcim.add_powerporttemplate",
1451
+ "dcim.add_poweroutlettemplate",
1452
+ "dcim.add_interfacetemplate",
1453
+ "dcim.add_frontporttemplate",
1454
+ "dcim.add_rearporttemplate",
1455
+ "dcim.add_modulebaytemplate",
1456
+ )
1457
+
1458
+ form_data = {"data": IMPORT_DATA, "format": "yaml"}
1459
+ response = self.client.post(reverse("dcim:moduletype_import"), data=form_data, follow=True)
1460
+ self.assertHttpStatus(response, 200)
1461
+ mt = ModuleType.objects.get(model="TEST-2000")
1462
+
1463
+ # Verify all of the components were created with appropriate "other" types
1464
+ self.assertEqual(mt.console_port_templates.count(), 1)
1465
+ cpt = ConsolePortTemplate.objects.filter(module_type=mt).first()
1466
+ self.assertEqual(cpt.name, "Console Port Alpha-Beta")
1467
+ self.assertEqual(cpt.type, ConsolePortTypeChoices.TYPE_OTHER)
1468
+
1469
+ self.assertEqual(mt.console_server_port_templates.count(), 1)
1470
+ cspt = ConsoleServerPortTemplate.objects.filter(module_type=mt).first()
1471
+ self.assertEqual(cspt.name, "Console Server Port Pineapple")
1472
+ self.assertEqual(cspt.type, ConsolePortTypeChoices.TYPE_OTHER)
1473
+
1474
+ self.assertEqual(mt.power_port_templates.count(), 1)
1475
+ ppt = PowerPortTemplate.objects.filter(module_type=mt).first()
1476
+ self.assertEqual(ppt.name, "Power Port Fred")
1477
+ self.assertEqual(ppt.type, PowerPortTypeChoices.TYPE_OTHER)
1478
+
1479
+ self.assertEqual(mt.power_outlet_templates.count(), 1)
1480
+ pot = PowerOutletTemplate.objects.filter(module_type=mt).first()
1481
+ self.assertEqual(pot.name, "Power Outlet Rick")
1482
+ self.assertEqual(pot.type, PowerOutletTypeChoices.TYPE_OTHER)
1483
+ self.assertEqual(pot.power_port_template, ppt)
1484
+
1485
+ self.assertEqual(mt.interface_templates.count(), 1)
1486
+ it = InterfaceTemplate.objects.filter(module_type=mt).first()
1487
+ self.assertEqual(it.name, "Interface North")
1488
+ self.assertEqual(it.type, InterfaceTypeChoices.TYPE_OTHER)
1489
+
1490
+ self.assertEqual(mt.rear_port_templates.count(), 1)
1491
+ rpt = RearPortTemplate.objects.filter(module_type=mt).first()
1492
+ self.assertEqual(rpt.name, "Rear Port Foosball")
1493
+ self.assertEqual(rpt.type, PortTypeChoices.TYPE_OTHER)
1494
+
1495
+ self.assertEqual(mt.front_port_templates.count(), 1)
1496
+ fpt = FrontPortTemplate.objects.filter(module_type=mt).first()
1497
+ self.assertEqual(fpt.name, "Front Port Pickleball")
1498
+ self.assertEqual(fpt.type, PortTypeChoices.TYPE_OTHER)
1499
+
1500
+ self.assertEqual(mt.module_bay_templates.count(), 3)
1501
+ # ModuleBayTemplate doesn't have a type field.
1502
+ mbt = ModuleBayTemplate.objects.filter(module_type=mt).first()
1503
+ self.assertEqual(mbt.position, "1")
1504
+ self.assertEqual(mbt.name, "Module Bay 1")
1505
+
1506
+ def test_moduletype_export(self):
1507
+ url = reverse("dcim:moduletype_list")
1508
+ self.add_permissions("dcim.view_moduletype")
1509
+
1510
+ response = self.client.get(f"{url}?export")
1511
+ self.assertEqual(response.status_code, 200)
1512
+ data = list(yaml.load_all(response.content, Loader=yaml.SafeLoader))
1513
+ module_types = ModuleType.objects.all()
1514
+ module_type = module_types.first()
1515
+
1516
+ self.assertEqual(len(data), module_types.count())
1517
+ self.assertEqual(data[0]["manufacturer"], module_type.manufacturer.name)
1518
+ self.assertEqual(data[0]["model"], module_type.model)
1519
+
1520
+
1521
+ #
1522
+ # DeviceType components
1523
+ #
1524
+
1525
+
1526
+ class ConsolePortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
1527
+ model = ConsolePortTemplate
1528
+
1529
+ @classmethod
1530
+ def setUpTestData(cls):
1531
+ manufacturer = Manufacturer.objects.first()
1532
+ devicetypes = (
1533
+ DeviceType.objects.create(manufacturer=manufacturer, model="Device Type 1"),
1534
+ DeviceType.objects.create(manufacturer=manufacturer, model="Device Type 2"),
1535
+ )
1536
+
1537
+ ConsolePortTemplate.objects.create(device_type=devicetypes[0], name="Console Port Template 1")
1538
+ ConsolePortTemplate.objects.create(device_type=devicetypes[0], name="Console Port Template 2")
1539
+ ConsolePortTemplate.objects.create(device_type=devicetypes[0], name="Console Port Template 3")
1540
+
1541
+ cls.form_data = {
1542
+ "device_type": devicetypes[1].pk,
1543
+ "name": "Console Port Template X",
1544
+ "type": ConsolePortTypeChoices.TYPE_RJ45,
1545
+ }
1546
+
1547
+ cls.bulk_create_data = {
1548
+ "device_type": devicetypes[1].pk,
1549
+ "name_pattern": "Console Port Template [4-6]",
1550
+ "description": "View Test Bulk Create Console Ports",
1551
+ "type": ConsolePortTypeChoices.TYPE_RJ45,
1552
+ }
1553
+
1554
+ cls.bulk_edit_data = {
1555
+ "type": ConsolePortTypeChoices.TYPE_RJ45,
1556
+ }
1557
+
1558
+ test_instance = cls.model.objects.first()
1559
+ cls.update_data = {
1560
+ "name": test_instance.name,
1561
+ "device_type": getattr(getattr(test_instance, "device_type", None), "pk", None),
1562
+ "module_type": getattr(getattr(test_instance, "module_type", None), "pk", None),
1563
+ "label": "new test label",
1564
+ "description": "new test description",
1565
+ }
1566
+
1567
+
1568
+ class ConsoleServerPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
1569
+ model = ConsoleServerPortTemplate
1570
+
1571
+ @classmethod
1572
+ def setUpTestData(cls):
1573
+ manufacturer = Manufacturer.objects.first()
1574
+ devicetypes = (
1575
+ DeviceType.objects.create(manufacturer=manufacturer, model="Device Type 1"),
1576
+ DeviceType.objects.create(manufacturer=manufacturer, model="Device Type 2"),
1577
+ )
1578
+
1579
+ ConsoleServerPortTemplate.objects.create(device_type=devicetypes[0], name="Console Server Port Template 1")
1580
+ ConsoleServerPortTemplate.objects.create(device_type=devicetypes[0], name="Console Server Port Template 2")
1581
+ ConsoleServerPortTemplate.objects.create(device_type=devicetypes[0], name="Console Server Port Template 3")
1582
+
1583
+ cls.form_data = {
1584
+ "device_type": devicetypes[1].pk,
1585
+ "name": "Console Server Port Template X",
1586
+ "type": ConsolePortTypeChoices.TYPE_RJ45,
1587
+ }
1588
+
1589
+ cls.bulk_create_data = {
1590
+ "device_type": devicetypes[1].pk,
1591
+ "name_pattern": "Console Server Port Template [4-6]",
1592
+ "description": "View Test Bulk Create Console Server Ports",
1593
+ "type": ConsolePortTypeChoices.TYPE_RJ45,
1594
+ }
1595
+
1596
+ cls.bulk_edit_data = {
1597
+ "type": ConsolePortTypeChoices.TYPE_RJ45,
1598
+ }
1599
+
1600
+ test_instance = cls.model.objects.first()
1601
+ cls.update_data = {
1602
+ "name": test_instance.name,
1603
+ "device_type": getattr(getattr(test_instance, "device_type", None), "pk", None),
1604
+ "module_type": getattr(getattr(test_instance, "module_type", None), "pk", None),
1605
+ "label": "new test label",
1606
+ "description": "new test description",
1607
+ }
1608
+
1609
+
1610
+ class PowerPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
1611
+ model = PowerPortTemplate
1612
+
1613
+ @classmethod
1614
+ def setUpTestData(cls):
1615
+ manufacturer = Manufacturer.objects.first()
1616
+ devicetypes = (
1617
+ DeviceType.objects.create(manufacturer=manufacturer, model="Device Type 1"),
1618
+ DeviceType.objects.create(manufacturer=manufacturer, model="Device Type 2"),
1619
+ )
1620
+
1621
+ PowerPortTemplate.objects.create(device_type=devicetypes[0], name="Power Port Template 1")
1622
+ PowerPortTemplate.objects.create(device_type=devicetypes[0], name="Power Port Template 2")
1623
+ PowerPortTemplate.objects.create(device_type=devicetypes[0], name="Power Port Template 3")
1624
+
1625
+ cls.form_data = {
1626
+ "device_type": devicetypes[1].pk,
1627
+ "name": "Power Port Template X",
1628
+ "type": PowerPortTypeChoices.TYPE_IEC_C14,
1629
+ "maximum_draw": 100,
1630
+ "allocated_draw": 50,
1631
+ }
1632
+
1633
+ cls.bulk_create_data = {
1634
+ "device_type": devicetypes[1].pk,
1635
+ "name_pattern": "Power Port Template [4-6]",
1636
+ "description": "View Test Bulk Create Power Ports",
1637
+ "type": PowerPortTypeChoices.TYPE_IEC_C14,
1638
+ "maximum_draw": 100,
1639
+ "allocated_draw": 50,
1640
+ }
1641
+
1642
+ cls.bulk_edit_data = {
1643
+ "type": PowerPortTypeChoices.TYPE_IEC_C14,
1644
+ "maximum_draw": 100,
1645
+ "allocated_draw": 50,
1646
+ }
1647
+
1648
+ test_instance = cls.model.objects.first()
1649
+ cls.update_data = {
1650
+ "name": test_instance.name,
1651
+ "device_type": getattr(getattr(test_instance, "device_type", None), "pk", None),
1652
+ "module_type": getattr(getattr(test_instance, "module_type", None), "pk", None),
1653
+ "label": "new test label",
1654
+ "description": "new test description",
1655
+ }
1656
+
1657
+
1658
+ class PowerOutletTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
1659
+ model = PowerOutletTemplate
1660
+
1661
+ @classmethod
1662
+ def setUpTestData(cls):
1663
+ manufacturer = Manufacturer.objects.first()
1664
+ devicetype = DeviceType.objects.create(manufacturer=manufacturer, model="Device Type 1")
1665
+
1666
+ PowerOutletTemplate.objects.create(device_type=devicetype, name="Power Outlet Template 1")
1667
+ PowerOutletTemplate.objects.create(device_type=devicetype, name="Power Outlet Template 2")
1668
+ PowerOutletTemplate.objects.create(device_type=devicetype, name="Power Outlet Template 3")
1669
+
1670
+ powerports = (PowerPortTemplate.objects.create(device_type=devicetype, name="Power Port Template 1"),)
1671
+
1672
+ cls.form_data = {
1260
1673
  "device_type": devicetype.pk,
1261
1674
  "name": "Power Outlet Template X",
1262
1675
  "type": PowerOutletTypeChoices.TYPE_IEC_C13,
@@ -1267,6 +1680,7 @@ class PowerOutletTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestC
1267
1680
  cls.bulk_create_data = {
1268
1681
  "device_type": devicetype.pk,
1269
1682
  "name_pattern": "Power Outlet Template [4-6]",
1683
+ "description": "View Test Bulk Create Power Outlets",
1270
1684
  "type": PowerOutletTypeChoices.TYPE_IEC_C13,
1271
1685
  "power_port_template": powerports[0].pk,
1272
1686
  "feed_leg": PowerOutletFeedLegChoices.FEED_LEG_B,
@@ -1277,6 +1691,17 @@ class PowerOutletTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestC
1277
1691
  "feed_leg": PowerOutletFeedLegChoices.FEED_LEG_B,
1278
1692
  }
1279
1693
 
1694
+ test_instance = cls.model.objects.first()
1695
+ cls.update_data = {
1696
+ "name": test_instance.name,
1697
+ "device_type": getattr(getattr(test_instance, "device_type", None), "pk", None),
1698
+ "module_type": getattr(getattr(test_instance, "module_type", None), "pk", None),
1699
+ # power_port_template must match the parent device/module type
1700
+ "power_port_template": getattr(test_instance.power_port_template, "pk", None),
1701
+ "label": "new test label",
1702
+ "description": "new test description",
1703
+ }
1704
+
1280
1705
 
1281
1706
  class InterfaceTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
1282
1707
  model = InterfaceTemplate
@@ -1289,9 +1714,21 @@ class InterfaceTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCas
1289
1714
  DeviceType.objects.create(manufacturer=manufacturer, model="Device Type 2"),
1290
1715
  )
1291
1716
 
1292
- InterfaceTemplate.objects.create(device_type=devicetypes[0], name="Interface Template 1")
1293
- InterfaceTemplate.objects.create(device_type=devicetypes[0], name="Interface Template 2")
1294
- InterfaceTemplate.objects.create(device_type=devicetypes[0], name="Interface Template 3")
1717
+ InterfaceTemplate.objects.create(
1718
+ device_type=devicetypes[0],
1719
+ type=InterfaceTypeChoices.TYPE_100GE_QSFP_DD,
1720
+ name="Interface Template 1",
1721
+ )
1722
+ InterfaceTemplate.objects.create(
1723
+ device_type=devicetypes[0],
1724
+ type=InterfaceTypeChoices.TYPE_100GE_QSFP_DD,
1725
+ name="Interface Template 2",
1726
+ )
1727
+ InterfaceTemplate.objects.create(
1728
+ device_type=devicetypes[0],
1729
+ type=InterfaceTypeChoices.TYPE_100GE_QSFP_DD,
1730
+ name="Interface Template 3",
1731
+ )
1295
1732
 
1296
1733
  cls.form_data = {
1297
1734
  "device_type": devicetypes[1].pk,
@@ -1305,6 +1742,7 @@ class InterfaceTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCas
1305
1742
  "name_pattern": "Interface Template [4-6]",
1306
1743
  # Test that a label can be applied to each generated interface templates
1307
1744
  "label_pattern": "Interface Template Label [3-5]",
1745
+ "description": "View Test Bulk Create Interfaces",
1308
1746
  "type": InterfaceTypeChoices.TYPE_1GE_GBIC,
1309
1747
  "mgmt_only": True,
1310
1748
  }
@@ -1314,6 +1752,16 @@ class InterfaceTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCas
1314
1752
  "mgmt_only": True,
1315
1753
  }
1316
1754
 
1755
+ test_instance = cls.model.objects.first()
1756
+ cls.update_data = {
1757
+ "name": test_instance.name,
1758
+ "device_type": getattr(getattr(test_instance, "device_type", None), "pk", None),
1759
+ "module_type": getattr(getattr(test_instance, "module_type", None), "pk", None),
1760
+ "type": test_instance.type,
1761
+ "label": "new test label",
1762
+ "description": "new test description",
1763
+ }
1764
+
1317
1765
 
1318
1766
  class FrontPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
1319
1767
  model = FrontPortTemplate
@@ -1324,29 +1772,62 @@ class FrontPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCas
1324
1772
  devicetype = DeviceType.objects.create(manufacturer=manufacturer, model="Device Type 1")
1325
1773
 
1326
1774
  rearports = (
1327
- RearPortTemplate.objects.create(device_type=devicetype, name="Rear Port Template 1"),
1328
- RearPortTemplate.objects.create(device_type=devicetype, name="Rear Port Template 2"),
1329
- RearPortTemplate.objects.create(device_type=devicetype, name="Rear Port Template 3"),
1330
- RearPortTemplate.objects.create(device_type=devicetype, name="Rear Port Template 4"),
1331
- RearPortTemplate.objects.create(device_type=devicetype, name="Rear Port Template 5"),
1332
- RearPortTemplate.objects.create(device_type=devicetype, name="Rear Port Template 6"),
1775
+ RearPortTemplate.objects.create(
1776
+ device_type=devicetype,
1777
+ type=PortTypeChoices.TYPE_8P8C,
1778
+ positions=24,
1779
+ name="Rear Port Template 1",
1780
+ ),
1781
+ RearPortTemplate.objects.create(
1782
+ device_type=devicetype,
1783
+ type=PortTypeChoices.TYPE_8P8C,
1784
+ positions=24,
1785
+ name="Rear Port Template 2",
1786
+ ),
1787
+ RearPortTemplate.objects.create(
1788
+ device_type=devicetype,
1789
+ type=PortTypeChoices.TYPE_8P8C,
1790
+ positions=24,
1791
+ name="Rear Port Template 3",
1792
+ ),
1793
+ RearPortTemplate.objects.create(
1794
+ device_type=devicetype,
1795
+ type=PortTypeChoices.TYPE_8P8C,
1796
+ positions=24,
1797
+ name="Rear Port Template 4",
1798
+ ),
1799
+ RearPortTemplate.objects.create(
1800
+ device_type=devicetype,
1801
+ type=PortTypeChoices.TYPE_8P8C,
1802
+ positions=24,
1803
+ name="Rear Port Template 5",
1804
+ ),
1805
+ RearPortTemplate.objects.create(
1806
+ device_type=devicetype,
1807
+ type=PortTypeChoices.TYPE_8P8C,
1808
+ positions=24,
1809
+ name="Rear Port Template 6",
1810
+ ),
1333
1811
  )
1334
1812
 
1335
1813
  FrontPortTemplate.objects.create(
1336
1814
  device_type=devicetype,
1337
- name="Front Port Template 1",
1815
+ name="View Test Front Port Template 1",
1816
+ type=PortTypeChoices.TYPE_8P8C,
1338
1817
  rear_port_template=rearports[0],
1339
1818
  rear_port_position=1,
1340
1819
  )
1341
1820
  FrontPortTemplate.objects.create(
1342
1821
  device_type=devicetype,
1343
- name="Front Port Template 2",
1822
+ name="View Test Front Port Template 2",
1823
+ type=PortTypeChoices.TYPE_8P8C,
1344
1824
  rear_port_template=rearports[1],
1345
1825
  rear_port_position=1,
1346
1826
  )
1347
1827
  FrontPortTemplate.objects.create(
1348
1828
  device_type=devicetype,
1349
- name="Front Port Template 3",
1829
+ name="View Test Front Port Template 3",
1830
+ type=PortTypeChoices.TYPE_8P8C,
1350
1831
  rear_port_template=rearports[2],
1351
1832
  rear_port_position=1,
1352
1833
  )
@@ -1361,13 +1842,26 @@ class FrontPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCas
1361
1842
 
1362
1843
  cls.bulk_create_data = {
1363
1844
  "device_type": devicetype.pk,
1364
- "name_pattern": "Front Port [4-6]",
1845
+ "name_pattern": "View Test Front Port [4-6]",
1846
+ "description": "View Test Bulk Create Front Ports",
1365
1847
  "type": PortTypeChoices.TYPE_8P8C,
1366
1848
  "rear_port_template_set": [f"{rp.pk}:1" for rp in rearports[3:6]],
1367
1849
  }
1368
1850
 
1369
1851
  cls.bulk_edit_data = {
1370
- "type": PortTypeChoices.TYPE_8P8C,
1852
+ "type": PortTypeChoices.TYPE_4P4C,
1853
+ }
1854
+
1855
+ test_instance = cls.model.objects.first()
1856
+ cls.update_data = {
1857
+ "name": test_instance.name,
1858
+ "device_type": getattr(getattr(test_instance, "device_type", None), "pk", None),
1859
+ "module_type": getattr(getattr(test_instance, "module_type", None), "pk", None),
1860
+ "rear_port_template": test_instance.rear_port_template.pk,
1861
+ "rear_port_position": test_instance.rear_port_position,
1862
+ "type": test_instance.type,
1863
+ "label": "new test label",
1864
+ "description": "new test description",
1371
1865
  }
1372
1866
 
1373
1867
 
@@ -1382,9 +1876,24 @@ class RearPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase
1382
1876
  DeviceType.objects.create(manufacturer=manufacturer, model="Device Type 2"),
1383
1877
  )
1384
1878
 
1385
- RearPortTemplate.objects.create(device_type=devicetypes[0], name="Rear Port Template 1")
1386
- RearPortTemplate.objects.create(device_type=devicetypes[0], name="Rear Port Template 2")
1387
- RearPortTemplate.objects.create(device_type=devicetypes[0], name="Rear Port Template 3")
1879
+ RearPortTemplate.objects.create(
1880
+ device_type=devicetypes[0],
1881
+ type=PortTypeChoices.TYPE_8P8C,
1882
+ positions=24,
1883
+ name="Rear Port Template 1",
1884
+ )
1885
+ RearPortTemplate.objects.create(
1886
+ device_type=devicetypes[0],
1887
+ type=PortTypeChoices.TYPE_8P8C,
1888
+ positions=24,
1889
+ name="Rear Port Template 2",
1890
+ )
1891
+ RearPortTemplate.objects.create(
1892
+ device_type=devicetypes[0],
1893
+ type=PortTypeChoices.TYPE_8P8C,
1894
+ positions=24,
1895
+ name="Rear Port Template 3",
1896
+ )
1388
1897
 
1389
1898
  cls.form_data = {
1390
1899
  "device_type": devicetypes[1].pk,
@@ -1396,6 +1905,7 @@ class RearPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase
1396
1905
  cls.bulk_create_data = {
1397
1906
  "device_type": devicetypes[1].pk,
1398
1907
  "name_pattern": "Rear Port Template [4-6]",
1908
+ "description": "View Test Bulk Create Rear Ports",
1399
1909
  "type": PortTypeChoices.TYPE_8P8C,
1400
1910
  "positions": 2,
1401
1911
  }
@@ -1404,6 +1914,17 @@ class RearPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase
1404
1914
  "type": PortTypeChoices.TYPE_8P8C,
1405
1915
  }
1406
1916
 
1917
+ test_instance = cls.model.objects.first()
1918
+ cls.update_data = {
1919
+ "name": test_instance.name,
1920
+ "device_type": getattr(getattr(test_instance, "device_type", None), "pk", None),
1921
+ "module_type": getattr(getattr(test_instance, "module_type", None), "pk", None),
1922
+ "positions": test_instance.positions,
1923
+ "type": test_instance.type,
1924
+ "label": "new test label",
1925
+ "description": "new test description",
1926
+ }
1927
+
1407
1928
 
1408
1929
  class DeviceBayTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
1409
1930
  model = DeviceBayTemplate
@@ -1436,31 +1957,80 @@ class DeviceBayTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCas
1436
1957
  cls.bulk_create_data = {
1437
1958
  "device_type": devicetypes[1].pk,
1438
1959
  "name_pattern": "Device Bay Template [4-6]",
1960
+ "description": "View Test Bulk Create Device Bays",
1439
1961
  }
1440
1962
 
1441
1963
  cls.bulk_edit_data = {
1442
1964
  "description": "Foo bar",
1443
1965
  }
1444
1966
 
1967
+ test_instance = cls.model.objects.first()
1968
+ cls.update_data = {
1969
+ "name": test_instance.name,
1970
+ "device_type": test_instance.device_type.pk,
1971
+ "label": "new test label",
1972
+ "description": "new test description",
1973
+ }
1445
1974
 
1446
- class PlatformTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
1447
- model = Platform
1975
+
1976
+ class ModuleBayTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
1977
+ model = ModuleBayTemplate
1448
1978
 
1449
1979
  @classmethod
1450
1980
  def setUpTestData(cls):
1451
- manufacturer = Manufacturer.objects.first()
1452
-
1453
- # Protected FK to SoftwareImageFile prevents deletion
1454
- DeviceTypeToSoftwareImageFile.objects.all().delete()
1455
- # Protected FK to SoftwareVersion prevents deletion
1456
- Device.objects.all().update(software_version=None)
1981
+ device_type = DeviceType.objects.first()
1982
+ module_type = ModuleType.objects.first()
1457
1983
 
1458
1984
  cls.form_data = {
1459
- "name": "Platform X",
1460
- "manufacturer": manufacturer.pk,
1461
- "napalm_driver": "junos",
1462
- "napalm_args": None,
1463
- "network_driver": "juniper_junos",
1985
+ "device_type": device_type.pk,
1986
+ "module_type": None,
1987
+ "name": "Module Bay Template X",
1988
+ "position": "Test modulebaytemplate position",
1989
+ "description": "Test modulebaytemplate description",
1990
+ "label": "Test modulebaytemplate label",
1991
+ }
1992
+
1993
+ cls.bulk_create_data = {
1994
+ "module_type": module_type.pk,
1995
+ "name_pattern": "Test Module Bay Template [5-7]",
1996
+ "position_pattern": "Test Module Bay Template Position [10-12]",
1997
+ "label_pattern": "Test modulebaytemplate label [1-3]",
1998
+ "description": "Test modulebaytemplate description",
1999
+ }
2000
+
2001
+ cls.bulk_edit_data = {
2002
+ "description": "Description changed",
2003
+ }
2004
+
2005
+ test_instance = cls.model.objects.first()
2006
+ cls.update_data = {
2007
+ "name": test_instance.name,
2008
+ "device_type": getattr(getattr(test_instance, "device_type", None), "pk", None),
2009
+ "module_type": getattr(getattr(test_instance, "module_type", None), "pk", None),
2010
+ "position": "new test position",
2011
+ "label": "new test label",
2012
+ "description": "new test description",
2013
+ }
2014
+
2015
+
2016
+ class PlatformTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
2017
+ model = Platform
2018
+
2019
+ @classmethod
2020
+ def setUpTestData(cls):
2021
+ manufacturer = Manufacturer.objects.first()
2022
+
2023
+ # Protected FK to SoftwareImageFile prevents deletion
2024
+ DeviceTypeToSoftwareImageFile.objects.all().delete()
2025
+ # Protected FK to SoftwareVersion prevents deletion
2026
+ Device.objects.all().update(software_version=None)
2027
+
2028
+ cls.form_data = {
2029
+ "name": "Platform X",
2030
+ "manufacturer": manufacturer.pk,
2031
+ "napalm_driver": "junos",
2032
+ "napalm_args": None,
2033
+ "network_driver": "juniper_junos",
1464
2034
  "description": "A new platform",
1465
2035
  }
1466
2036
 
@@ -1593,11 +2163,11 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
1593
2163
  )
1594
2164
 
1595
2165
  intf_status = Status.objects.get_for_model(Interface).first()
1596
-
2166
+ intf_role = Role.objects.get_for_model(Interface).first()
1597
2167
  cls.interfaces = (
1598
- Interface.objects.create(device=devices[0], name="Interface 1", status=intf_status),
1599
- Interface.objects.create(device=devices[0], name="Interface 2", status=intf_status),
1600
- Interface.objects.create(device=devices[0], name="Interface 3", status=intf_status),
2168
+ Interface.objects.create(device=devices[0], name="Interface A1", status=intf_status, role=intf_role),
2169
+ Interface.objects.create(device=devices[0], name="Interface A2", status=intf_status),
2170
+ Interface.objects.create(device=devices[0], name="Interface A3", status=intf_status, role=intf_role),
1601
2171
  )
1602
2172
 
1603
2173
  for device, ipaddress in zip(devices, ipaddresses):
@@ -1651,55 +2221,446 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
1651
2221
  }
1652
2222
 
1653
2223
  @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
1654
- def test_device_consoleports(self):
1655
- device = Device.objects.first()
2224
+ def test_device_consoleports(self):
2225
+ device = Device.objects.first()
2226
+
2227
+ ConsolePort.objects.create(device=device, name="Console Port 1")
2228
+ ConsolePort.objects.create(device=device, name="Console Port 2")
2229
+ ConsolePort.objects.create(device=device, name="Console Port 3")
2230
+
2231
+ url = reverse("dcim:device_consoleports", kwargs={"pk": device.pk})
2232
+ self.assertHttpStatus(self.client.get(url), 200)
2233
+
2234
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
2235
+ def test_device_consoleserverports(self):
2236
+ device = Device.objects.first()
2237
+
2238
+ ConsoleServerPort.objects.create(device=device, name="Console Server Port 1")
2239
+ ConsoleServerPort.objects.create(device=device, name="Console Server Port 2")
2240
+ ConsoleServerPort.objects.create(device=device, name="Console Server Port 3")
2241
+
2242
+ url = reverse("dcim:device_consoleserverports", kwargs={"pk": device.pk})
2243
+ self.assertHttpStatus(self.client.get(url), 200)
2244
+
2245
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
2246
+ def test_device_powerports(self):
2247
+ device = Device.objects.first()
2248
+
2249
+ PowerPort.objects.create(device=device, name="Power Port 1")
2250
+ PowerPort.objects.create(device=device, name="Power Port 2")
2251
+ PowerPort.objects.create(device=device, name="Power Port 3")
2252
+
2253
+ url = reverse("dcim:device_powerports", kwargs={"pk": device.pk})
2254
+ self.assertHttpStatus(self.client.get(url), 200)
2255
+
2256
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
2257
+ def test_device_poweroutlets(self):
2258
+ device = Device.objects.first()
2259
+
2260
+ PowerOutlet.objects.create(device=device, name="Power Outlet 1")
2261
+ PowerOutlet.objects.create(device=device, name="Power Outlet 2")
2262
+ PowerOutlet.objects.create(device=device, name="Power Outlet 3")
2263
+
2264
+ url = reverse("dcim:device_poweroutlets", kwargs={"pk": device.pk})
2265
+ self.assertHttpStatus(self.client.get(url), 200)
2266
+
2267
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
2268
+ def test_device_interfaces(self):
2269
+ device = Device.objects.filter(interfaces__isnull=False).first()
2270
+ self.add_permissions("ipam.add_ipaddress", "dcim.change_interface")
2271
+
2272
+ url = reverse("dcim:device_interfaces", kwargs={"pk": device.pk})
2273
+ response = self.client.get(url)
2274
+ self.assertHttpStatus(response, 200)
2275
+ response_body = response.content.decode(response.charset)
2276
+ # Count the number of occurrences of "Add IP address" in the response_body
2277
+ count = response_body.count("Add IP address")
2278
+ # Assert that "Add IP address" appears for each of the three interfaces
2279
+ self.assertEqual(count, 3)
2280
+
2281
+ def test_device_interface_assign_ipaddress(self):
2282
+ device = Device.objects.first()
2283
+ self.add_permissions(
2284
+ "ipam.add_ipaddress",
2285
+ "extras.view_status",
2286
+ "ipam.view_namespace",
2287
+ "dcim.view_device",
2288
+ "dcim.view_interface",
2289
+ )
2290
+ device_list_url = reverse("dcim:device_interfaces", args=(device.pk,))
2291
+ namespace = Namespace.objects.first()
2292
+ ipaddresses = [str(ipadress) for ipadress in IPAddress.objects.values_list("pk", flat=True)[:3]]
2293
+ add_new_ip_form_data = {
2294
+ "namespace": namespace.pk,
2295
+ "address": "1.1.1.7/24",
2296
+ "tenant": None,
2297
+ "status": Status.objects.get_for_model(IPAddress).first().pk,
2298
+ "type": IPAddressTypeChoices.TYPE_DHCP,
2299
+ "role": None,
2300
+ "nat_inside": None,
2301
+ "dns_name": None,
2302
+ "description": None,
2303
+ "tags": [],
2304
+ "interface": self.interfaces[0].id,
2305
+ }
2306
+ add_new_ip_request = {
2307
+ "path": reverse("ipam:ipaddress_add") + f"?interface={self.interfaces[0].id}&return_url={device_list_url}",
2308
+ "data": post_data(add_new_ip_form_data),
2309
+ }
2310
+ assign_ip_form_data = {"pk": ipaddresses}
2311
+ assign_ip_request = {
2312
+ "path": reverse("ipam:ipaddress_assign")
2313
+ + f"?interface={self.interfaces[1].id}&return_url={device_list_url}",
2314
+ "data": post_data(assign_ip_form_data),
2315
+ }
2316
+
2317
+ with self.subTest("Assert Cannnot assign IPAddress('Add New') without permission"):
2318
+ # Assert Add new IPAddress
2319
+ response = self.client.post(**add_new_ip_request, follow=True)
2320
+ response_body = response.content.decode(response.charset)
2321
+ self.assertHttpStatus(response, 200)
2322
+ self.interfaces[0].refresh_from_db()
2323
+ self.assertEqual(self.interfaces[0].ip_addresses.all().count(), 0)
2324
+ self.assertIn(
2325
+ f"Interface with id &quot;{self.interfaces[0].pk}&quot; not found",
2326
+ response_body,
2327
+ )
2328
+
2329
+ with self.subTest("Assert Cannnot assign IPAddress(Exsisting IP) without permission"):
2330
+ # Assert Assign Exsisting IPAddress
2331
+ response = self.client.post(**assign_ip_request, follow=True)
2332
+ response_body = response.content.decode(response.charset)
2333
+ self.assertHttpStatus(response, 200)
2334
+ self.interfaces[1].refresh_from_db()
2335
+ self.assertEqual(self.interfaces[1].ip_addresses.all().count(), 0)
2336
+ self.assertIn(
2337
+ f"Interface with id &quot;{self.interfaces[1].pk}&quot; not found",
2338
+ response_body,
2339
+ )
2340
+
2341
+ self.add_permissions("dcim.change_interface", "ipam.view_ipaddress")
2342
+
2343
+ with self.subTest("Assert Create and Assign IPAddress"):
2344
+ self.assertHttpStatus(self.client.post(**add_new_ip_request), 302)
2345
+ self.interfaces[0].refresh_from_db()
2346
+ self.assertEqual(
2347
+ str(self.interfaces[0].ip_addresses.all().first().address),
2348
+ add_new_ip_form_data["address"],
2349
+ )
2350
+
2351
+ with self.subTest("Assert Assign IPAddress"):
2352
+ response = self.client.post(**assign_ip_request)
2353
+ self.assertHttpStatus(response, 302)
2354
+ self.interfaces[1].refresh_from_db()
2355
+ self.assertEqual(self.interfaces[1].ip_addresses.count(), 3)
2356
+ interface_ips = [str(ip) for ip in self.interfaces[1].ip_addresses.values_list("pk", flat=True)]
2357
+ self.assertEqual(
2358
+ sorted(ipaddresses),
2359
+ sorted(interface_ips),
2360
+ )
2361
+
2362
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
2363
+ def test_device_rearports(self):
2364
+ device = Device.objects.first()
2365
+
2366
+ RearPort.objects.create(device=device, name="Rear Port 1")
2367
+ RearPort.objects.create(device=device, name="Rear Port 2")
2368
+ RearPort.objects.create(device=device, name="Rear Port 3")
2369
+
2370
+ url = reverse("dcim:device_rearports", kwargs={"pk": device.pk})
2371
+ self.assertHttpStatus(self.client.get(url), 200)
2372
+
2373
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
2374
+ def test_device_frontports(self):
2375
+ device = Device.objects.first()
2376
+ rear_ports = (
2377
+ RearPort.objects.create(device=device, name="Rear Port 1"),
2378
+ RearPort.objects.create(device=device, name="Rear Port 2"),
2379
+ RearPort.objects.create(device=device, name="Rear Port 3"),
2380
+ )
2381
+
2382
+ FrontPort.objects.create(
2383
+ device=device,
2384
+ name="Front Port 1",
2385
+ rear_port=rear_ports[0],
2386
+ rear_port_position=1,
2387
+ )
2388
+ FrontPort.objects.create(
2389
+ device=device,
2390
+ name="Front Port 2",
2391
+ rear_port=rear_ports[1],
2392
+ rear_port_position=1,
2393
+ )
2394
+ FrontPort.objects.create(
2395
+ device=device,
2396
+ name="Front Port 3",
2397
+ rear_port=rear_ports[2],
2398
+ rear_port_position=1,
2399
+ )
2400
+
2401
+ url = reverse("dcim:device_frontports", kwargs={"pk": device.pk})
2402
+ self.assertHttpStatus(self.client.get(url), 200)
2403
+
2404
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
2405
+ def test_device_devicebays(self):
2406
+ device = Device.objects.first()
2407
+
2408
+ # Device Bay 1 was already created in setUpTestData()
2409
+ DeviceBay.objects.create(device=device, name="Device Bay 2")
2410
+ DeviceBay.objects.create(device=device, name="Device Bay 3")
2411
+
2412
+ url = reverse("dcim:device_devicebays", kwargs={"pk": device.pk})
2413
+ self.assertHttpStatus(self.client.get(url), 200)
2414
+
2415
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
2416
+ def test_device_inventory(self):
2417
+ device = Device.objects.first()
2418
+
2419
+ InventoryItem.objects.create(device=device, name="Inventory Item 1")
2420
+ InventoryItem.objects.create(device=device, name="Inventory Item 2")
2421
+ InventoryItem.objects.create(device=device, name="Inventory Item 3")
2422
+
2423
+ url = reverse("dcim:device_inventory", kwargs={"pk": device.pk})
2424
+ self.assertHttpStatus(self.client.get(url), 200)
2425
+
2426
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
2427
+ def test_device_primary_ips(self):
2428
+ """Test assigning a primary IP to a device."""
2429
+ self.add_permissions("dcim.change_device")
2430
+
2431
+ # Create an interface and assign an IP to it.
2432
+ device = Device.objects.filter(interfaces__isnull=False).first()
2433
+ interface = device.interfaces.first()
2434
+ namespace = Namespace.objects.first()
2435
+ Prefix.objects.create(prefix="1.2.3.0/24", namespace=namespace, status=self.prefix_status)
2436
+ ip_address = IPAddress.objects.create(address="1.2.3.4/32", namespace=namespace, status=self.ipaddr_status)
2437
+ interface.ip_addresses.add(ip_address)
2438
+
2439
+ # Dupe the form data and populated primary_ip4 w/ ip_address
2440
+ form_data = self.form_data.copy()
2441
+ form_data["primary_ip4"] = ip_address.pk
2442
+ # Assert that update succeeds.
2443
+ request = {
2444
+ "path": self._get_url("edit", device),
2445
+ "data": post_data(form_data),
2446
+ }
2447
+ self.assertHttpStatus(self.client.post(**request), 302)
2448
+ self.assertInstanceEqual(self._get_queryset().order_by("last_updated").last(), form_data)
2449
+
2450
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
2451
+ def test_local_config_context_schema_validation_pass(self):
2452
+ """
2453
+ Given a config context schema
2454
+ And a device with local context that conforms to that schema
2455
+ Assert that the local context passes schema validation via full_clean()
2456
+ """
2457
+ schema = ConfigContextSchema.objects.create(
2458
+ name="Schema 1",
2459
+ data_schema={"type": "object", "properties": {"foo": {"type": "string"}}},
2460
+ )
2461
+ self.add_permissions("dcim.add_device")
2462
+
2463
+ form_data = self.form_data.copy()
2464
+ form_data["local_config_context_schema"] = schema.pk
2465
+ form_data["local_config_context_data"] = '{"foo": "bar"}'
2466
+
2467
+ # Try POST with model-level permission
2468
+ request = {
2469
+ "path": self._get_url("add"),
2470
+ "data": post_data(form_data),
2471
+ }
2472
+ self.assertHttpStatus(self.client.post(**request), 302)
2473
+ self.assertEqual(
2474
+ self._get_queryset().get(name="Device X").local_config_context_schema.pk,
2475
+ schema.pk,
2476
+ )
2477
+
2478
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
2479
+ def test_local_config_context_schema_validation_fails(self):
2480
+ """
2481
+ Given a config context schema
2482
+ And a device with local context that *does not* conform to that schema
2483
+ Assert that the local context fails schema validation via full_clean()
2484
+ """
2485
+ schema = ConfigContextSchema.objects.create(
2486
+ name="Schema 1",
2487
+ data_schema={"type": "object", "properties": {"foo": {"type": "integer"}}},
2488
+ )
2489
+ self.add_permissions("dcim.add_device")
2490
+
2491
+ form_data = self.form_data.copy()
2492
+ form_data["local_config_context_schema"] = schema.pk
2493
+ form_data["local_config_context_data"] = '{"foo": "bar"}'
2494
+
2495
+ # Try POST with model-level permission
2496
+ request = {
2497
+ "path": self._get_url("add"),
2498
+ "data": post_data(form_data),
2499
+ }
2500
+ self.assertHttpStatus(self.client.post(**request), 200)
2501
+ self.assertEqual(self._get_queryset().filter(name="Device X").count(), 0)
2502
+
2503
+
2504
+ class ModuleTestCase(ViewTestCases.PrimaryObjectViewTestCase):
2505
+ model = Module
2506
+
2507
+ @classmethod
2508
+ def setUpTestData(cls):
2509
+ Module.objects.all().delete()
2510
+ locations = Location.objects.filter(location_type=LocationType.objects.get(name="Campus"))[:2]
2511
+ manufacturer = Manufacturer.objects.first()
2512
+
2513
+ moduletypes = (
2514
+ ModuleType.objects.create(model="Module Type 1", manufacturer=manufacturer),
2515
+ ModuleType.objects.create(model="Module Type 2", manufacturer=manufacturer),
2516
+ )
2517
+
2518
+ moduleroles = Role.objects.get_for_model(Module)[:2]
2519
+
2520
+ statuses = Status.objects.get_for_model(Module)
2521
+ status_active = statuses[0]
2522
+
2523
+ cls.custom_fields = (
2524
+ CustomField.objects.create(
2525
+ type=CustomFieldTypeChoices.TYPE_INTEGER,
2526
+ label="Crash Counter",
2527
+ default=0,
2528
+ ),
2529
+ )
2530
+ cls.custom_fields[0].content_types.set([ContentType.objects.get_for_model(Module)])
2531
+
2532
+ modules = (
2533
+ Module.objects.create(
2534
+ location=locations[0],
2535
+ module_type=moduletypes[0],
2536
+ role=moduleroles[0],
2537
+ status=status_active,
2538
+ _custom_field_data={"crash_counter": 5},
2539
+ ),
2540
+ Module.objects.create(
2541
+ location=locations[0],
2542
+ module_type=moduletypes[0],
2543
+ role=moduleroles[0],
2544
+ status=status_active,
2545
+ _custom_field_data={"crash_counter": 10},
2546
+ ),
2547
+ Module.objects.create(
2548
+ location=locations[0],
2549
+ module_type=moduletypes[0],
2550
+ role=moduleroles[0],
2551
+ status=status_active,
2552
+ _custom_field_data={"crash_counter": 15},
2553
+ ),
2554
+ )
2555
+
2556
+ cls.relationships = (
2557
+ Relationship(
2558
+ label="BGP Router-ID",
2559
+ key="router_id",
2560
+ type=RelationshipTypeChoices.TYPE_ONE_TO_ONE,
2561
+ source_type=ContentType.objects.get_for_model(Module),
2562
+ source_label="BGP Router ID",
2563
+ destination_type=ContentType.objects.get_for_model(IPAddress),
2564
+ destination_label="Module using this as BGP router-ID",
2565
+ ),
2566
+ )
2567
+ for relationship in cls.relationships:
2568
+ relationship.validated_save()
2569
+
2570
+ cls.ipaddr_status = Status.objects.get_for_model(IPAddress).first()
2571
+ cls.prefix_status = Status.objects.get_for_model(Prefix).first()
2572
+ namespace = Namespace.objects.first()
2573
+ Prefix.objects.create(prefix="1.1.1.1/24", namespace=namespace, status=cls.prefix_status)
2574
+ Prefix.objects.create(prefix="2.2.2.2/24", namespace=namespace, status=cls.prefix_status)
2575
+ Prefix.objects.create(prefix="3.3.3.3/24", namespace=namespace, status=cls.prefix_status)
2576
+ ipaddresses = (
2577
+ IPAddress.objects.create(address="1.1.1.1/32", namespace=namespace, status=cls.ipaddr_status),
2578
+ IPAddress.objects.create(address="2.2.2.2/32", namespace=namespace, status=cls.ipaddr_status),
2579
+ IPAddress.objects.create(address="3.3.3.3/32", namespace=namespace, status=cls.ipaddr_status),
2580
+ )
2581
+
2582
+ intf_status = Status.objects.get_for_model(Interface).first()
2583
+ intf_role = Role.objects.get_for_model(Interface).first()
2584
+ cls.interfaces = (
2585
+ Interface.objects.create(module=modules[0], name="Interface A1", status=intf_status, role=intf_role),
2586
+ Interface.objects.create(module=modules[0], name="Interface A2", status=intf_status),
2587
+ Interface.objects.create(module=modules[0], name="Interface A3", status=intf_status, role=intf_role),
2588
+ )
2589
+
2590
+ for module, ipaddress in zip(modules, ipaddresses):
2591
+ RelationshipAssociation(
2592
+ relationship=cls.relationships[0], source=module, destination=ipaddress
2593
+ ).validated_save()
2594
+
2595
+ cls.form_data = {
2596
+ "module_type": moduletypes[1].pk,
2597
+ "role": moduleroles[1].pk,
2598
+ "tenant": None,
2599
+ "serial": "VMWARE-XX XX XX XX XX XX XX XX-XX XX XX XX XX XX XX XX",
2600
+ "asset_tag": generate_random_device_asset_tag_of_specified_size(100),
2601
+ "location": locations[1].pk,
2602
+ "status": statuses[1].pk,
2603
+ "tags": [t.pk for t in Tag.objects.get_for_model(Module)],
2604
+ "cf_crash_counter": -1,
2605
+ "cr_router-id": None,
2606
+ }
2607
+
2608
+ cls.bulk_edit_data = {
2609
+ "role": moduleroles[1].pk,
2610
+ "tenant": None,
2611
+ "status": statuses[2].pk,
2612
+ }
2613
+
2614
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
2615
+ def test_module_consoleports(self):
2616
+ module = Module.objects.first()
1656
2617
 
1657
- ConsolePort.objects.create(device=device, name="Console Port 1")
1658
- ConsolePort.objects.create(device=device, name="Console Port 2")
1659
- ConsolePort.objects.create(device=device, name="Console Port 3")
2618
+ ConsolePort.objects.create(module=module, name="Console Port 1")
2619
+ ConsolePort.objects.create(module=module, name="Console Port 2")
2620
+ ConsolePort.objects.create(module=module, name="Console Port 3")
1660
2621
 
1661
- url = reverse("dcim:device_consoleports", kwargs={"pk": device.pk})
2622
+ url = reverse("dcim:module_consoleports", kwargs={"pk": module.pk})
1662
2623
  self.assertHttpStatus(self.client.get(url), 200)
1663
2624
 
1664
2625
  @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
1665
- def test_device_consoleserverports(self):
1666
- device = Device.objects.first()
2626
+ def test_module_consoleserverports(self):
2627
+ module = Module.objects.first()
1667
2628
 
1668
- ConsoleServerPort.objects.create(device=device, name="Console Server Port 1")
1669
- ConsoleServerPort.objects.create(device=device, name="Console Server Port 2")
1670
- ConsoleServerPort.objects.create(device=device, name="Console Server Port 3")
2629
+ ConsoleServerPort.objects.create(module=module, name="Console Server Port 1")
2630
+ ConsoleServerPort.objects.create(module=module, name="Console Server Port 2")
2631
+ ConsoleServerPort.objects.create(module=module, name="Console Server Port 3")
1671
2632
 
1672
- url = reverse("dcim:device_consoleserverports", kwargs={"pk": device.pk})
2633
+ url = reverse("dcim:module_consoleserverports", kwargs={"pk": module.pk})
1673
2634
  self.assertHttpStatus(self.client.get(url), 200)
1674
2635
 
1675
2636
  @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
1676
- def test_device_powerports(self):
1677
- device = Device.objects.first()
2637
+ def test_module_powerports(self):
2638
+ module = Module.objects.first()
1678
2639
 
1679
- PowerPort.objects.create(device=device, name="Power Port 1")
1680
- PowerPort.objects.create(device=device, name="Power Port 2")
1681
- PowerPort.objects.create(device=device, name="Power Port 3")
2640
+ PowerPort.objects.create(module=module, name="Power Port 1")
2641
+ PowerPort.objects.create(module=module, name="Power Port 2")
2642
+ PowerPort.objects.create(module=module, name="Power Port 3")
1682
2643
 
1683
- url = reverse("dcim:device_powerports", kwargs={"pk": device.pk})
2644
+ url = reverse("dcim:module_powerports", kwargs={"pk": module.pk})
1684
2645
  self.assertHttpStatus(self.client.get(url), 200)
1685
2646
 
1686
2647
  @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
1687
- def test_device_poweroutlets(self):
1688
- device = Device.objects.first()
2648
+ def test_module_poweroutlets(self):
2649
+ module = Module.objects.first()
1689
2650
 
1690
- PowerOutlet.objects.create(device=device, name="Power Outlet 1")
1691
- PowerOutlet.objects.create(device=device, name="Power Outlet 2")
1692
- PowerOutlet.objects.create(device=device, name="Power Outlet 3")
2651
+ PowerOutlet.objects.create(module=module, name="Power Outlet 1")
2652
+ PowerOutlet.objects.create(module=module, name="Power Outlet 2")
2653
+ PowerOutlet.objects.create(module=module, name="Power Outlet 3")
1693
2654
 
1694
- url = reverse("dcim:device_poweroutlets", kwargs={"pk": device.pk})
2655
+ url = reverse("dcim:module_poweroutlets", kwargs={"pk": module.pk})
1695
2656
  self.assertHttpStatus(self.client.get(url), 200)
1696
2657
 
1697
2658
  @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
1698
- def test_device_interfaces(self):
1699
- device = Device.objects.filter(interfaces__isnull=False).first()
2659
+ def test_module_interfaces(self):
2660
+ module = Module.objects.filter(interfaces__isnull=False).first()
1700
2661
  self.add_permissions("ipam.add_ipaddress", "dcim.change_interface")
1701
2662
 
1702
- url = reverse("dcim:device_interfaces", kwargs={"pk": device.pk})
2663
+ url = reverse("dcim:module_interfaces", kwargs={"pk": module.pk})
1703
2664
  response = self.client.get(url)
1704
2665
  self.assertHttpStatus(response, 200)
1705
2666
  response_body = response.content.decode(response.charset)
@@ -1708,16 +2669,16 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
1708
2669
  # Assert that "Add IP address" appears for each of the three interfaces
1709
2670
  self.assertEqual(count, 3)
1710
2671
 
1711
- def test_device_interface_assign_ipaddress(self):
1712
- device = Device.objects.first()
2672
+ def test_module_interface_assign_ipaddress(self):
2673
+ module = Module.objects.first()
1713
2674
  self.add_permissions(
1714
2675
  "ipam.add_ipaddress",
1715
2676
  "extras.view_status",
1716
2677
  "ipam.view_namespace",
1717
- "dcim.view_device",
2678
+ "dcim.view_module",
1718
2679
  "dcim.view_interface",
1719
2680
  )
1720
- device_list_url = reverse("dcim:device_interfaces", args=(device.pk,))
2681
+ module_list_url = reverse("dcim:module_interfaces", args=(module.pk,))
1721
2682
  namespace = Namespace.objects.first()
1722
2683
  ipaddresses = [str(ipadress) for ipadress in IPAddress.objects.values_list("pk", flat=True)[:3]]
1723
2684
  add_new_ip_form_data = {
@@ -1734,13 +2695,13 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
1734
2695
  "interface": self.interfaces[0].id,
1735
2696
  }
1736
2697
  add_new_ip_request = {
1737
- "path": reverse("ipam:ipaddress_add") + f"?interface={self.interfaces[0].id}&return_url={device_list_url}",
2698
+ "path": reverse("ipam:ipaddress_add") + f"?interface={self.interfaces[0].id}&return_url={module_list_url}",
1738
2699
  "data": post_data(add_new_ip_form_data),
1739
2700
  }
1740
2701
  assign_ip_form_data = {"pk": ipaddresses}
1741
2702
  assign_ip_request = {
1742
2703
  "path": reverse("ipam:ipaddress_assign")
1743
- + f"?interface={self.interfaces[1].id}&return_url={device_list_url}",
2704
+ + f"?interface={self.interfaces[1].id}&return_url={module_list_url}",
1744
2705
  "data": post_data(assign_ip_form_data),
1745
2706
  }
1746
2707
 
@@ -1789,160 +2750,59 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
1789
2750
  sorted(interface_ips),
1790
2751
  )
1791
2752
 
1792
- with self.subTest("Assert Assigning IPAddress Without Selecting Any IPAddress Raises Exception"):
1793
- assign_ip_form_data["pk"] = []
1794
- assign_ip_request = {
1795
- "path": reverse("ipam:ipaddress_assign")
1796
- + f"?interface={self.interfaces[1].id}&return_url={device_list_url}",
1797
- "data": post_data(assign_ip_form_data),
1798
- }
1799
- response = self.client.post(**assign_ip_request, follow=True)
1800
- self.assertHttpStatus(response, 200)
1801
- self.assertIn(
1802
- "Please select at least one IP Address from the table.", response.content.decode(response.charset)
1803
- )
1804
-
1805
2753
  @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
1806
- def test_device_rearports(self):
1807
- device = Device.objects.first()
2754
+ def test_module_rearports(self):
2755
+ module = Module.objects.first()
1808
2756
 
1809
- RearPort.objects.create(device=device, name="Rear Port 1")
1810
- RearPort.objects.create(device=device, name="Rear Port 2")
1811
- RearPort.objects.create(device=device, name="Rear Port 3")
2757
+ RearPort.objects.create(module=module, name="Rear Port 1")
2758
+ RearPort.objects.create(module=module, name="Rear Port 2")
2759
+ RearPort.objects.create(module=module, name="Rear Port 3")
1812
2760
 
1813
- url = reverse("dcim:device_rearports", kwargs={"pk": device.pk})
2761
+ url = reverse("dcim:module_rearports", kwargs={"pk": module.pk})
1814
2762
  self.assertHttpStatus(self.client.get(url), 200)
1815
2763
 
1816
2764
  @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
1817
- def test_device_frontports(self):
1818
- device = Device.objects.first()
2765
+ def test_module_frontports(self):
2766
+ module = Module.objects.first()
1819
2767
  rear_ports = (
1820
- RearPort.objects.create(device=device, name="Rear Port 1"),
1821
- RearPort.objects.create(device=device, name="Rear Port 2"),
1822
- RearPort.objects.create(device=device, name="Rear Port 3"),
2768
+ RearPort.objects.create(module=module, name="Rear Port 1"),
2769
+ RearPort.objects.create(module=module, name="Rear Port 2"),
2770
+ RearPort.objects.create(module=module, name="Rear Port 3"),
1823
2771
  )
1824
2772
 
1825
2773
  FrontPort.objects.create(
1826
- device=device,
2774
+ module=module,
1827
2775
  name="Front Port 1",
1828
2776
  rear_port=rear_ports[0],
1829
2777
  rear_port_position=1,
1830
2778
  )
1831
2779
  FrontPort.objects.create(
1832
- device=device,
2780
+ module=module,
1833
2781
  name="Front Port 2",
1834
2782
  rear_port=rear_ports[1],
1835
2783
  rear_port_position=1,
1836
2784
  )
1837
2785
  FrontPort.objects.create(
1838
- device=device,
2786
+ module=module,
1839
2787
  name="Front Port 3",
1840
2788
  rear_port=rear_ports[2],
1841
2789
  rear_port_position=1,
1842
2790
  )
1843
2791
 
1844
- url = reverse("dcim:device_frontports", kwargs={"pk": device.pk})
1845
- self.assertHttpStatus(self.client.get(url), 200)
1846
-
1847
- @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
1848
- def test_device_devicebays(self):
1849
- device = Device.objects.first()
1850
-
1851
- # Device Bay 1 was already created in setUpTestData()
1852
- DeviceBay.objects.create(device=device, name="Device Bay 2")
1853
- DeviceBay.objects.create(device=device, name="Device Bay 3")
1854
-
1855
- url = reverse("dcim:device_devicebays", kwargs={"pk": device.pk})
2792
+ url = reverse("dcim:module_frontports", kwargs={"pk": module.pk})
1856
2793
  self.assertHttpStatus(self.client.get(url), 200)
1857
2794
 
1858
2795
  @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
1859
- def test_device_inventory(self):
1860
- device = Device.objects.first()
2796
+ def test_module_modulebays(self):
2797
+ module = Module.objects.first()
1861
2798
 
1862
- InventoryItem.objects.create(device=device, name="Inventory Item 1")
1863
- InventoryItem.objects.create(device=device, name="Inventory Item 2")
1864
- InventoryItem.objects.create(device=device, name="Inventory Item 3")
2799
+ ModuleBay.objects.create(parent_module=module, name="Test View Module Bay 1")
2800
+ ModuleBay.objects.create(parent_module=module, name="Test View Module Bay 2")
2801
+ ModuleBay.objects.create(parent_module=module, name="Test View Module Bay 3")
1865
2802
 
1866
- url = reverse("dcim:device_inventory", kwargs={"pk": device.pk})
2803
+ url = reverse("dcim:module_modulebays", kwargs={"pk": module.pk})
1867
2804
  self.assertHttpStatus(self.client.get(url), 200)
1868
2805
 
1869
- @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
1870
- def test_device_primary_ips(self):
1871
- """Test assigning a primary IP to a device."""
1872
- self.add_permissions("dcim.change_device")
1873
-
1874
- # Create an interface and assign an IP to it.
1875
- device = Device.objects.filter(interfaces__isnull=False).first()
1876
- interface = device.interfaces.first()
1877
- namespace = Namespace.objects.first()
1878
- Prefix.objects.create(prefix="1.2.3.0/24", namespace=namespace, status=self.prefix_status)
1879
- ip_address = IPAddress.objects.create(address="1.2.3.4/32", namespace=namespace, status=self.ipaddr_status)
1880
- interface.ip_addresses.add(ip_address)
1881
-
1882
- # Dupe the form data and populated primary_ip4 w/ ip_address
1883
- form_data = self.form_data.copy()
1884
- form_data["primary_ip4"] = ip_address.pk
1885
- # Assert that update succeeds.
1886
- request = {
1887
- "path": self._get_url("edit", device),
1888
- "data": post_data(form_data),
1889
- }
1890
- self.assertHttpStatus(self.client.post(**request), 302)
1891
- self.assertInstanceEqual(self._get_queryset().order_by("last_updated").last(), form_data)
1892
-
1893
- @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
1894
- def test_local_config_context_schema_validation_pass(self):
1895
- """
1896
- Given a config context schema
1897
- And a device with local context that conforms to that schema
1898
- Assert that the local context passes schema validation via full_clean()
1899
- """
1900
- schema = ConfigContextSchema.objects.create(
1901
- name="Schema 1",
1902
- data_schema={"type": "object", "properties": {"foo": {"type": "string"}}},
1903
- )
1904
- self.add_permissions("dcim.add_device")
1905
-
1906
- form_data = self.form_data.copy()
1907
- form_data["local_config_context_schema"] = schema.pk
1908
- form_data["local_config_context_data"] = '{"foo": "bar"}'
1909
-
1910
- # Try POST with model-level permission
1911
- request = {
1912
- "path": self._get_url("add"),
1913
- "data": post_data(form_data),
1914
- }
1915
- self.assertHttpStatus(self.client.post(**request), 302)
1916
- self.assertEqual(
1917
- self._get_queryset().get(name="Device X").local_config_context_schema.pk,
1918
- schema.pk,
1919
- )
1920
-
1921
- @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
1922
- def test_local_config_context_schema_validation_fails(self):
1923
- """
1924
- Given a config context schema
1925
- And a device with local context that *does not* conform to that schema
1926
- Assert that the local context fails schema validation via full_clean()
1927
- """
1928
- schema = ConfigContextSchema.objects.create(
1929
- name="Schema 1",
1930
- data_schema={"type": "object", "properties": {"foo": {"type": "integer"}}},
1931
- )
1932
- self.add_permissions("dcim.add_device")
1933
-
1934
- form_data = self.form_data.copy()
1935
- form_data["local_config_context_schema"] = schema.pk
1936
- form_data["local_config_context_data"] = '{"foo": "bar"}'
1937
-
1938
- # Try POST with model-level permission
1939
- request = {
1940
- "path": self._get_url("add"),
1941
- "data": post_data(form_data),
1942
- }
1943
- self.assertHttpStatus(self.client.post(**request), 200)
1944
- self.assertEqual(self._get_queryset().filter(name="Device X").count(), 0)
1945
-
1946
2806
 
1947
2807
  class ConsolePortTestCase(ViewTestCases.DeviceComponentViewTestCase):
1948
2808
  model = ConsolePort
@@ -1983,6 +2843,15 @@ class ConsolePortTestCase(ViewTestCases.DeviceComponentViewTestCase):
1983
2843
  "description": "New description",
1984
2844
  }
1985
2845
 
2846
+ test_instance = cls.model.objects.first()
2847
+ cls.update_data = {
2848
+ "name": test_instance.name,
2849
+ "device": getattr(getattr(test_instance, "device", None), "pk", None),
2850
+ "module": getattr(getattr(test_instance, "module", None), "pk", None),
2851
+ "label": "new test label",
2852
+ "description": "new test description",
2853
+ }
2854
+
1986
2855
 
1987
2856
  class ConsoleServerPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
1988
2857
  model = ConsoleServerPort
@@ -2022,6 +2891,15 @@ class ConsoleServerPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
2022
2891
  "description": "New description",
2023
2892
  }
2024
2893
 
2894
+ test_instance = cls.model.objects.first()
2895
+ cls.update_data = {
2896
+ "name": test_instance.name,
2897
+ "device": getattr(getattr(test_instance, "device", None), "pk", None),
2898
+ "module": getattr(getattr(test_instance, "module", None), "pk", None),
2899
+ "label": "new test label",
2900
+ "description": "new test description",
2901
+ }
2902
+
2025
2903
 
2026
2904
  class PowerPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
2027
2905
  model = PowerPort
@@ -2066,12 +2944,22 @@ class PowerPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
2066
2944
  "description": "New description",
2067
2945
  }
2068
2946
 
2947
+ test_instance = cls.model.objects.first()
2948
+ cls.update_data = {
2949
+ "name": test_instance.name,
2950
+ "device": getattr(getattr(test_instance, "device", None), "pk", None),
2951
+ "module": getattr(getattr(test_instance, "module", None), "pk", None),
2952
+ "label": "new test label",
2953
+ "description": "new test description",
2954
+ }
2955
+
2069
2956
 
2070
2957
  class PowerOutletTestCase(ViewTestCases.DeviceComponentViewTestCase):
2071
2958
  model = PowerOutlet
2072
2959
 
2073
2960
  @classmethod
2074
2961
  def setUpTestData(cls):
2962
+ PowerOutlet.objects.all().delete()
2075
2963
  device = create_test_device("Device 1")
2076
2964
 
2077
2965
  powerports = (
@@ -2124,12 +3012,23 @@ class PowerOutletTestCase(ViewTestCases.DeviceComponentViewTestCase):
2124
3012
  "description": "New description",
2125
3013
  }
2126
3014
 
3015
+ test_instance = cls.model.objects.first()
3016
+ cls.update_data = {
3017
+ "name": test_instance.name,
3018
+ "device_type": getattr(getattr(test_instance, "device_type", None), "pk", None),
3019
+ "module_type": getattr(getattr(test_instance, "module_type", None), "pk", None),
3020
+ "power_port": getattr(test_instance.power_port, "pk", None), # power_port must match parent device/module
3021
+ "label": "new test label",
3022
+ "description": "new test description",
3023
+ }
3024
+
2127
3025
 
2128
3026
  class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
2129
3027
  model = Interface
2130
3028
 
2131
3029
  @classmethod
2132
3030
  def setUpTestData(cls):
3031
+ Interface.objects.all().delete()
2133
3032
  device = create_test_device("Device 1")
2134
3033
  vrfs = list(VRF.objects.all()[:3])
2135
3034
  for vrf in vrfs:
@@ -2137,22 +3036,24 @@ class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
2137
3036
 
2138
3037
  statuses = Status.objects.get_for_model(Interface)
2139
3038
  status_active = statuses[0]
2140
-
3039
+ role = Role.objects.get_for_model(Interface).first()
2141
3040
  interfaces = (
2142
- Interface.objects.create(device=device, name="Interface 1", status=status_active),
2143
- Interface.objects.create(device=device, name="Interface 2", status=status_active),
2144
- Interface.objects.create(device=device, name="Interface 3", status=status_active),
3041
+ Interface.objects.create(device=device, name="Interface A1", status=status_active, role=role),
3042
+ Interface.objects.create(device=device, name="Interface A2", status=status_active),
3043
+ Interface.objects.create(device=device, name="Interface A3", status=status_active, role=role),
2145
3044
  Interface.objects.create(
2146
3045
  device=device,
2147
3046
  name="LAG",
2148
3047
  status=status_active,
2149
3048
  type=InterfaceTypeChoices.TYPE_LAG,
3049
+ role=role,
2150
3050
  ),
2151
3051
  Interface.objects.create(
2152
3052
  device=device,
2153
3053
  name="BRIDGE",
2154
3054
  status=status_active,
2155
3055
  type=InterfaceTypeChoices.TYPE_BRIDGE,
3056
+ role=role,
2156
3057
  ),
2157
3058
  )
2158
3059
  cls.lag_interface = interfaces[3]
@@ -2199,6 +3100,7 @@ class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
2199
3100
  "type": InterfaceTypeChoices.TYPE_1GE_GBIC,
2200
3101
  "enabled": False,
2201
3102
  "status": status_active.pk,
3103
+ "role": role.pk,
2202
3104
  "lag": interfaces[3].pk,
2203
3105
  "mac_address": EUI("01:02:03:04:05:06"),
2204
3106
  "mtu": 2000,
@@ -2227,6 +3129,7 @@ class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
2227
3129
  "tagged_vlans": [v.pk for v in vlans[1:4]],
2228
3130
  "tags": [t.pk for t in Tag.objects.get_for_model(Interface)],
2229
3131
  "status": status_active.pk,
3132
+ "role": role.pk,
2230
3133
  "vrf": vrfs[0].pk,
2231
3134
  }
2232
3135
 
@@ -2235,6 +3138,7 @@ class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
2235
3138
  "name_pattern": "Interface [4-6]",
2236
3139
  "label_pattern": "Interface Number [4-6]",
2237
3140
  "status": status_active.pk,
3141
+ "role": role.pk,
2238
3142
  "type": InterfaceTypeChoices.TYPE_1GE_GBIC,
2239
3143
  "enabled": True,
2240
3144
  "mtu": 1500,
@@ -2257,9 +3161,21 @@ class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
2257
3161
  "untagged_vlan": vlans[0].pk,
2258
3162
  "tagged_vlans": [v.pk for v in vlans[1:4]],
2259
3163
  "status": status_active.pk,
3164
+ "role": role.pk,
2260
3165
  "vrf": vrfs[2].pk,
2261
3166
  }
2262
3167
 
3168
+ test_instance = cls.model.objects.first()
3169
+ cls.update_data = {
3170
+ "name": test_instance.name,
3171
+ "device": getattr(getattr(test_instance, "device", None), "pk", None),
3172
+ "module": getattr(getattr(test_instance, "module", None), "pk", None),
3173
+ "status": test_instance.status.pk,
3174
+ "type": test_instance.type,
3175
+ "label": "new test label",
3176
+ "description": "new test description",
3177
+ }
3178
+
2263
3179
  @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
2264
3180
  def test_create_virtual_interface_with_parent_lag(self):
2265
3181
  """https://github.com/nautobot/nautobot/issues/4436."""
@@ -2305,18 +3221,66 @@ class FrontPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
2305
3221
  cls.device = device
2306
3222
 
2307
3223
  rearports = (
2308
- RearPort.objects.create(device=device, name="Rear Port 1"),
2309
- RearPort.objects.create(device=device, name="Rear Port 2"),
2310
- RearPort.objects.create(device=device, name="Rear Port 3"),
2311
- RearPort.objects.create(device=device, name="Rear Port 4"),
2312
- RearPort.objects.create(device=device, name="Rear Port 5"),
2313
- RearPort.objects.create(device=device, name="Rear Port 6"),
3224
+ RearPort.objects.create(
3225
+ device=device,
3226
+ type=PortTypeChoices.TYPE_8P8C,
3227
+ positions=24,
3228
+ name="Rear Port 1",
3229
+ ),
3230
+ RearPort.objects.create(
3231
+ device=device,
3232
+ type=PortTypeChoices.TYPE_8P8C,
3233
+ positions=24,
3234
+ name="Rear Port 2",
3235
+ ),
3236
+ RearPort.objects.create(
3237
+ device=device,
3238
+ type=PortTypeChoices.TYPE_8P8C,
3239
+ positions=24,
3240
+ name="Rear Port 3",
3241
+ ),
3242
+ RearPort.objects.create(
3243
+ device=device,
3244
+ type=PortTypeChoices.TYPE_8P8C,
3245
+ positions=24,
3246
+ name="Rear Port 4",
3247
+ ),
3248
+ RearPort.objects.create(
3249
+ device=device,
3250
+ type=PortTypeChoices.TYPE_8P8C,
3251
+ positions=24,
3252
+ name="Rear Port 5",
3253
+ ),
3254
+ RearPort.objects.create(
3255
+ device=device,
3256
+ type=PortTypeChoices.TYPE_8P8C,
3257
+ positions=24,
3258
+ name="Rear Port 6",
3259
+ ),
2314
3260
  )
2315
3261
 
2316
3262
  frontports = (
2317
- FrontPort.objects.create(device=device, name="Front Port 1", rear_port=rearports[0]),
2318
- FrontPort.objects.create(device=device, name="Front Port 2", rear_port=rearports[1]),
2319
- FrontPort.objects.create(device=device, name="Front Port 3", rear_port=rearports[2]),
3263
+ FrontPort.objects.create(
3264
+ device=device,
3265
+ name="Front Port 1",
3266
+ type=PortTypeChoices.TYPE_8P8C,
3267
+ rear_port=rearports[0],
3268
+ rear_port_position=12,
3269
+ ),
3270
+ FrontPort.objects.create(
3271
+ device=device,
3272
+ name="Front Port 2",
3273
+ type=PortTypeChoices.TYPE_8P8C,
3274
+ rear_port=rearports[1],
3275
+ rear_port_position=12,
3276
+ ),
3277
+ FrontPort.objects.create(
3278
+ device=device,
3279
+ name="Front Port 3",
3280
+ type=PortTypeChoices.TYPE_8P8C,
3281
+ rear_port=rearports[2],
3282
+ rear_port_position=12,
3283
+ ),
2320
3284
  )
2321
3285
  # Required by ViewTestCases.DeviceComponentViewTestCase.test_bulk_rename
2322
3286
  cls.selected_objects = frontports
@@ -2346,6 +3310,18 @@ class FrontPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
2346
3310
  "description": "New description",
2347
3311
  }
2348
3312
 
3313
+ test_instance = cls.model.objects.first()
3314
+ cls.update_data = {
3315
+ "name": test_instance.name,
3316
+ "device": getattr(getattr(test_instance, "device", None), "pk", None),
3317
+ "module": getattr(getattr(test_instance, "module", None), "pk", None),
3318
+ "rear_port": test_instance.rear_port.pk, # rear_port must match the parent device/module
3319
+ "rear_port_position": test_instance.rear_port_position,
3320
+ "type": test_instance.type,
3321
+ "label": "new test label",
3322
+ "description": "new test description",
3323
+ }
3324
+
2349
3325
  @unittest.skip("No DeviceBulkAddFrontPortView exists at present")
2350
3326
  def test_bulk_add_component(self):
2351
3327
  pass
@@ -2359,9 +3335,24 @@ class RearPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
2359
3335
  device = create_test_device("Device 1")
2360
3336
 
2361
3337
  rearports = (
2362
- RearPort.objects.create(device=device, name="Rear Port 1"),
2363
- RearPort.objects.create(device=device, name="Rear Port 2"),
2364
- RearPort.objects.create(device=device, name="Rear Port 3"),
3338
+ RearPort.objects.create(
3339
+ device=device,
3340
+ type=PortTypeChoices.TYPE_8P8C,
3341
+ positions=24,
3342
+ name="Rear Port 1",
3343
+ ),
3344
+ RearPort.objects.create(
3345
+ device=device,
3346
+ type=PortTypeChoices.TYPE_8P8C,
3347
+ positions=24,
3348
+ name="Rear Port 2",
3349
+ ),
3350
+ RearPort.objects.create(
3351
+ device=device,
3352
+ type=PortTypeChoices.TYPE_8P8C,
3353
+ positions=24,
3354
+ name="Rear Port 3",
3355
+ ),
2365
3356
  )
2366
3357
  # Required by ViewTestCases.DeviceComponentViewTestCase.test_bulk_rename
2367
3358
  cls.selected_objects = rearports
@@ -2390,6 +3381,17 @@ class RearPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
2390
3381
  "description": "New description",
2391
3382
  }
2392
3383
 
3384
+ test_instance = cls.model.objects.first()
3385
+ cls.update_data = {
3386
+ "name": test_instance.name,
3387
+ "device": getattr(getattr(test_instance, "device", None), "pk", None),
3388
+ "module": getattr(getattr(test_instance, "module", None), "pk", None),
3389
+ "positions": test_instance.positions,
3390
+ "type": test_instance.type,
3391
+ "label": "new test label",
3392
+ "description": "new test description",
3393
+ }
3394
+
2393
3395
 
2394
3396
  class DeviceBayTestCase(ViewTestCases.DeviceComponentViewTestCase):
2395
3397
  model = DeviceBay
@@ -2428,6 +3430,113 @@ class DeviceBayTestCase(ViewTestCases.DeviceComponentViewTestCase):
2428
3430
  "description": "New description",
2429
3431
  }
2430
3432
 
3433
+ test_instance = cls.model.objects.first()
3434
+ cls.update_data = {
3435
+ "name": test_instance.name,
3436
+ "device": test_instance.device.pk,
3437
+ "label": "new test label",
3438
+ "description": "new test description",
3439
+ }
3440
+
3441
+
3442
+ class ModuleBayTestCase(ViewTestCases.DeviceComponentViewTestCase):
3443
+ model = ModuleBay
3444
+
3445
+ @classmethod
3446
+ def setUpTestData(cls):
3447
+ device = Device.objects.first()
3448
+ module = Module.objects.first()
3449
+
3450
+ module_bays = (
3451
+ ModuleBay.objects.create(parent_device=device, name="Test View Module Bay 1"),
3452
+ ModuleBay.objects.create(parent_device=device, name="Test View Module Bay 2"),
3453
+ ModuleBay.objects.create(parent_device=device, name="Test View Module Bay 3"),
3454
+ )
3455
+ # Required by ViewTestCases.DeviceComponentViewTestCase.test_bulk_rename
3456
+ cls.selected_objects = module_bays
3457
+ cls.selected_objects_parent_name = device.name
3458
+
3459
+ cls.form_data = {
3460
+ "parent_device": device.pk,
3461
+ "name": "Test ModuleBay 1",
3462
+ "position": 1,
3463
+ "description": "Test modulebay description",
3464
+ "label": "Test modulebay label",
3465
+ "tags": sorted([t.pk for t in Tag.objects.get_for_model(ModuleBay)]),
3466
+ }
3467
+
3468
+ cls.bulk_create_data = {
3469
+ "parent_module": module.pk,
3470
+ "name_pattern": "Test ModuleBay [0-2]",
3471
+ "position_pattern": "[1-3]",
3472
+ # Test that a label can be applied to each generated module bay
3473
+ "label_pattern": "Slot[1-3]",
3474
+ "description": "Test modulebay description",
3475
+ "tags": sorted([t.pk for t in Tag.objects.get_for_model(ModuleBay)]),
3476
+ }
3477
+
3478
+ cls.bulk_edit_data = {
3479
+ "position": "new position",
3480
+ "description": "New description",
3481
+ "label": "New label",
3482
+ }
3483
+
3484
+ test_instance = cls.model.objects.first()
3485
+ cls.update_data = {
3486
+ "name": test_instance.name,
3487
+ "parent_device": getattr(getattr(test_instance, "parent_device", None), "pk", None),
3488
+ "parent_module": getattr(getattr(test_instance, "parent_module", None), "pk", None),
3489
+ "position": "new test position",
3490
+ "label": "new test label",
3491
+ "description": "new test description",
3492
+ }
3493
+
3494
+ def get_deletable_object_pks(self):
3495
+ # Since Modules and ModuleBays are nestable, we need to delete ModuleBays that don't have any child ModuleBays
3496
+ return ModuleBay.objects.filter(installed_module__isnull=True).values_list("pk", flat=True)[:3]
3497
+
3498
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
3499
+ def test_bulk_add_component(self):
3500
+ """Test bulk-adding this component to modules."""
3501
+ obj_perm = ObjectPermission(name="Test permission", actions=["add"])
3502
+ obj_perm.save()
3503
+ obj_perm.users.add(self.user)
3504
+ obj_perm.object_types.add(ContentType.objects.get_for_model(self.model))
3505
+
3506
+ initial_count = self._get_queryset().count()
3507
+
3508
+ data = self.bulk_create_data.copy()
3509
+
3510
+ # Load the module-bulk-add form
3511
+ module_perm = ObjectPermission(name="Module permission", actions=["change"])
3512
+ module_perm.save()
3513
+ module_perm.users.add(self.user)
3514
+ module_perm.object_types.add(ContentType.objects.get_for_model(Module))
3515
+ url = reverse(f"dcim:module_bulk_add_{self.model._meta.model_name}")
3516
+ request = {
3517
+ "path": url,
3518
+ "data": post_data({"pk": data["parent_module"]}),
3519
+ }
3520
+ self.assertHttpStatus(self.client.post(**request), 200)
3521
+
3522
+ # Post to the module-bulk-add form to create records
3523
+ data["pk"] = data.pop("parent_module")
3524
+ data["_create"] = ""
3525
+ request["data"] = post_data(data)
3526
+ self.assertHttpStatus(self.client.post(**request), 302)
3527
+
3528
+ updated_count = self._get_queryset().count()
3529
+ self.assertEqual(updated_count, initial_count + self.bulk_create_count)
3530
+
3531
+ matching_count = 0
3532
+ for instance in self._get_queryset().all():
3533
+ try:
3534
+ self.assertInstanceEqual(instance, self.bulk_create_data)
3535
+ matching_count += 1
3536
+ except AssertionError:
3537
+ pass
3538
+ self.assertEqual(matching_count, self.bulk_create_count)
3539
+
2431
3540
 
2432
3541
  class InventoryItemTestCase(ViewTestCases.DeviceComponentViewTestCase):
2433
3542
  model = InventoryItem
@@ -2480,6 +3589,14 @@ class InventoryItemTestCase(ViewTestCases.DeviceComponentViewTestCase):
2480
3589
  "software_version": software_versions[2].pk,
2481
3590
  }
2482
3591
 
3592
+ test_instance = cls.model.objects.first()
3593
+ cls.update_data = {
3594
+ "name": test_instance.name,
3595
+ "device": test_instance.device.pk,
3596
+ "label": "new test label",
3597
+ "description": "new test description",
3598
+ }
3599
+
2483
3600
  def test_table_with_indentation_is_removed_on_filter_or_sort(self):
2484
3601
  self.skipTest("InventoryItem table has no implementation of indentation.")
2485
3602
 
@@ -2540,73 +3657,73 @@ class CableTestCase(
2540
3657
  interfaces = (
2541
3658
  Interface.objects.create(
2542
3659
  device=devices[0],
2543
- name="Interface 1",
3660
+ name="Interface A1",
2544
3661
  type=InterfaceTypeChoices.TYPE_1GE_FIXED,
2545
3662
  status=interface_status,
2546
3663
  ),
2547
3664
  Interface.objects.create(
2548
3665
  device=devices[0],
2549
- name="Interface 2",
3666
+ name="Interface A2",
2550
3667
  type=InterfaceTypeChoices.TYPE_1GE_FIXED,
2551
3668
  status=interface_status,
2552
3669
  ),
2553
3670
  Interface.objects.create(
2554
3671
  device=devices[0],
2555
- name="Interface 3",
3672
+ name="Interface A3",
2556
3673
  type=InterfaceTypeChoices.TYPE_1GE_FIXED,
2557
3674
  status=interface_status,
2558
3675
  ),
2559
3676
  Interface.objects.create(
2560
3677
  device=devices[1],
2561
- name="Interface 1",
3678
+ name="Interface A1",
2562
3679
  type=InterfaceTypeChoices.TYPE_1GE_FIXED,
2563
3680
  status=interface_status,
2564
3681
  ),
2565
3682
  Interface.objects.create(
2566
3683
  device=devices[1],
2567
- name="Interface 2",
3684
+ name="Interface A2",
2568
3685
  type=InterfaceTypeChoices.TYPE_1GE_FIXED,
2569
3686
  status=interface_status,
2570
3687
  ),
2571
3688
  Interface.objects.create(
2572
3689
  device=devices[1],
2573
- name="Interface 3",
3690
+ name="Interface A3",
2574
3691
  type=InterfaceTypeChoices.TYPE_1GE_FIXED,
2575
3692
  status=interface_status,
2576
3693
  ),
2577
3694
  Interface.objects.create(
2578
3695
  device=devices[2],
2579
- name="Interface 1",
3696
+ name="Interface A1",
2580
3697
  type=InterfaceTypeChoices.TYPE_1GE_FIXED,
2581
3698
  status=interface_status,
2582
3699
  ),
2583
3700
  Interface.objects.create(
2584
3701
  device=devices[2],
2585
- name="Interface 2",
3702
+ name="Interface A2",
2586
3703
  type=InterfaceTypeChoices.TYPE_1GE_FIXED,
2587
3704
  status=interface_status,
2588
3705
  ),
2589
3706
  Interface.objects.create(
2590
3707
  device=devices[2],
2591
- name="Interface 3",
3708
+ name="Interface A3",
2592
3709
  type=InterfaceTypeChoices.TYPE_1GE_FIXED,
2593
3710
  status=interface_status,
2594
3711
  ),
2595
3712
  Interface.objects.create(
2596
3713
  device=devices[3],
2597
- name="Interface 1",
3714
+ name="Interface A1",
2598
3715
  type=InterfaceTypeChoices.TYPE_1GE_FIXED,
2599
3716
  status=interface_status,
2600
3717
  ),
2601
3718
  Interface.objects.create(
2602
3719
  device=devices[3],
2603
- name="Interface 2",
3720
+ name="Interface A2",
2604
3721
  type=InterfaceTypeChoices.TYPE_1GE_FIXED,
2605
3722
  status=interface_status,
2606
3723
  ),
2607
3724
  Interface.objects.create(
2608
3725
  device=devices[3],
2609
- name="Interface 3",
3726
+ name="Interface A3",
2610
3727
  type=InterfaceTypeChoices.TYPE_1GE_FIXED,
2611
3728
  status=interface_status,
2612
3729
  ),
@@ -2758,6 +3875,9 @@ class ConsoleConnectionsTestCase(ViewTestCases.ListObjectsViewTestCase):
2758
3875
  def _get_base_url(self):
2759
3876
  return "dcim:console_connections_{}"
2760
3877
 
3878
+ def _get_queryset(self):
3879
+ return ConsolePort.objects.filter(cable__isnull=False)
3880
+
2761
3881
  def get_list_url(self):
2762
3882
  return "/dcim/console-connections/"
2763
3883
 
@@ -2810,15 +3930,18 @@ class PowerConnectionsTestCase(ViewTestCases.ListObjectsViewTestCase):
2810
3930
  Test the PowerConnectionsListView.
2811
3931
  """
2812
3932
 
3933
+ def _get_base_url(self):
3934
+ return "dcim:power_connections_{}"
3935
+
3936
+ def _get_queryset(self):
3937
+ return PowerPort.objects.filter(cable__isnull=False)
3938
+
2813
3939
  def get_list_url(self):
2814
3940
  return "/dcim/power-connections/"
2815
3941
 
2816
3942
  def get_title(self):
2817
3943
  return "Power Connections"
2818
3944
 
2819
- def _get_base_url(self):
2820
- return "dcim:power_connections_{}"
2821
-
2822
3945
  def get_list_view(self):
2823
3946
  return PowerConnectionsListView
2824
3947
 
@@ -2875,6 +3998,9 @@ class InterfaceConnectionsTestCase(ViewTestCases.ListObjectsViewTestCase):
2875
3998
  def _get_base_url(self):
2876
3999
  return "dcim:interface_connections_{}"
2877
4000
 
4001
+ def _get_queryset(self):
4002
+ return Interface.objects.filter(cable__isnull=False)
4003
+
2878
4004
  def get_list_url(self):
2879
4005
  return "/dcim/interface-connections/"
2880
4006
 
@@ -2895,22 +4021,25 @@ class InterfaceConnectionsTestCase(ViewTestCases.ListObjectsViewTestCase):
2895
4021
  device_2 = create_test_device("Device 2")
2896
4022
 
2897
4023
  interface_status = Status.objects.get_for_model(Interface).first()
4024
+ interface_role = Role.objects.get_for_model(Interface).first()
2898
4025
  cls.interfaces = (
2899
4026
  Interface.objects.create(
2900
4027
  device=device_1,
2901
- name="Interface 1",
4028
+ name="Interface A1",
2902
4029
  type=InterfaceTypeChoices.TYPE_1GE_SFP,
2903
4030
  status=interface_status,
4031
+ role=interface_role,
2904
4032
  ),
2905
4033
  Interface.objects.create(
2906
4034
  device=device_1,
2907
- name="Interface 2",
4035
+ name="Interface A2",
2908
4036
  type=InterfaceTypeChoices.TYPE_1GE_SFP,
2909
4037
  status=interface_status,
4038
+ role=interface_role,
2910
4039
  ),
2911
4040
  Interface.objects.create(
2912
4041
  device=device_1,
2913
- name="Interface 3",
4042
+ name="Interface A3",
2914
4043
  type=InterfaceTypeChoices.TYPE_1GE_SFP,
2915
4044
  status=interface_status,
2916
4045
  ),
@@ -2918,9 +4047,10 @@ class InterfaceConnectionsTestCase(ViewTestCases.ListObjectsViewTestCase):
2918
4047
 
2919
4048
  cls.device_2_interface = Interface.objects.create(
2920
4049
  device=device_2,
2921
- name="Interface 1",
4050
+ name="Interface A1",
2922
4051
  type=InterfaceTypeChoices.TYPE_1GE_SFP,
2923
4052
  status=interface_status,
4053
+ role=interface_role,
2924
4054
  )
2925
4055
  rearport = RearPort.objects.create(device=device_2, type=PortTypeChoices.TYPE_8P8C)
2926
4056
 
@@ -3326,11 +4456,11 @@ class InterfaceRedundancyGroupTestCase(ViewTestCases.PrimaryObjectViewTestCase):
3326
4456
  status=status_active,
3327
4457
  )
3328
4458
  intf_status = Status.objects.get_for_model(Interface).first()
3329
-
4459
+ intf_role = Role.objects.get_for_model(Interface).first()
3330
4460
  cls.interfaces = (
3331
- Interface.objects.create(device=device, name="Interface 1", status=intf_status),
3332
- Interface.objects.create(device=device, name="Interface 2", status=intf_status),
3333
- Interface.objects.create(device=device, name="Interface 3", status=intf_status),
4461
+ Interface.objects.create(device=device, name="Interface A1", status=intf_status, role=intf_role),
4462
+ Interface.objects.create(device=device, name="Interface A2", status=intf_status),
4463
+ Interface.objects.create(device=device, name="Interface A3", status=intf_status, role=intf_role),
3334
4464
  )
3335
4465
 
3336
4466
  cls.form_data = {