nautobot 2.2.8__py3-none-any.whl → 2.3.0__py3-none-any.whl

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

Potentially problematic release.


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

Files changed (704) hide show
  1. nautobot/apps/forms.py +4 -0
  2. nautobot/apps/models.py +10 -1
  3. nautobot/circuits/__init__.py +0 -1
  4. nautobot/circuits/apps.py +1 -0
  5. nautobot/circuits/factory.py +15 -3
  6. nautobot/circuits/filters.py +13 -0
  7. nautobot/circuits/forms.py +13 -0
  8. nautobot/circuits/migrations/0021_alter_circuit_status_alter_circuittermination__path.py +32 -0
  9. nautobot/circuits/migrations/0022_circuittermination_cloud_network.py +25 -0
  10. nautobot/circuits/models.py +16 -3
  11. nautobot/circuits/tables.py +16 -2
  12. nautobot/circuits/templates/circuits/circuittermination_create.html +10 -2
  13. nautobot/circuits/templates/circuits/circuittermination_retrieve.html +6 -0
  14. nautobot/circuits/templates/circuits/inc/circuit_termination.html +6 -1
  15. nautobot/circuits/tests/test_api.py +7 -5
  16. nautobot/circuits/tests/test_filters.py +12 -5
  17. nautobot/circuits/tests/test_models.py +33 -2
  18. nautobot/circuits/views.py +2 -3
  19. nautobot/cloud/__init__.py +0 -0
  20. nautobot/cloud/api/__init__.py +0 -0
  21. nautobot/cloud/api/serializers.py +54 -0
  22. nautobot/cloud/api/urls.py +16 -0
  23. nautobot/cloud/api/views.py +48 -0
  24. nautobot/cloud/apps.py +13 -0
  25. nautobot/cloud/factory.py +113 -0
  26. nautobot/cloud/filters.py +187 -0
  27. nautobot/cloud/forms.py +339 -0
  28. nautobot/cloud/homepage.py +43 -0
  29. nautobot/cloud/migrations/0001_initial.py +304 -0
  30. nautobot/cloud/migrations/__init__.py +0 -0
  31. nautobot/cloud/models.py +246 -0
  32. nautobot/cloud/navigation.py +85 -0
  33. nautobot/cloud/tables.py +157 -0
  34. nautobot/cloud/templates/cloud/cloudaccount_retrieve.html +43 -0
  35. nautobot/cloud/templates/cloud/cloudnetwork_retrieve.html +122 -0
  36. nautobot/cloud/templates/cloud/cloudnetwork_update.html +33 -0
  37. nautobot/cloud/templates/cloud/cloudresourcetype_retrieve.html +111 -0
  38. nautobot/cloud/templates/cloud/cloudservice_retrieve.html +69 -0
  39. nautobot/cloud/templates/cloud/cloudservice_update.html +25 -0
  40. nautobot/cloud/tests/__init__.py +0 -0
  41. nautobot/cloud/tests/test_api.py +248 -0
  42. nautobot/cloud/tests/test_filters.py +125 -0
  43. nautobot/cloud/tests/test_models.py +43 -0
  44. nautobot/cloud/tests/test_views.py +153 -0
  45. nautobot/cloud/urls.py +14 -0
  46. nautobot/cloud/views.py +181 -0
  47. nautobot/core/__init__.py +0 -3
  48. nautobot/core/api/metadata.py +1 -0
  49. nautobot/core/api/parsers.py +7 -1
  50. nautobot/core/api/urls.py +1 -0
  51. nautobot/core/api/utils.py +1 -0
  52. nautobot/core/api/views.py +4 -0
  53. nautobot/core/apps/__init__.py +6 -3
  54. nautobot/core/constants.py +8 -0
  55. nautobot/core/factory.py +32 -1
  56. nautobot/core/filters.py +110 -14
  57. nautobot/core/forms/fields.py +10 -4
  58. nautobot/core/forms/forms.py +11 -3
  59. nautobot/core/forms/widgets.py +18 -1
  60. nautobot/core/graphql/generators.py +2 -2
  61. nautobot/core/graphql/schema.py +28 -7
  62. nautobot/core/jobs/__init__.py +20 -3
  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 +15 -19
  73. nautobot/core/settings.yaml +48 -13
  74. nautobot/core/settings_funcs.py +103 -0
  75. nautobot/core/tables.py +130 -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 +28 -8
  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 +63 -14
  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 +204 -2
  120. nautobot/core/tests/test_tables.py +3 -1
  121. nautobot/core/tests/test_templatetags_helpers.py +12 -5
  122. nautobot/core/tests/test_templatetags_netutils.py +3 -3
  123. nautobot/core/tests/test_utils.py +31 -20
  124. nautobot/core/tests/test_views.py +6 -6
  125. nautobot/core/urls.py +8 -3
  126. nautobot/core/utils/deprecation.py +29 -0
  127. nautobot/core/utils/filtering.py +12 -9
  128. nautobot/core/utils/lookup.py +37 -2
  129. nautobot/core/utils/requests.py +4 -1
  130. nautobot/core/views/__init__.py +137 -24
  131. nautobot/core/views/generic.py +119 -67
  132. nautobot/core/views/mixins.py +105 -36
  133. nautobot/core/views/paginator.py +9 -3
  134. nautobot/core/views/renderers.py +121 -56
  135. nautobot/core/views/utils.py +81 -1
  136. nautobot/dcim/__init__.py +0 -1
  137. nautobot/dcim/api/serializers.py +180 -44
  138. nautobot/dcim/api/urls.py +7 -3
  139. nautobot/dcim/api/views.py +53 -7
  140. nautobot/dcim/apps.py +3 -0
  141. nautobot/dcim/choices.py +25 -0
  142. nautobot/dcim/constants.py +7 -0
  143. nautobot/dcim/factory.py +252 -18
  144. nautobot/dcim/filters/__init__.py +373 -193
  145. nautobot/dcim/filters/mixins.py +274 -1
  146. nautobot/dcim/forms.py +834 -121
  147. nautobot/dcim/graphql/types.py +2 -2
  148. nautobot/dcim/homepage.py +1 -1
  149. nautobot/dcim/migrations/0059_add_role_field_to_interface_models.py +27 -0
  150. nautobot/dcim/migrations/0060_alter_cable_status_alter_consoleport__path_and_more.py +303 -0
  151. nautobot/dcim/migrations/0061_module_models.py +862 -0
  152. nautobot/dcim/migrations/0062_module_data_migration.py +25 -0
  153. nautobot/dcim/models/__init__.py +8 -0
  154. nautobot/dcim/models/cables.py +15 -0
  155. nautobot/dcim/models/device_component_templates.py +207 -53
  156. nautobot/dcim/models/device_components.py +282 -99
  157. nautobot/dcim/models/devices.py +472 -13
  158. nautobot/dcim/models/racks.py +0 -1
  159. nautobot/dcim/navigation.py +47 -0
  160. nautobot/dcim/signals.py +3 -3
  161. nautobot/dcim/tables/__init__.py +35 -23
  162. nautobot/dcim/tables/devices.py +248 -47
  163. nautobot/dcim/tables/devicetypes.py +65 -9
  164. nautobot/dcim/tables/racks.py +5 -1
  165. nautobot/dcim/tables/template_code.py +46 -26
  166. nautobot/dcim/templates/dcim/cable_connect.html +76 -3
  167. nautobot/dcim/templates/dcim/console_port_connection_list.html +7 -5
  168. nautobot/dcim/templates/dcim/device/base.html +14 -6
  169. nautobot/dcim/templates/dcim/device/consoleports.html +2 -3
  170. nautobot/dcim/templates/dcim/device/consoleserverports.html +2 -3
  171. nautobot/dcim/templates/dcim/device/devicebays.html +6 -7
  172. nautobot/dcim/templates/dcim/device/frontports.html +2 -3
  173. nautobot/dcim/templates/dcim/device/interfaces.html +2 -3
  174. nautobot/dcim/templates/dcim/device/inventory.html +2 -3
  175. nautobot/dcim/templates/dcim/device/modulebays.html +49 -0
  176. nautobot/dcim/templates/dcim/device/poweroutlets.html +2 -3
  177. nautobot/dcim/templates/dcim/device/powerports.html +2 -3
  178. nautobot/dcim/templates/dcim/device/rearports.html +2 -3
  179. nautobot/dcim/templates/dcim/device.html +45 -1
  180. nautobot/dcim/templates/dcim/device_component.html +13 -5
  181. nautobot/dcim/templates/dcim/device_list.html +2 -1
  182. nautobot/dcim/templates/dcim/deviceredundancygroup_retrieve.html +6 -0
  183. nautobot/dcim/templates/dcim/devicetype.html +99 -98
  184. nautobot/dcim/templates/dcim/devicetype_list.html +8 -16
  185. nautobot/dcim/templates/dcim/inc/devicetype_component_table.html +1 -1
  186. nautobot/dcim/templates/dcim/inc/moduletype_component_table.html +39 -0
  187. nautobot/dcim/templates/dcim/interface.html +17 -2
  188. nautobot/dcim/templates/dcim/interface_connection_list.html +7 -5
  189. nautobot/dcim/templates/dcim/interface_edit.html +1 -0
  190. nautobot/dcim/templates/dcim/manufacturer.html +24 -0
  191. nautobot/dcim/templates/dcim/module/base.html +97 -0
  192. nautobot/dcim/templates/dcim/module_bulk_destroy.html +5 -0
  193. nautobot/dcim/templates/dcim/module_consoleports.html +53 -0
  194. nautobot/dcim/templates/dcim/module_consoleserverports.html +53 -0
  195. nautobot/dcim/templates/dcim/module_destroy.html +5 -0
  196. nautobot/dcim/templates/dcim/module_frontports.html +53 -0
  197. nautobot/dcim/templates/dcim/module_interfaces.html +57 -0
  198. nautobot/dcim/templates/dcim/module_list.html +20 -0
  199. nautobot/dcim/templates/dcim/module_modulebays.html +49 -0
  200. nautobot/dcim/templates/dcim/module_poweroutlets.html +53 -0
  201. nautobot/dcim/templates/dcim/module_powerports.html +53 -0
  202. nautobot/dcim/templates/dcim/module_rearports.html +53 -0
  203. nautobot/dcim/templates/dcim/module_retrieve.html +63 -0
  204. nautobot/dcim/templates/dcim/module_update.html +71 -0
  205. nautobot/dcim/templates/dcim/modulebay_bulk_destroy.html +5 -0
  206. nautobot/dcim/templates/dcim/modulebay_destroy.html +8 -0
  207. nautobot/dcim/templates/dcim/modulebay_retrieve.html +101 -0
  208. nautobot/dcim/templates/dcim/moduletype_list.html +11 -0
  209. nautobot/dcim/templates/dcim/moduletype_retrieve.html +159 -0
  210. nautobot/dcim/templates/dcim/power_port_connection_list.html +7 -5
  211. nautobot/dcim/templates/dcim/softwareimagefile_retrieve.html +65 -19
  212. nautobot/dcim/tests/integration/test_cable_connect_form.py +4 -4
  213. nautobot/dcim/tests/test_api.py +693 -208
  214. nautobot/dcim/tests/test_filters.py +843 -217
  215. nautobot/dcim/tests/test_models.py +1103 -8
  216. nautobot/dcim/tests/test_views.py +1525 -343
  217. nautobot/dcim/urls.py +17 -2
  218. nautobot/dcim/utils.py +2 -3
  219. nautobot/dcim/views.py +1109 -113
  220. nautobot/extras/__init__.py +0 -1
  221. nautobot/extras/api/serializers.py +115 -3
  222. nautobot/extras/api/urls.py +12 -0
  223. nautobot/extras/api/views.py +73 -59
  224. nautobot/extras/apps.py +2 -2
  225. nautobot/extras/choices.py +43 -0
  226. nautobot/extras/context_managers.py +13 -8
  227. nautobot/extras/datasources/git.py +2 -0
  228. nautobot/extras/factory.py +460 -9
  229. nautobot/extras/filters/__init__.py +174 -3
  230. nautobot/extras/filters/mixins.py +46 -43
  231. nautobot/extras/forms/base.py +24 -5
  232. nautobot/extras/forms/forms.py +227 -8
  233. nautobot/extras/forms/mixins.py +93 -0
  234. nautobot/extras/graphql/types.py +23 -10
  235. nautobot/extras/homepage.py +26 -3
  236. nautobot/extras/jobs.py +2 -2
  237. nautobot/extras/management/__init__.py +1 -0
  238. nautobot/extras/management/commands/refresh_dynamic_group_member_caches.py +1 -16
  239. nautobot/extras/migrations/0021_customfield_changelog_data.py +1 -0
  240. nautobot/extras/migrations/0109_dynamicgroup_group_type_dynamicgroup_tags_and_more.py +108 -0
  241. nautobot/extras/migrations/0110_alter_configcontext_cluster_groups_and_more.py +111 -0
  242. nautobot/extras/migrations/0111_metadata.py +162 -0
  243. nautobot/extras/migrations/0112_dynamic_group_group_type_data_migration.py +28 -0
  244. nautobot/extras/migrations/0113_saved_views.py +77 -0
  245. nautobot/extras/models/__init__.py +15 -1
  246. nautobot/extras/models/change_logging.py +3 -3
  247. nautobot/extras/models/contacts.py +4 -0
  248. nautobot/extras/models/customfields.py +18 -3
  249. nautobot/extras/models/groups.py +389 -225
  250. nautobot/extras/models/jobs.py +87 -3
  251. nautobot/extras/models/metadata.py +441 -0
  252. nautobot/extras/models/mixins.py +72 -62
  253. nautobot/extras/models/models.py +118 -9
  254. nautobot/extras/models/relationships.py +9 -2
  255. nautobot/extras/models/tags.py +13 -2
  256. nautobot/extras/navigation.py +57 -0
  257. nautobot/extras/plugins/__init__.py +3 -1
  258. nautobot/extras/querysets.py +30 -66
  259. nautobot/extras/signals.py +109 -101
  260. nautobot/extras/tables.py +201 -17
  261. nautobot/extras/templates/extras/dynamicgroup.html +44 -15
  262. nautobot/extras/templates/extras/dynamicgroup_edit.html +2 -0
  263. nautobot/extras/templates/extras/job.html +1 -1
  264. nautobot/extras/templates/extras/job_detail.html +11 -0
  265. nautobot/extras/templates/extras/jobresult.html +61 -74
  266. nautobot/extras/templates/extras/metadatatype_create.html +89 -0
  267. nautobot/extras/templates/extras/metadatatype_retrieve.html +67 -0
  268. nautobot/extras/templates/extras/object_dynamicgroups.html +7 -0
  269. nautobot/extras/templates/extras/objectchange_list.html +0 -12
  270. nautobot/extras/templates/extras/plugins_list.html +1 -3
  271. nautobot/extras/templates/extras/role_retrieve.html +48 -0
  272. nautobot/extras/templates/extras/staticgroupassociation_retrieve.html +20 -0
  273. nautobot/extras/tests/integration/test_customfields.py +1 -0
  274. nautobot/extras/tests/test_api.py +509 -23
  275. nautobot/extras/tests/test_changelog.py +20 -9
  276. nautobot/extras/tests/test_context_managers.py +22 -15
  277. nautobot/extras/tests/test_datasources.py +13 -1
  278. nautobot/extras/tests/test_dynamicgroups.py +201 -171
  279. nautobot/extras/tests/test_filters.py +211 -12
  280. nautobot/extras/tests/test_jobs.py +6 -6
  281. nautobot/extras/tests/test_models.py +501 -4
  282. nautobot/extras/tests/test_relationships.py +1 -0
  283. nautobot/extras/tests/test_views.py +586 -8
  284. nautobot/extras/tests/test_webhooks.py +1 -1
  285. nautobot/extras/urls.py +5 -0
  286. nautobot/extras/utils.py +85 -16
  287. nautobot/extras/views.py +562 -122
  288. nautobot/ipam/__init__.py +0 -1
  289. nautobot/ipam/apps.py +1 -0
  290. nautobot/ipam/factory.py +17 -19
  291. nautobot/ipam/filters.py +13 -0
  292. nautobot/ipam/forms.py +8 -4
  293. nautobot/ipam/graphql/types.py +2 -2
  294. nautobot/ipam/migrations/0047_alter_ipaddress_role_alter_ipaddress_status_and_more.py +59 -0
  295. nautobot/ipam/models.py +20 -20
  296. nautobot/ipam/querysets.py +1 -1
  297. nautobot/ipam/signals.py +4 -2
  298. nautobot/ipam/tables.py +5 -0
  299. nautobot/ipam/templates/ipam/ipaddress_interfaces.html +1 -1
  300. nautobot/ipam/templates/ipam/ipaddress_vm_interfaces.html +1 -1
  301. nautobot/ipam/templates/ipam/prefix.html +1 -0
  302. nautobot/ipam/tests/test_api.py +37 -18
  303. nautobot/ipam/tests/test_filters.py +26 -2
  304. nautobot/ipam/tests/test_models.py +9 -2
  305. nautobot/ipam/tests/test_querysets.py +1 -1
  306. nautobot/ipam/tests/test_views.py +3 -2
  307. nautobot/ipam/urls.py +2 -2
  308. nautobot/ipam/views.py +20 -34
  309. nautobot/project-static/css/base.css +21 -0
  310. nautobot/project-static/css/dark.css +11 -0
  311. nautobot/project-static/docs/404.html +894 -90
  312. nautobot/project-static/docs/apps/index.html +894 -90
  313. nautobot/project-static/docs/apps/nautobot-apps.html +894 -90
  314. nautobot/project-static/docs/assets/_mkdocstrings.css +5 -0
  315. nautobot/project-static/docs/assets/stylesheets/main.3cba04c6.min.css +1 -0
  316. nautobot/project-static/docs/assets/stylesheets/main.3cba04c6.min.css.map +1 -0
  317. nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +921 -122
  318. nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +906 -103
  319. nautobot/project-static/docs/code-reference/nautobot/apps/api.html +1620 -905
  320. nautobot/project-static/docs/code-reference/nautobot/apps/change_logging.html +937 -146
  321. nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +979 -190
  322. nautobot/project-static/docs/code-reference/nautobot/apps/config.html +903 -101
  323. nautobot/project-static/docs/code-reference/nautobot/apps/constants.html +899 -95
  324. nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +993 -195
  325. nautobot/project-static/docs/code-reference/nautobot/apps/exceptions.html +976 -133
  326. nautobot/project-static/docs/code-reference/nautobot/apps/factory.html +1080 -274
  327. nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +1244 -336
  328. nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +1729 -877
  329. nautobot/project-static/docs/code-reference/nautobot/apps/graphql.html +1166 -383
  330. nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +2090 -1376
  331. nautobot/project-static/docs/code-reference/nautobot/apps/models.html +2248 -1424
  332. nautobot/project-static/docs/code-reference/nautobot/apps/querysets.html +914 -113
  333. nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +965 -165
  334. nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +1012 -225
  335. nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +1915 -1279
  336. nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +1848 -1104
  337. nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +906 -103
  338. nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +2335 -1701
  339. nautobot/project-static/docs/code-reference/nautobot/apps/views.html +1804 -1026
  340. nautobot/project-static/docs/development/apps/api/configuration-view.html +894 -90
  341. nautobot/project-static/docs/development/apps/api/database-backend-config.html +894 -90
  342. nautobot/project-static/docs/development/apps/api/models/django-admin.html +894 -90
  343. nautobot/project-static/docs/development/apps/api/models/global-search.html +894 -90
  344. nautobot/project-static/docs/development/apps/api/models/graphql.html +894 -90
  345. nautobot/project-static/docs/development/apps/api/models/index.html +944 -92
  346. nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +894 -90
  347. nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +894 -90
  348. nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +894 -90
  349. nautobot/project-static/docs/development/apps/api/platform-features/git-repository-content.html +894 -90
  350. nautobot/project-static/docs/development/apps/api/platform-features/index.html +894 -90
  351. nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +894 -90
  352. nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +894 -90
  353. nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +894 -90
  354. nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +894 -90
  355. nautobot/project-static/docs/development/apps/api/platform-features/uniquely-identify-objects.html +894 -90
  356. nautobot/project-static/docs/development/apps/api/prometheus.html +894 -90
  357. nautobot/project-static/docs/development/apps/api/setup.html +894 -90
  358. nautobot/project-static/docs/development/apps/api/testing.html +894 -90
  359. nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +894 -90
  360. nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +894 -90
  361. nautobot/project-static/docs/development/apps/api/ui-extensions/index.html +894 -90
  362. nautobot/project-static/docs/development/apps/api/ui-extensions/navigation.html +894 -90
  363. nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +894 -90
  364. nautobot/project-static/docs/development/apps/api/views/base-template.html +894 -90
  365. nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +894 -90
  366. nautobot/project-static/docs/development/apps/api/views/django-generic-views.html +894 -90
  367. nautobot/project-static/docs/development/apps/api/views/help-documentation.html +894 -90
  368. nautobot/project-static/docs/development/apps/api/views/index.html +894 -90
  369. nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +894 -90
  370. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +894 -90
  371. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +894 -90
  372. nautobot/project-static/docs/development/apps/api/views/notes.html +894 -90
  373. nautobot/project-static/docs/development/apps/api/views/rest-api.html +894 -90
  374. nautobot/project-static/docs/development/apps/api/views/urls.html +894 -90
  375. nautobot/project-static/docs/development/apps/index.html +894 -90
  376. nautobot/project-static/docs/development/apps/migration/code-updates.html +894 -90
  377. nautobot/project-static/docs/development/apps/migration/dependency-updates.html +894 -90
  378. nautobot/project-static/docs/development/apps/migration/from-v1.html +894 -90
  379. nautobot/project-static/docs/development/apps/migration/model-updates/dcim.html +894 -90
  380. nautobot/project-static/docs/development/apps/migration/model-updates/extras.html +894 -90
  381. nautobot/project-static/docs/development/apps/migration/model-updates/global.html +894 -90
  382. nautobot/project-static/docs/development/apps/migration/model-updates/ipam.html +894 -90
  383. nautobot/project-static/docs/development/apps/porting-from-netbox.html +894 -90
  384. nautobot/project-static/docs/development/core/application-registry.html +894 -90
  385. nautobot/project-static/docs/development/core/best-practices.html +895 -90
  386. nautobot/project-static/docs/development/core/bootstrap-ui.html +894 -90
  387. nautobot/project-static/docs/development/core/caching.html +894 -90
  388. nautobot/project-static/docs/development/core/controllers.html +894 -90
  389. nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +894 -90
  390. nautobot/project-static/docs/development/core/generic-views.html +894 -90
  391. nautobot/project-static/docs/development/core/getting-started.html +894 -90
  392. nautobot/project-static/docs/development/core/homepage.html +894 -90
  393. nautobot/project-static/docs/development/core/index.html +905 -90
  394. nautobot/project-static/docs/development/core/model-checklist.html +903 -91
  395. nautobot/project-static/docs/development/core/model-features.html +894 -90
  396. nautobot/project-static/docs/development/core/natural-keys.html +894 -90
  397. nautobot/project-static/docs/development/core/navigation-menu.html +894 -90
  398. nautobot/project-static/docs/development/core/release-checklist.html +897 -93
  399. nautobot/project-static/docs/development/core/role-internals.html +894 -90
  400. nautobot/project-static/docs/development/core/settings.html +894 -90
  401. nautobot/project-static/docs/development/core/style-guide.html +895 -91
  402. nautobot/project-static/docs/development/core/templates.html +906 -91
  403. nautobot/project-static/docs/development/core/testing.html +894 -90
  404. nautobot/project-static/docs/development/core/user-preferences.html +894 -90
  405. nautobot/project-static/docs/development/index.html +894 -90
  406. nautobot/project-static/docs/development/jobs/index.html +1271 -453
  407. nautobot/project-static/docs/development/jobs/migration/from-v1.html +894 -90
  408. nautobot/project-static/docs/index.html +9032 -13
  409. nautobot/project-static/docs/media/models/cloud_aws_direct_connect_dark.png +0 -0
  410. nautobot/project-static/docs/media/models/cloud_aws_direct_connect_light.png +0 -0
  411. nautobot/project-static/docs/models/cloud/cloudaccount.html +15 -0
  412. nautobot/project-static/docs/models/cloud/cloudnetwork.html +15 -0
  413. nautobot/project-static/docs/models/cloud/cloudnetworkprefixassignment.html +15 -0
  414. nautobot/project-static/docs/models/cloud/cloudresourcetype.html +15 -0
  415. nautobot/project-static/docs/models/cloud/cloudservice.html +15 -0
  416. nautobot/project-static/docs/models/cloud/cloudservicenetworkassignment.html +15 -0
  417. nautobot/project-static/docs/models/dcim/module.html +15 -0
  418. nautobot/project-static/docs/models/dcim/modulebay.html +15 -0
  419. nautobot/project-static/docs/models/dcim/modulebaytemplate.html +15 -0
  420. nautobot/project-static/docs/models/dcim/moduletype.html +15 -0
  421. nautobot/project-static/docs/models/extras/metadatachoice.html +15 -0
  422. nautobot/project-static/docs/models/extras/metadatatype.html +15 -0
  423. nautobot/project-static/docs/models/extras/objectmetadata.html +15 -0
  424. nautobot/project-static/docs/models/extras/role.html +15 -0
  425. nautobot/project-static/docs/models/extras/savedview.html +15 -0
  426. nautobot/project-static/docs/models/extras/staticgroupassociation.html +15 -0
  427. nautobot/project-static/docs/models/extras/status.html +15 -0
  428. nautobot/project-static/docs/objects.inv +0 -0
  429. nautobot/project-static/docs/overview/application_stack.html +902 -91
  430. nautobot/project-static/docs/overview/design_philosophy.html +896 -92
  431. nautobot/project-static/docs/overview/index.html +13 -8228
  432. nautobot/project-static/docs/release-notes/index.html +1131 -94
  433. nautobot/project-static/docs/release-notes/version-1.0.html +894 -90
  434. nautobot/project-static/docs/release-notes/version-1.1.html +894 -90
  435. nautobot/project-static/docs/release-notes/version-1.2.html +894 -90
  436. nautobot/project-static/docs/release-notes/version-1.3.html +894 -90
  437. nautobot/project-static/docs/release-notes/version-1.4.html +894 -90
  438. nautobot/project-static/docs/release-notes/version-1.5.html +895 -91
  439. nautobot/project-static/docs/release-notes/version-1.6.html +895 -91
  440. nautobot/project-static/docs/release-notes/version-2.0.html +894 -90
  441. nautobot/project-static/docs/release-notes/version-2.1.html +894 -90
  442. nautobot/project-static/docs/release-notes/version-2.2.html +1137 -196
  443. nautobot/project-static/docs/release-notes/version-2.3.html +9954 -0
  444. nautobot/project-static/docs/requirements.txt +5 -5
  445. nautobot/project-static/docs/search/search_index.json +1 -1
  446. nautobot/project-static/docs/sitemap.xml +335 -260
  447. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  448. nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +894 -90
  449. nautobot/project-static/docs/user-guide/administration/configuration/authentication/remote.html +894 -90
  450. nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +894 -90
  451. nautobot/project-static/docs/user-guide/administration/configuration/index.html +894 -90
  452. nautobot/project-static/docs/user-guide/administration/configuration/optional-settings.html +1025 -175
  453. nautobot/project-static/docs/user-guide/administration/configuration/required-settings.html +894 -90
  454. nautobot/project-static/docs/user-guide/administration/configuration/time-zones.html +894 -90
  455. nautobot/project-static/docs/user-guide/administration/guides/caching.html +894 -90
  456. nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +902 -90
  457. nautobot/project-static/docs/user-guide/administration/guides/healthcheck.html +894 -90
  458. nautobot/project-static/docs/user-guide/administration/guides/permissions.html +894 -90
  459. nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +894 -90
  460. nautobot/project-static/docs/user-guide/administration/guides/replicating-nautobot.html +894 -90
  461. nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +894 -90
  462. nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +894 -90
  463. nautobot/project-static/docs/user-guide/administration/installation/app-install.html +894 -90
  464. nautobot/project-static/docs/user-guide/administration/installation/external-authentication.html +894 -90
  465. nautobot/project-static/docs/user-guide/administration/installation/http-server.html +946 -155
  466. nautobot/project-static/docs/user-guide/administration/installation/index.html +903 -95
  467. nautobot/project-static/docs/user-guide/administration/installation/install_system.html +936 -124
  468. nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +956 -159
  469. nautobot/project-static/docs/user-guide/administration/installation/services.html +915 -114
  470. nautobot/project-static/docs/user-guide/administration/installation-extras/docker.html +910 -101
  471. nautobot/project-static/docs/user-guide/administration/installation-extras/health-checks.html +894 -90
  472. nautobot/project-static/docs/user-guide/administration/installation-extras/selinux-troubleshooting.html +894 -90
  473. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-netbox.html +894 -90
  474. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +894 -90
  475. nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +977 -121
  476. nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +894 -90
  477. nautobot/project-static/docs/user-guide/administration/upgrading/database-backup.html +894 -90
  478. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/after-you-upgrade.html +894 -90
  479. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/before-you-upgrade.html +894 -90
  480. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/for-developers.html +894 -90
  481. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/index.html +894 -90
  482. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/whats-changed.html +894 -90
  483. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/region-and-site-data-migration-guide.html +894 -90
  484. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +894 -90
  485. nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +894 -90
  486. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuit.html +894 -90
  487. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittermination.html +895 -91
  488. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittype.html +894 -90
  489. nautobot/project-static/docs/user-guide/core-data-model/circuits/provider.html +898 -90
  490. nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +897 -93
  491. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloud.html +8984 -0
  492. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudaccount.html +8828 -0
  493. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetwork.html +8829 -0
  494. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetworkprefixassignment.html +8828 -0
  495. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudresourcetype.html +8829 -0
  496. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservice.html +8833 -0
  497. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservicenetworkassignment.html +8828 -0
  498. nautobot/project-static/docs/user-guide/core-data-model/dcim/cable.html +908 -104
  499. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +925 -107
  500. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +925 -107
  501. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +920 -102
  502. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +925 -107
  503. nautobot/project-static/docs/user-guide/core-data-model/dcim/controller.html +908 -104
  504. nautobot/project-static/docs/user-guide/core-data-model/dcim/controllermanageddevicegroup.html +908 -104
  505. nautobot/project-static/docs/user-guide/core-data-model/dcim/device.html +915 -107
  506. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +922 -118
  507. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +923 -119
  508. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicefamily.html +920 -116
  509. nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +908 -104
  510. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +916 -107
  511. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +928 -110
  512. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +938 -120
  513. nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +930 -108
  514. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +908 -104
  515. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +939 -121
  516. nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +930 -112
  517. nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +920 -116
  518. nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +923 -119
  519. nautobot/project-static/docs/user-guide/core-data-model/dcim/manufacturer.html +925 -117
  520. nautobot/project-static/docs/user-guide/core-data-model/dcim/module.html +8828 -0
  521. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebay.html +8846 -0
  522. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebaytemplate.html +8843 -0
  523. nautobot/project-static/docs/user-guide/core-data-model/dcim/moduletype.html +8823 -0
  524. nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +918 -114
  525. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerfeed.html +908 -104
  526. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +942 -85
  527. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +926 -108
  528. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerpanel.html +908 -104
  529. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +945 -88
  530. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +923 -105
  531. nautobot/project-static/docs/user-guide/core-data-model/dcim/rack.html +931 -127
  532. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackgroup.html +920 -116
  533. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackreservation.html +908 -104
  534. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +924 -106
  535. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +926 -108
  536. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +908 -104
  537. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareversion.html +908 -104
  538. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualchassis.html +908 -104
  539. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +938 -90
  540. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +894 -90
  541. nautobot/project-static/docs/user-guide/core-data-model/extras/contact.html +899 -91
  542. nautobot/project-static/docs/user-guide/core-data-model/extras/team.html +899 -91
  543. nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +894 -90
  544. nautobot/project-static/docs/user-guide/core-data-model/ipam/namespace.html +894 -90
  545. nautobot/project-static/docs/user-guide/core-data-model/ipam/prefix.html +894 -90
  546. nautobot/project-static/docs/user-guide/core-data-model/ipam/rir.html +894 -90
  547. nautobot/project-static/docs/user-guide/core-data-model/ipam/routetarget.html +894 -90
  548. nautobot/project-static/docs/user-guide/core-data-model/ipam/service.html +894 -90
  549. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +894 -90
  550. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlangroup.html +894 -90
  551. nautobot/project-static/docs/user-guide/core-data-model/ipam/vrf.html +894 -90
  552. nautobot/project-static/docs/user-guide/core-data-model/overview/introduction.html +894 -90
  553. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenant.html +903 -98
  554. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenantgroup.html +894 -90
  555. nautobot/project-static/docs/user-guide/core-data-model/virtualization/cluster.html +894 -90
  556. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustergroup.html +894 -90
  557. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustertype.html +894 -90
  558. nautobot/project-static/docs/user-guide/core-data-model/virtualization/virtualmachine.html +894 -90
  559. nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +899 -91
  560. nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +894 -90
  561. nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +894 -90
  562. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +894 -90
  563. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +894 -90
  564. nautobot/project-static/docs/user-guide/feature-guides/getting-started/index.html +894 -90
  565. nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +894 -90
  566. nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +894 -90
  567. nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +894 -90
  568. nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +894 -90
  569. nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +894 -90
  570. nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +894 -90
  571. nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +894 -90
  572. nautobot/project-static/docs/user-guide/feature-guides/graphql.html +894 -90
  573. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/clear-view-button.png +0 -0
  574. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/cleared-view.png +0 -0
  575. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/config-table-columns-to-locations.png +0 -0
  576. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/configure-button.png +0 -0
  577. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/create-saved-view-success.png +0 -0
  578. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/current-saved-view-drop-down-menu.png +0 -0
  579. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/default-location-list-view.png +0 -0
  580. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/dropdown-button-after-new-saved-view.png +0 -0
  581. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/filter-application-to-locations.png +0 -0
  582. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/filter-button.png +0 -0
  583. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/global-default-location-list-view.png +0 -0
  584. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/location-list-view-with-saved-views.png +0 -0
  585. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/navigation-menu.png +0 -0
  586. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/save-as-new-view-drop-down.png +0 -0
  587. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/save-view-modal.png +0 -0
  588. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/saved-view-admin-edit-buttons.png +0 -0
  589. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/saved-view-admin-edit-success.png +0 -0
  590. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/saved-view-admin-edit-view-unchecked.png +0 -0
  591. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/saved-view-admin-edit-view.png +0 -0
  592. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/saved-view-different-user.png +0 -0
  593. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/saved-view-modal-unchecked.png +0 -0
  594. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/set-as-my-default-button.png +0 -0
  595. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/set-as-my-default-success.png +0 -0
  596. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/unsaved-saved-view.png +0 -0
  597. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/updated-saved-view.png +0 -0
  598. nautobot/project-static/docs/user-guide/feature-guides/ip-address-merge-tool.html +894 -90
  599. nautobot/project-static/docs/user-guide/feature-guides/relationships.html +894 -90
  600. nautobot/project-static/docs/user-guide/feature-guides/software-image-files-and-versions.html +894 -90
  601. nautobot/project-static/docs/user-guide/index.html +894 -90
  602. nautobot/project-static/docs/user-guide/platform-functionality/change-logging.html +894 -90
  603. nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +894 -90
  604. nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +894 -90
  605. nautobot/project-static/docs/user-guide/platform-functionality/customlink.html +894 -90
  606. nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +1260 -787
  607. nautobot/project-static/docs/user-guide/platform-functionality/exporttemplate.html +897 -93
  608. nautobot/project-static/docs/user-guide/platform-functionality/externalintegration.html +894 -90
  609. nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +894 -90
  610. nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +894 -90
  611. nautobot/project-static/docs/user-guide/platform-functionality/graphqlquery.html +894 -90
  612. nautobot/project-static/docs/user-guide/platform-functionality/imageattachment.html +894 -90
  613. nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +894 -90
  614. nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +894 -90
  615. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +894 -90
  616. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +894 -90
  617. nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +898 -90
  618. nautobot/project-static/docs/user-guide/platform-functionality/napalm.html +894 -90
  619. nautobot/project-static/docs/user-guide/platform-functionality/note.html +897 -93
  620. nautobot/project-static/docs/user-guide/platform-functionality/objectmetadata.html +9061 -0
  621. nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +897 -93
  622. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +894 -90
  623. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +894 -90
  624. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +894 -90
  625. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/ui-related-endpoints.html +894 -90
  626. nautobot/project-static/docs/user-guide/platform-functionality/role.html +897 -93
  627. nautobot/project-static/docs/user-guide/platform-functionality/savedview.html +9137 -0
  628. nautobot/project-static/docs/user-guide/platform-functionality/secret.html +897 -93
  629. nautobot/project-static/docs/user-guide/platform-functionality/staticgroupassociation.html +8933 -0
  630. nautobot/project-static/docs/user-guide/platform-functionality/status.html +894 -90
  631. nautobot/project-static/docs/user-guide/platform-functionality/tag.html +894 -90
  632. nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +952 -123
  633. nautobot/project-static/docs/user-guide/platform-functionality/users/objectpermission.html +894 -90
  634. nautobot/project-static/docs/user-guide/platform-functionality/users/token.html +894 -90
  635. nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +894 -90
  636. nautobot/project-static/js/forms.js +71 -0
  637. nautobot/project-static/js/table_sorting_indicator.js +46 -0
  638. nautobot/project-static/js/tableconfig.js +6 -1
  639. nautobot/project-static/materialdesignicons-7.4.47/css/materialdesignicons.min.css +3 -0
  640. nautobot/project-static/{materialdesignicons-6.5.95 → materialdesignicons-7.4.47}/fonts/materialdesignicons-webfont.eot +0 -0
  641. nautobot/project-static/{materialdesignicons-6.5.95 → materialdesignicons-7.4.47}/fonts/materialdesignicons-webfont.ttf +0 -0
  642. nautobot/project-static/materialdesignicons-7.4.47/fonts/materialdesignicons-webfont.woff +0 -0
  643. nautobot/project-static/materialdesignicons-7.4.47/fonts/materialdesignicons-webfont.woff2 +0 -0
  644. nautobot/tenancy/__init__.py +0 -1
  645. nautobot/tenancy/apps.py +1 -0
  646. nautobot/tenancy/factory.py +3 -2
  647. nautobot/tenancy/filters/__init__.py +1 -0
  648. nautobot/tenancy/forms.py +1 -1
  649. nautobot/tenancy/templates/tenancy/tenant.html +24 -20
  650. nautobot/tenancy/views.py +11 -10
  651. nautobot/users/__init__.py +0 -1
  652. nautobot/users/api/serializers.py +1 -1
  653. nautobot/users/api/views.py +4 -2
  654. nautobot/users/apps.py +3 -2
  655. nautobot/users/factory.py +3 -3
  656. nautobot/users/migrations/0010_user_default_saved_views.py +20 -0
  657. nautobot/users/models.py +12 -0
  658. nautobot/users/tests/test_filters.py +6 -3
  659. nautobot/users/urls.py +8 -0
  660. nautobot/virtualization/__init__.py +0 -1
  661. nautobot/virtualization/apps.py +1 -0
  662. nautobot/virtualization/filters.py +6 -1
  663. nautobot/virtualization/forms.py +11 -3
  664. nautobot/virtualization/graphql/types.py +2 -2
  665. nautobot/virtualization/migrations/0029_add_role_field_to_interface_models.py +27 -0
  666. nautobot/virtualization/migrations/0030_alter_virtualmachine_local_config_context_data_owner_content_type_and_more.py +67 -0
  667. nautobot/virtualization/models.py +0 -2
  668. nautobot/virtualization/tables.py +12 -8
  669. nautobot/virtualization/templates/virtualization/virtualmachine.html +1 -1
  670. nautobot/virtualization/templates/virtualization/vminterface.html +7 -1
  671. nautobot/virtualization/templates/virtualization/vminterface_edit.html +1 -0
  672. nautobot/virtualization/tests/test_api.py +9 -4
  673. nautobot/virtualization/tests/test_filters.py +22 -0
  674. nautobot/virtualization/tests/test_models.py +7 -3
  675. nautobot/virtualization/tests/test_views.py +19 -3
  676. nautobot/virtualization/urls.py +2 -2
  677. nautobot/virtualization/views.py +10 -32
  678. {nautobot-2.2.8.dist-info → nautobot-2.3.0.dist-info}/METADATA +21 -19
  679. {nautobot-2.2.8.dist-info → nautobot-2.3.0.dist-info}/RECORD +684 -564
  680. nautobot/project-static/docs/assets/stylesheets/main.76a95c52.min.css +0 -1
  681. nautobot/project-static/docs/assets/stylesheets/main.76a95c52.min.css.map +0 -1
  682. nautobot/project-static/materialdesignicons-6.5.95/.github/ISSUE_TEMPLATE.md +0 -3
  683. nautobot/project-static/materialdesignicons-6.5.95/README.md +0 -25
  684. nautobot/project-static/materialdesignicons-6.5.95/css/materialdesignicons.css +0 -26654
  685. nautobot/project-static/materialdesignicons-6.5.95/css/materialdesignicons.css.map +0 -16
  686. nautobot/project-static/materialdesignicons-6.5.95/css/materialdesignicons.min.css +0 -3
  687. nautobot/project-static/materialdesignicons-6.5.95/css/materialdesignicons.min.css.map +0 -16
  688. nautobot/project-static/materialdesignicons-6.5.95/fonts/materialdesignicons-webfont.woff +0 -0
  689. nautobot/project-static/materialdesignicons-6.5.95/fonts/materialdesignicons-webfont.woff2 +0 -0
  690. nautobot/project-static/materialdesignicons-6.5.95/package.json +0 -28
  691. nautobot/project-static/materialdesignicons-6.5.95/preview.html +0 -717
  692. nautobot/project-static/materialdesignicons-6.5.95/scss/_animated.scss +0 -27
  693. nautobot/project-static/materialdesignicons-6.5.95/scss/_core.scss +0 -10
  694. nautobot/project-static/materialdesignicons-6.5.95/scss/_extras.scss +0 -65
  695. nautobot/project-static/materialdesignicons-6.5.95/scss/_functions.scss +0 -20
  696. nautobot/project-static/materialdesignicons-6.5.95/scss/_icons.scss +0 -10
  697. nautobot/project-static/materialdesignicons-6.5.95/scss/_path.scss +0 -10
  698. nautobot/project-static/materialdesignicons-6.5.95/scss/_variables.scss +0 -6606
  699. nautobot/project-static/materialdesignicons-6.5.95/scss/materialdesignicons.scss +0 -8
  700. /nautobot/project-static/{materialdesignicons-6.5.95 → materialdesignicons-7.4.47}/LICENSE +0 -0
  701. {nautobot-2.2.8.dist-info → nautobot-2.3.0.dist-info}/LICENSE.txt +0 -0
  702. {nautobot-2.2.8.dist-info → nautobot-2.3.0.dist-info}/NOTICE +0 -0
  703. {nautobot-2.2.8.dist-info → nautobot-2.3.0.dist-info}/WHEEL +0 -0
  704. {nautobot-2.2.8.dist-info → nautobot-2.3.0.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,9 +815,9 @@ 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,
820
+ "comments": "changed comment",
809
821
  }
810
822
 
811
823
  def test_list_has_correct_links(self):
@@ -817,29 +829,32 @@ class DeviceTypeTestCase(
817
829
 
818
830
  yaml_import_url = reverse("dcim:devicetype_import")
819
831
  csv_import_url = job_import_url(ContentType.objects.get_for_model(DeviceType))
820
- # Main import button links to YAML/JSON import
832
+ # Dropdown provides both YAML/JSON and CSV import as options
821
833
  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>',
834
+ 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>',
835
+ content,
836
+ )
837
+ self.assertInHTML(
838
+ 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
839
  content,
825
840
  )
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
841
 
830
842
  export_url = job_export_url()
831
843
  # Export is a little trickier to check since it's done as a form submission rather than an <a> element.
832
844
  self.assertIn(f'<form action="{export_url}" method="post">', content)
833
845
  self.assertInHTML(
834
- f'<input type="hidden" name="content_type" value="{ContentType.objects.get_for_model(DeviceType).pk}">',
846
+ f'<input type="hidden" name="content_type" value="{ContentType.objects.get_for_model(self.model).pk}">',
835
847
  content,
836
848
  )
837
849
  self.assertInHTML('<input type="hidden" name="export_format" value="yaml">', content)
838
850
  self.assertInHTML(
839
- '<button type="submit" class="btn btn-link" form="export_default">YAML format</button>',
851
+ '<button type="submit"><span class="mdi mdi-database-export text-muted" aria-hidden="true"></span> Export as YAML</button>',
852
+ content,
853
+ )
854
+ self.assertInHTML(
855
+ '<button type="submit"><span class="mdi mdi-database-export text-muted" aria-hidden="true"></span> Export as CSV</button>',
840
856
  content,
841
857
  )
842
- self.assertInHTML('<button type="submit" class="btn btn-link">CSV format</button>', content)
843
858
 
844
859
  @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
845
860
  def test_import_objects(self):
@@ -920,6 +935,13 @@ device-bays:
920
935
  - name: Device Bay 1
921
936
  - name: Device Bay 2
922
937
  - name: Device Bay 3
938
+ module-bays:
939
+ - name: Module Bay 1
940
+ position: 1
941
+ - name: Module Bay 2
942
+ position: 2
943
+ - name: Module Bay 3
944
+ position: 3
923
945
  """
924
946
 
925
947
  # Add all required permissions to the test user
@@ -934,6 +956,7 @@ device-bays:
934
956
  "dcim.add_frontporttemplate",
935
957
  "dcim.add_rearporttemplate",
936
958
  "dcim.add_devicebaytemplate",
959
+ "dcim.add_modulebaytemplate",
937
960
  )
938
961
 
939
962
  form_data = {"data": IMPORT_DATA, "format": "yaml"}
@@ -944,47 +967,51 @@ device-bays:
944
967
 
945
968
  # Verify all of the components were created
946
969
  self.assertEqual(dt.console_port_templates.count(), 3)
947
- cp1 = ConsolePortTemplate.objects.first()
970
+ cp1 = dt.console_port_templates.first()
948
971
  self.assertEqual(cp1.name, "Console Port 1")
949
972
  self.assertEqual(cp1.type, ConsolePortTypeChoices.TYPE_DE9)
950
973
 
951
974
  self.assertEqual(dt.console_server_port_templates.count(), 3)
952
- csp1 = ConsoleServerPortTemplate.objects.first()
975
+ csp1 = dt.console_server_port_templates.first()
953
976
  self.assertEqual(csp1.name, "Console Server Port 1")
954
977
  self.assertEqual(csp1.type, ConsolePortTypeChoices.TYPE_RJ45)
955
978
 
956
979
  self.assertEqual(dt.power_port_templates.count(), 3)
957
- pp1 = PowerPortTemplate.objects.first()
980
+ pp1 = dt.power_port_templates.first()
958
981
  self.assertEqual(pp1.name, "Power Port 1")
959
982
  self.assertEqual(pp1.type, PowerPortTypeChoices.TYPE_IEC_C14)
960
983
 
961
984
  self.assertEqual(dt.power_outlet_templates.count(), 3)
962
- po1 = PowerOutletTemplate.objects.first()
985
+ po1 = dt.power_outlet_templates.first()
963
986
  self.assertEqual(po1.name, "Power Outlet 1")
964
987
  self.assertEqual(po1.type, PowerOutletTypeChoices.TYPE_IEC_C13)
965
988
  self.assertEqual(po1.power_port_template, pp1)
966
989
  self.assertEqual(po1.feed_leg, PowerOutletFeedLegChoices.FEED_LEG_A)
967
990
 
968
991
  self.assertEqual(dt.interface_templates.count(), 3)
969
- iface1 = InterfaceTemplate.objects.first()
992
+ iface1 = dt.interface_templates.first()
970
993
  self.assertEqual(iface1.name, "Interface 1")
971
994
  self.assertEqual(iface1.type, InterfaceTypeChoices.TYPE_1GE_FIXED)
972
995
  self.assertTrue(iface1.mgmt_only)
973
996
 
974
997
  self.assertEqual(dt.rear_port_templates.count(), 3)
975
- rp1 = RearPortTemplate.objects.first()
998
+ rp1 = dt.rear_port_templates.first()
976
999
  self.assertEqual(rp1.name, "Rear Port 1")
977
1000
 
978
1001
  self.assertEqual(dt.front_port_templates.count(), 3)
979
- fp1 = FrontPortTemplate.objects.first()
1002
+ fp1 = dt.front_port_templates.first()
980
1003
  self.assertEqual(fp1.name, "Front Port 1")
981
1004
  self.assertEqual(fp1.rear_port_template, rp1)
982
1005
  self.assertEqual(fp1.rear_port_position, 1)
983
1006
 
984
1007
  self.assertEqual(dt.device_bay_templates.count(), 3)
985
- db1 = DeviceBayTemplate.objects.first()
1008
+ db1 = dt.device_bay_templates.first()
986
1009
  self.assertEqual(db1.name, "Device Bay 1")
987
1010
 
1011
+ self.assertEqual(dt.module_bay_templates.count(), 3)
1012
+ mb1 = dt.module_bay_templates.first()
1013
+ self.assertEqual(mb1.name, "Module Bay 1")
1014
+
988
1015
  def test_import_objects_unknown_type_enums(self):
989
1016
  """
990
1017
  YAML import of data with `type` values that we don't recognize should remap those to "other" rather than fail.
@@ -1023,6 +1050,13 @@ device-bays:
1023
1050
  - name: Device Bay of Uncertain Type
1024
1051
  type: unknown # should be ignored
1025
1052
  - name: Device Bay of Unspecified Type
1053
+ module-bays:
1054
+ - name: Module Bay 1
1055
+ position: 1
1056
+ - name: Module Bay 2
1057
+ position: 2
1058
+ - name: Module Bay 3
1059
+ position: 3
1026
1060
  """
1027
1061
  # Add all required permissions to the test user
1028
1062
  self.add_permissions(
@@ -1037,6 +1071,7 @@ device-bays:
1037
1071
  "dcim.add_frontporttemplate",
1038
1072
  "dcim.add_rearporttemplate",
1039
1073
  "dcim.add_devicebaytemplate",
1074
+ "dcim.add_modulebaytemplate",
1040
1075
  )
1041
1076
 
1042
1077
  form_data = {"data": IMPORT_DATA, "format": "yaml"}
@@ -1085,6 +1120,12 @@ device-bays:
1085
1120
  self.assertEqual(dt.device_bay_templates.count(), 2)
1086
1121
  # DeviceBayTemplate doesn't have a type field.
1087
1122
 
1123
+ self.assertEqual(dt.module_bay_templates.count(), 3)
1124
+ # ModuleBayTemplate doesn't have a type field.
1125
+ mbt = ModuleBayTemplate.objects.filter(device_type=dt).first()
1126
+ self.assertEqual(mbt.position, "1")
1127
+ self.assertEqual(mbt.name, "Module Bay 1")
1128
+
1088
1129
  def test_devicetype_export(self):
1089
1130
  url = reverse("dcim:devicetype_list")
1090
1131
  self.add_permissions("dcim.view_devicetype")
@@ -1135,128 +1176,504 @@ device-bays:
1135
1176
  self.assertIn("failed validation", response.content.decode(response.charset))
1136
1177
 
1137
1178
 
1138
- #
1139
- # DeviceType components
1140
- #
1141
-
1142
-
1143
- class ConsolePortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
1144
- model = ConsolePortTemplate
1179
+ class ModuleTypeTestCase(
1180
+ ViewTestCases.GetObjectViewTestCase,
1181
+ ViewTestCases.GetObjectChangelogViewTestCase,
1182
+ ViewTestCases.CreateObjectViewTestCase,
1183
+ ViewTestCases.EditObjectViewTestCase,
1184
+ ViewTestCases.DeleteObjectViewTestCase,
1185
+ ViewTestCases.ListObjectsViewTestCase,
1186
+ ViewTestCases.BulkEditObjectsViewTestCase,
1187
+ ViewTestCases.BulkDeleteObjectsViewTestCase,
1188
+ ):
1189
+ model = ModuleType
1145
1190
 
1146
1191
  @classmethod
1147
1192
  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
- )
1193
+ manufacturers = Manufacturer.objects.all()[:2]
1194
+ Module.objects.all().delete()
1195
+ ModuleType.objects.all().delete()
1153
1196
 
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")
1197
+ ModuleType.objects.create(
1198
+ model="Test Module Type 1",
1199
+ manufacturer=manufacturers[0],
1200
+ comments="test comment",
1201
+ )
1202
+ ModuleType.objects.create(
1203
+ model="Test Module Type 2",
1204
+ manufacturer=manufacturers[0],
1205
+ )
1206
+ ModuleType.objects.create(
1207
+ model="Test Module Type 3",
1208
+ manufacturer=manufacturers[0],
1209
+ )
1210
+ ModuleType.objects.create(
1211
+ model="Test Module Type 4",
1212
+ manufacturer=manufacturers[1],
1213
+ )
1157
1214
 
1158
1215
  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,
1216
+ "manufacturer": manufacturers[0].pk,
1217
+ "model": "Test Module Type X",
1218
+ "part_number": "123ABC",
1219
+ "tags": [t.pk for t in Tag.objects.get_for_model(ModuleType)],
1220
+ "comments": "test comment",
1168
1221
  }
1169
1222
 
1170
1223
  cls.bulk_edit_data = {
1171
- "type": ConsolePortTypeChoices.TYPE_RJ45,
1224
+ "manufacturer": manufacturers[1].pk,
1225
+ "comments": "changed comment",
1172
1226
  }
1173
1227
 
1228
+ def test_list_has_correct_links(self):
1229
+ """Assert that the ModuleType list view has import/export buttons for both CSV and YAML/JSON formats."""
1230
+ self.add_permissions("dcim.add_moduletype", "dcim.view_moduletype")
1231
+ response = self.client.get(reverse("dcim:moduletype_list"))
1232
+ self.assertHttpStatus(response, 200)
1233
+ content = extract_page_body(response.content.decode(response.charset))
1174
1234
 
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"),
1235
+ yaml_import_url = reverse("dcim:moduletype_import")
1236
+ csv_import_url = job_import_url(ContentType.objects.get_for_model(ModuleType))
1237
+ # Dropdown provides both YAML/JSON and CSV import as options
1238
+ self.assertInHTML(
1239
+ 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>',
1240
+ content,
1241
+ )
1242
+ self.assertInHTML(
1243
+ f'<a href="{csv_import_url}"><span class="mdi mdi-database-import text-muted" aria-hidden="true"></span> Import from CSV (multiple records)</a>',
1244
+ content,
1184
1245
  )
1185
1246
 
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
1247
+ export_url = job_export_url()
1248
+ # Export is a little trickier to check since it's done as a form submission rather than an <a> element.
1249
+ self.assertIn(f'<form action="{export_url}" method="post">', content)
1250
+ self.assertInHTML(
1251
+ f'<input type="hidden" name="content_type" value="{ContentType.objects.get_for_model(self.model).pk}">',
1252
+ content,
1253
+ )
1254
+ self.assertInHTML('<input type="hidden" name="export_format" value="yaml">', content)
1255
+ self.assertInHTML(
1256
+ '<button type="submit"><span class="mdi mdi-database-export text-muted" aria-hidden="true"></span> Export as YAML</button>',
1257
+ content,
1258
+ )
1259
+ self.assertInHTML(
1260
+ '<button type="submit"><span class="mdi mdi-database-export text-muted" aria-hidden="true"></span> Export as CSV</button>',
1261
+ content,
1262
+ )
1209
1263
 
1210
- @classmethod
1211
- def setUpTestData(cls):
1264
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
1265
+ def test_import_objects(self):
1266
+ """
1267
+ Custom import test for YAML-based imports (versus CSV)
1268
+ """
1269
+ # Note use of "power-outlets.power_port" (not "power_port_template") and "front-ports.rear_port"
1270
+ # (not "rear_port_template"). Note also inclusion of "slug" even though we removed DeviceType.slug in 2.0.
1271
+ # This is intentional as we are testing backwards compatibility with the netbox/devicetype-library repository.
1212
1272
  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"),
1273
+ IMPORT_DATA = f"""
1274
+ manufacturer: {manufacturer.name}
1275
+ model: TEST-1000
1276
+ slug: test-1000
1277
+ console-ports:
1278
+ - name: Console Port 1
1279
+ type: de-9
1280
+ - name: Console Port 2
1281
+ type: de-9
1282
+ - name: Console Port 3
1283
+ type: de-9
1284
+ console-server-ports:
1285
+ - name: Console Server Port 1
1286
+ type: rj-45
1287
+ - name: Console Server Port 2
1288
+ type: rj-45
1289
+ - name: Console Server Port 3
1290
+ type: rj-45
1291
+ power-ports:
1292
+ - name: Power Port 1
1293
+ type: iec-60320-c14
1294
+ - name: Power Port 2
1295
+ type: iec-60320-c14
1296
+ - name: Power Port 3
1297
+ type: iec-60320-c14
1298
+ power-outlets:
1299
+ - name: Power Outlet 1
1300
+ type: iec-60320-c13
1301
+ power_port: Power Port 1
1302
+ feed_leg: A
1303
+ - name: Power Outlet 2
1304
+ type: iec-60320-c13
1305
+ power_port: Power Port 1
1306
+ feed_leg: A
1307
+ - name: Power Outlet 3
1308
+ type: iec-60320-c13
1309
+ power_port: Power Port 1
1310
+ feed_leg: A
1311
+ interfaces:
1312
+ - name: Interface 1
1313
+ type: 1000base-t
1314
+ mgmt_only: true
1315
+ - name: Interface 2
1316
+ type: 1000base-t
1317
+ - name: Interface 3
1318
+ type: 1000base-t
1319
+ rear-ports:
1320
+ - name: Rear Port 1
1321
+ type: 8p8c
1322
+ - name: Rear Port 2
1323
+ type: 8p8c
1324
+ - name: Rear Port 3
1325
+ type: 8p8c
1326
+ front-ports:
1327
+ - name: Front Port 1
1328
+ type: 8p8c
1329
+ rear_port: Rear Port 1
1330
+ - name: Front Port 2
1331
+ type: 8p8c
1332
+ rear_port: Rear Port 2
1333
+ - name: Front Port 3
1334
+ type: 8p8c
1335
+ rear_port: Rear Port 3
1336
+ module-bays:
1337
+ - name: Module Bay 1
1338
+ position: 1
1339
+ - name: Module Bay 2
1340
+ position: 2
1341
+ - name: Module Bay 3
1342
+ position: 3
1343
+ """
1344
+
1345
+ # Add all required permissions to the test user
1346
+ self.add_permissions(
1347
+ "dcim.view_moduletype",
1348
+ "dcim.add_moduletype",
1349
+ "dcim.add_consoleporttemplate",
1350
+ "dcim.add_consoleserverporttemplate",
1351
+ "dcim.add_powerporttemplate",
1352
+ "dcim.add_poweroutlettemplate",
1353
+ "dcim.add_interfacetemplate",
1354
+ "dcim.add_frontporttemplate",
1355
+ "dcim.add_rearporttemplate",
1356
+ "dcim.add_modulebaytemplate",
1216
1357
  )
1217
1358
 
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")
1359
+ form_data = {"data": IMPORT_DATA, "format": "yaml"}
1360
+ response = self.client.post(reverse("dcim:moduletype_import"), data=form_data, follow=True)
1361
+ self.assertHttpStatus(response, 200)
1362
+ mt = ModuleType.objects.get(model="TEST-1000")
1221
1363
 
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
- }
1364
+ # Verify all of the components were created
1365
+ self.assertEqual(mt.console_port_templates.count(), 3)
1366
+ cp1 = mt.console_port_templates.first()
1367
+ self.assertEqual(cp1.name, "Console Port 1")
1368
+ self.assertEqual(cp1.type, ConsolePortTypeChoices.TYPE_DE9)
1229
1369
 
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
- }
1370
+ self.assertEqual(mt.console_server_port_templates.count(), 3)
1371
+ csp1 = mt.console_server_port_templates.first()
1372
+ self.assertEqual(csp1.name, "Console Server Port 1")
1373
+ self.assertEqual(csp1.type, ConsolePortTypeChoices.TYPE_RJ45)
1237
1374
 
1238
- cls.bulk_edit_data = {
1239
- "type": PowerPortTypeChoices.TYPE_IEC_C14,
1240
- "maximum_draw": 100,
1241
- "allocated_draw": 50,
1242
- }
1375
+ self.assertEqual(mt.power_port_templates.count(), 3)
1376
+ pp1 = mt.power_port_templates.first()
1377
+ self.assertEqual(pp1.name, "Power Port 1")
1378
+ self.assertEqual(pp1.type, PowerPortTypeChoices.TYPE_IEC_C14)
1243
1379
 
1380
+ self.assertEqual(mt.power_outlet_templates.count(), 3)
1381
+ po1 = mt.power_outlet_templates.first()
1382
+ self.assertEqual(po1.name, "Power Outlet 1")
1383
+ self.assertEqual(po1.type, PowerOutletTypeChoices.TYPE_IEC_C13)
1384
+ self.assertEqual(po1.power_port_template, pp1)
1385
+ self.assertEqual(po1.feed_leg, PowerOutletFeedLegChoices.FEED_LEG_A)
1244
1386
 
1245
- class PowerOutletTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
1246
- model = PowerOutletTemplate
1387
+ self.assertEqual(mt.interface_templates.count(), 3)
1388
+ iface1 = mt.interface_templates.first()
1389
+ self.assertEqual(iface1.name, "Interface 1")
1390
+ self.assertEqual(iface1.type, InterfaceTypeChoices.TYPE_1GE_FIXED)
1391
+ self.assertTrue(iface1.mgmt_only)
1247
1392
 
1248
- @classmethod
1249
- def setUpTestData(cls):
1250
- manufacturer = Manufacturer.objects.first()
1251
- devicetype = DeviceType.objects.create(manufacturer=manufacturer, model="Device Type 1")
1393
+ self.assertEqual(mt.rear_port_templates.count(), 3)
1394
+ rp1 = mt.rear_port_templates.first()
1395
+ self.assertEqual(rp1.name, "Rear Port 1")
1252
1396
 
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")
1397
+ self.assertEqual(mt.front_port_templates.count(), 3)
1398
+ fp1 = mt.front_port_templates.first()
1399
+ self.assertEqual(fp1.name, "Front Port 1")
1400
+ self.assertEqual(fp1.rear_port_template, rp1)
1401
+ self.assertEqual(fp1.rear_port_position, 1)
1256
1402
 
1257
- powerports = (PowerPortTemplate.objects.create(device_type=devicetype, name="Power Port Template 1"),)
1403
+ self.assertEqual(mt.module_bay_templates.count(), 3)
1404
+ mb1 = mt.module_bay_templates.first()
1405
+ self.assertEqual(mb1.name, "Module Bay 1")
1406
+ self.assertEqual(mb1.position, "1")
1258
1407
 
1259
- cls.form_data = {
1408
+ def test_import_objects_unknown_type_enums(self):
1409
+ """
1410
+ YAML import of data with `type` values that we don't recognize should remap those to "other" rather than fail.
1411
+ """
1412
+ manufacturer = Manufacturer.objects.first()
1413
+ IMPORT_DATA = f"""
1414
+ manufacturer: {manufacturer.name}
1415
+ model: TEST-2000
1416
+ console-ports:
1417
+ - name: Console Port Alpha-Beta
1418
+ type: alpha-beta
1419
+ console-server-ports:
1420
+ - name: Console Server Port Pineapple
1421
+ type: pineapple
1422
+ power-ports:
1423
+ - name: Power Port Fred
1424
+ type: frederick
1425
+ power-outlets:
1426
+ - name: Power Outlet Rick
1427
+ type: frederick
1428
+ power_port_template: Power Port Fred
1429
+ interfaces:
1430
+ - name: Interface North
1431
+ type: northern
1432
+ rear-ports:
1433
+ - name: Rear Port Foosball
1434
+ type: foosball
1435
+ front-ports:
1436
+ - name: Front Port Pickleball
1437
+ type: pickleball
1438
+ rear_port_template: Rear Port Foosball
1439
+ module-bays:
1440
+ - name: Module Bay 1
1441
+ position: 1
1442
+ - name: Module Bay 2
1443
+ position: 2
1444
+ - name: Module Bay 3
1445
+ position: 3
1446
+ """
1447
+ # Add all required permissions to the test user
1448
+ self.add_permissions(
1449
+ "dcim.view_moduletype",
1450
+ "dcim.view_manufacturer",
1451
+ "dcim.add_moduletype",
1452
+ "dcim.add_consoleporttemplate",
1453
+ "dcim.add_consoleserverporttemplate",
1454
+ "dcim.add_powerporttemplate",
1455
+ "dcim.add_poweroutlettemplate",
1456
+ "dcim.add_interfacetemplate",
1457
+ "dcim.add_frontporttemplate",
1458
+ "dcim.add_rearporttemplate",
1459
+ "dcim.add_modulebaytemplate",
1460
+ )
1461
+
1462
+ form_data = {"data": IMPORT_DATA, "format": "yaml"}
1463
+ response = self.client.post(reverse("dcim:moduletype_import"), data=form_data, follow=True)
1464
+ self.assertHttpStatus(response, 200)
1465
+ mt = ModuleType.objects.get(model="TEST-2000")
1466
+
1467
+ # Verify all of the components were created with appropriate "other" types
1468
+ self.assertEqual(mt.console_port_templates.count(), 1)
1469
+ cpt = ConsolePortTemplate.objects.filter(module_type=mt).first()
1470
+ self.assertEqual(cpt.name, "Console Port Alpha-Beta")
1471
+ self.assertEqual(cpt.type, ConsolePortTypeChoices.TYPE_OTHER)
1472
+
1473
+ self.assertEqual(mt.console_server_port_templates.count(), 1)
1474
+ cspt = ConsoleServerPortTemplate.objects.filter(module_type=mt).first()
1475
+ self.assertEqual(cspt.name, "Console Server Port Pineapple")
1476
+ self.assertEqual(cspt.type, ConsolePortTypeChoices.TYPE_OTHER)
1477
+
1478
+ self.assertEqual(mt.power_port_templates.count(), 1)
1479
+ ppt = PowerPortTemplate.objects.filter(module_type=mt).first()
1480
+ self.assertEqual(ppt.name, "Power Port Fred")
1481
+ self.assertEqual(ppt.type, PowerPortTypeChoices.TYPE_OTHER)
1482
+
1483
+ self.assertEqual(mt.power_outlet_templates.count(), 1)
1484
+ pot = PowerOutletTemplate.objects.filter(module_type=mt).first()
1485
+ self.assertEqual(pot.name, "Power Outlet Rick")
1486
+ self.assertEqual(pot.type, PowerOutletTypeChoices.TYPE_OTHER)
1487
+ self.assertEqual(pot.power_port_template, ppt)
1488
+
1489
+ self.assertEqual(mt.interface_templates.count(), 1)
1490
+ it = InterfaceTemplate.objects.filter(module_type=mt).first()
1491
+ self.assertEqual(it.name, "Interface North")
1492
+ self.assertEqual(it.type, InterfaceTypeChoices.TYPE_OTHER)
1493
+
1494
+ self.assertEqual(mt.rear_port_templates.count(), 1)
1495
+ rpt = RearPortTemplate.objects.filter(module_type=mt).first()
1496
+ self.assertEqual(rpt.name, "Rear Port Foosball")
1497
+ self.assertEqual(rpt.type, PortTypeChoices.TYPE_OTHER)
1498
+
1499
+ self.assertEqual(mt.front_port_templates.count(), 1)
1500
+ fpt = FrontPortTemplate.objects.filter(module_type=mt).first()
1501
+ self.assertEqual(fpt.name, "Front Port Pickleball")
1502
+ self.assertEqual(fpt.type, PortTypeChoices.TYPE_OTHER)
1503
+
1504
+ self.assertEqual(mt.module_bay_templates.count(), 3)
1505
+ # ModuleBayTemplate doesn't have a type field.
1506
+ mbt = ModuleBayTemplate.objects.filter(module_type=mt).first()
1507
+ self.assertEqual(mbt.position, "1")
1508
+ self.assertEqual(mbt.name, "Module Bay 1")
1509
+
1510
+ def test_moduletype_export(self):
1511
+ url = reverse("dcim:moduletype_list")
1512
+ self.add_permissions("dcim.view_moduletype")
1513
+
1514
+ response = self.client.get(f"{url}?export")
1515
+ self.assertEqual(response.status_code, 200)
1516
+ data = list(yaml.load_all(response.content, Loader=yaml.SafeLoader))
1517
+ module_types = ModuleType.objects.all()
1518
+ module_type = module_types.first()
1519
+
1520
+ self.assertEqual(len(data), module_types.count())
1521
+ self.assertEqual(data[0]["manufacturer"], module_type.manufacturer.name)
1522
+ self.assertEqual(data[0]["model"], module_type.model)
1523
+
1524
+
1525
+ #
1526
+ # DeviceType components
1527
+ #
1528
+
1529
+
1530
+ class ConsolePortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
1531
+ model = ConsolePortTemplate
1532
+
1533
+ @classmethod
1534
+ def setUpTestData(cls):
1535
+ manufacturer = Manufacturer.objects.first()
1536
+ devicetypes = (
1537
+ DeviceType.objects.create(manufacturer=manufacturer, model="Device Type 1"),
1538
+ DeviceType.objects.create(manufacturer=manufacturer, model="Device Type 2"),
1539
+ )
1540
+
1541
+ ConsolePortTemplate.objects.create(device_type=devicetypes[0], name="Console Port Template 1")
1542
+ ConsolePortTemplate.objects.create(device_type=devicetypes[0], name="Console Port Template 2")
1543
+ ConsolePortTemplate.objects.create(device_type=devicetypes[0], name="Console Port Template 3")
1544
+
1545
+ cls.form_data = {
1546
+ "device_type": devicetypes[1].pk,
1547
+ "name": "Console Port Template X",
1548
+ "type": ConsolePortTypeChoices.TYPE_RJ45,
1549
+ }
1550
+
1551
+ cls.bulk_create_data = {
1552
+ "device_type": devicetypes[1].pk,
1553
+ "name_pattern": "Console Port Template [4-6]",
1554
+ "description": "View Test Bulk Create Console Ports",
1555
+ "type": ConsolePortTypeChoices.TYPE_RJ45,
1556
+ }
1557
+
1558
+ cls.bulk_edit_data = {
1559
+ "type": ConsolePortTypeChoices.TYPE_RJ45,
1560
+ }
1561
+
1562
+ test_instance = cls.model.objects.first()
1563
+ cls.update_data = {
1564
+ "name": test_instance.name,
1565
+ "device_type": getattr(getattr(test_instance, "device_type", None), "pk", None),
1566
+ "module_type": getattr(getattr(test_instance, "module_type", None), "pk", None),
1567
+ "label": "new test label",
1568
+ "description": "new test description",
1569
+ }
1570
+
1571
+
1572
+ class ConsoleServerPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
1573
+ model = ConsoleServerPortTemplate
1574
+
1575
+ @classmethod
1576
+ def setUpTestData(cls):
1577
+ manufacturer = Manufacturer.objects.first()
1578
+ devicetypes = (
1579
+ DeviceType.objects.create(manufacturer=manufacturer, model="Device Type 1"),
1580
+ DeviceType.objects.create(manufacturer=manufacturer, model="Device Type 2"),
1581
+ )
1582
+
1583
+ ConsoleServerPortTemplate.objects.create(device_type=devicetypes[0], name="Console Server Port Template 1")
1584
+ ConsoleServerPortTemplate.objects.create(device_type=devicetypes[0], name="Console Server Port Template 2")
1585
+ ConsoleServerPortTemplate.objects.create(device_type=devicetypes[0], name="Console Server Port Template 3")
1586
+
1587
+ cls.form_data = {
1588
+ "device_type": devicetypes[1].pk,
1589
+ "name": "Console Server Port Template X",
1590
+ "type": ConsolePortTypeChoices.TYPE_RJ45,
1591
+ }
1592
+
1593
+ cls.bulk_create_data = {
1594
+ "device_type": devicetypes[1].pk,
1595
+ "name_pattern": "Console Server Port Template [4-6]",
1596
+ "description": "View Test Bulk Create Console Server Ports",
1597
+ "type": ConsolePortTypeChoices.TYPE_RJ45,
1598
+ }
1599
+
1600
+ cls.bulk_edit_data = {
1601
+ "type": ConsolePortTypeChoices.TYPE_RJ45,
1602
+ }
1603
+
1604
+ test_instance = cls.model.objects.first()
1605
+ cls.update_data = {
1606
+ "name": test_instance.name,
1607
+ "device_type": getattr(getattr(test_instance, "device_type", None), "pk", None),
1608
+ "module_type": getattr(getattr(test_instance, "module_type", None), "pk", None),
1609
+ "label": "new test label",
1610
+ "description": "new test description",
1611
+ }
1612
+
1613
+
1614
+ class PowerPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
1615
+ model = PowerPortTemplate
1616
+
1617
+ @classmethod
1618
+ def setUpTestData(cls):
1619
+ manufacturer = Manufacturer.objects.first()
1620
+ devicetypes = (
1621
+ DeviceType.objects.create(manufacturer=manufacturer, model="Device Type 1"),
1622
+ DeviceType.objects.create(manufacturer=manufacturer, model="Device Type 2"),
1623
+ )
1624
+
1625
+ PowerPortTemplate.objects.create(device_type=devicetypes[0], name="Power Port Template 1")
1626
+ PowerPortTemplate.objects.create(device_type=devicetypes[0], name="Power Port Template 2")
1627
+ PowerPortTemplate.objects.create(device_type=devicetypes[0], name="Power Port Template 3")
1628
+
1629
+ cls.form_data = {
1630
+ "device_type": devicetypes[1].pk,
1631
+ "name": "Power Port Template X",
1632
+ "type": PowerPortTypeChoices.TYPE_IEC_C14,
1633
+ "maximum_draw": 100,
1634
+ "allocated_draw": 50,
1635
+ }
1636
+
1637
+ cls.bulk_create_data = {
1638
+ "device_type": devicetypes[1].pk,
1639
+ "name_pattern": "Power Port Template [4-6]",
1640
+ "description": "View Test Bulk Create Power Ports",
1641
+ "type": PowerPortTypeChoices.TYPE_IEC_C14,
1642
+ "maximum_draw": 100,
1643
+ "allocated_draw": 50,
1644
+ }
1645
+
1646
+ cls.bulk_edit_data = {
1647
+ "type": PowerPortTypeChoices.TYPE_IEC_C14,
1648
+ "maximum_draw": 100,
1649
+ "allocated_draw": 50,
1650
+ }
1651
+
1652
+ test_instance = cls.model.objects.first()
1653
+ cls.update_data = {
1654
+ "name": test_instance.name,
1655
+ "device_type": getattr(getattr(test_instance, "device_type", None), "pk", None),
1656
+ "module_type": getattr(getattr(test_instance, "module_type", None), "pk", None),
1657
+ "label": "new test label",
1658
+ "description": "new test description",
1659
+ }
1660
+
1661
+
1662
+ class PowerOutletTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
1663
+ model = PowerOutletTemplate
1664
+
1665
+ @classmethod
1666
+ def setUpTestData(cls):
1667
+ manufacturer = Manufacturer.objects.first()
1668
+ devicetype = DeviceType.objects.create(manufacturer=manufacturer, model="Device Type 1")
1669
+
1670
+ PowerOutletTemplate.objects.create(device_type=devicetype, name="Power Outlet Template 1")
1671
+ PowerOutletTemplate.objects.create(device_type=devicetype, name="Power Outlet Template 2")
1672
+ PowerOutletTemplate.objects.create(device_type=devicetype, name="Power Outlet Template 3")
1673
+
1674
+ powerports = (PowerPortTemplate.objects.create(device_type=devicetype, name="Power Port Template 1"),)
1675
+
1676
+ cls.form_data = {
1260
1677
  "device_type": devicetype.pk,
1261
1678
  "name": "Power Outlet Template X",
1262
1679
  "type": PowerOutletTypeChoices.TYPE_IEC_C13,
@@ -1267,6 +1684,7 @@ class PowerOutletTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestC
1267
1684
  cls.bulk_create_data = {
1268
1685
  "device_type": devicetype.pk,
1269
1686
  "name_pattern": "Power Outlet Template [4-6]",
1687
+ "description": "View Test Bulk Create Power Outlets",
1270
1688
  "type": PowerOutletTypeChoices.TYPE_IEC_C13,
1271
1689
  "power_port_template": powerports[0].pk,
1272
1690
  "feed_leg": PowerOutletFeedLegChoices.FEED_LEG_B,
@@ -1277,6 +1695,17 @@ class PowerOutletTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestC
1277
1695
  "feed_leg": PowerOutletFeedLegChoices.FEED_LEG_B,
1278
1696
  }
1279
1697
 
1698
+ test_instance = cls.model.objects.first()
1699
+ cls.update_data = {
1700
+ "name": test_instance.name,
1701
+ "device_type": getattr(getattr(test_instance, "device_type", None), "pk", None),
1702
+ "module_type": getattr(getattr(test_instance, "module_type", None), "pk", None),
1703
+ # power_port_template must match the parent device/module type
1704
+ "power_port_template": getattr(test_instance.power_port_template, "pk", None),
1705
+ "label": "new test label",
1706
+ "description": "new test description",
1707
+ }
1708
+
1280
1709
 
1281
1710
  class InterfaceTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
1282
1711
  model = InterfaceTemplate
@@ -1289,9 +1718,21 @@ class InterfaceTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCas
1289
1718
  DeviceType.objects.create(manufacturer=manufacturer, model="Device Type 2"),
1290
1719
  )
1291
1720
 
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")
1721
+ InterfaceTemplate.objects.create(
1722
+ device_type=devicetypes[0],
1723
+ type=InterfaceTypeChoices.TYPE_100GE_QSFP_DD,
1724
+ name="Interface Template 1",
1725
+ )
1726
+ InterfaceTemplate.objects.create(
1727
+ device_type=devicetypes[0],
1728
+ type=InterfaceTypeChoices.TYPE_100GE_QSFP_DD,
1729
+ name="Interface Template 2",
1730
+ )
1731
+ InterfaceTemplate.objects.create(
1732
+ device_type=devicetypes[0],
1733
+ type=InterfaceTypeChoices.TYPE_100GE_QSFP_DD,
1734
+ name="Interface Template 3",
1735
+ )
1295
1736
 
1296
1737
  cls.form_data = {
1297
1738
  "device_type": devicetypes[1].pk,
@@ -1305,6 +1746,7 @@ class InterfaceTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCas
1305
1746
  "name_pattern": "Interface Template [4-6]",
1306
1747
  # Test that a label can be applied to each generated interface templates
1307
1748
  "label_pattern": "Interface Template Label [3-5]",
1749
+ "description": "View Test Bulk Create Interfaces",
1308
1750
  "type": InterfaceTypeChoices.TYPE_1GE_GBIC,
1309
1751
  "mgmt_only": True,
1310
1752
  }
@@ -1314,6 +1756,16 @@ class InterfaceTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCas
1314
1756
  "mgmt_only": True,
1315
1757
  }
1316
1758
 
1759
+ test_instance = cls.model.objects.first()
1760
+ cls.update_data = {
1761
+ "name": test_instance.name,
1762
+ "device_type": getattr(getattr(test_instance, "device_type", None), "pk", None),
1763
+ "module_type": getattr(getattr(test_instance, "module_type", None), "pk", None),
1764
+ "type": test_instance.type,
1765
+ "label": "new test label",
1766
+ "description": "new test description",
1767
+ }
1768
+
1317
1769
 
1318
1770
  class FrontPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
1319
1771
  model = FrontPortTemplate
@@ -1324,29 +1776,62 @@ class FrontPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCas
1324
1776
  devicetype = DeviceType.objects.create(manufacturer=manufacturer, model="Device Type 1")
1325
1777
 
1326
1778
  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"),
1779
+ RearPortTemplate.objects.create(
1780
+ device_type=devicetype,
1781
+ type=PortTypeChoices.TYPE_8P8C,
1782
+ positions=24,
1783
+ name="Rear Port Template 1",
1784
+ ),
1785
+ RearPortTemplate.objects.create(
1786
+ device_type=devicetype,
1787
+ type=PortTypeChoices.TYPE_8P8C,
1788
+ positions=24,
1789
+ name="Rear Port Template 2",
1790
+ ),
1791
+ RearPortTemplate.objects.create(
1792
+ device_type=devicetype,
1793
+ type=PortTypeChoices.TYPE_8P8C,
1794
+ positions=24,
1795
+ name="Rear Port Template 3",
1796
+ ),
1797
+ RearPortTemplate.objects.create(
1798
+ device_type=devicetype,
1799
+ type=PortTypeChoices.TYPE_8P8C,
1800
+ positions=24,
1801
+ name="Rear Port Template 4",
1802
+ ),
1803
+ RearPortTemplate.objects.create(
1804
+ device_type=devicetype,
1805
+ type=PortTypeChoices.TYPE_8P8C,
1806
+ positions=24,
1807
+ name="Rear Port Template 5",
1808
+ ),
1809
+ RearPortTemplate.objects.create(
1810
+ device_type=devicetype,
1811
+ type=PortTypeChoices.TYPE_8P8C,
1812
+ positions=24,
1813
+ name="Rear Port Template 6",
1814
+ ),
1333
1815
  )
1334
1816
 
1335
1817
  FrontPortTemplate.objects.create(
1336
1818
  device_type=devicetype,
1337
- name="Front Port Template 1",
1819
+ name="View Test Front Port Template 1",
1820
+ type=PortTypeChoices.TYPE_8P8C,
1338
1821
  rear_port_template=rearports[0],
1339
1822
  rear_port_position=1,
1340
1823
  )
1341
1824
  FrontPortTemplate.objects.create(
1342
1825
  device_type=devicetype,
1343
- name="Front Port Template 2",
1826
+ name="View Test Front Port Template 2",
1827
+ type=PortTypeChoices.TYPE_8P8C,
1344
1828
  rear_port_template=rearports[1],
1345
1829
  rear_port_position=1,
1346
1830
  )
1347
1831
  FrontPortTemplate.objects.create(
1348
1832
  device_type=devicetype,
1349
- name="Front Port Template 3",
1833
+ name="View Test Front Port Template 3",
1834
+ type=PortTypeChoices.TYPE_8P8C,
1350
1835
  rear_port_template=rearports[2],
1351
1836
  rear_port_position=1,
1352
1837
  )
@@ -1361,13 +1846,26 @@ class FrontPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCas
1361
1846
 
1362
1847
  cls.bulk_create_data = {
1363
1848
  "device_type": devicetype.pk,
1364
- "name_pattern": "Front Port [4-6]",
1849
+ "name_pattern": "View Test Front Port [4-6]",
1850
+ "description": "View Test Bulk Create Front Ports",
1365
1851
  "type": PortTypeChoices.TYPE_8P8C,
1366
1852
  "rear_port_template_set": [f"{rp.pk}:1" for rp in rearports[3:6]],
1367
1853
  }
1368
1854
 
1369
1855
  cls.bulk_edit_data = {
1370
- "type": PortTypeChoices.TYPE_8P8C,
1856
+ "type": PortTypeChoices.TYPE_4P4C,
1857
+ }
1858
+
1859
+ test_instance = cls.model.objects.first()
1860
+ cls.update_data = {
1861
+ "name": test_instance.name,
1862
+ "device_type": getattr(getattr(test_instance, "device_type", None), "pk", None),
1863
+ "module_type": getattr(getattr(test_instance, "module_type", None), "pk", None),
1864
+ "rear_port_template": test_instance.rear_port_template.pk,
1865
+ "rear_port_position": test_instance.rear_port_position,
1866
+ "type": test_instance.type,
1867
+ "label": "new test label",
1868
+ "description": "new test description",
1371
1869
  }
1372
1870
 
1373
1871
 
@@ -1382,9 +1880,24 @@ class RearPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase
1382
1880
  DeviceType.objects.create(manufacturer=manufacturer, model="Device Type 2"),
1383
1881
  )
1384
1882
 
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")
1883
+ RearPortTemplate.objects.create(
1884
+ device_type=devicetypes[0],
1885
+ type=PortTypeChoices.TYPE_8P8C,
1886
+ positions=24,
1887
+ name="Rear Port Template 1",
1888
+ )
1889
+ RearPortTemplate.objects.create(
1890
+ device_type=devicetypes[0],
1891
+ type=PortTypeChoices.TYPE_8P8C,
1892
+ positions=24,
1893
+ name="Rear Port Template 2",
1894
+ )
1895
+ RearPortTemplate.objects.create(
1896
+ device_type=devicetypes[0],
1897
+ type=PortTypeChoices.TYPE_8P8C,
1898
+ positions=24,
1899
+ name="Rear Port Template 3",
1900
+ )
1388
1901
 
1389
1902
  cls.form_data = {
1390
1903
  "device_type": devicetypes[1].pk,
@@ -1396,6 +1909,7 @@ class RearPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase
1396
1909
  cls.bulk_create_data = {
1397
1910
  "device_type": devicetypes[1].pk,
1398
1911
  "name_pattern": "Rear Port Template [4-6]",
1912
+ "description": "View Test Bulk Create Rear Ports",
1399
1913
  "type": PortTypeChoices.TYPE_8P8C,
1400
1914
  "positions": 2,
1401
1915
  }
@@ -1404,6 +1918,17 @@ class RearPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase
1404
1918
  "type": PortTypeChoices.TYPE_8P8C,
1405
1919
  }
1406
1920
 
1921
+ test_instance = cls.model.objects.first()
1922
+ cls.update_data = {
1923
+ "name": test_instance.name,
1924
+ "device_type": getattr(getattr(test_instance, "device_type", None), "pk", None),
1925
+ "module_type": getattr(getattr(test_instance, "module_type", None), "pk", None),
1926
+ "positions": test_instance.positions,
1927
+ "type": test_instance.type,
1928
+ "label": "new test label",
1929
+ "description": "new test description",
1930
+ }
1931
+
1407
1932
 
1408
1933
  class DeviceBayTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
1409
1934
  model = DeviceBayTemplate
@@ -1436,31 +1961,80 @@ class DeviceBayTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCas
1436
1961
  cls.bulk_create_data = {
1437
1962
  "device_type": devicetypes[1].pk,
1438
1963
  "name_pattern": "Device Bay Template [4-6]",
1964
+ "description": "View Test Bulk Create Device Bays",
1439
1965
  }
1440
1966
 
1441
1967
  cls.bulk_edit_data = {
1442
1968
  "description": "Foo bar",
1443
1969
  }
1444
1970
 
1971
+ test_instance = cls.model.objects.first()
1972
+ cls.update_data = {
1973
+ "name": test_instance.name,
1974
+ "device_type": test_instance.device_type.pk,
1975
+ "label": "new test label",
1976
+ "description": "new test description",
1977
+ }
1445
1978
 
1446
- class PlatformTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
1447
- model = Platform
1979
+
1980
+ class ModuleBayTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
1981
+ model = ModuleBayTemplate
1448
1982
 
1449
1983
  @classmethod
1450
1984
  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)
1985
+ device_type = DeviceType.objects.first()
1986
+ module_type = ModuleType.objects.first()
1457
1987
 
1458
1988
  cls.form_data = {
1459
- "name": "Platform X",
1460
- "manufacturer": manufacturer.pk,
1461
- "napalm_driver": "junos",
1462
- "napalm_args": None,
1463
- "network_driver": "juniper_junos",
1989
+ "device_type": device_type.pk,
1990
+ "module_type": None,
1991
+ "name": "Module Bay Template X",
1992
+ "position": "Test modulebaytemplate position",
1993
+ "description": "Test modulebaytemplate description",
1994
+ "label": "Test modulebaytemplate label",
1995
+ }
1996
+
1997
+ cls.bulk_create_data = {
1998
+ "module_type": module_type.pk,
1999
+ "name_pattern": "Test Module Bay Template [5-7]",
2000
+ "position_pattern": "Test Module Bay Template Position [10-12]",
2001
+ "label_pattern": "Test modulebaytemplate label [1-3]",
2002
+ "description": "Test modulebaytemplate description",
2003
+ }
2004
+
2005
+ cls.bulk_edit_data = {
2006
+ "description": "Description changed",
2007
+ }
2008
+
2009
+ test_instance = cls.model.objects.first()
2010
+ cls.update_data = {
2011
+ "name": test_instance.name,
2012
+ "device_type": getattr(getattr(test_instance, "device_type", None), "pk", None),
2013
+ "module_type": getattr(getattr(test_instance, "module_type", None), "pk", None),
2014
+ "position": "new test position",
2015
+ "label": "new test label",
2016
+ "description": "new test description",
2017
+ }
2018
+
2019
+
2020
+ class PlatformTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
2021
+ model = Platform
2022
+
2023
+ @classmethod
2024
+ def setUpTestData(cls):
2025
+ manufacturer = Manufacturer.objects.first()
2026
+
2027
+ # Protected FK to SoftwareImageFile prevents deletion
2028
+ DeviceTypeToSoftwareImageFile.objects.all().delete()
2029
+ # Protected FK to SoftwareVersion prevents deletion
2030
+ Device.objects.all().update(software_version=None)
2031
+
2032
+ cls.form_data = {
2033
+ "name": "Platform X",
2034
+ "manufacturer": manufacturer.pk,
2035
+ "napalm_driver": "junos",
2036
+ "napalm_args": None,
2037
+ "network_driver": "juniper_junos",
1464
2038
  "description": "A new platform",
1465
2039
  }
1466
2040
 
@@ -1593,11 +2167,11 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
1593
2167
  )
1594
2168
 
1595
2169
  intf_status = Status.objects.get_for_model(Interface).first()
1596
-
2170
+ intf_role = Role.objects.get_for_model(Interface).first()
1597
2171
  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),
2172
+ Interface.objects.create(device=devices[0], name="Interface A1", status=intf_status, role=intf_role),
2173
+ Interface.objects.create(device=devices[0], name="Interface A2", status=intf_status),
2174
+ Interface.objects.create(device=devices[0], name="Interface A3", status=intf_status, role=intf_role),
1601
2175
  )
1602
2176
 
1603
2177
  for device, ipaddress in zip(devices, ipaddresses):
@@ -1654,52 +2228,456 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
1654
2228
  def test_device_consoleports(self):
1655
2229
  device = Device.objects.first()
1656
2230
 
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")
2231
+ ConsolePort.objects.create(device=device, name="Console Port 1")
2232
+ ConsolePort.objects.create(device=device, name="Console Port 2")
2233
+ ConsolePort.objects.create(device=device, name="Console Port 3")
2234
+
2235
+ url = reverse("dcim:device_consoleports", kwargs={"pk": device.pk})
2236
+ self.assertHttpStatus(self.client.get(url), 200)
2237
+
2238
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
2239
+ def test_device_consoleserverports(self):
2240
+ device = Device.objects.first()
2241
+
2242
+ ConsoleServerPort.objects.create(device=device, name="Console Server Port 1")
2243
+ ConsoleServerPort.objects.create(device=device, name="Console Server Port 2")
2244
+ ConsoleServerPort.objects.create(device=device, name="Console Server Port 3")
2245
+
2246
+ url = reverse("dcim:device_consoleserverports", kwargs={"pk": device.pk})
2247
+ self.assertHttpStatus(self.client.get(url), 200)
2248
+
2249
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
2250
+ def test_device_powerports(self):
2251
+ device = Device.objects.first()
2252
+
2253
+ PowerPort.objects.create(device=device, name="Power Port 1")
2254
+ PowerPort.objects.create(device=device, name="Power Port 2")
2255
+ PowerPort.objects.create(device=device, name="Power Port 3")
2256
+
2257
+ url = reverse("dcim:device_powerports", kwargs={"pk": device.pk})
2258
+ self.assertHttpStatus(self.client.get(url), 200)
2259
+
2260
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
2261
+ def test_device_poweroutlets(self):
2262
+ device = Device.objects.first()
2263
+
2264
+ PowerOutlet.objects.create(device=device, name="Power Outlet 1")
2265
+ PowerOutlet.objects.create(device=device, name="Power Outlet 2")
2266
+ PowerOutlet.objects.create(device=device, name="Power Outlet 3")
2267
+
2268
+ url = reverse("dcim:device_poweroutlets", kwargs={"pk": device.pk})
2269
+ self.assertHttpStatus(self.client.get(url), 200)
2270
+
2271
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
2272
+ def test_device_interfaces(self):
2273
+ device = Device.objects.filter(interfaces__isnull=False).first()
2274
+ self.add_permissions("ipam.add_ipaddress", "dcim.change_interface")
2275
+
2276
+ url = reverse("dcim:device_interfaces", kwargs={"pk": device.pk})
2277
+ response = self.client.get(url)
2278
+ self.assertHttpStatus(response, 200)
2279
+ response_body = response.content.decode(response.charset)
2280
+ # Count the number of occurrences of "Add IP address" in the response_body
2281
+ count = response_body.count("Add IP address")
2282
+ # Assert that "Add IP address" appears for each of the three interfaces
2283
+ self.assertEqual(count, 3)
2284
+
2285
+ def test_device_interface_assign_ipaddress(self):
2286
+ device = Device.objects.first()
2287
+ self.add_permissions(
2288
+ "ipam.add_ipaddress",
2289
+ "extras.view_status",
2290
+ "ipam.view_namespace",
2291
+ "dcim.view_device",
2292
+ "dcim.view_interface",
2293
+ )
2294
+ device_list_url = reverse("dcim:device_interfaces", args=(device.pk,))
2295
+ namespace = Namespace.objects.first()
2296
+ ipaddresses = [str(ipadress) for ipadress in IPAddress.objects.values_list("pk", flat=True)[:3]]
2297
+ add_new_ip_form_data = {
2298
+ "namespace": namespace.pk,
2299
+ "address": "1.1.1.7/24",
2300
+ "tenant": None,
2301
+ "status": Status.objects.get_for_model(IPAddress).first().pk,
2302
+ "type": IPAddressTypeChoices.TYPE_DHCP,
2303
+ "role": None,
2304
+ "nat_inside": None,
2305
+ "dns_name": None,
2306
+ "description": None,
2307
+ "tags": [],
2308
+ "interface": self.interfaces[0].id,
2309
+ }
2310
+ add_new_ip_request = {
2311
+ "path": reverse("ipam:ipaddress_add") + f"?interface={self.interfaces[0].id}&return_url={device_list_url}",
2312
+ "data": post_data(add_new_ip_form_data),
2313
+ }
2314
+ assign_ip_form_data = {"pk": ipaddresses}
2315
+ assign_ip_request = {
2316
+ "path": reverse("ipam:ipaddress_assign")
2317
+ + f"?interface={self.interfaces[1].id}&return_url={device_list_url}",
2318
+ "data": post_data(assign_ip_form_data),
2319
+ }
2320
+
2321
+ with self.subTest("Assert Cannnot assign IPAddress('Add New') without permission"):
2322
+ # Assert Add new IPAddress
2323
+ response = self.client.post(**add_new_ip_request, follow=True)
2324
+ response_body = response.content.decode(response.charset)
2325
+ self.assertHttpStatus(response, 200)
2326
+ self.interfaces[0].refresh_from_db()
2327
+ self.assertEqual(self.interfaces[0].ip_addresses.all().count(), 0)
2328
+ self.assertIn(
2329
+ f"Interface with id &quot;{self.interfaces[0].pk}&quot; not found",
2330
+ response_body,
2331
+ )
2332
+
2333
+ with self.subTest("Assert Cannnot assign IPAddress(Exsisting IP) without permission"):
2334
+ # Assert Assign Exsisting IPAddress
2335
+ response = self.client.post(**assign_ip_request, follow=True)
2336
+ response_body = response.content.decode(response.charset)
2337
+ self.assertHttpStatus(response, 200)
2338
+ self.interfaces[1].refresh_from_db()
2339
+ self.assertEqual(self.interfaces[1].ip_addresses.all().count(), 0)
2340
+ self.assertIn(
2341
+ f"Interface with id &quot;{self.interfaces[1].pk}&quot; not found",
2342
+ response_body,
2343
+ )
2344
+
2345
+ self.add_permissions("dcim.change_interface", "ipam.view_ipaddress")
2346
+
2347
+ with self.subTest("Assert Create and Assign IPAddress"):
2348
+ self.assertHttpStatus(self.client.post(**add_new_ip_request), 302)
2349
+ self.interfaces[0].refresh_from_db()
2350
+ self.assertEqual(
2351
+ str(self.interfaces[0].ip_addresses.all().first().address),
2352
+ add_new_ip_form_data["address"],
2353
+ )
2354
+
2355
+ with self.subTest("Assert Assign IPAddress"):
2356
+ response = self.client.post(**assign_ip_request)
2357
+ self.assertHttpStatus(response, 302)
2358
+ self.interfaces[1].refresh_from_db()
2359
+ self.assertEqual(self.interfaces[1].ip_addresses.count(), 3)
2360
+ interface_ips = [str(ip) for ip in self.interfaces[1].ip_addresses.values_list("pk", flat=True)]
2361
+ self.assertEqual(
2362
+ sorted(ipaddresses),
2363
+ sorted(interface_ips),
2364
+ )
2365
+
2366
+ with self.subTest("Assert Assigning IPAddress Without Selecting Any IPAddress Raises Exception"):
2367
+ assign_ip_form_data["pk"] = []
2368
+ assign_ip_request = {
2369
+ "path": reverse("ipam:ipaddress_assign")
2370
+ + f"?interface={self.interfaces[1].id}&return_url={device_list_url}",
2371
+ "data": post_data(assign_ip_form_data),
2372
+ }
2373
+ response = self.client.post(**assign_ip_request, follow=True)
2374
+ self.assertHttpStatus(response, 200)
2375
+ self.assertIn(
2376
+ "Please select at least one IP Address from the table.", response.content.decode(response.charset)
2377
+ )
2378
+
2379
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
2380
+ def test_device_rearports(self):
2381
+ device = Device.objects.first()
2382
+
2383
+ RearPort.objects.create(device=device, name="Rear Port 1")
2384
+ RearPort.objects.create(device=device, name="Rear Port 2")
2385
+ RearPort.objects.create(device=device, name="Rear Port 3")
2386
+
2387
+ url = reverse("dcim:device_rearports", kwargs={"pk": device.pk})
2388
+ self.assertHttpStatus(self.client.get(url), 200)
2389
+
2390
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
2391
+ def test_device_frontports(self):
2392
+ device = Device.objects.first()
2393
+ rear_ports = (
2394
+ RearPort.objects.create(device=device, name="Rear Port 1"),
2395
+ RearPort.objects.create(device=device, name="Rear Port 2"),
2396
+ RearPort.objects.create(device=device, name="Rear Port 3"),
2397
+ )
2398
+
2399
+ FrontPort.objects.create(
2400
+ device=device,
2401
+ name="Front Port 1",
2402
+ rear_port=rear_ports[0],
2403
+ rear_port_position=1,
2404
+ )
2405
+ FrontPort.objects.create(
2406
+ device=device,
2407
+ name="Front Port 2",
2408
+ rear_port=rear_ports[1],
2409
+ rear_port_position=1,
2410
+ )
2411
+ FrontPort.objects.create(
2412
+ device=device,
2413
+ name="Front Port 3",
2414
+ rear_port=rear_ports[2],
2415
+ rear_port_position=1,
2416
+ )
2417
+
2418
+ url = reverse("dcim:device_frontports", kwargs={"pk": device.pk})
2419
+ self.assertHttpStatus(self.client.get(url), 200)
2420
+
2421
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
2422
+ def test_device_devicebays(self):
2423
+ device = Device.objects.first()
2424
+
2425
+ # Device Bay 1 was already created in setUpTestData()
2426
+ DeviceBay.objects.create(device=device, name="Device Bay 2")
2427
+ DeviceBay.objects.create(device=device, name="Device Bay 3")
2428
+
2429
+ url = reverse("dcim:device_devicebays", kwargs={"pk": device.pk})
2430
+ self.assertHttpStatus(self.client.get(url), 200)
2431
+
2432
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
2433
+ def test_device_inventory(self):
2434
+ device = Device.objects.first()
2435
+
2436
+ InventoryItem.objects.create(device=device, name="Inventory Item 1")
2437
+ InventoryItem.objects.create(device=device, name="Inventory Item 2")
2438
+ InventoryItem.objects.create(device=device, name="Inventory Item 3")
2439
+
2440
+ url = reverse("dcim:device_inventory", kwargs={"pk": device.pk})
2441
+ self.assertHttpStatus(self.client.get(url), 200)
2442
+
2443
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
2444
+ def test_device_primary_ips(self):
2445
+ """Test assigning a primary IP to a device."""
2446
+ self.add_permissions("dcim.change_device")
2447
+
2448
+ # Create an interface and assign an IP to it.
2449
+ device = Device.objects.filter(interfaces__isnull=False).first()
2450
+ interface = device.interfaces.first()
2451
+ namespace = Namespace.objects.first()
2452
+ Prefix.objects.create(prefix="1.2.3.0/24", namespace=namespace, status=self.prefix_status)
2453
+ ip_address = IPAddress.objects.create(address="1.2.3.4/32", namespace=namespace, status=self.ipaddr_status)
2454
+ interface.ip_addresses.add(ip_address)
2455
+
2456
+ # Dupe the form data and populated primary_ip4 w/ ip_address
2457
+ form_data = self.form_data.copy()
2458
+ form_data["primary_ip4"] = ip_address.pk
2459
+ # Assert that update succeeds.
2460
+ request = {
2461
+ "path": self._get_url("edit", device),
2462
+ "data": post_data(form_data),
2463
+ }
2464
+ self.assertHttpStatus(self.client.post(**request), 302)
2465
+ self.assertInstanceEqual(self._get_queryset().order_by("last_updated").last(), form_data)
2466
+
2467
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
2468
+ def test_local_config_context_schema_validation_pass(self):
2469
+ """
2470
+ Given a config context schema
2471
+ And a device with local context that conforms to that schema
2472
+ Assert that the local context passes schema validation via full_clean()
2473
+ """
2474
+ schema = ConfigContextSchema.objects.create(
2475
+ name="Schema 1",
2476
+ data_schema={"type": "object", "properties": {"foo": {"type": "string"}}},
2477
+ )
2478
+ self.add_permissions("dcim.add_device")
2479
+
2480
+ form_data = self.form_data.copy()
2481
+ form_data["local_config_context_schema"] = schema.pk
2482
+ form_data["local_config_context_data"] = '{"foo": "bar"}'
2483
+
2484
+ # Try POST with model-level permission
2485
+ request = {
2486
+ "path": self._get_url("add"),
2487
+ "data": post_data(form_data),
2488
+ }
2489
+ self.assertHttpStatus(self.client.post(**request), 302)
2490
+ self.assertEqual(
2491
+ self._get_queryset().get(name="Device X").local_config_context_schema.pk,
2492
+ schema.pk,
2493
+ )
2494
+
2495
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
2496
+ def test_local_config_context_schema_validation_fails(self):
2497
+ """
2498
+ Given a config context schema
2499
+ And a device with local context that *does not* conform to that schema
2500
+ Assert that the local context fails schema validation via full_clean()
2501
+ """
2502
+ schema = ConfigContextSchema.objects.create(
2503
+ name="Schema 1",
2504
+ data_schema={"type": "object", "properties": {"foo": {"type": "integer"}}},
2505
+ )
2506
+ self.add_permissions("dcim.add_device")
2507
+
2508
+ form_data = self.form_data.copy()
2509
+ form_data["local_config_context_schema"] = schema.pk
2510
+ form_data["local_config_context_data"] = '{"foo": "bar"}'
2511
+
2512
+ # Try POST with model-level permission
2513
+ request = {
2514
+ "path": self._get_url("add"),
2515
+ "data": post_data(form_data),
2516
+ }
2517
+ self.assertHttpStatus(self.client.post(**request), 200)
2518
+ self.assertEqual(self._get_queryset().filter(name="Device X").count(), 0)
2519
+
2520
+
2521
+ class ModuleTestCase(ViewTestCases.PrimaryObjectViewTestCase):
2522
+ model = Module
2523
+
2524
+ @classmethod
2525
+ def setUpTestData(cls):
2526
+ Module.objects.all().delete()
2527
+ locations = Location.objects.filter(location_type=LocationType.objects.get(name="Campus"))[:2]
2528
+ manufacturer = Manufacturer.objects.first()
2529
+
2530
+ moduletypes = (
2531
+ ModuleType.objects.create(model="Module Type 1", manufacturer=manufacturer),
2532
+ ModuleType.objects.create(model="Module Type 2", manufacturer=manufacturer),
2533
+ )
2534
+
2535
+ moduleroles = Role.objects.get_for_model(Module)[:2]
2536
+
2537
+ statuses = Status.objects.get_for_model(Module)
2538
+ status_active = statuses[0]
2539
+
2540
+ cls.custom_fields = (
2541
+ CustomField.objects.create(
2542
+ type=CustomFieldTypeChoices.TYPE_INTEGER,
2543
+ label="Crash Counter",
2544
+ default=0,
2545
+ ),
2546
+ )
2547
+ cls.custom_fields[0].content_types.set([ContentType.objects.get_for_model(Module)])
2548
+
2549
+ modules = (
2550
+ Module.objects.create(
2551
+ location=locations[0],
2552
+ module_type=moduletypes[0],
2553
+ role=moduleroles[0],
2554
+ status=status_active,
2555
+ _custom_field_data={"crash_counter": 5},
2556
+ ),
2557
+ Module.objects.create(
2558
+ location=locations[0],
2559
+ module_type=moduletypes[0],
2560
+ role=moduleroles[0],
2561
+ status=status_active,
2562
+ _custom_field_data={"crash_counter": 10},
2563
+ ),
2564
+ Module.objects.create(
2565
+ location=locations[0],
2566
+ module_type=moduletypes[0],
2567
+ role=moduleroles[0],
2568
+ status=status_active,
2569
+ _custom_field_data={"crash_counter": 15},
2570
+ ),
2571
+ )
2572
+
2573
+ cls.relationships = (
2574
+ Relationship(
2575
+ label="BGP Router-ID",
2576
+ key="router_id",
2577
+ type=RelationshipTypeChoices.TYPE_ONE_TO_ONE,
2578
+ source_type=ContentType.objects.get_for_model(Module),
2579
+ source_label="BGP Router ID",
2580
+ destination_type=ContentType.objects.get_for_model(IPAddress),
2581
+ destination_label="Module using this as BGP router-ID",
2582
+ ),
2583
+ )
2584
+ for relationship in cls.relationships:
2585
+ relationship.validated_save()
2586
+
2587
+ cls.ipaddr_status = Status.objects.get_for_model(IPAddress).first()
2588
+ cls.prefix_status = Status.objects.get_for_model(Prefix).first()
2589
+ namespace = Namespace.objects.first()
2590
+ Prefix.objects.create(prefix="1.1.1.1/24", namespace=namespace, status=cls.prefix_status)
2591
+ Prefix.objects.create(prefix="2.2.2.2/24", namespace=namespace, status=cls.prefix_status)
2592
+ Prefix.objects.create(prefix="3.3.3.3/24", namespace=namespace, status=cls.prefix_status)
2593
+ ipaddresses = (
2594
+ IPAddress.objects.create(address="1.1.1.1/32", namespace=namespace, status=cls.ipaddr_status),
2595
+ IPAddress.objects.create(address="2.2.2.2/32", namespace=namespace, status=cls.ipaddr_status),
2596
+ IPAddress.objects.create(address="3.3.3.3/32", namespace=namespace, status=cls.ipaddr_status),
2597
+ )
2598
+
2599
+ intf_status = Status.objects.get_for_model(Interface).first()
2600
+ intf_role = Role.objects.get_for_model(Interface).first()
2601
+ cls.interfaces = (
2602
+ Interface.objects.create(module=modules[0], name="Interface A1", status=intf_status, role=intf_role),
2603
+ Interface.objects.create(module=modules[0], name="Interface A2", status=intf_status),
2604
+ Interface.objects.create(module=modules[0], name="Interface A3", status=intf_status, role=intf_role),
2605
+ )
2606
+
2607
+ for module, ipaddress in zip(modules, ipaddresses):
2608
+ RelationshipAssociation(
2609
+ relationship=cls.relationships[0], source=module, destination=ipaddress
2610
+ ).validated_save()
2611
+
2612
+ cls.form_data = {
2613
+ "module_type": moduletypes[1].pk,
2614
+ "role": moduleroles[1].pk,
2615
+ "tenant": None,
2616
+ "serial": "VMWARE-XX XX XX XX XX XX XX XX-XX XX XX XX XX XX XX XX",
2617
+ "asset_tag": generate_random_device_asset_tag_of_specified_size(100),
2618
+ "location": locations[1].pk,
2619
+ "status": statuses[1].pk,
2620
+ "tags": [t.pk for t in Tag.objects.get_for_model(Module)],
2621
+ "cf_crash_counter": -1,
2622
+ "cr_router-id": None,
2623
+ }
2624
+
2625
+ cls.bulk_edit_data = {
2626
+ "role": moduleroles[1].pk,
2627
+ "tenant": None,
2628
+ "status": statuses[2].pk,
2629
+ }
2630
+
2631
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
2632
+ def test_module_consoleports(self):
2633
+ module = Module.objects.first()
2634
+
2635
+ ConsolePort.objects.create(module=module, name="Console Port 1")
2636
+ ConsolePort.objects.create(module=module, name="Console Port 2")
2637
+ ConsolePort.objects.create(module=module, name="Console Port 3")
1660
2638
 
1661
- url = reverse("dcim:device_consoleports", kwargs={"pk": device.pk})
2639
+ url = reverse("dcim:module_consoleports", kwargs={"pk": module.pk})
1662
2640
  self.assertHttpStatus(self.client.get(url), 200)
1663
2641
 
1664
2642
  @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
1665
- def test_device_consoleserverports(self):
1666
- device = Device.objects.first()
2643
+ def test_module_consoleserverports(self):
2644
+ module = Module.objects.first()
1667
2645
 
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")
2646
+ ConsoleServerPort.objects.create(module=module, name="Console Server Port 1")
2647
+ ConsoleServerPort.objects.create(module=module, name="Console Server Port 2")
2648
+ ConsoleServerPort.objects.create(module=module, name="Console Server Port 3")
1671
2649
 
1672
- url = reverse("dcim:device_consoleserverports", kwargs={"pk": device.pk})
2650
+ url = reverse("dcim:module_consoleserverports", kwargs={"pk": module.pk})
1673
2651
  self.assertHttpStatus(self.client.get(url), 200)
1674
2652
 
1675
2653
  @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
1676
- def test_device_powerports(self):
1677
- device = Device.objects.first()
2654
+ def test_module_powerports(self):
2655
+ module = Module.objects.first()
1678
2656
 
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")
2657
+ PowerPort.objects.create(module=module, name="Power Port 1")
2658
+ PowerPort.objects.create(module=module, name="Power Port 2")
2659
+ PowerPort.objects.create(module=module, name="Power Port 3")
1682
2660
 
1683
- url = reverse("dcim:device_powerports", kwargs={"pk": device.pk})
2661
+ url = reverse("dcim:module_powerports", kwargs={"pk": module.pk})
1684
2662
  self.assertHttpStatus(self.client.get(url), 200)
1685
2663
 
1686
2664
  @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
1687
- def test_device_poweroutlets(self):
1688
- device = Device.objects.first()
2665
+ def test_module_poweroutlets(self):
2666
+ module = Module.objects.first()
1689
2667
 
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")
2668
+ PowerOutlet.objects.create(module=module, name="Power Outlet 1")
2669
+ PowerOutlet.objects.create(module=module, name="Power Outlet 2")
2670
+ PowerOutlet.objects.create(module=module, name="Power Outlet 3")
1693
2671
 
1694
- url = reverse("dcim:device_poweroutlets", kwargs={"pk": device.pk})
2672
+ url = reverse("dcim:module_poweroutlets", kwargs={"pk": module.pk})
1695
2673
  self.assertHttpStatus(self.client.get(url), 200)
1696
2674
 
1697
2675
  @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
1698
- def test_device_interfaces(self):
1699
- device = Device.objects.filter(interfaces__isnull=False).first()
2676
+ def test_module_interfaces(self):
2677
+ module = Module.objects.filter(interfaces__isnull=False).first()
1700
2678
  self.add_permissions("ipam.add_ipaddress", "dcim.change_interface")
1701
2679
 
1702
- url = reverse("dcim:device_interfaces", kwargs={"pk": device.pk})
2680
+ url = reverse("dcim:module_interfaces", kwargs={"pk": module.pk})
1703
2681
  response = self.client.get(url)
1704
2682
  self.assertHttpStatus(response, 200)
1705
2683
  response_body = response.content.decode(response.charset)
@@ -1708,16 +2686,16 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
1708
2686
  # Assert that "Add IP address" appears for each of the three interfaces
1709
2687
  self.assertEqual(count, 3)
1710
2688
 
1711
- def test_device_interface_assign_ipaddress(self):
1712
- device = Device.objects.first()
2689
+ def test_module_interface_assign_ipaddress(self):
2690
+ module = Module.objects.first()
1713
2691
  self.add_permissions(
1714
2692
  "ipam.add_ipaddress",
1715
2693
  "extras.view_status",
1716
2694
  "ipam.view_namespace",
1717
- "dcim.view_device",
2695
+ "dcim.view_module",
1718
2696
  "dcim.view_interface",
1719
2697
  )
1720
- device_list_url = reverse("dcim:device_interfaces", args=(device.pk,))
2698
+ module_list_url = reverse("dcim:module_interfaces", args=(module.pk,))
1721
2699
  namespace = Namespace.objects.first()
1722
2700
  ipaddresses = [str(ipadress) for ipadress in IPAddress.objects.values_list("pk", flat=True)[:3]]
1723
2701
  add_new_ip_form_data = {
@@ -1734,13 +2712,13 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
1734
2712
  "interface": self.interfaces[0].id,
1735
2713
  }
1736
2714
  add_new_ip_request = {
1737
- "path": reverse("ipam:ipaddress_add") + f"?interface={self.interfaces[0].id}&return_url={device_list_url}",
2715
+ "path": reverse("ipam:ipaddress_add") + f"?interface={self.interfaces[0].id}&return_url={module_list_url}",
1738
2716
  "data": post_data(add_new_ip_form_data),
1739
2717
  }
1740
2718
  assign_ip_form_data = {"pk": ipaddresses}
1741
2719
  assign_ip_request = {
1742
2720
  "path": reverse("ipam:ipaddress_assign")
1743
- + f"?interface={self.interfaces[1].id}&return_url={device_list_url}",
2721
+ + f"?interface={self.interfaces[1].id}&return_url={module_list_url}",
1744
2722
  "data": post_data(assign_ip_form_data),
1745
2723
  }
1746
2724
 
@@ -1790,146 +2768,58 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
1790
2768
  )
1791
2769
 
1792
2770
  @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
1793
- def test_device_rearports(self):
1794
- device = Device.objects.first()
2771
+ def test_module_rearports(self):
2772
+ module = Module.objects.first()
1795
2773
 
1796
- RearPort.objects.create(device=device, name="Rear Port 1")
1797
- RearPort.objects.create(device=device, name="Rear Port 2")
1798
- RearPort.objects.create(device=device, name="Rear Port 3")
2774
+ RearPort.objects.create(module=module, name="Rear Port 1")
2775
+ RearPort.objects.create(module=module, name="Rear Port 2")
2776
+ RearPort.objects.create(module=module, name="Rear Port 3")
1799
2777
 
1800
- url = reverse("dcim:device_rearports", kwargs={"pk": device.pk})
2778
+ url = reverse("dcim:module_rearports", kwargs={"pk": module.pk})
1801
2779
  self.assertHttpStatus(self.client.get(url), 200)
1802
2780
 
1803
2781
  @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
1804
- def test_device_frontports(self):
1805
- device = Device.objects.first()
2782
+ def test_module_frontports(self):
2783
+ module = Module.objects.first()
1806
2784
  rear_ports = (
1807
- RearPort.objects.create(device=device, name="Rear Port 1"),
1808
- RearPort.objects.create(device=device, name="Rear Port 2"),
1809
- RearPort.objects.create(device=device, name="Rear Port 3"),
2785
+ RearPort.objects.create(module=module, name="Rear Port 1"),
2786
+ RearPort.objects.create(module=module, name="Rear Port 2"),
2787
+ RearPort.objects.create(module=module, name="Rear Port 3"),
1810
2788
  )
1811
2789
 
1812
2790
  FrontPort.objects.create(
1813
- device=device,
2791
+ module=module,
1814
2792
  name="Front Port 1",
1815
2793
  rear_port=rear_ports[0],
1816
2794
  rear_port_position=1,
1817
2795
  )
1818
2796
  FrontPort.objects.create(
1819
- device=device,
2797
+ module=module,
1820
2798
  name="Front Port 2",
1821
2799
  rear_port=rear_ports[1],
1822
2800
  rear_port_position=1,
1823
2801
  )
1824
2802
  FrontPort.objects.create(
1825
- device=device,
2803
+ module=module,
1826
2804
  name="Front Port 3",
1827
2805
  rear_port=rear_ports[2],
1828
2806
  rear_port_position=1,
1829
2807
  )
1830
2808
 
1831
- url = reverse("dcim:device_frontports", kwargs={"pk": device.pk})
1832
- self.assertHttpStatus(self.client.get(url), 200)
1833
-
1834
- @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
1835
- def test_device_devicebays(self):
1836
- device = Device.objects.first()
1837
-
1838
- # Device Bay 1 was already created in setUpTestData()
1839
- DeviceBay.objects.create(device=device, name="Device Bay 2")
1840
- DeviceBay.objects.create(device=device, name="Device Bay 3")
1841
-
1842
- url = reverse("dcim:device_devicebays", kwargs={"pk": device.pk})
2809
+ url = reverse("dcim:module_frontports", kwargs={"pk": module.pk})
1843
2810
  self.assertHttpStatus(self.client.get(url), 200)
1844
2811
 
1845
2812
  @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
1846
- def test_device_inventory(self):
1847
- device = Device.objects.first()
2813
+ def test_module_modulebays(self):
2814
+ module = Module.objects.first()
1848
2815
 
1849
- InventoryItem.objects.create(device=device, name="Inventory Item 1")
1850
- InventoryItem.objects.create(device=device, name="Inventory Item 2")
1851
- InventoryItem.objects.create(device=device, name="Inventory Item 3")
2816
+ ModuleBay.objects.create(parent_module=module, name="Test View Module Bay 1")
2817
+ ModuleBay.objects.create(parent_module=module, name="Test View Module Bay 2")
2818
+ ModuleBay.objects.create(parent_module=module, name="Test View Module Bay 3")
1852
2819
 
1853
- url = reverse("dcim:device_inventory", kwargs={"pk": device.pk})
2820
+ url = reverse("dcim:module_modulebays", kwargs={"pk": module.pk})
1854
2821
  self.assertHttpStatus(self.client.get(url), 200)
1855
2822
 
1856
- @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
1857
- def test_device_primary_ips(self):
1858
- """Test assigning a primary IP to a device."""
1859
- self.add_permissions("dcim.change_device")
1860
-
1861
- # Create an interface and assign an IP to it.
1862
- device = Device.objects.filter(interfaces__isnull=False).first()
1863
- interface = device.interfaces.first()
1864
- namespace = Namespace.objects.first()
1865
- Prefix.objects.create(prefix="1.2.3.0/24", namespace=namespace, status=self.prefix_status)
1866
- ip_address = IPAddress.objects.create(address="1.2.3.4/32", namespace=namespace, status=self.ipaddr_status)
1867
- interface.ip_addresses.add(ip_address)
1868
-
1869
- # Dupe the form data and populated primary_ip4 w/ ip_address
1870
- form_data = self.form_data.copy()
1871
- form_data["primary_ip4"] = ip_address.pk
1872
- # Assert that update succeeds.
1873
- request = {
1874
- "path": self._get_url("edit", device),
1875
- "data": post_data(form_data),
1876
- }
1877
- self.assertHttpStatus(self.client.post(**request), 302)
1878
- self.assertInstanceEqual(self._get_queryset().order_by("last_updated").last(), form_data)
1879
-
1880
- @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
1881
- def test_local_config_context_schema_validation_pass(self):
1882
- """
1883
- Given a config context schema
1884
- And a device with local context that conforms to that schema
1885
- Assert that the local context passes schema validation via full_clean()
1886
- """
1887
- schema = ConfigContextSchema.objects.create(
1888
- name="Schema 1",
1889
- data_schema={"type": "object", "properties": {"foo": {"type": "string"}}},
1890
- )
1891
- self.add_permissions("dcim.add_device")
1892
-
1893
- form_data = self.form_data.copy()
1894
- form_data["local_config_context_schema"] = schema.pk
1895
- form_data["local_config_context_data"] = '{"foo": "bar"}'
1896
-
1897
- # Try POST with model-level permission
1898
- request = {
1899
- "path": self._get_url("add"),
1900
- "data": post_data(form_data),
1901
- }
1902
- self.assertHttpStatus(self.client.post(**request), 302)
1903
- self.assertEqual(
1904
- self._get_queryset().get(name="Device X").local_config_context_schema.pk,
1905
- schema.pk,
1906
- )
1907
-
1908
- @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
1909
- def test_local_config_context_schema_validation_fails(self):
1910
- """
1911
- Given a config context schema
1912
- And a device with local context that *does not* conform to that schema
1913
- Assert that the local context fails schema validation via full_clean()
1914
- """
1915
- schema = ConfigContextSchema.objects.create(
1916
- name="Schema 1",
1917
- data_schema={"type": "object", "properties": {"foo": {"type": "integer"}}},
1918
- )
1919
- self.add_permissions("dcim.add_device")
1920
-
1921
- form_data = self.form_data.copy()
1922
- form_data["local_config_context_schema"] = schema.pk
1923
- form_data["local_config_context_data"] = '{"foo": "bar"}'
1924
-
1925
- # Try POST with model-level permission
1926
- request = {
1927
- "path": self._get_url("add"),
1928
- "data": post_data(form_data),
1929
- }
1930
- self.assertHttpStatus(self.client.post(**request), 200)
1931
- self.assertEqual(self._get_queryset().filter(name="Device X").count(), 0)
1932
-
1933
2823
 
1934
2824
  class ConsolePortTestCase(ViewTestCases.DeviceComponentViewTestCase):
1935
2825
  model = ConsolePort
@@ -1970,6 +2860,15 @@ class ConsolePortTestCase(ViewTestCases.DeviceComponentViewTestCase):
1970
2860
  "description": "New description",
1971
2861
  }
1972
2862
 
2863
+ test_instance = cls.model.objects.first()
2864
+ cls.update_data = {
2865
+ "name": test_instance.name,
2866
+ "device": getattr(getattr(test_instance, "device", None), "pk", None),
2867
+ "module": getattr(getattr(test_instance, "module", None), "pk", None),
2868
+ "label": "new test label",
2869
+ "description": "new test description",
2870
+ }
2871
+
1973
2872
 
1974
2873
  class ConsoleServerPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
1975
2874
  model = ConsoleServerPort
@@ -2009,6 +2908,15 @@ class ConsoleServerPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
2009
2908
  "description": "New description",
2010
2909
  }
2011
2910
 
2911
+ test_instance = cls.model.objects.first()
2912
+ cls.update_data = {
2913
+ "name": test_instance.name,
2914
+ "device": getattr(getattr(test_instance, "device", None), "pk", None),
2915
+ "module": getattr(getattr(test_instance, "module", None), "pk", None),
2916
+ "label": "new test label",
2917
+ "description": "new test description",
2918
+ }
2919
+
2012
2920
 
2013
2921
  class PowerPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
2014
2922
  model = PowerPort
@@ -2053,12 +2961,22 @@ class PowerPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
2053
2961
  "description": "New description",
2054
2962
  }
2055
2963
 
2964
+ test_instance = cls.model.objects.first()
2965
+ cls.update_data = {
2966
+ "name": test_instance.name,
2967
+ "device": getattr(getattr(test_instance, "device", None), "pk", None),
2968
+ "module": getattr(getattr(test_instance, "module", None), "pk", None),
2969
+ "label": "new test label",
2970
+ "description": "new test description",
2971
+ }
2972
+
2056
2973
 
2057
2974
  class PowerOutletTestCase(ViewTestCases.DeviceComponentViewTestCase):
2058
2975
  model = PowerOutlet
2059
2976
 
2060
2977
  @classmethod
2061
2978
  def setUpTestData(cls):
2979
+ PowerOutlet.objects.all().delete()
2062
2980
  device = create_test_device("Device 1")
2063
2981
 
2064
2982
  powerports = (
@@ -2111,12 +3029,23 @@ class PowerOutletTestCase(ViewTestCases.DeviceComponentViewTestCase):
2111
3029
  "description": "New description",
2112
3030
  }
2113
3031
 
3032
+ test_instance = cls.model.objects.first()
3033
+ cls.update_data = {
3034
+ "name": test_instance.name,
3035
+ "device_type": getattr(getattr(test_instance, "device_type", None), "pk", None),
3036
+ "module_type": getattr(getattr(test_instance, "module_type", None), "pk", None),
3037
+ "power_port": getattr(test_instance.power_port, "pk", None), # power_port must match parent device/module
3038
+ "label": "new test label",
3039
+ "description": "new test description",
3040
+ }
3041
+
2114
3042
 
2115
3043
  class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
2116
3044
  model = Interface
2117
3045
 
2118
3046
  @classmethod
2119
3047
  def setUpTestData(cls):
3048
+ Interface.objects.all().delete()
2120
3049
  device = create_test_device("Device 1")
2121
3050
  vrfs = list(VRF.objects.all()[:3])
2122
3051
  for vrf in vrfs:
@@ -2124,22 +3053,24 @@ class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
2124
3053
 
2125
3054
  statuses = Status.objects.get_for_model(Interface)
2126
3055
  status_active = statuses[0]
2127
-
3056
+ role = Role.objects.get_for_model(Interface).first()
2128
3057
  interfaces = (
2129
- Interface.objects.create(device=device, name="Interface 1", status=status_active),
2130
- Interface.objects.create(device=device, name="Interface 2", status=status_active),
2131
- Interface.objects.create(device=device, name="Interface 3", status=status_active),
3058
+ Interface.objects.create(device=device, name="Interface A1", status=status_active, role=role),
3059
+ Interface.objects.create(device=device, name="Interface A2", status=status_active),
3060
+ Interface.objects.create(device=device, name="Interface A3", status=status_active, role=role),
2132
3061
  Interface.objects.create(
2133
3062
  device=device,
2134
3063
  name="LAG",
2135
3064
  status=status_active,
2136
3065
  type=InterfaceTypeChoices.TYPE_LAG,
3066
+ role=role,
2137
3067
  ),
2138
3068
  Interface.objects.create(
2139
3069
  device=device,
2140
3070
  name="BRIDGE",
2141
3071
  status=status_active,
2142
3072
  type=InterfaceTypeChoices.TYPE_BRIDGE,
3073
+ role=role,
2143
3074
  ),
2144
3075
  )
2145
3076
  cls.lag_interface = interfaces[3]
@@ -2186,6 +3117,7 @@ class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
2186
3117
  "type": InterfaceTypeChoices.TYPE_1GE_GBIC,
2187
3118
  "enabled": False,
2188
3119
  "status": status_active.pk,
3120
+ "role": role.pk,
2189
3121
  "lag": interfaces[3].pk,
2190
3122
  "mac_address": EUI("01:02:03:04:05:06"),
2191
3123
  "mtu": 2000,
@@ -2214,6 +3146,7 @@ class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
2214
3146
  "tagged_vlans": [v.pk for v in vlans[1:4]],
2215
3147
  "tags": [t.pk for t in Tag.objects.get_for_model(Interface)],
2216
3148
  "status": status_active.pk,
3149
+ "role": role.pk,
2217
3150
  "vrf": vrfs[0].pk,
2218
3151
  }
2219
3152
 
@@ -2222,6 +3155,7 @@ class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
2222
3155
  "name_pattern": "Interface [4-6]",
2223
3156
  "label_pattern": "Interface Number [4-6]",
2224
3157
  "status": status_active.pk,
3158
+ "role": role.pk,
2225
3159
  "type": InterfaceTypeChoices.TYPE_1GE_GBIC,
2226
3160
  "enabled": True,
2227
3161
  "mtu": 1500,
@@ -2244,9 +3178,21 @@ class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
2244
3178
  "untagged_vlan": vlans[0].pk,
2245
3179
  "tagged_vlans": [v.pk for v in vlans[1:4]],
2246
3180
  "status": status_active.pk,
3181
+ "role": role.pk,
2247
3182
  "vrf": vrfs[2].pk,
2248
3183
  }
2249
3184
 
3185
+ test_instance = cls.model.objects.first()
3186
+ cls.update_data = {
3187
+ "name": test_instance.name,
3188
+ "device": getattr(getattr(test_instance, "device", None), "pk", None),
3189
+ "module": getattr(getattr(test_instance, "module", None), "pk", None),
3190
+ "status": test_instance.status.pk,
3191
+ "type": test_instance.type,
3192
+ "label": "new test label",
3193
+ "description": "new test description",
3194
+ }
3195
+
2250
3196
  @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
2251
3197
  def test_create_virtual_interface_with_parent_lag(self):
2252
3198
  """https://github.com/nautobot/nautobot/issues/4436."""
@@ -2292,18 +3238,66 @@ class FrontPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
2292
3238
  cls.device = device
2293
3239
 
2294
3240
  rearports = (
2295
- RearPort.objects.create(device=device, name="Rear Port 1"),
2296
- RearPort.objects.create(device=device, name="Rear Port 2"),
2297
- RearPort.objects.create(device=device, name="Rear Port 3"),
2298
- RearPort.objects.create(device=device, name="Rear Port 4"),
2299
- RearPort.objects.create(device=device, name="Rear Port 5"),
2300
- RearPort.objects.create(device=device, name="Rear Port 6"),
3241
+ RearPort.objects.create(
3242
+ device=device,
3243
+ type=PortTypeChoices.TYPE_8P8C,
3244
+ positions=24,
3245
+ name="Rear Port 1",
3246
+ ),
3247
+ RearPort.objects.create(
3248
+ device=device,
3249
+ type=PortTypeChoices.TYPE_8P8C,
3250
+ positions=24,
3251
+ name="Rear Port 2",
3252
+ ),
3253
+ RearPort.objects.create(
3254
+ device=device,
3255
+ type=PortTypeChoices.TYPE_8P8C,
3256
+ positions=24,
3257
+ name="Rear Port 3",
3258
+ ),
3259
+ RearPort.objects.create(
3260
+ device=device,
3261
+ type=PortTypeChoices.TYPE_8P8C,
3262
+ positions=24,
3263
+ name="Rear Port 4",
3264
+ ),
3265
+ RearPort.objects.create(
3266
+ device=device,
3267
+ type=PortTypeChoices.TYPE_8P8C,
3268
+ positions=24,
3269
+ name="Rear Port 5",
3270
+ ),
3271
+ RearPort.objects.create(
3272
+ device=device,
3273
+ type=PortTypeChoices.TYPE_8P8C,
3274
+ positions=24,
3275
+ name="Rear Port 6",
3276
+ ),
2301
3277
  )
2302
3278
 
2303
3279
  frontports = (
2304
- FrontPort.objects.create(device=device, name="Front Port 1", rear_port=rearports[0]),
2305
- FrontPort.objects.create(device=device, name="Front Port 2", rear_port=rearports[1]),
2306
- FrontPort.objects.create(device=device, name="Front Port 3", rear_port=rearports[2]),
3280
+ FrontPort.objects.create(
3281
+ device=device,
3282
+ name="Front Port 1",
3283
+ type=PortTypeChoices.TYPE_8P8C,
3284
+ rear_port=rearports[0],
3285
+ rear_port_position=12,
3286
+ ),
3287
+ FrontPort.objects.create(
3288
+ device=device,
3289
+ name="Front Port 2",
3290
+ type=PortTypeChoices.TYPE_8P8C,
3291
+ rear_port=rearports[1],
3292
+ rear_port_position=12,
3293
+ ),
3294
+ FrontPort.objects.create(
3295
+ device=device,
3296
+ name="Front Port 3",
3297
+ type=PortTypeChoices.TYPE_8P8C,
3298
+ rear_port=rearports[2],
3299
+ rear_port_position=12,
3300
+ ),
2307
3301
  )
2308
3302
  # Required by ViewTestCases.DeviceComponentViewTestCase.test_bulk_rename
2309
3303
  cls.selected_objects = frontports
@@ -2333,6 +3327,18 @@ class FrontPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
2333
3327
  "description": "New description",
2334
3328
  }
2335
3329
 
3330
+ test_instance = cls.model.objects.first()
3331
+ cls.update_data = {
3332
+ "name": test_instance.name,
3333
+ "device": getattr(getattr(test_instance, "device", None), "pk", None),
3334
+ "module": getattr(getattr(test_instance, "module", None), "pk", None),
3335
+ "rear_port": test_instance.rear_port.pk, # rear_port must match the parent device/module
3336
+ "rear_port_position": test_instance.rear_port_position,
3337
+ "type": test_instance.type,
3338
+ "label": "new test label",
3339
+ "description": "new test description",
3340
+ }
3341
+
2336
3342
  @unittest.skip("No DeviceBulkAddFrontPortView exists at present")
2337
3343
  def test_bulk_add_component(self):
2338
3344
  pass
@@ -2346,9 +3352,24 @@ class RearPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
2346
3352
  device = create_test_device("Device 1")
2347
3353
 
2348
3354
  rearports = (
2349
- RearPort.objects.create(device=device, name="Rear Port 1"),
2350
- RearPort.objects.create(device=device, name="Rear Port 2"),
2351
- RearPort.objects.create(device=device, name="Rear Port 3"),
3355
+ RearPort.objects.create(
3356
+ device=device,
3357
+ type=PortTypeChoices.TYPE_8P8C,
3358
+ positions=24,
3359
+ name="Rear Port 1",
3360
+ ),
3361
+ RearPort.objects.create(
3362
+ device=device,
3363
+ type=PortTypeChoices.TYPE_8P8C,
3364
+ positions=24,
3365
+ name="Rear Port 2",
3366
+ ),
3367
+ RearPort.objects.create(
3368
+ device=device,
3369
+ type=PortTypeChoices.TYPE_8P8C,
3370
+ positions=24,
3371
+ name="Rear Port 3",
3372
+ ),
2352
3373
  )
2353
3374
  # Required by ViewTestCases.DeviceComponentViewTestCase.test_bulk_rename
2354
3375
  cls.selected_objects = rearports
@@ -2377,6 +3398,17 @@ class RearPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
2377
3398
  "description": "New description",
2378
3399
  }
2379
3400
 
3401
+ test_instance = cls.model.objects.first()
3402
+ cls.update_data = {
3403
+ "name": test_instance.name,
3404
+ "device": getattr(getattr(test_instance, "device", None), "pk", None),
3405
+ "module": getattr(getattr(test_instance, "module", None), "pk", None),
3406
+ "positions": test_instance.positions,
3407
+ "type": test_instance.type,
3408
+ "label": "new test label",
3409
+ "description": "new test description",
3410
+ }
3411
+
2380
3412
 
2381
3413
  class DeviceBayTestCase(ViewTestCases.DeviceComponentViewTestCase):
2382
3414
  model = DeviceBay
@@ -2415,6 +3447,113 @@ class DeviceBayTestCase(ViewTestCases.DeviceComponentViewTestCase):
2415
3447
  "description": "New description",
2416
3448
  }
2417
3449
 
3450
+ test_instance = cls.model.objects.first()
3451
+ cls.update_data = {
3452
+ "name": test_instance.name,
3453
+ "device": test_instance.device.pk,
3454
+ "label": "new test label",
3455
+ "description": "new test description",
3456
+ }
3457
+
3458
+
3459
+ class ModuleBayTestCase(ViewTestCases.DeviceComponentViewTestCase):
3460
+ model = ModuleBay
3461
+
3462
+ @classmethod
3463
+ def setUpTestData(cls):
3464
+ device = Device.objects.first()
3465
+ module = Module.objects.first()
3466
+
3467
+ module_bays = (
3468
+ ModuleBay.objects.create(parent_device=device, name="Test View Module Bay 1"),
3469
+ ModuleBay.objects.create(parent_device=device, name="Test View Module Bay 2"),
3470
+ ModuleBay.objects.create(parent_device=device, name="Test View Module Bay 3"),
3471
+ )
3472
+ # Required by ViewTestCases.DeviceComponentViewTestCase.test_bulk_rename
3473
+ cls.selected_objects = module_bays
3474
+ cls.selected_objects_parent_name = device.name
3475
+
3476
+ cls.form_data = {
3477
+ "parent_device": device.pk,
3478
+ "name": "Test ModuleBay 1",
3479
+ "position": 1,
3480
+ "description": "Test modulebay description",
3481
+ "label": "Test modulebay label",
3482
+ "tags": sorted([t.pk for t in Tag.objects.get_for_model(ModuleBay)]),
3483
+ }
3484
+
3485
+ cls.bulk_create_data = {
3486
+ "parent_module": module.pk,
3487
+ "name_pattern": "Test ModuleBay [0-2]",
3488
+ "position_pattern": "[1-3]",
3489
+ # Test that a label can be applied to each generated module bay
3490
+ "label_pattern": "Slot[1-3]",
3491
+ "description": "Test modulebay description",
3492
+ "tags": sorted([t.pk for t in Tag.objects.get_for_model(ModuleBay)]),
3493
+ }
3494
+
3495
+ cls.bulk_edit_data = {
3496
+ "position": "new position",
3497
+ "description": "New description",
3498
+ "label": "New label",
3499
+ }
3500
+
3501
+ test_instance = cls.model.objects.first()
3502
+ cls.update_data = {
3503
+ "name": test_instance.name,
3504
+ "parent_device": getattr(getattr(test_instance, "parent_device", None), "pk", None),
3505
+ "parent_module": getattr(getattr(test_instance, "parent_module", None), "pk", None),
3506
+ "position": "new test position",
3507
+ "label": "new test label",
3508
+ "description": "new test description",
3509
+ }
3510
+
3511
+ def get_deletable_object_pks(self):
3512
+ # Since Modules and ModuleBays are nestable, we need to delete ModuleBays that don't have any child ModuleBays
3513
+ return ModuleBay.objects.filter(installed_module__isnull=True).values_list("pk", flat=True)[:3]
3514
+
3515
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
3516
+ def test_bulk_add_component(self):
3517
+ """Test bulk-adding this component to modules."""
3518
+ obj_perm = ObjectPermission(name="Test permission", actions=["add"])
3519
+ obj_perm.save()
3520
+ obj_perm.users.add(self.user)
3521
+ obj_perm.object_types.add(ContentType.objects.get_for_model(self.model))
3522
+
3523
+ initial_count = self._get_queryset().count()
3524
+
3525
+ data = self.bulk_create_data.copy()
3526
+
3527
+ # Load the module-bulk-add form
3528
+ module_perm = ObjectPermission(name="Module permission", actions=["change"])
3529
+ module_perm.save()
3530
+ module_perm.users.add(self.user)
3531
+ module_perm.object_types.add(ContentType.objects.get_for_model(Module))
3532
+ url = reverse(f"dcim:module_bulk_add_{self.model._meta.model_name}")
3533
+ request = {
3534
+ "path": url,
3535
+ "data": post_data({"pk": data["parent_module"]}),
3536
+ }
3537
+ self.assertHttpStatus(self.client.post(**request), 200)
3538
+
3539
+ # Post to the module-bulk-add form to create records
3540
+ data["pk"] = data.pop("parent_module")
3541
+ data["_create"] = ""
3542
+ request["data"] = post_data(data)
3543
+ self.assertHttpStatus(self.client.post(**request), 302)
3544
+
3545
+ updated_count = self._get_queryset().count()
3546
+ self.assertEqual(updated_count, initial_count + self.bulk_create_count)
3547
+
3548
+ matching_count = 0
3549
+ for instance in self._get_queryset().all():
3550
+ try:
3551
+ self.assertInstanceEqual(instance, self.bulk_create_data)
3552
+ matching_count += 1
3553
+ except AssertionError:
3554
+ pass
3555
+ self.assertEqual(matching_count, self.bulk_create_count)
3556
+
2418
3557
 
2419
3558
  class InventoryItemTestCase(ViewTestCases.DeviceComponentViewTestCase):
2420
3559
  model = InventoryItem
@@ -2467,6 +3606,14 @@ class InventoryItemTestCase(ViewTestCases.DeviceComponentViewTestCase):
2467
3606
  "software_version": software_versions[2].pk,
2468
3607
  }
2469
3608
 
3609
+ test_instance = cls.model.objects.first()
3610
+ cls.update_data = {
3611
+ "name": test_instance.name,
3612
+ "device": test_instance.device.pk,
3613
+ "label": "new test label",
3614
+ "description": "new test description",
3615
+ }
3616
+
2470
3617
  def test_table_with_indentation_is_removed_on_filter_or_sort(self):
2471
3618
  self.skipTest("InventoryItem table has no implementation of indentation.")
2472
3619
 
@@ -2527,73 +3674,73 @@ class CableTestCase(
2527
3674
  interfaces = (
2528
3675
  Interface.objects.create(
2529
3676
  device=devices[0],
2530
- name="Interface 1",
3677
+ name="Interface A1",
2531
3678
  type=InterfaceTypeChoices.TYPE_1GE_FIXED,
2532
3679
  status=interface_status,
2533
3680
  ),
2534
3681
  Interface.objects.create(
2535
3682
  device=devices[0],
2536
- name="Interface 2",
3683
+ name="Interface A2",
2537
3684
  type=InterfaceTypeChoices.TYPE_1GE_FIXED,
2538
3685
  status=interface_status,
2539
3686
  ),
2540
3687
  Interface.objects.create(
2541
3688
  device=devices[0],
2542
- name="Interface 3",
3689
+ name="Interface A3",
2543
3690
  type=InterfaceTypeChoices.TYPE_1GE_FIXED,
2544
3691
  status=interface_status,
2545
3692
  ),
2546
3693
  Interface.objects.create(
2547
3694
  device=devices[1],
2548
- name="Interface 1",
3695
+ name="Interface A1",
2549
3696
  type=InterfaceTypeChoices.TYPE_1GE_FIXED,
2550
3697
  status=interface_status,
2551
3698
  ),
2552
3699
  Interface.objects.create(
2553
3700
  device=devices[1],
2554
- name="Interface 2",
3701
+ name="Interface A2",
2555
3702
  type=InterfaceTypeChoices.TYPE_1GE_FIXED,
2556
3703
  status=interface_status,
2557
3704
  ),
2558
3705
  Interface.objects.create(
2559
3706
  device=devices[1],
2560
- name="Interface 3",
3707
+ name="Interface A3",
2561
3708
  type=InterfaceTypeChoices.TYPE_1GE_FIXED,
2562
3709
  status=interface_status,
2563
3710
  ),
2564
3711
  Interface.objects.create(
2565
3712
  device=devices[2],
2566
- name="Interface 1",
3713
+ name="Interface A1",
2567
3714
  type=InterfaceTypeChoices.TYPE_1GE_FIXED,
2568
3715
  status=interface_status,
2569
3716
  ),
2570
3717
  Interface.objects.create(
2571
3718
  device=devices[2],
2572
- name="Interface 2",
3719
+ name="Interface A2",
2573
3720
  type=InterfaceTypeChoices.TYPE_1GE_FIXED,
2574
3721
  status=interface_status,
2575
3722
  ),
2576
3723
  Interface.objects.create(
2577
3724
  device=devices[2],
2578
- name="Interface 3",
3725
+ name="Interface A3",
2579
3726
  type=InterfaceTypeChoices.TYPE_1GE_FIXED,
2580
3727
  status=interface_status,
2581
3728
  ),
2582
3729
  Interface.objects.create(
2583
3730
  device=devices[3],
2584
- name="Interface 1",
3731
+ name="Interface A1",
2585
3732
  type=InterfaceTypeChoices.TYPE_1GE_FIXED,
2586
3733
  status=interface_status,
2587
3734
  ),
2588
3735
  Interface.objects.create(
2589
3736
  device=devices[3],
2590
- name="Interface 2",
3737
+ name="Interface A2",
2591
3738
  type=InterfaceTypeChoices.TYPE_1GE_FIXED,
2592
3739
  status=interface_status,
2593
3740
  ),
2594
3741
  Interface.objects.create(
2595
3742
  device=devices[3],
2596
- name="Interface 3",
3743
+ name="Interface A3",
2597
3744
  type=InterfaceTypeChoices.TYPE_1GE_FIXED,
2598
3745
  status=interface_status,
2599
3746
  ),
@@ -2745,6 +3892,9 @@ class ConsoleConnectionsTestCase(ViewTestCases.ListObjectsViewTestCase):
2745
3892
  def _get_base_url(self):
2746
3893
  return "dcim:console_connections_{}"
2747
3894
 
3895
+ def _get_queryset(self):
3896
+ return ConsolePort.objects.filter(cable__isnull=False)
3897
+
2748
3898
  def get_list_url(self):
2749
3899
  return "/dcim/console-connections/"
2750
3900
 
@@ -2797,15 +3947,18 @@ class PowerConnectionsTestCase(ViewTestCases.ListObjectsViewTestCase):
2797
3947
  Test the PowerConnectionsListView.
2798
3948
  """
2799
3949
 
3950
+ def _get_base_url(self):
3951
+ return "dcim:power_connections_{}"
3952
+
3953
+ def _get_queryset(self):
3954
+ return PowerPort.objects.filter(cable__isnull=False)
3955
+
2800
3956
  def get_list_url(self):
2801
3957
  return "/dcim/power-connections/"
2802
3958
 
2803
3959
  def get_title(self):
2804
3960
  return "Power Connections"
2805
3961
 
2806
- def _get_base_url(self):
2807
- return "dcim:power_connections_{}"
2808
-
2809
3962
  def get_list_view(self):
2810
3963
  return PowerConnectionsListView
2811
3964
 
@@ -2862,6 +4015,9 @@ class InterfaceConnectionsTestCase(ViewTestCases.ListObjectsViewTestCase):
2862
4015
  def _get_base_url(self):
2863
4016
  return "dcim:interface_connections_{}"
2864
4017
 
4018
+ def _get_queryset(self):
4019
+ return Interface.objects.filter(cable__isnull=False)
4020
+
2865
4021
  def get_list_url(self):
2866
4022
  return "/dcim/interface-connections/"
2867
4023
 
@@ -2882,22 +4038,25 @@ class InterfaceConnectionsTestCase(ViewTestCases.ListObjectsViewTestCase):
2882
4038
  device_2 = create_test_device("Device 2")
2883
4039
 
2884
4040
  interface_status = Status.objects.get_for_model(Interface).first()
4041
+ interface_role = Role.objects.get_for_model(Interface).first()
2885
4042
  cls.interfaces = (
2886
4043
  Interface.objects.create(
2887
4044
  device=device_1,
2888
- name="Interface 1",
4045
+ name="Interface A1",
2889
4046
  type=InterfaceTypeChoices.TYPE_1GE_SFP,
2890
4047
  status=interface_status,
4048
+ role=interface_role,
2891
4049
  ),
2892
4050
  Interface.objects.create(
2893
4051
  device=device_1,
2894
- name="Interface 2",
4052
+ name="Interface A2",
2895
4053
  type=InterfaceTypeChoices.TYPE_1GE_SFP,
2896
4054
  status=interface_status,
4055
+ role=interface_role,
2897
4056
  ),
2898
4057
  Interface.objects.create(
2899
4058
  device=device_1,
2900
- name="Interface 3",
4059
+ name="Interface A3",
2901
4060
  type=InterfaceTypeChoices.TYPE_1GE_SFP,
2902
4061
  status=interface_status,
2903
4062
  ),
@@ -2905,9 +4064,10 @@ class InterfaceConnectionsTestCase(ViewTestCases.ListObjectsViewTestCase):
2905
4064
 
2906
4065
  cls.device_2_interface = Interface.objects.create(
2907
4066
  device=device_2,
2908
- name="Interface 1",
4067
+ name="Interface A1",
2909
4068
  type=InterfaceTypeChoices.TYPE_1GE_SFP,
2910
4069
  status=interface_status,
4070
+ role=interface_role,
2911
4071
  )
2912
4072
  rearport = RearPort.objects.create(device=device_2, type=PortTypeChoices.TYPE_8P8C)
2913
4073
 
@@ -3029,6 +4189,28 @@ class VirtualChassisTestCase(ViewTestCases.PrimaryObjectViewTestCase):
3029
4189
  "domain": "domain-x",
3030
4190
  }
3031
4191
 
4192
+ def test_device_interfaces_count_correct(self):
4193
+ """
4194
+ This checks whether the other memebers' interfaces are included in the
4195
+ interfaces tab of the master device and whether the interface count on the tab header is
4196
+ rendered correctly.
4197
+ """
4198
+ self.user.is_superuser = True
4199
+ self.user.save()
4200
+ interface_status = Status.objects.get_for_model(Interface).first()
4201
+ Interface.objects.create(device=self.devices[0], name="eth0", status=interface_status)
4202
+ Interface.objects.create(device=self.devices[0], name="eth1", status=interface_status)
4203
+ Interface.objects.create(device=self.devices[1], name="device 1 interface 1", status=interface_status)
4204
+ Interface.objects.create(device=self.devices[1], name="device 1 interface 2", status=interface_status)
4205
+ Interface.objects.create(device=self.devices[2], name="device 2 interface 1", status=interface_status)
4206
+ Interface.objects.create(device=self.devices[2], name="device 2 interface 2", status=interface_status)
4207
+ response = self.client.get(reverse("dcim:device_interfaces", kwargs={"pk": self.devices[0].pk}))
4208
+ self.assertIn('Interfaces <span class="badge">6</span>', str(response.content))
4209
+ self.assertIn("device 1 interface 1", str(response.content))
4210
+ self.assertIn("device 1 interface 2", str(response.content))
4211
+ self.assertIn("device 2 interface 1", str(response.content))
4212
+ self.assertIn("device 2 interface 2", str(response.content))
4213
+
3032
4214
  def test_device_column_visible(self):
3033
4215
  """
3034
4216
  This checks whether the device column on a device's interfaces
@@ -3313,11 +4495,11 @@ class InterfaceRedundancyGroupTestCase(ViewTestCases.PrimaryObjectViewTestCase):
3313
4495
  status=status_active,
3314
4496
  )
3315
4497
  intf_status = Status.objects.get_for_model(Interface).first()
3316
-
4498
+ intf_role = Role.objects.get_for_model(Interface).first()
3317
4499
  cls.interfaces = (
3318
- Interface.objects.create(device=device, name="Interface 1", status=intf_status),
3319
- Interface.objects.create(device=device, name="Interface 2", status=intf_status),
3320
- Interface.objects.create(device=device, name="Interface 3", status=intf_status),
4500
+ Interface.objects.create(device=device, name="Interface A1", status=intf_status, role=intf_role),
4501
+ Interface.objects.create(device=device, name="Interface A2", status=intf_status),
4502
+ Interface.objects.create(device=device, name="Interface A3", status=intf_status, role=intf_role),
3321
4503
  )
3322
4504
 
3323
4505
  cls.form_data = {