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

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

Potentially problematic release.


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

Files changed (697) hide show
  1. nautobot/apps/forms.py +4 -0
  2. nautobot/apps/models.py +10 -1
  3. nautobot/circuits/__init__.py +0 -1
  4. nautobot/circuits/apps.py +1 -0
  5. nautobot/circuits/factory.py +15 -3
  6. nautobot/circuits/filters.py +13 -0
  7. nautobot/circuits/forms.py +13 -0
  8. nautobot/circuits/migrations/0021_alter_circuit_status_alter_circuittermination__path.py +32 -0
  9. nautobot/circuits/migrations/0022_circuittermination_cloud_network.py +25 -0
  10. nautobot/circuits/models.py +16 -3
  11. nautobot/circuits/tables.py +16 -2
  12. nautobot/circuits/templates/circuits/circuittermination_create.html +10 -2
  13. nautobot/circuits/templates/circuits/circuittermination_retrieve.html +6 -0
  14. nautobot/circuits/templates/circuits/inc/circuit_termination.html +6 -1
  15. nautobot/circuits/tests/test_api.py +7 -5
  16. nautobot/circuits/tests/test_filters.py +12 -5
  17. nautobot/circuits/tests/test_models.py +33 -2
  18. nautobot/circuits/views.py +2 -3
  19. nautobot/cloud/__init__.py +0 -0
  20. nautobot/cloud/api/__init__.py +0 -0
  21. nautobot/cloud/api/serializers.py +54 -0
  22. nautobot/cloud/api/urls.py +16 -0
  23. nautobot/cloud/api/views.py +48 -0
  24. nautobot/cloud/apps.py +13 -0
  25. nautobot/cloud/factory.py +113 -0
  26. nautobot/cloud/filters.py +187 -0
  27. nautobot/cloud/forms.py +339 -0
  28. nautobot/cloud/homepage.py +43 -0
  29. nautobot/cloud/migrations/0001_initial.py +304 -0
  30. nautobot/cloud/migrations/__init__.py +0 -0
  31. nautobot/cloud/models.py +246 -0
  32. nautobot/cloud/navigation.py +85 -0
  33. nautobot/cloud/tables.py +157 -0
  34. nautobot/cloud/templates/cloud/cloudaccount_retrieve.html +43 -0
  35. nautobot/cloud/templates/cloud/cloudnetwork_retrieve.html +122 -0
  36. nautobot/cloud/templates/cloud/cloudnetwork_update.html +33 -0
  37. nautobot/cloud/templates/cloud/cloudresourcetype_retrieve.html +111 -0
  38. nautobot/cloud/templates/cloud/cloudservice_retrieve.html +69 -0
  39. nautobot/cloud/templates/cloud/cloudservice_update.html +25 -0
  40. nautobot/cloud/tests/__init__.py +0 -0
  41. nautobot/cloud/tests/test_api.py +248 -0
  42. nautobot/cloud/tests/test_filters.py +125 -0
  43. nautobot/cloud/tests/test_models.py +43 -0
  44. nautobot/cloud/tests/test_views.py +153 -0
  45. nautobot/cloud/urls.py +14 -0
  46. nautobot/cloud/views.py +181 -0
  47. nautobot/core/__init__.py +0 -3
  48. nautobot/core/api/metadata.py +1 -0
  49. nautobot/core/api/parsers.py +7 -1
  50. nautobot/core/api/urls.py +1 -0
  51. nautobot/core/api/utils.py +1 -0
  52. nautobot/core/api/views.py +4 -0
  53. nautobot/core/apps/__init__.py +6 -3
  54. nautobot/core/constants.py +8 -0
  55. nautobot/core/factory.py +32 -1
  56. nautobot/core/filters.py +95 -13
  57. nautobot/core/forms/fields.py +10 -4
  58. nautobot/core/forms/forms.py +11 -3
  59. nautobot/core/forms/widgets.py +18 -1
  60. nautobot/core/graphql/schema.py +26 -4
  61. nautobot/core/jobs/__init__.py +16 -2
  62. nautobot/core/jobs/cleanup.py +100 -0
  63. nautobot/core/jobs/groups.py +38 -0
  64. nautobot/core/management/commands/generate_test_data.py +116 -3
  65. nautobot/core/models/__init__.py +34 -9
  66. nautobot/core/models/generics.py +19 -3
  67. nautobot/core/models/name_color_content_types.py +7 -28
  68. nautobot/core/models/querysets.py +4 -3
  69. nautobot/core/models/tree_queries.py +1 -1
  70. nautobot/core/models/utils.py +21 -5
  71. nautobot/core/settings.py +2 -17
  72. nautobot/core/settings.yaml +34 -13
  73. nautobot/core/settings_funcs.py +103 -0
  74. nautobot/core/tables.py +130 -56
  75. nautobot/core/templates/admin/search_form.html +1 -1
  76. nautobot/core/templates/buttons/add.html +11 -3
  77. nautobot/core/templates/buttons/consolidated_bulk_action_buttons.html +13 -0
  78. nautobot/core/templates/buttons/consolidated_detail_view_action_buttons.html +13 -0
  79. nautobot/core/templates/buttons/export.html +101 -53
  80. nautobot/core/templates/buttons/job_import.html +11 -3
  81. nautobot/core/templates/generic/object_bulk_destroy.html +3 -1
  82. nautobot/core/templates/generic/object_bulk_update.html +3 -1
  83. nautobot/core/templates/generic/object_changelog.html +0 -9
  84. nautobot/core/templates/generic/object_list.html +156 -17
  85. nautobot/core/templates/generic/object_retrieve.html +80 -16
  86. nautobot/core/templates/inc/extras_features_edit_form_fields.html +8 -0
  87. nautobot/core/templates/inc/javascript.html +2 -0
  88. nautobot/core/templates/inc/media.html +2 -2
  89. nautobot/core/templates/inc/nav_menu.html +1 -0
  90. nautobot/core/templates/inc/paginator.html +7 -7
  91. nautobot/core/templates/inc/search_panel.html +2 -2
  92. nautobot/core/templates/inc/table.html +2 -2
  93. nautobot/core/templates/nautobot_config.py.j2 +13 -8
  94. nautobot/core/templates/utilities/templatetags/dynamic_group_assignment_modal.html +37 -0
  95. nautobot/core/templates/utilities/templatetags/filter_form_modal.html +2 -2
  96. nautobot/core/templates/utilities/templatetags/saved_view_modal.html +38 -0
  97. nautobot/core/templates/utilities/theme_preview.html +25 -8
  98. nautobot/core/templates/utilities/worker_status.html +152 -0
  99. nautobot/core/templatetags/buttons.py +335 -38
  100. nautobot/core/templatetags/form_helpers.py +1 -1
  101. nautobot/core/templatetags/helpers.py +181 -11
  102. nautobot/core/testing/api.py +5 -4
  103. nautobot/core/testing/filters.py +63 -14
  104. nautobot/core/testing/mixins.py +46 -0
  105. nautobot/core/testing/models.py +22 -0
  106. nautobot/core/testing/schema.py +4 -8
  107. nautobot/core/testing/views.py +31 -14
  108. nautobot/core/tests/integration/test_import_objects_ui.py +1 -0
  109. nautobot/core/tests/integration/test_swagger.py +1 -1
  110. nautobot/core/tests/nautobot_config.py +0 -1
  111. nautobot/core/tests/runner.py +2 -2
  112. nautobot/core/tests/test_api.py +1 -0
  113. nautobot/core/tests/test_authentication.py +7 -2
  114. nautobot/core/tests/test_filters.py +11 -9
  115. nautobot/core/tests/test_forms.py +9 -0
  116. nautobot/core/tests/test_graphql.py +27 -16
  117. nautobot/core/tests/test_jobs.py +122 -0
  118. nautobot/core/tests/test_tables.py +3 -1
  119. nautobot/core/tests/test_templatetags_helpers.py +12 -5
  120. nautobot/core/tests/test_utils.py +31 -20
  121. nautobot/core/tests/test_views.py +6 -6
  122. nautobot/core/urls.py +8 -3
  123. nautobot/core/utils/deprecation.py +29 -0
  124. nautobot/core/utils/filtering.py +12 -9
  125. nautobot/core/utils/lookup.py +37 -2
  126. nautobot/core/utils/requests.py +4 -1
  127. nautobot/core/views/__init__.py +137 -24
  128. nautobot/core/views/generic.py +119 -67
  129. nautobot/core/views/mixins.py +105 -36
  130. nautobot/core/views/paginator.py +9 -3
  131. nautobot/core/views/renderers.py +121 -56
  132. nautobot/core/views/utils.py +81 -1
  133. nautobot/dcim/__init__.py +0 -1
  134. nautobot/dcim/api/serializers.py +180 -44
  135. nautobot/dcim/api/urls.py +7 -3
  136. nautobot/dcim/api/views.py +53 -7
  137. nautobot/dcim/apps.py +3 -0
  138. nautobot/dcim/choices.py +25 -0
  139. nautobot/dcim/constants.py +7 -0
  140. nautobot/dcim/factory.py +252 -18
  141. nautobot/dcim/filters/__init__.py +373 -193
  142. nautobot/dcim/filters/mixins.py +274 -1
  143. nautobot/dcim/forms.py +834 -121
  144. nautobot/dcim/graphql/types.py +2 -2
  145. nautobot/dcim/homepage.py +1 -1
  146. nautobot/dcim/migrations/0059_add_role_field_to_interface_models.py +27 -0
  147. nautobot/dcim/migrations/0060_alter_cable_status_alter_consoleport__path_and_more.py +303 -0
  148. nautobot/dcim/migrations/0061_module_models.py +862 -0
  149. nautobot/dcim/migrations/0062_module_data_migration.py +25 -0
  150. nautobot/dcim/models/__init__.py +8 -0
  151. nautobot/dcim/models/cables.py +15 -0
  152. nautobot/dcim/models/device_component_templates.py +207 -53
  153. nautobot/dcim/models/device_components.py +275 -99
  154. nautobot/dcim/models/devices.py +468 -13
  155. nautobot/dcim/models/racks.py +0 -1
  156. nautobot/dcim/navigation.py +47 -0
  157. nautobot/dcim/signals.py +3 -3
  158. nautobot/dcim/tables/__init__.py +35 -23
  159. nautobot/dcim/tables/devices.py +229 -43
  160. nautobot/dcim/tables/devicetypes.py +65 -9
  161. nautobot/dcim/tables/racks.py +5 -1
  162. nautobot/dcim/tables/template_code.py +46 -26
  163. nautobot/dcim/templates/dcim/cable_connect.html +76 -3
  164. nautobot/dcim/templates/dcim/console_port_connection_list.html +7 -5
  165. nautobot/dcim/templates/dcim/device/base.html +14 -6
  166. nautobot/dcim/templates/dcim/device/consoleports.html +2 -3
  167. nautobot/dcim/templates/dcim/device/consoleserverports.html +2 -3
  168. nautobot/dcim/templates/dcim/device/devicebays.html +6 -7
  169. nautobot/dcim/templates/dcim/device/frontports.html +2 -3
  170. nautobot/dcim/templates/dcim/device/interfaces.html +2 -3
  171. nautobot/dcim/templates/dcim/device/inventory.html +2 -3
  172. nautobot/dcim/templates/dcim/device/modulebays.html +49 -0
  173. nautobot/dcim/templates/dcim/device/poweroutlets.html +2 -3
  174. nautobot/dcim/templates/dcim/device/powerports.html +2 -3
  175. nautobot/dcim/templates/dcim/device/rearports.html +2 -3
  176. nautobot/dcim/templates/dcim/device.html +45 -1
  177. nautobot/dcim/templates/dcim/device_component.html +13 -5
  178. nautobot/dcim/templates/dcim/device_list.html +2 -1
  179. nautobot/dcim/templates/dcim/devicetype.html +99 -98
  180. nautobot/dcim/templates/dcim/devicetype_list.html +8 -16
  181. nautobot/dcim/templates/dcim/inc/devicetype_component_table.html +1 -1
  182. nautobot/dcim/templates/dcim/inc/moduletype_component_table.html +39 -0
  183. nautobot/dcim/templates/dcim/interface.html +17 -2
  184. nautobot/dcim/templates/dcim/interface_connection_list.html +7 -5
  185. nautobot/dcim/templates/dcim/interface_edit.html +1 -0
  186. nautobot/dcim/templates/dcim/manufacturer.html +24 -0
  187. nautobot/dcim/templates/dcim/module/base.html +97 -0
  188. nautobot/dcim/templates/dcim/module_bulk_destroy.html +5 -0
  189. nautobot/dcim/templates/dcim/module_consoleports.html +53 -0
  190. nautobot/dcim/templates/dcim/module_consoleserverports.html +53 -0
  191. nautobot/dcim/templates/dcim/module_destroy.html +5 -0
  192. nautobot/dcim/templates/dcim/module_frontports.html +53 -0
  193. nautobot/dcim/templates/dcim/module_interfaces.html +57 -0
  194. nautobot/dcim/templates/dcim/module_list.html +20 -0
  195. nautobot/dcim/templates/dcim/module_modulebays.html +49 -0
  196. nautobot/dcim/templates/dcim/module_poweroutlets.html +53 -0
  197. nautobot/dcim/templates/dcim/module_powerports.html +53 -0
  198. nautobot/dcim/templates/dcim/module_rearports.html +53 -0
  199. nautobot/dcim/templates/dcim/module_retrieve.html +63 -0
  200. nautobot/dcim/templates/dcim/module_update.html +71 -0
  201. nautobot/dcim/templates/dcim/modulebay_bulk_destroy.html +5 -0
  202. nautobot/dcim/templates/dcim/modulebay_destroy.html +8 -0
  203. nautobot/dcim/templates/dcim/modulebay_retrieve.html +101 -0
  204. nautobot/dcim/templates/dcim/moduletype_list.html +11 -0
  205. nautobot/dcim/templates/dcim/moduletype_retrieve.html +159 -0
  206. nautobot/dcim/templates/dcim/power_port_connection_list.html +7 -5
  207. nautobot/dcim/templates/dcim/softwareimagefile_retrieve.html +65 -19
  208. nautobot/dcim/tests/integration/test_cable_connect_form.py +4 -4
  209. nautobot/dcim/tests/test_api.py +693 -208
  210. nautobot/dcim/tests/test_filters.py +843 -217
  211. nautobot/dcim/tests/test_models.py +1072 -8
  212. nautobot/dcim/tests/test_views.py +1510 -341
  213. nautobot/dcim/urls.py +17 -2
  214. nautobot/dcim/utils.py +2 -3
  215. nautobot/dcim/views.py +1106 -116
  216. nautobot/extras/__init__.py +0 -1
  217. nautobot/extras/api/serializers.py +115 -3
  218. nautobot/extras/api/urls.py +12 -0
  219. nautobot/extras/api/views.py +66 -0
  220. nautobot/extras/apps.py +2 -2
  221. nautobot/extras/choices.py +43 -0
  222. nautobot/extras/context_managers.py +13 -8
  223. nautobot/extras/datasources/git.py +2 -0
  224. nautobot/extras/factory.py +460 -9
  225. nautobot/extras/filters/__init__.py +174 -3
  226. nautobot/extras/filters/mixins.py +46 -43
  227. nautobot/extras/forms/base.py +24 -5
  228. nautobot/extras/forms/forms.py +227 -8
  229. nautobot/extras/forms/mixins.py +93 -0
  230. nautobot/extras/graphql/types.py +23 -10
  231. nautobot/extras/homepage.py +14 -1
  232. nautobot/extras/management/__init__.py +1 -0
  233. nautobot/extras/management/commands/refresh_dynamic_group_member_caches.py +1 -16
  234. nautobot/extras/migrations/0021_customfield_changelog_data.py +1 -0
  235. nautobot/extras/migrations/0109_dynamicgroup_group_type_dynamicgroup_tags_and_more.py +108 -0
  236. nautobot/extras/migrations/0110_alter_configcontext_cluster_groups_and_more.py +111 -0
  237. nautobot/extras/migrations/0111_metadata.py +162 -0
  238. nautobot/extras/migrations/0112_dynamic_group_group_type_data_migration.py +28 -0
  239. nautobot/extras/migrations/0113_saved_views.py +77 -0
  240. nautobot/extras/models/__init__.py +15 -1
  241. nautobot/extras/models/change_logging.py +3 -3
  242. nautobot/extras/models/contacts.py +4 -0
  243. nautobot/extras/models/customfields.py +18 -3
  244. nautobot/extras/models/groups.py +389 -225
  245. nautobot/extras/models/jobs.py +6 -3
  246. nautobot/extras/models/metadata.py +441 -0
  247. nautobot/extras/models/mixins.py +72 -62
  248. nautobot/extras/models/models.py +118 -9
  249. nautobot/extras/models/relationships.py +9 -2
  250. nautobot/extras/models/tags.py +13 -2
  251. nautobot/extras/navigation.py +57 -0
  252. nautobot/extras/plugins/__init__.py +3 -1
  253. nautobot/extras/querysets.py +30 -66
  254. nautobot/extras/signals.py +95 -100
  255. nautobot/extras/tables.py +165 -12
  256. nautobot/extras/templates/extras/dynamicgroup.html +44 -15
  257. nautobot/extras/templates/extras/dynamicgroup_edit.html +2 -0
  258. nautobot/extras/templates/extras/job.html +1 -1
  259. nautobot/extras/templates/extras/jobresult.html +61 -74
  260. nautobot/extras/templates/extras/metadatatype_create.html +89 -0
  261. nautobot/extras/templates/extras/metadatatype_retrieve.html +67 -0
  262. nautobot/extras/templates/extras/object_dynamicgroups.html +7 -0
  263. nautobot/extras/templates/extras/objectchange_list.html +0 -12
  264. nautobot/extras/templates/extras/plugins_list.html +1 -3
  265. nautobot/extras/templates/extras/role_retrieve.html +48 -0
  266. nautobot/extras/templates/extras/staticgroupassociation_retrieve.html +20 -0
  267. nautobot/extras/tests/integration/test_customfields.py +1 -0
  268. nautobot/extras/tests/test_api.py +509 -23
  269. nautobot/extras/tests/test_changelog.py +20 -9
  270. nautobot/extras/tests/test_context_managers.py +22 -15
  271. nautobot/extras/tests/test_datasources.py +13 -1
  272. nautobot/extras/tests/test_dynamicgroups.py +201 -171
  273. nautobot/extras/tests/test_filters.py +211 -12
  274. nautobot/extras/tests/test_jobs.py +6 -6
  275. nautobot/extras/tests/test_models.py +501 -4
  276. nautobot/extras/tests/test_relationships.py +1 -0
  277. nautobot/extras/tests/test_views.py +565 -8
  278. nautobot/extras/tests/test_webhooks.py +1 -1
  279. nautobot/extras/urls.py +5 -0
  280. nautobot/extras/utils.py +51 -11
  281. nautobot/extras/views.py +542 -76
  282. nautobot/ipam/__init__.py +0 -1
  283. nautobot/ipam/apps.py +1 -0
  284. nautobot/ipam/factory.py +17 -19
  285. nautobot/ipam/filters.py +13 -0
  286. nautobot/ipam/forms.py +8 -4
  287. nautobot/ipam/graphql/types.py +2 -2
  288. nautobot/ipam/migrations/0047_alter_ipaddress_role_alter_ipaddress_status_and_more.py +59 -0
  289. nautobot/ipam/models.py +11 -8
  290. nautobot/ipam/querysets.py +1 -1
  291. nautobot/ipam/signals.py +4 -2
  292. nautobot/ipam/tables.py +5 -0
  293. nautobot/ipam/templates/ipam/ipaddress_interfaces.html +1 -1
  294. nautobot/ipam/templates/ipam/ipaddress_vm_interfaces.html +1 -1
  295. nautobot/ipam/templates/ipam/prefix.html +1 -0
  296. nautobot/ipam/tests/test_api.py +37 -18
  297. nautobot/ipam/tests/test_filters.py +26 -2
  298. nautobot/ipam/tests/test_models.py +6 -0
  299. nautobot/ipam/tests/test_querysets.py +1 -1
  300. nautobot/ipam/tests/test_views.py +3 -2
  301. nautobot/ipam/urls.py +2 -2
  302. nautobot/ipam/views.py +18 -26
  303. nautobot/project-static/css/base.css +20 -0
  304. nautobot/project-static/css/dark.css +11 -0
  305. nautobot/project-static/docs/404.html +892 -88
  306. nautobot/project-static/docs/apps/index.html +892 -88
  307. nautobot/project-static/docs/apps/nautobot-apps.html +892 -88
  308. nautobot/project-static/docs/assets/_mkdocstrings.css +5 -0
  309. nautobot/project-static/docs/assets/stylesheets/main.3cba04c6.min.css +1 -0
  310. nautobot/project-static/docs/assets/stylesheets/main.3cba04c6.min.css.map +1 -0
  311. nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +919 -120
  312. nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +904 -101
  313. nautobot/project-static/docs/code-reference/nautobot/apps/api.html +1618 -903
  314. nautobot/project-static/docs/code-reference/nautobot/apps/change_logging.html +935 -144
  315. nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +977 -188
  316. nautobot/project-static/docs/code-reference/nautobot/apps/config.html +901 -99
  317. nautobot/project-static/docs/code-reference/nautobot/apps/constants.html +897 -93
  318. nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +991 -193
  319. nautobot/project-static/docs/code-reference/nautobot/apps/exceptions.html +974 -131
  320. nautobot/project-static/docs/code-reference/nautobot/apps/factory.html +1078 -272
  321. nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +1242 -334
  322. nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +1727 -875
  323. nautobot/project-static/docs/code-reference/nautobot/apps/graphql.html +1164 -381
  324. nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +2088 -1374
  325. nautobot/project-static/docs/code-reference/nautobot/apps/models.html +2246 -1422
  326. nautobot/project-static/docs/code-reference/nautobot/apps/querysets.html +912 -111
  327. nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +963 -163
  328. nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +1010 -223
  329. nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +1913 -1277
  330. nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +1846 -1102
  331. nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +904 -101
  332. nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +2331 -1699
  333. nautobot/project-static/docs/code-reference/nautobot/apps/views.html +1802 -1024
  334. nautobot/project-static/docs/development/apps/api/configuration-view.html +892 -88
  335. nautobot/project-static/docs/development/apps/api/database-backend-config.html +892 -88
  336. nautobot/project-static/docs/development/apps/api/models/django-admin.html +892 -88
  337. nautobot/project-static/docs/development/apps/api/models/global-search.html +892 -88
  338. nautobot/project-static/docs/development/apps/api/models/graphql.html +892 -88
  339. nautobot/project-static/docs/development/apps/api/models/index.html +942 -90
  340. nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +892 -88
  341. nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +892 -88
  342. nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +892 -88
  343. nautobot/project-static/docs/development/apps/api/platform-features/git-repository-content.html +892 -88
  344. nautobot/project-static/docs/development/apps/api/platform-features/index.html +892 -88
  345. nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +892 -88
  346. nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +892 -88
  347. nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +892 -88
  348. nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +892 -88
  349. nautobot/project-static/docs/development/apps/api/platform-features/uniquely-identify-objects.html +892 -88
  350. nautobot/project-static/docs/development/apps/api/prometheus.html +892 -88
  351. nautobot/project-static/docs/development/apps/api/setup.html +892 -88
  352. nautobot/project-static/docs/development/apps/api/testing.html +892 -88
  353. nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +892 -88
  354. nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +892 -88
  355. nautobot/project-static/docs/development/apps/api/ui-extensions/index.html +892 -88
  356. nautobot/project-static/docs/development/apps/api/ui-extensions/navigation.html +892 -88
  357. nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +892 -88
  358. nautobot/project-static/docs/development/apps/api/views/base-template.html +892 -88
  359. nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +892 -88
  360. nautobot/project-static/docs/development/apps/api/views/django-generic-views.html +892 -88
  361. nautobot/project-static/docs/development/apps/api/views/help-documentation.html +892 -88
  362. nautobot/project-static/docs/development/apps/api/views/index.html +892 -88
  363. nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +892 -88
  364. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +892 -88
  365. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +892 -88
  366. nautobot/project-static/docs/development/apps/api/views/notes.html +892 -88
  367. nautobot/project-static/docs/development/apps/api/views/rest-api.html +892 -88
  368. nautobot/project-static/docs/development/apps/api/views/urls.html +892 -88
  369. nautobot/project-static/docs/development/apps/index.html +892 -88
  370. nautobot/project-static/docs/development/apps/migration/code-updates.html +892 -88
  371. nautobot/project-static/docs/development/apps/migration/dependency-updates.html +892 -88
  372. nautobot/project-static/docs/development/apps/migration/from-v1.html +892 -88
  373. nautobot/project-static/docs/development/apps/migration/model-updates/dcim.html +892 -88
  374. nautobot/project-static/docs/development/apps/migration/model-updates/extras.html +892 -88
  375. nautobot/project-static/docs/development/apps/migration/model-updates/global.html +892 -88
  376. nautobot/project-static/docs/development/apps/migration/model-updates/ipam.html +892 -88
  377. nautobot/project-static/docs/development/apps/porting-from-netbox.html +892 -88
  378. nautobot/project-static/docs/development/core/application-registry.html +892 -88
  379. nautobot/project-static/docs/development/core/best-practices.html +893 -88
  380. nautobot/project-static/docs/development/core/bootstrap-ui.html +892 -88
  381. nautobot/project-static/docs/development/core/caching.html +892 -88
  382. nautobot/project-static/docs/development/core/controllers.html +892 -88
  383. nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +892 -88
  384. nautobot/project-static/docs/development/core/generic-views.html +892 -88
  385. nautobot/project-static/docs/development/core/getting-started.html +892 -88
  386. nautobot/project-static/docs/development/core/homepage.html +892 -88
  387. nautobot/project-static/docs/development/core/index.html +892 -88
  388. nautobot/project-static/docs/development/core/model-checklist.html +901 -89
  389. nautobot/project-static/docs/development/core/model-features.html +892 -88
  390. nautobot/project-static/docs/development/core/natural-keys.html +892 -88
  391. nautobot/project-static/docs/development/core/navigation-menu.html +892 -88
  392. nautobot/project-static/docs/development/core/release-checklist.html +895 -91
  393. nautobot/project-static/docs/development/core/role-internals.html +892 -88
  394. nautobot/project-static/docs/development/core/settings.html +892 -88
  395. nautobot/project-static/docs/development/core/style-guide.html +893 -89
  396. nautobot/project-static/docs/development/core/templates.html +904 -89
  397. nautobot/project-static/docs/development/core/testing.html +892 -88
  398. nautobot/project-static/docs/development/core/user-preferences.html +892 -88
  399. nautobot/project-static/docs/development/index.html +892 -88
  400. nautobot/project-static/docs/development/jobs/index.html +893 -89
  401. nautobot/project-static/docs/development/jobs/migration/from-v1.html +892 -88
  402. nautobot/project-static/docs/index.html +892 -88
  403. nautobot/project-static/docs/media/models/cloud_aws_direct_connect_dark.png +0 -0
  404. nautobot/project-static/docs/media/models/cloud_aws_direct_connect_light.png +0 -0
  405. nautobot/project-static/docs/models/cloud/cloudaccount.html +15 -0
  406. nautobot/project-static/docs/models/cloud/cloudnetwork.html +15 -0
  407. nautobot/project-static/docs/models/cloud/cloudnetworkprefixassignment.html +15 -0
  408. nautobot/project-static/docs/models/cloud/cloudresourcetype.html +15 -0
  409. nautobot/project-static/docs/models/cloud/cloudservice.html +15 -0
  410. nautobot/project-static/docs/models/cloud/cloudservicenetworkassignment.html +15 -0
  411. nautobot/project-static/docs/models/dcim/module.html +15 -0
  412. nautobot/project-static/docs/models/dcim/modulebay.html +15 -0
  413. nautobot/project-static/docs/models/dcim/modulebaytemplate.html +15 -0
  414. nautobot/project-static/docs/models/dcim/moduletype.html +15 -0
  415. nautobot/project-static/docs/models/extras/metadatachoice.html +15 -0
  416. nautobot/project-static/docs/models/extras/metadatatype.html +15 -0
  417. nautobot/project-static/docs/models/extras/objectmetadata.html +15 -0
  418. nautobot/project-static/docs/models/extras/role.html +15 -0
  419. nautobot/project-static/docs/models/extras/savedview.html +15 -0
  420. nautobot/project-static/docs/models/extras/staticgroupassociation.html +15 -0
  421. nautobot/project-static/docs/models/extras/status.html +15 -0
  422. nautobot/project-static/docs/objects.inv +0 -0
  423. nautobot/project-static/docs/overview/application_stack.html +900 -89
  424. nautobot/project-static/docs/overview/design_philosophy.html +892 -88
  425. nautobot/project-static/docs/release-notes/index.html +1129 -92
  426. nautobot/project-static/docs/release-notes/version-1.0.html +892 -88
  427. nautobot/project-static/docs/release-notes/version-1.1.html +892 -88
  428. nautobot/project-static/docs/release-notes/version-1.2.html +892 -88
  429. nautobot/project-static/docs/release-notes/version-1.3.html +892 -88
  430. nautobot/project-static/docs/release-notes/version-1.4.html +892 -88
  431. nautobot/project-static/docs/release-notes/version-1.5.html +893 -89
  432. nautobot/project-static/docs/release-notes/version-1.6.html +893 -89
  433. nautobot/project-static/docs/release-notes/version-2.0.html +892 -88
  434. nautobot/project-static/docs/release-notes/version-2.1.html +892 -88
  435. nautobot/project-static/docs/release-notes/version-2.2.html +895 -91
  436. nautobot/project-static/docs/release-notes/version-2.3.html +9954 -0
  437. nautobot/project-static/docs/requirements.txt +5 -5
  438. nautobot/project-static/docs/search/search_index.json +1 -1
  439. nautobot/project-static/docs/sitemap.xml +331 -256
  440. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  441. nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +892 -88
  442. nautobot/project-static/docs/user-guide/administration/configuration/authentication/remote.html +892 -88
  443. nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +892 -88
  444. nautobot/project-static/docs/user-guide/administration/configuration/index.html +892 -88
  445. nautobot/project-static/docs/user-guide/administration/configuration/optional-settings.html +992 -174
  446. nautobot/project-static/docs/user-guide/administration/configuration/required-settings.html +892 -88
  447. nautobot/project-static/docs/user-guide/administration/configuration/time-zones.html +892 -88
  448. nautobot/project-static/docs/user-guide/administration/guides/caching.html +892 -88
  449. nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +896 -88
  450. nautobot/project-static/docs/user-guide/administration/guides/healthcheck.html +892 -88
  451. nautobot/project-static/docs/user-guide/administration/guides/permissions.html +892 -88
  452. nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +892 -88
  453. nautobot/project-static/docs/user-guide/administration/guides/replicating-nautobot.html +892 -88
  454. nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +892 -88
  455. nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +892 -88
  456. nautobot/project-static/docs/user-guide/administration/installation/app-install.html +892 -88
  457. nautobot/project-static/docs/user-guide/administration/installation/external-authentication.html +892 -88
  458. nautobot/project-static/docs/user-guide/administration/installation/http-server.html +944 -153
  459. nautobot/project-static/docs/user-guide/administration/installation/index.html +901 -93
  460. nautobot/project-static/docs/user-guide/administration/installation/install_system.html +934 -122
  461. nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +954 -157
  462. nautobot/project-static/docs/user-guide/administration/installation/services.html +913 -112
  463. nautobot/project-static/docs/user-guide/administration/installation-extras/docker.html +908 -99
  464. nautobot/project-static/docs/user-guide/administration/installation-extras/health-checks.html +892 -88
  465. nautobot/project-static/docs/user-guide/administration/installation-extras/selinux-troubleshooting.html +892 -88
  466. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-netbox.html +892 -88
  467. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +892 -88
  468. nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +893 -89
  469. nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +892 -88
  470. nautobot/project-static/docs/user-guide/administration/upgrading/database-backup.html +892 -88
  471. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/after-you-upgrade.html +892 -88
  472. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/before-you-upgrade.html +892 -88
  473. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/for-developers.html +892 -88
  474. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/index.html +892 -88
  475. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/whats-changed.html +892 -88
  476. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/region-and-site-data-migration-guide.html +892 -88
  477. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +892 -88
  478. nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +892 -88
  479. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuit.html +892 -88
  480. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittermination.html +893 -89
  481. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittype.html +892 -88
  482. nautobot/project-static/docs/user-guide/core-data-model/circuits/provider.html +896 -88
  483. nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +895 -91
  484. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloud.html +8984 -0
  485. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudaccount.html +8828 -0
  486. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetwork.html +8829 -0
  487. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetworkprefixassignment.html +8828 -0
  488. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudresourcetype.html +8829 -0
  489. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservice.html +8833 -0
  490. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservicenetworkassignment.html +8828 -0
  491. nautobot/project-static/docs/user-guide/core-data-model/dcim/cable.html +906 -102
  492. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +923 -105
  493. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +923 -105
  494. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +918 -100
  495. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +923 -105
  496. nautobot/project-static/docs/user-guide/core-data-model/dcim/controller.html +906 -102
  497. nautobot/project-static/docs/user-guide/core-data-model/dcim/controllermanageddevicegroup.html +906 -102
  498. nautobot/project-static/docs/user-guide/core-data-model/dcim/device.html +913 -105
  499. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +920 -116
  500. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +921 -117
  501. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicefamily.html +918 -114
  502. nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +906 -102
  503. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +914 -105
  504. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +926 -108
  505. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +936 -118
  506. nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +928 -106
  507. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +906 -102
  508. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +937 -119
  509. nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +928 -110
  510. nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +918 -114
  511. nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +921 -117
  512. nautobot/project-static/docs/user-guide/core-data-model/dcim/manufacturer.html +923 -115
  513. nautobot/project-static/docs/user-guide/core-data-model/dcim/module.html +8828 -0
  514. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebay.html +8846 -0
  515. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebaytemplate.html +8843 -0
  516. nautobot/project-static/docs/user-guide/core-data-model/dcim/moduletype.html +8823 -0
  517. nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +916 -112
  518. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerfeed.html +906 -102
  519. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +940 -83
  520. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +924 -106
  521. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerpanel.html +906 -102
  522. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +943 -86
  523. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +921 -103
  524. nautobot/project-static/docs/user-guide/core-data-model/dcim/rack.html +929 -125
  525. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackgroup.html +918 -114
  526. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackreservation.html +906 -102
  527. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +922 -104
  528. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +924 -106
  529. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +906 -102
  530. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareversion.html +906 -102
  531. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualchassis.html +906 -102
  532. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +936 -88
  533. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +892 -88
  534. nautobot/project-static/docs/user-guide/core-data-model/extras/contact.html +897 -89
  535. nautobot/project-static/docs/user-guide/core-data-model/extras/team.html +897 -89
  536. nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +892 -88
  537. nautobot/project-static/docs/user-guide/core-data-model/ipam/namespace.html +892 -88
  538. nautobot/project-static/docs/user-guide/core-data-model/ipam/prefix.html +892 -88
  539. nautobot/project-static/docs/user-guide/core-data-model/ipam/rir.html +892 -88
  540. nautobot/project-static/docs/user-guide/core-data-model/ipam/routetarget.html +892 -88
  541. nautobot/project-static/docs/user-guide/core-data-model/ipam/service.html +892 -88
  542. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +892 -88
  543. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlangroup.html +892 -88
  544. nautobot/project-static/docs/user-guide/core-data-model/ipam/vrf.html +892 -88
  545. nautobot/project-static/docs/user-guide/core-data-model/overview/introduction.html +892 -88
  546. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenant.html +901 -96
  547. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenantgroup.html +892 -88
  548. nautobot/project-static/docs/user-guide/core-data-model/virtualization/cluster.html +892 -88
  549. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustergroup.html +892 -88
  550. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustertype.html +892 -88
  551. nautobot/project-static/docs/user-guide/core-data-model/virtualization/virtualmachine.html +892 -88
  552. nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +897 -89
  553. nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +892 -88
  554. nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +892 -88
  555. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +892 -88
  556. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +892 -88
  557. nautobot/project-static/docs/user-guide/feature-guides/getting-started/index.html +892 -88
  558. nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +892 -88
  559. nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +892 -88
  560. nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +892 -88
  561. nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +892 -88
  562. nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +892 -88
  563. nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +892 -88
  564. nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +892 -88
  565. nautobot/project-static/docs/user-guide/feature-guides/graphql.html +892 -88
  566. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/clear-view-button.png +0 -0
  567. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/cleared-view.png +0 -0
  568. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/config-table-columns-to-locations.png +0 -0
  569. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/configure-button.png +0 -0
  570. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/create-saved-view-success.png +0 -0
  571. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/current-saved-view-drop-down-menu.png +0 -0
  572. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/default-location-list-view.png +0 -0
  573. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/dropdown-button-after-new-saved-view.png +0 -0
  574. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/filter-application-to-locations.png +0 -0
  575. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/filter-button.png +0 -0
  576. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/global-default-location-list-view.png +0 -0
  577. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/location-list-view-with-saved-views.png +0 -0
  578. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/navigation-menu.png +0 -0
  579. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/save-as-new-view-drop-down.png +0 -0
  580. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/save-view-modal.png +0 -0
  581. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/saved-view-admin-edit-buttons.png +0 -0
  582. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/saved-view-admin-edit-success.png +0 -0
  583. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/saved-view-admin-edit-view-unchecked.png +0 -0
  584. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/saved-view-admin-edit-view.png +0 -0
  585. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/saved-view-different-user.png +0 -0
  586. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/saved-view-modal-unchecked.png +0 -0
  587. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/set-as-my-default-button.png +0 -0
  588. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/set-as-my-default-success.png +0 -0
  589. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/unsaved-saved-view.png +0 -0
  590. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/updated-saved-view.png +0 -0
  591. nautobot/project-static/docs/user-guide/feature-guides/ip-address-merge-tool.html +892 -88
  592. nautobot/project-static/docs/user-guide/feature-guides/relationships.html +892 -88
  593. nautobot/project-static/docs/user-guide/feature-guides/software-image-files-and-versions.html +892 -88
  594. nautobot/project-static/docs/user-guide/index.html +892 -88
  595. nautobot/project-static/docs/user-guide/platform-functionality/change-logging.html +892 -88
  596. nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +892 -88
  597. nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +892 -88
  598. nautobot/project-static/docs/user-guide/platform-functionality/customlink.html +892 -88
  599. nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +1258 -785
  600. nautobot/project-static/docs/user-guide/platform-functionality/exporttemplate.html +895 -91
  601. nautobot/project-static/docs/user-guide/platform-functionality/externalintegration.html +892 -88
  602. nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +892 -88
  603. nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +892 -88
  604. nautobot/project-static/docs/user-guide/platform-functionality/graphqlquery.html +892 -88
  605. nautobot/project-static/docs/user-guide/platform-functionality/imageattachment.html +892 -88
  606. nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +892 -88
  607. nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +892 -88
  608. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +892 -88
  609. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +892 -88
  610. nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +896 -88
  611. nautobot/project-static/docs/user-guide/platform-functionality/napalm.html +892 -88
  612. nautobot/project-static/docs/user-guide/platform-functionality/note.html +895 -91
  613. nautobot/project-static/docs/user-guide/platform-functionality/objectmetadata.html +9061 -0
  614. nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +895 -91
  615. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +892 -88
  616. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +892 -88
  617. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +892 -88
  618. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/ui-related-endpoints.html +892 -88
  619. nautobot/project-static/docs/user-guide/platform-functionality/role.html +895 -91
  620. nautobot/project-static/docs/user-guide/platform-functionality/savedview.html +9137 -0
  621. nautobot/project-static/docs/user-guide/platform-functionality/secret.html +895 -91
  622. nautobot/project-static/docs/user-guide/platform-functionality/staticgroupassociation.html +8933 -0
  623. nautobot/project-static/docs/user-guide/platform-functionality/status.html +892 -88
  624. nautobot/project-static/docs/user-guide/platform-functionality/tag.html +892 -88
  625. nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +950 -121
  626. nautobot/project-static/docs/user-guide/platform-functionality/users/objectpermission.html +892 -88
  627. nautobot/project-static/docs/user-guide/platform-functionality/users/token.html +892 -88
  628. nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +892 -88
  629. nautobot/project-static/js/forms.js +71 -0
  630. nautobot/project-static/js/table_sorting_indicator.js +46 -0
  631. nautobot/project-static/js/tableconfig.js +6 -1
  632. nautobot/project-static/materialdesignicons-7.4.47/css/materialdesignicons.min.css +3 -0
  633. nautobot/project-static/{materialdesignicons-6.5.95 → materialdesignicons-7.4.47}/fonts/materialdesignicons-webfont.eot +0 -0
  634. nautobot/project-static/{materialdesignicons-6.5.95 → materialdesignicons-7.4.47}/fonts/materialdesignicons-webfont.ttf +0 -0
  635. nautobot/project-static/materialdesignicons-7.4.47/fonts/materialdesignicons-webfont.woff +0 -0
  636. nautobot/project-static/materialdesignicons-7.4.47/fonts/materialdesignicons-webfont.woff2 +0 -0
  637. nautobot/tenancy/__init__.py +0 -1
  638. nautobot/tenancy/apps.py +1 -0
  639. nautobot/tenancy/factory.py +3 -2
  640. nautobot/tenancy/filters/__init__.py +1 -0
  641. nautobot/tenancy/forms.py +1 -1
  642. nautobot/tenancy/templates/tenancy/tenant.html +24 -20
  643. nautobot/tenancy/views.py +11 -10
  644. nautobot/users/__init__.py +0 -1
  645. nautobot/users/api/serializers.py +1 -1
  646. nautobot/users/api/views.py +4 -2
  647. nautobot/users/apps.py +3 -2
  648. nautobot/users/factory.py +3 -3
  649. nautobot/users/migrations/0010_user_default_saved_views.py +20 -0
  650. nautobot/users/models.py +12 -0
  651. nautobot/users/tests/test_filters.py +6 -3
  652. nautobot/users/urls.py +8 -0
  653. nautobot/virtualization/__init__.py +0 -1
  654. nautobot/virtualization/apps.py +1 -0
  655. nautobot/virtualization/filters.py +6 -1
  656. nautobot/virtualization/forms.py +11 -3
  657. nautobot/virtualization/graphql/types.py +2 -2
  658. nautobot/virtualization/migrations/0029_add_role_field_to_interface_models.py +27 -0
  659. nautobot/virtualization/migrations/0030_alter_virtualmachine_local_config_context_data_owner_content_type_and_more.py +67 -0
  660. nautobot/virtualization/models.py +0 -2
  661. nautobot/virtualization/tables.py +10 -3
  662. nautobot/virtualization/templates/virtualization/virtualmachine.html +1 -1
  663. nautobot/virtualization/templates/virtualization/vminterface.html +7 -1
  664. nautobot/virtualization/templates/virtualization/vminterface_edit.html +1 -0
  665. nautobot/virtualization/tests/test_api.py +9 -4
  666. nautobot/virtualization/tests/test_filters.py +22 -0
  667. nautobot/virtualization/tests/test_models.py +7 -3
  668. nautobot/virtualization/tests/test_views.py +19 -3
  669. nautobot/virtualization/urls.py +2 -2
  670. nautobot/virtualization/views.py +10 -32
  671. {nautobot-2.2.9.dist-info → nautobot-2.3.0.dist-info}/METADATA +20 -18
  672. {nautobot-2.2.9.dist-info → nautobot-2.3.0.dist-info}/RECORD +677 -557
  673. nautobot/project-static/docs/assets/stylesheets/main.76a95c52.min.css +0 -1
  674. nautobot/project-static/docs/assets/stylesheets/main.76a95c52.min.css.map +0 -1
  675. nautobot/project-static/materialdesignicons-6.5.95/.github/ISSUE_TEMPLATE.md +0 -3
  676. nautobot/project-static/materialdesignicons-6.5.95/README.md +0 -25
  677. nautobot/project-static/materialdesignicons-6.5.95/css/materialdesignicons.css +0 -26654
  678. nautobot/project-static/materialdesignicons-6.5.95/css/materialdesignicons.css.map +0 -16
  679. nautobot/project-static/materialdesignicons-6.5.95/css/materialdesignicons.min.css +0 -3
  680. nautobot/project-static/materialdesignicons-6.5.95/css/materialdesignicons.min.css.map +0 -16
  681. nautobot/project-static/materialdesignicons-6.5.95/fonts/materialdesignicons-webfont.woff +0 -0
  682. nautobot/project-static/materialdesignicons-6.5.95/fonts/materialdesignicons-webfont.woff2 +0 -0
  683. nautobot/project-static/materialdesignicons-6.5.95/package.json +0 -28
  684. nautobot/project-static/materialdesignicons-6.5.95/preview.html +0 -717
  685. nautobot/project-static/materialdesignicons-6.5.95/scss/_animated.scss +0 -27
  686. nautobot/project-static/materialdesignicons-6.5.95/scss/_core.scss +0 -10
  687. nautobot/project-static/materialdesignicons-6.5.95/scss/_extras.scss +0 -65
  688. nautobot/project-static/materialdesignicons-6.5.95/scss/_functions.scss +0 -20
  689. nautobot/project-static/materialdesignicons-6.5.95/scss/_icons.scss +0 -10
  690. nautobot/project-static/materialdesignicons-6.5.95/scss/_path.scss +0 -10
  691. nautobot/project-static/materialdesignicons-6.5.95/scss/_variables.scss +0 -6606
  692. nautobot/project-static/materialdesignicons-6.5.95/scss/materialdesignicons.scss +0 -8
  693. /nautobot/project-static/{materialdesignicons-6.5.95 → materialdesignicons-7.4.47}/LICENSE +0 -0
  694. {nautobot-2.2.9.dist-info → nautobot-2.3.0.dist-info}/LICENSE.txt +0 -0
  695. {nautobot-2.2.9.dist-info → nautobot-2.3.0.dist-info}/NOTICE +0 -0
  696. {nautobot-2.2.9.dist-info → nautobot-2.3.0.dist-info}/WHEEL +0 -0
  697. {nautobot-2.2.9.dist-info → nautobot-2.3.0.dist-info}/entry_points.txt +0 -0
@@ -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):
@@ -1680,26 +2254,430 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
1680
2254
  PowerPort.objects.create(device=device, name="Power Port 2")
1681
2255
  PowerPort.objects.create(device=device, name="Power Port 3")
1682
2256
 
1683
- url = reverse("dcim:device_powerports", kwargs={"pk": device.pk})
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")
2638
+
2639
+ url = reverse("dcim:module_consoleports", kwargs={"pk": module.pk})
2640
+ self.assertHttpStatus(self.client.get(url), 200)
2641
+
2642
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
2643
+ def test_module_consoleserverports(self):
2644
+ module = Module.objects.first()
2645
+
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")
2649
+
2650
+ url = reverse("dcim:module_consoleserverports", kwargs={"pk": module.pk})
2651
+ self.assertHttpStatus(self.client.get(url), 200)
2652
+
2653
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
2654
+ def test_module_powerports(self):
2655
+ module = Module.objects.first()
2656
+
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")
2660
+
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
 
@@ -1789,160 +2767,59 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
1789
2767
  sorted(interface_ips),
1790
2768
  )
1791
2769
 
1792
- with self.subTest("Assert Assigning IPAddress Without Selecting Any IPAddress Raises Exception"):
1793
- assign_ip_form_data["pk"] = []
1794
- assign_ip_request = {
1795
- "path": reverse("ipam:ipaddress_assign")
1796
- + f"?interface={self.interfaces[1].id}&return_url={device_list_url}",
1797
- "data": post_data(assign_ip_form_data),
1798
- }
1799
- response = self.client.post(**assign_ip_request, follow=True)
1800
- self.assertHttpStatus(response, 200)
1801
- self.assertIn(
1802
- "Please select at least one IP Address from the table.", response.content.decode(response.charset)
1803
- )
1804
-
1805
2770
  @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
1806
- def test_device_rearports(self):
1807
- device = Device.objects.first()
2771
+ def test_module_rearports(self):
2772
+ module = Module.objects.first()
1808
2773
 
1809
- RearPort.objects.create(device=device, name="Rear Port 1")
1810
- RearPort.objects.create(device=device, name="Rear Port 2")
1811
- RearPort.objects.create(device=device, name="Rear Port 3")
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")
1812
2777
 
1813
- url = reverse("dcim:device_rearports", kwargs={"pk": device.pk})
2778
+ url = reverse("dcim:module_rearports", kwargs={"pk": module.pk})
1814
2779
  self.assertHttpStatus(self.client.get(url), 200)
1815
2780
 
1816
2781
  @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
1817
- def test_device_frontports(self):
1818
- device = Device.objects.first()
2782
+ def test_module_frontports(self):
2783
+ module = Module.objects.first()
1819
2784
  rear_ports = (
1820
- RearPort.objects.create(device=device, name="Rear Port 1"),
1821
- RearPort.objects.create(device=device, name="Rear Port 2"),
1822
- RearPort.objects.create(device=device, name="Rear Port 3"),
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"),
1823
2788
  )
1824
2789
 
1825
2790
  FrontPort.objects.create(
1826
- device=device,
2791
+ module=module,
1827
2792
  name="Front Port 1",
1828
2793
  rear_port=rear_ports[0],
1829
2794
  rear_port_position=1,
1830
2795
  )
1831
2796
  FrontPort.objects.create(
1832
- device=device,
2797
+ module=module,
1833
2798
  name="Front Port 2",
1834
2799
  rear_port=rear_ports[1],
1835
2800
  rear_port_position=1,
1836
2801
  )
1837
2802
  FrontPort.objects.create(
1838
- device=device,
2803
+ module=module,
1839
2804
  name="Front Port 3",
1840
2805
  rear_port=rear_ports[2],
1841
2806
  rear_port_position=1,
1842
2807
  )
1843
2808
 
1844
- url = reverse("dcim:device_frontports", kwargs={"pk": device.pk})
1845
- self.assertHttpStatus(self.client.get(url), 200)
1846
-
1847
- @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
1848
- def test_device_devicebays(self):
1849
- device = Device.objects.first()
1850
-
1851
- # Device Bay 1 was already created in setUpTestData()
1852
- DeviceBay.objects.create(device=device, name="Device Bay 2")
1853
- DeviceBay.objects.create(device=device, name="Device Bay 3")
1854
-
1855
- url = reverse("dcim:device_devicebays", kwargs={"pk": device.pk})
2809
+ url = reverse("dcim:module_frontports", kwargs={"pk": module.pk})
1856
2810
  self.assertHttpStatus(self.client.get(url), 200)
1857
2811
 
1858
2812
  @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
1859
- def test_device_inventory(self):
1860
- device = Device.objects.first()
2813
+ def test_module_modulebays(self):
2814
+ module = Module.objects.first()
1861
2815
 
1862
- InventoryItem.objects.create(device=device, name="Inventory Item 1")
1863
- InventoryItem.objects.create(device=device, name="Inventory Item 2")
1864
- InventoryItem.objects.create(device=device, name="Inventory Item 3")
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")
1865
2819
 
1866
- url = reverse("dcim:device_inventory", kwargs={"pk": device.pk})
2820
+ url = reverse("dcim:module_modulebays", kwargs={"pk": module.pk})
1867
2821
  self.assertHttpStatus(self.client.get(url), 200)
1868
2822
 
1869
- @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
1870
- def test_device_primary_ips(self):
1871
- """Test assigning a primary IP to a device."""
1872
- self.add_permissions("dcim.change_device")
1873
-
1874
- # Create an interface and assign an IP to it.
1875
- device = Device.objects.filter(interfaces__isnull=False).first()
1876
- interface = device.interfaces.first()
1877
- namespace = Namespace.objects.first()
1878
- Prefix.objects.create(prefix="1.2.3.0/24", namespace=namespace, status=self.prefix_status)
1879
- ip_address = IPAddress.objects.create(address="1.2.3.4/32", namespace=namespace, status=self.ipaddr_status)
1880
- interface.ip_addresses.add(ip_address)
1881
-
1882
- # Dupe the form data and populated primary_ip4 w/ ip_address
1883
- form_data = self.form_data.copy()
1884
- form_data["primary_ip4"] = ip_address.pk
1885
- # Assert that update succeeds.
1886
- request = {
1887
- "path": self._get_url("edit", device),
1888
- "data": post_data(form_data),
1889
- }
1890
- self.assertHttpStatus(self.client.post(**request), 302)
1891
- self.assertInstanceEqual(self._get_queryset().order_by("last_updated").last(), form_data)
1892
-
1893
- @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
1894
- def test_local_config_context_schema_validation_pass(self):
1895
- """
1896
- Given a config context schema
1897
- And a device with local context that conforms to that schema
1898
- Assert that the local context passes schema validation via full_clean()
1899
- """
1900
- schema = ConfigContextSchema.objects.create(
1901
- name="Schema 1",
1902
- data_schema={"type": "object", "properties": {"foo": {"type": "string"}}},
1903
- )
1904
- self.add_permissions("dcim.add_device")
1905
-
1906
- form_data = self.form_data.copy()
1907
- form_data["local_config_context_schema"] = schema.pk
1908
- form_data["local_config_context_data"] = '{"foo": "bar"}'
1909
-
1910
- # Try POST with model-level permission
1911
- request = {
1912
- "path": self._get_url("add"),
1913
- "data": post_data(form_data),
1914
- }
1915
- self.assertHttpStatus(self.client.post(**request), 302)
1916
- self.assertEqual(
1917
- self._get_queryset().get(name="Device X").local_config_context_schema.pk,
1918
- schema.pk,
1919
- )
1920
-
1921
- @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
1922
- def test_local_config_context_schema_validation_fails(self):
1923
- """
1924
- Given a config context schema
1925
- And a device with local context that *does not* conform to that schema
1926
- Assert that the local context fails schema validation via full_clean()
1927
- """
1928
- schema = ConfigContextSchema.objects.create(
1929
- name="Schema 1",
1930
- data_schema={"type": "object", "properties": {"foo": {"type": "integer"}}},
1931
- )
1932
- self.add_permissions("dcim.add_device")
1933
-
1934
- form_data = self.form_data.copy()
1935
- form_data["local_config_context_schema"] = schema.pk
1936
- form_data["local_config_context_data"] = '{"foo": "bar"}'
1937
-
1938
- # Try POST with model-level permission
1939
- request = {
1940
- "path": self._get_url("add"),
1941
- "data": post_data(form_data),
1942
- }
1943
- self.assertHttpStatus(self.client.post(**request), 200)
1944
- self.assertEqual(self._get_queryset().filter(name="Device X").count(), 0)
1945
-
1946
2823
 
1947
2824
  class ConsolePortTestCase(ViewTestCases.DeviceComponentViewTestCase):
1948
2825
  model = ConsolePort
@@ -1983,6 +2860,15 @@ class ConsolePortTestCase(ViewTestCases.DeviceComponentViewTestCase):
1983
2860
  "description": "New description",
1984
2861
  }
1985
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
+
1986
2872
 
1987
2873
  class ConsoleServerPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
1988
2874
  model = ConsoleServerPort
@@ -2022,6 +2908,15 @@ class ConsoleServerPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
2022
2908
  "description": "New description",
2023
2909
  }
2024
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
+
2025
2920
 
2026
2921
  class PowerPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
2027
2922
  model = PowerPort
@@ -2066,12 +2961,22 @@ class PowerPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
2066
2961
  "description": "New description",
2067
2962
  }
2068
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
+
2069
2973
 
2070
2974
  class PowerOutletTestCase(ViewTestCases.DeviceComponentViewTestCase):
2071
2975
  model = PowerOutlet
2072
2976
 
2073
2977
  @classmethod
2074
2978
  def setUpTestData(cls):
2979
+ PowerOutlet.objects.all().delete()
2075
2980
  device = create_test_device("Device 1")
2076
2981
 
2077
2982
  powerports = (
@@ -2124,12 +3029,23 @@ class PowerOutletTestCase(ViewTestCases.DeviceComponentViewTestCase):
2124
3029
  "description": "New description",
2125
3030
  }
2126
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
+
2127
3042
 
2128
3043
  class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
2129
3044
  model = Interface
2130
3045
 
2131
3046
  @classmethod
2132
3047
  def setUpTestData(cls):
3048
+ Interface.objects.all().delete()
2133
3049
  device = create_test_device("Device 1")
2134
3050
  vrfs = list(VRF.objects.all()[:3])
2135
3051
  for vrf in vrfs:
@@ -2137,22 +3053,24 @@ class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
2137
3053
 
2138
3054
  statuses = Status.objects.get_for_model(Interface)
2139
3055
  status_active = statuses[0]
2140
-
3056
+ role = Role.objects.get_for_model(Interface).first()
2141
3057
  interfaces = (
2142
- Interface.objects.create(device=device, name="Interface 1", status=status_active),
2143
- Interface.objects.create(device=device, name="Interface 2", status=status_active),
2144
- Interface.objects.create(device=device, name="Interface 3", status=status_active),
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),
2145
3061
  Interface.objects.create(
2146
3062
  device=device,
2147
3063
  name="LAG",
2148
3064
  status=status_active,
2149
3065
  type=InterfaceTypeChoices.TYPE_LAG,
3066
+ role=role,
2150
3067
  ),
2151
3068
  Interface.objects.create(
2152
3069
  device=device,
2153
3070
  name="BRIDGE",
2154
3071
  status=status_active,
2155
3072
  type=InterfaceTypeChoices.TYPE_BRIDGE,
3073
+ role=role,
2156
3074
  ),
2157
3075
  )
2158
3076
  cls.lag_interface = interfaces[3]
@@ -2199,6 +3117,7 @@ class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
2199
3117
  "type": InterfaceTypeChoices.TYPE_1GE_GBIC,
2200
3118
  "enabled": False,
2201
3119
  "status": status_active.pk,
3120
+ "role": role.pk,
2202
3121
  "lag": interfaces[3].pk,
2203
3122
  "mac_address": EUI("01:02:03:04:05:06"),
2204
3123
  "mtu": 2000,
@@ -2227,6 +3146,7 @@ class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
2227
3146
  "tagged_vlans": [v.pk for v in vlans[1:4]],
2228
3147
  "tags": [t.pk for t in Tag.objects.get_for_model(Interface)],
2229
3148
  "status": status_active.pk,
3149
+ "role": role.pk,
2230
3150
  "vrf": vrfs[0].pk,
2231
3151
  }
2232
3152
 
@@ -2235,6 +3155,7 @@ class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
2235
3155
  "name_pattern": "Interface [4-6]",
2236
3156
  "label_pattern": "Interface Number [4-6]",
2237
3157
  "status": status_active.pk,
3158
+ "role": role.pk,
2238
3159
  "type": InterfaceTypeChoices.TYPE_1GE_GBIC,
2239
3160
  "enabled": True,
2240
3161
  "mtu": 1500,
@@ -2257,9 +3178,21 @@ class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
2257
3178
  "untagged_vlan": vlans[0].pk,
2258
3179
  "tagged_vlans": [v.pk for v in vlans[1:4]],
2259
3180
  "status": status_active.pk,
3181
+ "role": role.pk,
2260
3182
  "vrf": vrfs[2].pk,
2261
3183
  }
2262
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
+
2263
3196
  @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
2264
3197
  def test_create_virtual_interface_with_parent_lag(self):
2265
3198
  """https://github.com/nautobot/nautobot/issues/4436."""
@@ -2305,18 +3238,66 @@ class FrontPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
2305
3238
  cls.device = device
2306
3239
 
2307
3240
  rearports = (
2308
- RearPort.objects.create(device=device, name="Rear Port 1"),
2309
- RearPort.objects.create(device=device, name="Rear Port 2"),
2310
- RearPort.objects.create(device=device, name="Rear Port 3"),
2311
- RearPort.objects.create(device=device, name="Rear Port 4"),
2312
- RearPort.objects.create(device=device, name="Rear Port 5"),
2313
- RearPort.objects.create(device=device, name="Rear Port 6"),
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
+ ),
2314
3277
  )
2315
3278
 
2316
3279
  frontports = (
2317
- FrontPort.objects.create(device=device, name="Front Port 1", rear_port=rearports[0]),
2318
- FrontPort.objects.create(device=device, name="Front Port 2", rear_port=rearports[1]),
2319
- FrontPort.objects.create(device=device, name="Front Port 3", rear_port=rearports[2]),
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
+ ),
2320
3301
  )
2321
3302
  # Required by ViewTestCases.DeviceComponentViewTestCase.test_bulk_rename
2322
3303
  cls.selected_objects = frontports
@@ -2346,6 +3327,18 @@ class FrontPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
2346
3327
  "description": "New description",
2347
3328
  }
2348
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
+
2349
3342
  @unittest.skip("No DeviceBulkAddFrontPortView exists at present")
2350
3343
  def test_bulk_add_component(self):
2351
3344
  pass
@@ -2359,9 +3352,24 @@ class RearPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
2359
3352
  device = create_test_device("Device 1")
2360
3353
 
2361
3354
  rearports = (
2362
- RearPort.objects.create(device=device, name="Rear Port 1"),
2363
- RearPort.objects.create(device=device, name="Rear Port 2"),
2364
- RearPort.objects.create(device=device, name="Rear Port 3"),
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
+ ),
2365
3373
  )
2366
3374
  # Required by ViewTestCases.DeviceComponentViewTestCase.test_bulk_rename
2367
3375
  cls.selected_objects = rearports
@@ -2390,6 +3398,17 @@ class RearPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
2390
3398
  "description": "New description",
2391
3399
  }
2392
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
+
2393
3412
 
2394
3413
  class DeviceBayTestCase(ViewTestCases.DeviceComponentViewTestCase):
2395
3414
  model = DeviceBay
@@ -2428,6 +3447,113 @@ class DeviceBayTestCase(ViewTestCases.DeviceComponentViewTestCase):
2428
3447
  "description": "New description",
2429
3448
  }
2430
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
+
2431
3557
 
2432
3558
  class InventoryItemTestCase(ViewTestCases.DeviceComponentViewTestCase):
2433
3559
  model = InventoryItem
@@ -2480,6 +3606,14 @@ class InventoryItemTestCase(ViewTestCases.DeviceComponentViewTestCase):
2480
3606
  "software_version": software_versions[2].pk,
2481
3607
  }
2482
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
+
2483
3617
  def test_table_with_indentation_is_removed_on_filter_or_sort(self):
2484
3618
  self.skipTest("InventoryItem table has no implementation of indentation.")
2485
3619
 
@@ -2540,73 +3674,73 @@ class CableTestCase(
2540
3674
  interfaces = (
2541
3675
  Interface.objects.create(
2542
3676
  device=devices[0],
2543
- name="Interface 1",
3677
+ name="Interface A1",
2544
3678
  type=InterfaceTypeChoices.TYPE_1GE_FIXED,
2545
3679
  status=interface_status,
2546
3680
  ),
2547
3681
  Interface.objects.create(
2548
3682
  device=devices[0],
2549
- name="Interface 2",
3683
+ name="Interface A2",
2550
3684
  type=InterfaceTypeChoices.TYPE_1GE_FIXED,
2551
3685
  status=interface_status,
2552
3686
  ),
2553
3687
  Interface.objects.create(
2554
3688
  device=devices[0],
2555
- name="Interface 3",
3689
+ name="Interface A3",
2556
3690
  type=InterfaceTypeChoices.TYPE_1GE_FIXED,
2557
3691
  status=interface_status,
2558
3692
  ),
2559
3693
  Interface.objects.create(
2560
3694
  device=devices[1],
2561
- name="Interface 1",
3695
+ name="Interface A1",
2562
3696
  type=InterfaceTypeChoices.TYPE_1GE_FIXED,
2563
3697
  status=interface_status,
2564
3698
  ),
2565
3699
  Interface.objects.create(
2566
3700
  device=devices[1],
2567
- name="Interface 2",
3701
+ name="Interface A2",
2568
3702
  type=InterfaceTypeChoices.TYPE_1GE_FIXED,
2569
3703
  status=interface_status,
2570
3704
  ),
2571
3705
  Interface.objects.create(
2572
3706
  device=devices[1],
2573
- name="Interface 3",
3707
+ name="Interface A3",
2574
3708
  type=InterfaceTypeChoices.TYPE_1GE_FIXED,
2575
3709
  status=interface_status,
2576
3710
  ),
2577
3711
  Interface.objects.create(
2578
3712
  device=devices[2],
2579
- name="Interface 1",
3713
+ name="Interface A1",
2580
3714
  type=InterfaceTypeChoices.TYPE_1GE_FIXED,
2581
3715
  status=interface_status,
2582
3716
  ),
2583
3717
  Interface.objects.create(
2584
3718
  device=devices[2],
2585
- name="Interface 2",
3719
+ name="Interface A2",
2586
3720
  type=InterfaceTypeChoices.TYPE_1GE_FIXED,
2587
3721
  status=interface_status,
2588
3722
  ),
2589
3723
  Interface.objects.create(
2590
3724
  device=devices[2],
2591
- name="Interface 3",
3725
+ name="Interface A3",
2592
3726
  type=InterfaceTypeChoices.TYPE_1GE_FIXED,
2593
3727
  status=interface_status,
2594
3728
  ),
2595
3729
  Interface.objects.create(
2596
3730
  device=devices[3],
2597
- name="Interface 1",
3731
+ name="Interface A1",
2598
3732
  type=InterfaceTypeChoices.TYPE_1GE_FIXED,
2599
3733
  status=interface_status,
2600
3734
  ),
2601
3735
  Interface.objects.create(
2602
3736
  device=devices[3],
2603
- name="Interface 2",
3737
+ name="Interface A2",
2604
3738
  type=InterfaceTypeChoices.TYPE_1GE_FIXED,
2605
3739
  status=interface_status,
2606
3740
  ),
2607
3741
  Interface.objects.create(
2608
3742
  device=devices[3],
2609
- name="Interface 3",
3743
+ name="Interface A3",
2610
3744
  type=InterfaceTypeChoices.TYPE_1GE_FIXED,
2611
3745
  status=interface_status,
2612
3746
  ),
@@ -2758,6 +3892,9 @@ class ConsoleConnectionsTestCase(ViewTestCases.ListObjectsViewTestCase):
2758
3892
  def _get_base_url(self):
2759
3893
  return "dcim:console_connections_{}"
2760
3894
 
3895
+ def _get_queryset(self):
3896
+ return ConsolePort.objects.filter(cable__isnull=False)
3897
+
2761
3898
  def get_list_url(self):
2762
3899
  return "/dcim/console-connections/"
2763
3900
 
@@ -2810,15 +3947,18 @@ class PowerConnectionsTestCase(ViewTestCases.ListObjectsViewTestCase):
2810
3947
  Test the PowerConnectionsListView.
2811
3948
  """
2812
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
+
2813
3956
  def get_list_url(self):
2814
3957
  return "/dcim/power-connections/"
2815
3958
 
2816
3959
  def get_title(self):
2817
3960
  return "Power Connections"
2818
3961
 
2819
- def _get_base_url(self):
2820
- return "dcim:power_connections_{}"
2821
-
2822
3962
  def get_list_view(self):
2823
3963
  return PowerConnectionsListView
2824
3964
 
@@ -2875,6 +4015,9 @@ class InterfaceConnectionsTestCase(ViewTestCases.ListObjectsViewTestCase):
2875
4015
  def _get_base_url(self):
2876
4016
  return "dcim:interface_connections_{}"
2877
4017
 
4018
+ def _get_queryset(self):
4019
+ return Interface.objects.filter(cable__isnull=False)
4020
+
2878
4021
  def get_list_url(self):
2879
4022
  return "/dcim/interface-connections/"
2880
4023
 
@@ -2895,22 +4038,25 @@ class InterfaceConnectionsTestCase(ViewTestCases.ListObjectsViewTestCase):
2895
4038
  device_2 = create_test_device("Device 2")
2896
4039
 
2897
4040
  interface_status = Status.objects.get_for_model(Interface).first()
4041
+ interface_role = Role.objects.get_for_model(Interface).first()
2898
4042
  cls.interfaces = (
2899
4043
  Interface.objects.create(
2900
4044
  device=device_1,
2901
- name="Interface 1",
4045
+ name="Interface A1",
2902
4046
  type=InterfaceTypeChoices.TYPE_1GE_SFP,
2903
4047
  status=interface_status,
4048
+ role=interface_role,
2904
4049
  ),
2905
4050
  Interface.objects.create(
2906
4051
  device=device_1,
2907
- name="Interface 2",
4052
+ name="Interface A2",
2908
4053
  type=InterfaceTypeChoices.TYPE_1GE_SFP,
2909
4054
  status=interface_status,
4055
+ role=interface_role,
2910
4056
  ),
2911
4057
  Interface.objects.create(
2912
4058
  device=device_1,
2913
- name="Interface 3",
4059
+ name="Interface A3",
2914
4060
  type=InterfaceTypeChoices.TYPE_1GE_SFP,
2915
4061
  status=interface_status,
2916
4062
  ),
@@ -2918,9 +4064,10 @@ class InterfaceConnectionsTestCase(ViewTestCases.ListObjectsViewTestCase):
2918
4064
 
2919
4065
  cls.device_2_interface = Interface.objects.create(
2920
4066
  device=device_2,
2921
- name="Interface 1",
4067
+ name="Interface A1",
2922
4068
  type=InterfaceTypeChoices.TYPE_1GE_SFP,
2923
4069
  status=interface_status,
4070
+ role=interface_role,
2924
4071
  )
2925
4072
  rearport = RearPort.objects.create(device=device_2, type=PortTypeChoices.TYPE_8P8C)
2926
4073
 
@@ -3042,6 +4189,28 @@ class VirtualChassisTestCase(ViewTestCases.PrimaryObjectViewTestCase):
3042
4189
  "domain": "domain-x",
3043
4190
  }
3044
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
+
3045
4214
  def test_device_column_visible(self):
3046
4215
  """
3047
4216
  This checks whether the device column on a device's interfaces
@@ -3326,11 +4495,11 @@ class InterfaceRedundancyGroupTestCase(ViewTestCases.PrimaryObjectViewTestCase):
3326
4495
  status=status_active,
3327
4496
  )
3328
4497
  intf_status = Status.objects.get_for_model(Interface).first()
3329
-
4498
+ intf_role = Role.objects.get_for_model(Interface).first()
3330
4499
  cls.interfaces = (
3331
- Interface.objects.create(device=device, name="Interface 1", status=intf_status),
3332
- Interface.objects.create(device=device, name="Interface 2", status=intf_status),
3333
- Interface.objects.create(device=device, name="Interface 3", status=intf_status),
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),
3334
4503
  )
3335
4504
 
3336
4505
  cls.form_data = {