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
nautobot/dcim/views.py CHANGED
@@ -1,12 +1,14 @@
1
1
  from collections import OrderedDict
2
+ from copy import deepcopy
2
3
  import logging
4
+ import re
3
5
  import uuid
4
6
 
5
7
  from django.contrib import messages
6
8
  from django.contrib.contenttypes.models import ContentType
7
9
  from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
8
10
  from django.core.paginator import EmptyPage, PageNotAnInteger
9
- from django.db import transaction
11
+ from django.db import IntegrityError, transaction
10
12
  from django.db.models import F, Prefetch
11
13
  from django.forms import (
12
14
  modelformset_factory,
@@ -20,18 +22,31 @@ from django.utils.html import format_html
20
22
  from django.utils.http import url_has_allowed_host_and_scheme
21
23
  from django.views.generic import View
22
24
  from django_tables2 import RequestConfig
25
+ from rest_framework.decorators import action
26
+ from rest_framework.exceptions import MethodNotAllowed
27
+ from rest_framework.response import Response
23
28
 
24
29
  from nautobot.circuits.models import Circuit
25
- from nautobot.core.forms import ConfirmationForm, restrict_form_fields
30
+ from nautobot.cloud.models import CloudAccount
31
+ from nautobot.cloud.tables import CloudAccountTable
32
+ from nautobot.core.exceptions import AbortTransaction
33
+ from nautobot.core.forms import BulkRenameForm, ConfirmationForm, ImportForm, restrict_form_fields
26
34
  from nautobot.core.models.querysets import count_related
27
35
  from nautobot.core.templatetags.helpers import has_perms
36
+ from nautobot.core.utils.lookup import get_form_for_model
28
37
  from nautobot.core.utils.permissions import get_permission_for_model
29
38
  from nautobot.core.utils.requests import normalize_querydict
30
39
  from nautobot.core.views import generic
31
40
  from nautobot.core.views.mixins import (
32
41
  GetReturnURLMixin,
42
+ ObjectBulkDestroyViewMixin,
43
+ ObjectBulkUpdateViewMixin,
44
+ ObjectChangeLogViewMixin,
33
45
  ObjectDestroyViewMixin,
46
+ ObjectDetailViewMixin,
34
47
  ObjectEditViewMixin,
48
+ ObjectListViewMixin,
49
+ ObjectNotesViewMixin,
35
50
  ObjectPermissionRequiredMixin,
36
51
  )
37
52
  from nautobot.core.views.paginator import EnhancedPaginator, get_paginate_count
@@ -74,6 +89,10 @@ from .models import (
74
89
  Location,
75
90
  LocationType,
76
91
  Manufacturer,
92
+ Module,
93
+ ModuleBay,
94
+ ModuleBayTemplate,
95
+ ModuleType,
77
96
  PathEndpoint,
78
97
  Platform,
79
98
  PowerFeed,
@@ -158,7 +177,19 @@ class BaseDeviceComponentsBulkRenameView(generic.BulkRenameView):
158
177
  def get_selected_objects_parents_name(self, selected_objects):
159
178
  selected_object = selected_objects.first()
160
179
  if selected_object and selected_object.device:
161
- return selected_object.device.name
180
+ return selected_object.device.display
181
+ if selected_object and selected_object.module:
182
+ return selected_object.module.display
183
+ return ""
184
+
185
+
186
+ class BaseDeviceComponentTemplatesBulkRenameView(generic.BulkRenameView):
187
+ def get_selected_objects_parents_name(self, selected_objects):
188
+ selected_object = selected_objects.first()
189
+ if selected_object and selected_object.device_type:
190
+ return selected_object.device_type.display
191
+ if selected_object and selected_object.module_type:
192
+ return selected_object.module_type.display
162
193
  return ""
163
194
 
164
195
 
@@ -168,7 +199,7 @@ class BaseDeviceComponentsBulkRenameView(generic.BulkRenameView):
168
199
 
169
200
 
170
201
  class LocationTypeListView(generic.ObjectListView):
171
- queryset = LocationType.objects.with_tree_fields()
202
+ queryset = LocationType.objects.all()
172
203
  filterset = filters.LocationTypeFilterSet
173
204
  filterset_form = forms.LocationTypeFilterForm
174
205
  table = tables.LocationTypeTable
@@ -370,7 +401,7 @@ class MigrateLocationDataToContactView(generic.ObjectEditView):
370
401
 
371
402
  associated_object_id = obj.pk
372
403
  associated_object_content_type = ContentType.objects.get_for_model(Location)
373
- action = request.POST.get("action")
404
+ migrate_action = request.POST.get("action")
374
405
  try:
375
406
  with transaction.atomic():
376
407
  if not has_perms(request.user, ["extras.add_contactassociation"]):
@@ -379,7 +410,7 @@ class MigrateLocationDataToContactView(generic.ObjectEditView):
379
410
  )
380
411
  contact = None
381
412
  team = None
382
- if action == LocationDataToContactActionChoices.CREATE_AND_ASSIGN_NEW_CONTACT:
413
+ if migrate_action == LocationDataToContactActionChoices.CREATE_AND_ASSIGN_NEW_CONTACT:
383
414
  if not has_perms(request.user, ["extras.add_contact"]):
384
415
  raise PermissionDenied("ObjectPermission extras.add_contact is needed to perform this action")
385
416
  contact = Contact(
@@ -390,7 +421,7 @@ class MigrateLocationDataToContactView(generic.ObjectEditView):
390
421
  contact.validated_save()
391
422
  # Trigger permission check
392
423
  Contact.objects.restrict(request.user, "view").get(pk=contact.pk)
393
- elif action == LocationDataToContactActionChoices.CREATE_AND_ASSIGN_NEW_TEAM:
424
+ elif migrate_action == LocationDataToContactActionChoices.CREATE_AND_ASSIGN_NEW_TEAM:
394
425
  if not has_perms(request.user, ["extras.add_team"]):
395
426
  raise PermissionDenied("ObjectPermission extras.add_team is needed to perform this action")
396
427
  team = Team(
@@ -401,12 +432,12 @@ class MigrateLocationDataToContactView(generic.ObjectEditView):
401
432
  team.validated_save()
402
433
  # Trigger permission check
403
434
  Team.objects.restrict(request.user, "view").get(pk=team.pk)
404
- elif action == LocationDataToContactActionChoices.ASSOCIATE_EXISTING_CONTACT:
435
+ elif migrate_action == LocationDataToContactActionChoices.ASSOCIATE_EXISTING_CONTACT:
405
436
  contact = Contact.objects.restrict(request.user, "view").get(pk=request.POST.get("contact"))
406
- elif action == LocationDataToContactActionChoices.ASSOCIATE_EXISTING_TEAM:
437
+ elif migrate_action == LocationDataToContactActionChoices.ASSOCIATE_EXISTING_TEAM:
407
438
  team = Team.objects.restrict(request.user, "view").get(pk=request.POST.get("team"))
408
439
  else:
409
- raise ValueError(f"Invalid action {action} passed from the form")
440
+ raise ValueError(f"Invalid action {migrate_action} passed from the form")
410
441
 
411
442
  association = ContactAssociation(
412
443
  contact=contact,
@@ -470,7 +501,7 @@ class MigrateLocationDataToContactView(generic.ObjectEditView):
470
501
 
471
502
 
472
503
  class RackGroupListView(generic.ObjectListView):
473
- queryset = RackGroup.objects.annotate(rack_count=count_related(Rack, "rack_group"))
504
+ queryset = RackGroup.objects.all()
474
505
  filterset = filters.RackGroupFilterSet
475
506
  filterset_form = forms.RackGroupFilterForm
476
507
  table = tables.RackGroupTable
@@ -514,7 +545,7 @@ class RackGroupBulkImportView(generic.BulkImportView): # 3.0 TODO: remove, unus
514
545
 
515
546
 
516
547
  class RackGroupBulkDeleteView(generic.BulkDeleteView):
517
- queryset = RackGroup.objects.annotate(rack_count=count_related(Rack, "rack_group")).select_related("location")
548
+ queryset = RackGroup.objects.all()
518
549
  filterset = filters.RackGroupFilterSet
519
550
  table = tables.RackGroupTable
520
551
 
@@ -525,7 +556,7 @@ class RackGroupBulkDeleteView(generic.BulkDeleteView):
525
556
 
526
557
 
527
558
  class RackListView(generic.ObjectListView):
528
- queryset = Rack.objects.annotate(device_count=count_related(Device, "rack"))
559
+ queryset = Rack.objects.all()
529
560
  filterset = filters.RackFilterSet
530
561
  filterset_form = forms.RackFilterForm
531
562
  table = tables.RackDetailTable
@@ -635,14 +666,14 @@ class RackBulkImportView(generic.BulkImportView): # 3.0 TODO: remove, unused
635
666
 
636
667
 
637
668
  class RackBulkEditView(generic.BulkEditView):
638
- queryset = Rack.objects.select_related("location", "rack_group", "tenant", "role")
669
+ queryset = Rack.objects.all()
639
670
  filterset = filters.RackFilterSet
640
671
  table = tables.RackTable
641
672
  form = forms.RackBulkEditForm
642
673
 
643
674
 
644
675
  class RackBulkDeleteView(generic.BulkDeleteView):
645
- queryset = Rack.objects.select_related("location", "rack_group", "tenant", "role")
676
+ queryset = Rack.objects.all()
646
677
  filterset = filters.RackFilterSet
647
678
  table = tables.RackTable
648
679
 
@@ -704,11 +735,7 @@ class RackReservationBulkDeleteView(generic.BulkDeleteView):
704
735
 
705
736
 
706
737
  class ManufacturerListView(generic.ObjectListView):
707
- queryset = Manufacturer.objects.annotate(
708
- device_type_count=count_related(DeviceType, "manufacturer"),
709
- inventory_item_count=count_related(InventoryItem, "manufacturer"),
710
- platform_count=count_related(Platform, "manufacturer"),
711
- )
738
+ queryset = Manufacturer.objects.all()
712
739
  filterset = filters.ManufacturerFilterSet
713
740
  filterset_form = forms.ManufacturerFilterForm
714
741
  table = tables.ManufacturerTable
@@ -733,7 +760,25 @@ class ManufacturerView(generic.ObjectView):
733
760
  }
734
761
  RequestConfig(request, paginate).configure(device_table)
735
762
 
736
- return {"device_table": device_table, **super().get_extra_context(request, instance)}
763
+ # Cloud Accounts
764
+ cloud_accounts = (
765
+ CloudAccount.objects.restrict(request.user, "view")
766
+ .filter(provider=instance)
767
+ .select_related("secrets_group")
768
+ )
769
+
770
+ cloud_account_table = CloudAccountTable(cloud_accounts)
771
+ paginate = {
772
+ "paginator_class": EnhancedPaginator,
773
+ "per_page": get_paginate_count(request),
774
+ }
775
+ RequestConfig(request, paginate).configure(cloud_account_table)
776
+
777
+ return {
778
+ "device_table": device_table,
779
+ "cloud_account_table": cloud_account_table,
780
+ **super().get_extra_context(request, instance),
781
+ }
737
782
 
738
783
 
739
784
  class ManufacturerEditView(generic.ObjectEditView):
@@ -751,7 +796,7 @@ class ManufacturerBulkImportView(generic.BulkImportView): # 3.0 TODO: remove, u
751
796
 
752
797
 
753
798
  class ManufacturerBulkDeleteView(generic.BulkDeleteView):
754
- queryset = Manufacturer.objects.annotate(device_type_count=count_related(DeviceType, "manufacturer"))
799
+ queryset = Manufacturer.objects.all()
755
800
  table = tables.ManufacturerTable
756
801
  filterset = filters.ManufacturerFilterSet
757
802
 
@@ -762,7 +807,7 @@ class ManufacturerBulkDeleteView(generic.BulkDeleteView):
762
807
 
763
808
 
764
809
  class DeviceTypeListView(generic.ObjectListView):
765
- queryset = DeviceType.objects.annotate(device_count=count_related(Device, "device_type"))
810
+ queryset = DeviceType.objects.all()
766
811
  filterset = filters.DeviceTypeFilterSet
767
812
  filterset_form = forms.DeviceTypeFilterForm
768
813
  table = tables.DeviceTypeTable
@@ -810,6 +855,10 @@ class DeviceTypeView(generic.ObjectView):
810
855
  DeviceBayTemplate.objects.restrict(request.user, "view").filter(device_type=instance),
811
856
  orderable=False,
812
857
  )
858
+ modulebay_table = tables.ModuleBayTemplateTable(
859
+ ModuleBayTemplate.objects.restrict(request.user, "view").filter(device_type=instance),
860
+ orderable=False,
861
+ )
813
862
  if request.user.has_perm("dcim.change_devicetype"):
814
863
  consoleport_table.columns.show("pk")
815
864
  consoleserverport_table.columns.show("pk")
@@ -819,6 +868,7 @@ class DeviceTypeView(generic.ObjectView):
819
868
  front_port_table.columns.show("pk")
820
869
  rear_port_table.columns.show("pk")
821
870
  devicebay_table.columns.show("pk")
871
+ modulebay_table.columns.show("pk")
822
872
 
823
873
  software_image_files_table = tables.SoftwareImageFileTable(
824
874
  instance.software_image_files.restrict(request.user, "view").annotate(
@@ -838,6 +888,7 @@ class DeviceTypeView(generic.ObjectView):
838
888
  "front_port_table": front_port_table,
839
889
  "rear_port_table": rear_port_table,
840
890
  "devicebay_table": devicebay_table,
891
+ "modulebay_table": modulebay_table,
841
892
  "software_image_files_table": software_image_files_table,
842
893
  **super().get_extra_context(request, instance),
843
894
  }
@@ -864,6 +915,7 @@ class DeviceTypeImportView(generic.ObjectImportView):
864
915
  "dcim.add_frontporttemplate",
865
916
  "dcim.add_rearporttemplate",
866
917
  "dcim.add_devicebaytemplate",
918
+ "dcim.add_modulebaytemplate",
867
919
  ]
868
920
  queryset = DeviceType.objects.all()
869
921
  model_form = forms.DeviceTypeImportForm
@@ -877,31 +929,255 @@ class DeviceTypeImportView(generic.ObjectImportView):
877
929
  ("rear-ports", forms.RearPortTemplateImportForm),
878
930
  ("front-ports", forms.FrontPortTemplateImportForm),
879
931
  ("device-bays", forms.DeviceBayTemplateImportForm),
932
+ ("module-bays", forms.ModuleBayTemplateImportForm),
880
933
  )
881
934
  )
882
935
 
883
936
 
884
937
  class DeviceTypeBulkEditView(generic.BulkEditView):
885
- queryset = (
886
- DeviceType.objects.select_related("manufacturer")
887
- .prefetch_related("software_image_files")
888
- .annotate(device_count=count_related(Device, "device_type"))
889
- )
938
+ queryset = DeviceType.objects.all()
890
939
  filterset = filters.DeviceTypeFilterSet
891
940
  table = tables.DeviceTypeTable
892
941
  form = forms.DeviceTypeBulkEditForm
893
942
 
894
943
 
895
944
  class DeviceTypeBulkDeleteView(generic.BulkDeleteView):
896
- queryset = (
897
- DeviceType.objects.select_related("manufacturer")
898
- .prefetch_related("software_image_files")
899
- .annotate(device_count=count_related(Device, "device_type"))
900
- )
945
+ queryset = DeviceType.objects.all()
901
946
  filterset = filters.DeviceTypeFilterSet
902
947
  table = tables.DeviceTypeTable
903
948
 
904
949
 
950
+ #
951
+ # Module types
952
+ #
953
+
954
+
955
+ class ModuleTypeUIViewSet(
956
+ ObjectDetailViewMixin,
957
+ ObjectListViewMixin,
958
+ ObjectEditViewMixin,
959
+ ObjectDestroyViewMixin,
960
+ ObjectBulkDestroyViewMixin,
961
+ ObjectBulkUpdateViewMixin,
962
+ ObjectChangeLogViewMixin,
963
+ ObjectNotesViewMixin,
964
+ ):
965
+ queryset = ModuleType.objects.all()
966
+ filterset_class = filters.ModuleTypeFilterSet
967
+ filterset_form_class = forms.ModuleTypeFilterForm
968
+ form_class = forms.ModuleTypeForm
969
+ import_model_form = forms.ModuleTypeImportForm
970
+ bulk_update_form_class = forms.ModuleTypeBulkEditForm
971
+ serializer_class = serializers.ModuleTypeSerializer
972
+ table_class = tables.ModuleTypeTable
973
+ related_object_forms = {
974
+ "console-ports": forms.ConsolePortTemplateImportForm,
975
+ "console-server-ports": forms.ConsoleServerPortTemplateImportForm,
976
+ "power-ports": forms.PowerPortTemplateImportForm,
977
+ "power-outlets": forms.PowerOutletTemplateImportForm,
978
+ "interfaces": forms.InterfaceTemplateImportForm,
979
+ "rear-ports": forms.RearPortTemplateImportForm,
980
+ "front-ports": forms.FrontPortTemplateImportForm,
981
+ "module-bays": forms.ModuleBayTemplateImportForm,
982
+ }
983
+
984
+ def get_required_permission(self):
985
+ view_action = self.get_action()
986
+ if view_action == "import_view":
987
+ return [
988
+ *self.get_permissions_for_model(ModuleType, ["add"]),
989
+ *self.get_permissions_for_model(ConsolePortTemplate, ["add"]),
990
+ *self.get_permissions_for_model(ConsoleServerPortTemplate, ["add"]),
991
+ *self.get_permissions_for_model(PowerPortTemplate, ["add"]),
992
+ *self.get_permissions_for_model(PowerOutletTemplate, ["add"]),
993
+ *self.get_permissions_for_model(InterfaceTemplate, ["add"]),
994
+ *self.get_permissions_for_model(FrontPortTemplate, ["add"]),
995
+ *self.get_permissions_for_model(RearPortTemplate, ["add"]),
996
+ *self.get_permissions_for_model(ModuleBayTemplate, ["add"]),
997
+ ]
998
+
999
+ return super().get_required_permission()
1000
+
1001
+ def get_extra_context(self, request, instance):
1002
+ if not instance:
1003
+ return {}
1004
+
1005
+ instance_count = Module.objects.restrict(request.user).filter(module_type=instance).count()
1006
+
1007
+ # Component tables
1008
+ consoleport_table = tables.ConsolePortTemplateTable(
1009
+ ConsolePortTemplate.objects.restrict(request.user, "view").filter(module_type=instance),
1010
+ orderable=False,
1011
+ )
1012
+ consoleserverport_table = tables.ConsoleServerPortTemplateTable(
1013
+ ConsoleServerPortTemplate.objects.restrict(request.user, "view").filter(module_type=instance),
1014
+ orderable=False,
1015
+ )
1016
+ powerport_table = tables.PowerPortTemplateTable(
1017
+ PowerPortTemplate.objects.restrict(request.user, "view").filter(module_type=instance),
1018
+ orderable=False,
1019
+ )
1020
+ poweroutlet_table = tables.PowerOutletTemplateTable(
1021
+ PowerOutletTemplate.objects.restrict(request.user, "view").filter(module_type=instance),
1022
+ orderable=False,
1023
+ )
1024
+ interface_table = tables.InterfaceTemplateTable(
1025
+ list(InterfaceTemplate.objects.restrict(request.user, "view").filter(module_type=instance)),
1026
+ orderable=False,
1027
+ )
1028
+ front_port_table = tables.FrontPortTemplateTable(
1029
+ FrontPortTemplate.objects.restrict(request.user, "view").filter(module_type=instance),
1030
+ orderable=False,
1031
+ )
1032
+ rear_port_table = tables.RearPortTemplateTable(
1033
+ RearPortTemplate.objects.restrict(request.user, "view").filter(module_type=instance),
1034
+ orderable=False,
1035
+ )
1036
+ modulebay_table = tables.ModuleBayTemplateTable(
1037
+ ModuleBayTemplate.objects.restrict(request.user, "view").filter(module_type=instance),
1038
+ orderable=False,
1039
+ )
1040
+ if request.user.has_perm("dcim.change_moduletype"):
1041
+ consoleport_table.columns.show("pk")
1042
+ consoleserverport_table.columns.show("pk")
1043
+ powerport_table.columns.show("pk")
1044
+ poweroutlet_table.columns.show("pk")
1045
+ interface_table.columns.show("pk")
1046
+ front_port_table.columns.show("pk")
1047
+ rear_port_table.columns.show("pk")
1048
+ modulebay_table.columns.show("pk")
1049
+
1050
+ return {
1051
+ "instance_count": instance_count,
1052
+ "consoleport_table": consoleport_table,
1053
+ "consoleserverport_table": consoleserverport_table,
1054
+ "powerport_table": powerport_table,
1055
+ "poweroutlet_table": poweroutlet_table,
1056
+ "interface_table": interface_table,
1057
+ "front_port_table": front_port_table,
1058
+ "rear_port_table": rear_port_table,
1059
+ "modulebay_table": modulebay_table,
1060
+ }
1061
+
1062
+ @action(
1063
+ detail=False,
1064
+ methods=["GET", "POST"],
1065
+ url_name="import",
1066
+ url_path="import",
1067
+ )
1068
+ def import_view(self, request, *args, **kwargs):
1069
+ if request.method == "POST":
1070
+ form = ImportForm(request.POST)
1071
+
1072
+ if form.is_valid():
1073
+ self.logger.debug("Import form validation was successful")
1074
+
1075
+ # Initialize model form
1076
+ data = form.cleaned_data["data"]
1077
+ model_form = self.import_model_form(data)
1078
+ restrict_form_fields(model_form, request.user)
1079
+
1080
+ # Assign default values for any fields which were not specified. We have to do this manually because passing
1081
+ # 'initial=' to the form on initialization merely sets default values for the widgets. Since widgets are not
1082
+ # used for YAML/JSON import, we first bind the imported data normally, then update the form's data with the
1083
+ # applicable field defaults as needed prior to form validation.
1084
+ for field_name, field in model_form.fields.items():
1085
+ if field_name not in data and hasattr(field, "initial"):
1086
+ model_form.data[field_name] = field.initial
1087
+
1088
+ if model_form.is_valid():
1089
+ try:
1090
+ with transaction.atomic():
1091
+ # Save the primary object
1092
+ obj = model_form.save()
1093
+
1094
+ # Enforce object-level permissions
1095
+ self.queryset.get(pk=obj.pk)
1096
+
1097
+ self.logger.debug(f"Created {obj} (PK: {obj.pk})")
1098
+
1099
+ # Iterate through the related object forms (if any), validating and saving each instance.
1100
+ for (
1101
+ field_name,
1102
+ related_object_form,
1103
+ ) in self.related_object_forms.items():
1104
+ self.logger.debug(f"Processing form for related objects: {related_object_form}")
1105
+
1106
+ related_obj_pks = []
1107
+ for i, rel_obj_data in enumerate(data.get(field_name, [])):
1108
+ # add parent object key to related object data
1109
+ rel_obj_data[obj._meta.verbose_name.replace(" ", "_")] = str(obj.pk)
1110
+ f = related_object_form(rel_obj_data)
1111
+
1112
+ for subfield_name, field in f.fields.items():
1113
+ if subfield_name not in rel_obj_data and hasattr(field, "initial"):
1114
+ f.data[subfield_name] = field.initial
1115
+
1116
+ if f.is_valid():
1117
+ related_obj = f.save()
1118
+ related_obj_pks.append(related_obj.pk)
1119
+ else:
1120
+ # Replicate errors on the related object form to the primary form for display
1121
+ for subfield_name, errors in f.errors.items():
1122
+ for err in errors:
1123
+ err_msg = f"{field_name}[{i}] {subfield_name}: {err}"
1124
+ model_form.add_error(None, err_msg)
1125
+ raise AbortTransaction()
1126
+
1127
+ # Enforce object-level permissions on related objects
1128
+ model = related_object_form.Meta.model
1129
+ if model.objects.filter(pk__in=related_obj_pks).count() != len(related_obj_pks):
1130
+ raise ObjectDoesNotExist
1131
+
1132
+ except AbortTransaction:
1133
+ pass
1134
+
1135
+ except ObjectDoesNotExist:
1136
+ msg = "Object creation failed due to object-level permissions violation"
1137
+ self.logger.debug(msg)
1138
+ model_form.add_error(None, msg)
1139
+
1140
+ if not model_form.errors:
1141
+ self.logger.info(f"Import object {obj} (PK: {obj.pk})")
1142
+ messages.success(
1143
+ request,
1144
+ format_html('Imported object: <a href="{}">{}</a>', obj.get_absolute_url(), obj),
1145
+ )
1146
+
1147
+ if "_addanother" in request.POST:
1148
+ return redirect(request.get_full_path())
1149
+
1150
+ return_url = form.cleaned_data.get("return_url")
1151
+ if url_has_allowed_host_and_scheme(url=return_url, allowed_hosts=request.get_host()):
1152
+ return redirect(iri_to_uri(return_url))
1153
+ else:
1154
+ return redirect(self.get_return_url(request, obj))
1155
+
1156
+ else:
1157
+ self.logger.debug("Model form validation failed")
1158
+
1159
+ # Replicate model form errors for display
1160
+ for field, errors in model_form.errors.items():
1161
+ for err in errors:
1162
+ if field == "__all__":
1163
+ form.add_error(None, err)
1164
+ else:
1165
+ form.add_error(None, f"{field}: {err}")
1166
+
1167
+ else:
1168
+ self.logger.debug("Import form validation failed")
1169
+
1170
+ else:
1171
+ form = ImportForm()
1172
+
1173
+ return Response(
1174
+ {
1175
+ "template": "generic/object_import.html",
1176
+ "form": form,
1177
+ }
1178
+ )
1179
+
1180
+
905
1181
  #
906
1182
  # Console port templates
907
1183
  #
@@ -930,7 +1206,7 @@ class ConsolePortTemplateBulkEditView(generic.BulkEditView):
930
1206
  filterset = filters.ConsolePortTemplateFilterSet
931
1207
 
932
1208
 
933
- class ConsolePortTemplateBulkRenameView(generic.BulkRenameView):
1209
+ class ConsolePortTemplateBulkRenameView(BaseDeviceComponentTemplatesBulkRenameView):
934
1210
  queryset = ConsolePortTemplate.objects.all()
935
1211
 
936
1212
 
@@ -968,7 +1244,7 @@ class ConsoleServerPortTemplateBulkEditView(generic.BulkEditView):
968
1244
  filterset = filters.ConsoleServerPortTemplateFilterSet
969
1245
 
970
1246
 
971
- class ConsoleServerPortTemplateBulkRenameView(generic.BulkRenameView):
1247
+ class ConsoleServerPortTemplateBulkRenameView(BaseDeviceComponentTemplatesBulkRenameView):
972
1248
  queryset = ConsoleServerPortTemplate.objects.all()
973
1249
 
974
1250
 
@@ -1006,7 +1282,7 @@ class PowerPortTemplateBulkEditView(generic.BulkEditView):
1006
1282
  filterset = filters.PowerPortTemplateFilterSet
1007
1283
 
1008
1284
 
1009
- class PowerPortTemplateBulkRenameView(generic.BulkRenameView):
1285
+ class PowerPortTemplateBulkRenameView(BaseDeviceComponentTemplatesBulkRenameView):
1010
1286
  queryset = PowerPortTemplate.objects.all()
1011
1287
 
1012
1288
 
@@ -1044,7 +1320,7 @@ class PowerOutletTemplateBulkEditView(generic.BulkEditView):
1044
1320
  filterset = filters.PowerOutletTemplateFilterSet
1045
1321
 
1046
1322
 
1047
- class PowerOutletTemplateBulkRenameView(generic.BulkRenameView):
1323
+ class PowerOutletTemplateBulkRenameView(BaseDeviceComponentTemplatesBulkRenameView):
1048
1324
  queryset = PowerOutletTemplate.objects.all()
1049
1325
 
1050
1326
 
@@ -1081,7 +1357,7 @@ class InterfaceTemplateBulkEditView(generic.BulkEditView):
1081
1357
  filterset = filters.InterfaceTemplateFilterSet
1082
1358
 
1083
1359
 
1084
- class InterfaceTemplateBulkRenameView(generic.BulkRenameView):
1360
+ class InterfaceTemplateBulkRenameView(BaseDeviceComponentTemplatesBulkRenameView):
1085
1361
  queryset = InterfaceTemplate.objects.all()
1086
1362
 
1087
1363
 
@@ -1118,7 +1394,7 @@ class FrontPortTemplateBulkEditView(generic.BulkEditView):
1118
1394
  filterset = filters.FrontPortTemplateFilterSet
1119
1395
 
1120
1396
 
1121
- class FrontPortTemplateBulkRenameView(generic.BulkRenameView):
1397
+ class FrontPortTemplateBulkRenameView(BaseDeviceComponentTemplatesBulkRenameView):
1122
1398
  queryset = FrontPortTemplate.objects.all()
1123
1399
 
1124
1400
 
@@ -1155,7 +1431,7 @@ class RearPortTemplateBulkEditView(generic.BulkEditView):
1155
1431
  filterset = filters.RearPortTemplateFilterSet
1156
1432
 
1157
1433
 
1158
- class RearPortTemplateBulkRenameView(generic.BulkRenameView):
1434
+ class RearPortTemplateBulkRenameView(BaseDeviceComponentTemplatesBulkRenameView):
1159
1435
  queryset = RearPortTemplate.objects.all()
1160
1436
 
1161
1437
 
@@ -1192,7 +1468,7 @@ class DeviceBayTemplateBulkEditView(generic.BulkEditView):
1192
1468
  filterset = filters.DeviceBayTemplateFilterSet
1193
1469
 
1194
1470
 
1195
- class DeviceBayTemplateBulkRenameView(generic.BulkRenameView):
1471
+ class DeviceBayTemplateBulkRenameView(BaseDeviceComponentTemplatesBulkRenameView):
1196
1472
  queryset = DeviceBayTemplate.objects.all()
1197
1473
 
1198
1474
 
@@ -1202,16 +1478,212 @@ class DeviceBayTemplateBulkDeleteView(generic.BulkDeleteView):
1202
1478
  filterset = filters.DeviceBayTemplateFilterSet
1203
1479
 
1204
1480
 
1481
+ #
1482
+ # Module bay templates
1483
+ #
1484
+
1485
+
1486
+ class ModuleBayCommonViewSetMixin:
1487
+ """NautobotUIViewSet for ModuleBay views to handle templated create and bulk rename views."""
1488
+
1489
+ def create(self, request, *args, **kwargs):
1490
+ if request.method == "POST":
1491
+ return self.perform_create(request, *args, **kwargs)
1492
+
1493
+ form = self.create_form_class(initial=request.GET)
1494
+ model_form = self.model_form_class(request.GET)
1495
+
1496
+ return Response(
1497
+ {
1498
+ "template": self.create_template_name,
1499
+ "component_type": self.queryset.model._meta.verbose_name,
1500
+ "model_form": model_form,
1501
+ "form": form,
1502
+ "return_url": self.get_return_url(request),
1503
+ },
1504
+ )
1505
+
1506
+ def perform_create(self, request, *args, **kwargs):
1507
+ form = self.create_form_class(
1508
+ request.POST,
1509
+ initial=normalize_querydict(request.GET, form_class=self.create_form_class),
1510
+ )
1511
+ model_form = self.model_form_class(
1512
+ request.POST,
1513
+ initial=normalize_querydict(request.GET, form_class=self.model_form_class),
1514
+ )
1515
+
1516
+ if form.is_valid():
1517
+ new_components = []
1518
+ data = deepcopy(request.POST)
1519
+
1520
+ names = form.cleaned_data["name_pattern"]
1521
+ labels = form.cleaned_data.get("label_pattern")
1522
+ positions = form.cleaned_data.get("position_pattern")
1523
+ for i, name in enumerate(names):
1524
+ label = labels[i] if labels else None
1525
+ position = positions[i] if positions else None
1526
+ # Initialize the individual component form
1527
+ data["name"] = name
1528
+ data["label"] = label
1529
+ data["position"] = position
1530
+ component_form = self.model_form_class(
1531
+ data,
1532
+ initial=normalize_querydict(request.GET, form_class=self.model_form_class),
1533
+ )
1534
+ if component_form.is_valid():
1535
+ new_components.append(component_form)
1536
+ else:
1537
+ for field, errors in component_form.errors.as_data().items():
1538
+ # Assign errors on the child form's name/position/label field to *_pattern fields on the parent form
1539
+ if field.endswith("_pattern"):
1540
+ field = field[:-8]
1541
+ for e in errors:
1542
+ err_str = ", ".join(e)
1543
+ form.add_error(field, f"{name}: {err_str}")
1544
+
1545
+ if not form.errors:
1546
+ try:
1547
+ with transaction.atomic():
1548
+ # Create the new components
1549
+ new_objs = []
1550
+ for component_form in new_components:
1551
+ obj = component_form.save()
1552
+ new_objs.append(obj)
1553
+
1554
+ # Enforce object-level permissions
1555
+ if self.get_queryset().filter(pk__in=[obj.pk for obj in new_objs]).count() != len(new_objs):
1556
+ raise ObjectDoesNotExist
1557
+
1558
+ messages.success(
1559
+ request,
1560
+ f"Added {len(new_components)} {self.queryset.model._meta.verbose_name_plural}",
1561
+ )
1562
+ if "_addanother" in request.POST:
1563
+ return redirect(request.get_full_path())
1564
+ else:
1565
+ return redirect(self.get_return_url(request))
1566
+
1567
+ except ObjectDoesNotExist:
1568
+ msg = "Component creation failed due to object-level permissions violation"
1569
+ form.add_error(None, msg)
1570
+
1571
+ return Response(
1572
+ {
1573
+ "template": self.create_template_name,
1574
+ "component_type": self.queryset.model._meta.verbose_name,
1575
+ "form": form,
1576
+ "model_form": model_form,
1577
+ "return_url": self.get_return_url(request),
1578
+ },
1579
+ )
1580
+
1581
+ def _bulk_rename(self, request, *args, **kwargs):
1582
+ # TODO: This shouldn't be needed but default behavior of custom actions that don't support "GET" is broken
1583
+ if request.method != "POST":
1584
+ raise MethodNotAllowed(request.method)
1585
+
1586
+ query_pks = request.POST.getlist("pk")
1587
+ selected_objects = self.get_queryset().filter(pk__in=query_pks) if query_pks else None
1588
+
1589
+ # Create a new Form class from BulkRenameForm
1590
+ class _Form(BulkRenameForm):
1591
+ pk = ModelMultipleChoiceField(queryset=self.get_queryset(), widget=MultipleHiddenInput())
1592
+
1593
+ # selected_objects would return False; if no query_pks or invalid query_pks
1594
+ if not selected_objects:
1595
+ messages.warning(request, f"No valid {self.queryset.model._meta.verbose_name_plural} were selected.")
1596
+ return redirect(self.get_return_url(request))
1597
+
1598
+ if "_preview" in request.POST or "_apply" in request.POST:
1599
+ form = _Form(request.POST, initial={"pk": query_pks})
1600
+ if form.is_valid():
1601
+ try:
1602
+ with transaction.atomic():
1603
+ renamed_pks = []
1604
+ for obj in selected_objects:
1605
+ find = form.cleaned_data["find"]
1606
+ replace = form.cleaned_data["replace"]
1607
+ if form.cleaned_data["use_regex"]:
1608
+ try:
1609
+ obj.new_name = re.sub(find, replace, obj.name)
1610
+ # Catch regex group reference errors
1611
+ except re.error:
1612
+ obj.new_name = obj.name
1613
+ else:
1614
+ obj.new_name = obj.name.replace(find, replace)
1615
+ renamed_pks.append(obj.pk)
1616
+
1617
+ if "_apply" in request.POST:
1618
+ for obj in selected_objects:
1619
+ obj.name = obj.new_name
1620
+ obj.save()
1621
+
1622
+ # Enforce constrained permissions
1623
+ if self.get_queryset().filter(pk__in=renamed_pks).count() != len(selected_objects):
1624
+ raise ObjectDoesNotExist
1625
+
1626
+ messages.success(
1627
+ request,
1628
+ f"Renamed {len(selected_objects)} {self.queryset.model._meta.verbose_name_plural}",
1629
+ )
1630
+ return redirect(self.get_return_url(request))
1631
+
1632
+ except ObjectDoesNotExist:
1633
+ msg = "Object update failed due to object-level permissions violation"
1634
+ form.add_error(None, msg)
1635
+
1636
+ else:
1637
+ form = _Form(initial={"pk": query_pks})
1638
+
1639
+ return Response(
1640
+ {
1641
+ "template": "generic/object_bulk_rename.html",
1642
+ "form": form,
1643
+ "obj_type_plural": self.queryset.model._meta.verbose_name_plural,
1644
+ "selected_objects": selected_objects,
1645
+ "return_url": self.get_return_url(request),
1646
+ "parent_name": self.get_selected_objects_parents_name(selected_objects),
1647
+ }
1648
+ )
1649
+
1650
+
1651
+ class ModuleBayTemplateUIViewSet(
1652
+ ModuleBayCommonViewSetMixin,
1653
+ ObjectEditViewMixin,
1654
+ ObjectDestroyViewMixin,
1655
+ ObjectBulkDestroyViewMixin,
1656
+ ObjectBulkUpdateViewMixin,
1657
+ ):
1658
+ queryset = ModuleBayTemplate.objects.all()
1659
+ filterset_class = filters.ModuleBayTemplateFilterSet
1660
+ bulk_update_form_class = forms.ModuleBayTemplateBulkEditForm
1661
+ create_form_class = forms.ModuleBayTemplateCreateForm
1662
+ form_class = forms.ModuleBayTemplateForm
1663
+ model_form_class = forms.ModuleBayTemplateForm
1664
+ serializer_class = serializers.ModuleBayTemplateSerializer
1665
+ table_class = tables.ModuleBayTemplateTable
1666
+ create_template_name = "dcim/device_component_add.html"
1667
+
1668
+ def get_selected_objects_parents_name(self, selected_objects):
1669
+ selected_object = selected_objects.first()
1670
+ if selected_object:
1671
+ parent = selected_object.device_type or selected_object.module_type
1672
+ return parent.display
1673
+ return ""
1674
+
1675
+ @action(detail=False, methods=["GET", "POST"], url_path="rename", url_name="bulk_rename")
1676
+ def bulk_rename(self, request, *args, **kwargs):
1677
+ return self._bulk_rename(request, *args, **kwargs)
1678
+
1679
+
1205
1680
  #
1206
1681
  # Platforms
1207
1682
  #
1208
1683
 
1209
1684
 
1210
1685
  class PlatformListView(generic.ObjectListView):
1211
- queryset = Platform.objects.annotate(
1212
- device_count=count_related(Device, "platform"),
1213
- virtual_machine_count=count_related(VirtualMachine, "platform"),
1214
- )
1686
+ queryset = Platform.objects.all()
1215
1687
  filterset = filters.PlatformFilterSet
1216
1688
  filterset_form = forms.PlatformFilterForm
1217
1689
  table = tables.PlatformTable
@@ -1330,48 +1802,65 @@ class DeviceView(generic.ObjectView):
1330
1802
  else:
1331
1803
  software_version_images = []
1332
1804
 
1805
+ modulebay_count = instance.module_bays.count()
1806
+ module_count = instance.module_bays.filter(installed_module__isnull=False).count()
1807
+
1333
1808
  return {
1334
1809
  "services": services,
1335
1810
  "software_version_images": software_version_images,
1336
1811
  "vc_members": vc_members,
1337
1812
  "vrf_table": vrf_table,
1338
1813
  "active_tab": "device",
1814
+ "modulebay_count": modulebay_count,
1815
+ "module_count": f"{module_count}/{modulebay_count}",
1816
+ }
1817
+
1818
+
1819
+ class DeviceComponentTabView(generic.ObjectView):
1820
+ queryset = Device.objects.all()
1821
+
1822
+ def get_extra_context(self, request, instance):
1823
+ modulebay_count = instance.module_bays.count()
1824
+ module_count = instance.module_bays.filter(installed_module__isnull=False).count()
1825
+
1826
+ return {
1827
+ "modulebay_count": modulebay_count,
1828
+ "module_count": f"{module_count}/{modulebay_count}",
1339
1829
  }
1340
1830
 
1341
1831
 
1342
- class DeviceConsolePortsView(generic.ObjectView):
1832
+ class DeviceConsolePortsView(DeviceComponentTabView):
1343
1833
  queryset = Device.objects.all()
1344
1834
  template_name = "dcim/device/consoleports.html"
1345
1835
 
1346
1836
  def get_extra_context(self, request, instance):
1347
1837
  consoleports = (
1348
- ConsolePort.objects.restrict(request.user, "view")
1349
- .filter(device=instance)
1838
+ instance.all_console_ports.restrict(request.user, "view")
1350
1839
  .select_related("cable")
1351
1840
  .prefetch_related("_path__destination")
1352
1841
  )
1353
- consoleport_table = tables.DeviceConsolePortTable(data=consoleports, user=request.user, orderable=False)
1842
+ consoleport_table = tables.DeviceModuleConsolePortTable(data=consoleports, user=request.user, orderable=False)
1354
1843
  if request.user.has_perm("dcim.change_consoleport") or request.user.has_perm("dcim.delete_consoleport"):
1355
1844
  consoleport_table.columns.show("pk")
1356
1845
 
1357
1846
  return {
1847
+ **super().get_extra_context(request, instance),
1358
1848
  "consoleport_table": consoleport_table,
1359
1849
  "active_tab": "console-ports",
1360
1850
  }
1361
1851
 
1362
1852
 
1363
- class DeviceConsoleServerPortsView(generic.ObjectView):
1853
+ class DeviceConsoleServerPortsView(DeviceComponentTabView):
1364
1854
  queryset = Device.objects.all()
1365
1855
  template_name = "dcim/device/consoleserverports.html"
1366
1856
 
1367
1857
  def get_extra_context(self, request, instance):
1368
1858
  consoleserverports = (
1369
- ConsoleServerPort.objects.restrict(request.user, "view")
1370
- .filter(device=instance)
1859
+ instance.all_console_server_ports.restrict(request.user, "view")
1371
1860
  .select_related("cable")
1372
1861
  .prefetch_related("_path__destination")
1373
1862
  )
1374
- consoleserverport_table = tables.DeviceConsoleServerPortTable(
1863
+ consoleserverport_table = tables.DeviceModuleConsoleServerPortTable(
1375
1864
  data=consoleserverports, user=request.user, orderable=False
1376
1865
  )
1377
1866
  if request.user.has_perm("dcim.change_consoleserverport") or request.user.has_perm(
@@ -1380,54 +1869,55 @@ class DeviceConsoleServerPortsView(generic.ObjectView):
1380
1869
  consoleserverport_table.columns.show("pk")
1381
1870
 
1382
1871
  return {
1872
+ **super().get_extra_context(request, instance),
1383
1873
  "consoleserverport_table": consoleserverport_table,
1384
1874
  "active_tab": "console-server-ports",
1385
1875
  }
1386
1876
 
1387
1877
 
1388
- class DevicePowerPortsView(generic.ObjectView):
1878
+ class DevicePowerPortsView(DeviceComponentTabView):
1389
1879
  queryset = Device.objects.all()
1390
1880
  template_name = "dcim/device/powerports.html"
1391
1881
 
1392
1882
  def get_extra_context(self, request, instance):
1393
1883
  powerports = (
1394
- PowerPort.objects.restrict(request.user, "view")
1395
- .filter(device=instance)
1884
+ instance.all_power_ports.restrict(request.user, "view")
1396
1885
  .select_related("cable")
1397
1886
  .prefetch_related("_path__destination")
1398
1887
  )
1399
- powerport_table = tables.DevicePowerPortTable(data=powerports, user=request.user, orderable=False)
1888
+ powerport_table = tables.DeviceModulePowerPortTable(data=powerports, user=request.user, orderable=False)
1400
1889
  if request.user.has_perm("dcim.change_powerport") or request.user.has_perm("dcim.delete_powerport"):
1401
1890
  powerport_table.columns.show("pk")
1402
1891
 
1403
1892
  return {
1893
+ **super().get_extra_context(request, instance),
1404
1894
  "powerport_table": powerport_table,
1405
1895
  "active_tab": "power-ports",
1406
1896
  }
1407
1897
 
1408
1898
 
1409
- class DevicePowerOutletsView(generic.ObjectView):
1899
+ class DevicePowerOutletsView(DeviceComponentTabView):
1410
1900
  queryset = Device.objects.all()
1411
1901
  template_name = "dcim/device/poweroutlets.html"
1412
1902
 
1413
1903
  def get_extra_context(self, request, instance):
1414
1904
  poweroutlets = (
1415
- PowerOutlet.objects.restrict(request.user, "view")
1416
- .filter(device=instance)
1905
+ instance.all_power_outlets.restrict(request.user, "view")
1417
1906
  .select_related("cable", "power_port")
1418
1907
  .prefetch_related("_path__destination")
1419
1908
  )
1420
- poweroutlet_table = tables.DevicePowerOutletTable(data=poweroutlets, user=request.user, orderable=False)
1909
+ poweroutlet_table = tables.DeviceModulePowerOutletTable(data=poweroutlets, user=request.user, orderable=False)
1421
1910
  if request.user.has_perm("dcim.change_poweroutlet") or request.user.has_perm("dcim.delete_poweroutlet"):
1422
1911
  poweroutlet_table.columns.show("pk")
1423
1912
 
1424
1913
  return {
1914
+ **super().get_extra_context(request, instance),
1425
1915
  "poweroutlet_table": poweroutlet_table,
1426
1916
  "active_tab": "power-outlets",
1427
1917
  }
1428
1918
 
1429
1919
 
1430
- class DeviceInterfacesView(generic.ObjectView):
1920
+ class DeviceInterfacesView(DeviceComponentTabView):
1431
1921
  queryset = Device.objects.all()
1432
1922
  template_name = "dcim/device/interfaces.html"
1433
1923
 
@@ -1441,56 +1931,56 @@ class DeviceInterfacesView(generic.ObjectView):
1441
1931
  "tags",
1442
1932
  )
1443
1933
  .select_related("lag", "cable")
1934
+ .order_by("_name")
1444
1935
  )
1445
- interface_table = tables.DeviceInterfaceTable(data=interfaces, user=request.user, orderable=False)
1936
+ interface_table = tables.DeviceModuleInterfaceTable(data=interfaces, user=request.user, orderable=False)
1446
1937
  if VirtualChassis.objects.filter(master=instance).exists():
1447
1938
  interface_table.columns.show("device")
1448
1939
  if request.user.has_perm("dcim.change_interface") or request.user.has_perm("dcim.delete_interface"):
1449
1940
  interface_table.columns.show("pk")
1450
1941
 
1451
1942
  return {
1943
+ **super().get_extra_context(request, instance),
1452
1944
  "interface_table": interface_table,
1453
1945
  "active_tab": "interfaces",
1454
1946
  }
1455
1947
 
1456
1948
 
1457
- class DeviceFrontPortsView(generic.ObjectView):
1949
+ class DeviceFrontPortsView(DeviceComponentTabView):
1458
1950
  queryset = Device.objects.all()
1459
1951
  template_name = "dcim/device/frontports.html"
1460
1952
 
1461
1953
  def get_extra_context(self, request, instance):
1462
- frontports = (
1463
- FrontPort.objects.restrict(request.user, "view")
1464
- .filter(device=instance)
1465
- .select_related("cable", "rear_port")
1466
- )
1467
- frontport_table = tables.DeviceFrontPortTable(data=frontports, user=request.user, orderable=False)
1954
+ frontports = instance.all_front_ports.restrict(request.user, "view").select_related("cable", "rear_port")
1955
+ frontport_table = tables.DeviceModuleFrontPortTable(data=frontports, user=request.user, orderable=False)
1468
1956
  if request.user.has_perm("dcim.change_frontport") or request.user.has_perm("dcim.delete_frontport"):
1469
1957
  frontport_table.columns.show("pk")
1470
1958
 
1471
1959
  return {
1960
+ **super().get_extra_context(request, instance),
1472
1961
  "frontport_table": frontport_table,
1473
1962
  "active_tab": "front-ports",
1474
1963
  }
1475
1964
 
1476
1965
 
1477
- class DeviceRearPortsView(generic.ObjectView):
1966
+ class DeviceRearPortsView(DeviceComponentTabView):
1478
1967
  queryset = Device.objects.all()
1479
1968
  template_name = "dcim/device/rearports.html"
1480
1969
 
1481
1970
  def get_extra_context(self, request, instance):
1482
- rearports = RearPort.objects.restrict(request.user, "view").filter(device=instance).select_related("cable")
1483
- rearport_table = tables.DeviceRearPortTable(data=rearports, user=request.user, orderable=False)
1971
+ rearports = instance.all_rear_ports.restrict(request.user, "view").select_related("cable")
1972
+ rearport_table = tables.DeviceModuleRearPortTable(data=rearports, user=request.user, orderable=False)
1484
1973
  if request.user.has_perm("dcim.change_rearport") or request.user.has_perm("dcim.delete_rearport"):
1485
1974
  rearport_table.columns.show("pk")
1486
1975
 
1487
1976
  return {
1977
+ **super().get_extra_context(request, instance),
1488
1978
  "rearport_table": rearport_table,
1489
1979
  "active_tab": "rear-ports",
1490
1980
  }
1491
1981
 
1492
1982
 
1493
- class DeviceDeviceBaysView(generic.ObjectView):
1983
+ class DeviceDeviceBaysView(DeviceComponentTabView):
1494
1984
  queryset = Device.objects.all()
1495
1985
  template_name = "dcim/device/devicebays.html"
1496
1986
 
@@ -1507,11 +1997,34 @@ class DeviceDeviceBaysView(generic.ObjectView):
1507
1997
  devicebay_table.columns.show("pk")
1508
1998
 
1509
1999
  return {
2000
+ **super().get_extra_context(request, instance),
1510
2001
  "devicebay_table": devicebay_table,
1511
2002
  "active_tab": "device-bays",
1512
2003
  }
1513
2004
 
1514
2005
 
2006
+ class DeviceModuleBaysView(DeviceComponentTabView):
2007
+ queryset = Device.objects.all()
2008
+ template_name = "dcim/device/modulebays.html"
2009
+
2010
+ def get_extra_context(self, request, instance):
2011
+ # note: Device modules tab shouldn't show descendant modules until a proper tree view is implemented
2012
+ modulebays = (
2013
+ ModuleBay.objects.restrict(request.user, "view")
2014
+ .filter(parent_device=instance)
2015
+ .prefetch_related("installed_module__status", "installed_module")
2016
+ )
2017
+ modulebay_table = tables.DeviceModuleBayTable(data=modulebays, user=request.user, orderable=False)
2018
+ if request.user.has_perm("dcim.change_modulebay") or request.user.has_perm("dcim.delete_modulebay"):
2019
+ modulebay_table.columns.show("pk")
2020
+
2021
+ return {
2022
+ **super().get_extra_context(request, instance),
2023
+ "modulebay_table": modulebay_table,
2024
+ "active_tab": "module-bays",
2025
+ }
2026
+
2027
+
1515
2028
  class DeviceInventoryView(generic.ObjectView):
1516
2029
  queryset = Device.objects.all()
1517
2030
  template_name = "dcim/device/inventory.html"
@@ -1548,7 +2061,7 @@ class DeviceLLDPNeighborsView(generic.ObjectView):
1548
2061
 
1549
2062
  def get_extra_context(self, request, instance):
1550
2063
  interfaces = (
1551
- instance.vc_interfaces.restrict(request.user, "view")
2064
+ instance.all_interfaces.restrict(request.user, "view")
1552
2065
  .prefetch_related("_path__destination")
1553
2066
  .exclude(type__in=NONCONNECTABLE_IFACE_TYPES)
1554
2067
  )
@@ -1585,7 +2098,7 @@ class DeviceChangeLogView(ObjectChangeLogView):
1585
2098
  base_template = "dcim/device/base.html"
1586
2099
 
1587
2100
 
1588
- class DeviceDynamicGroupsView(ObjectDynamicGroupsView):
2101
+ class DeviceDynamicGroupsView(ObjectDynamicGroupsView): # 3.0 TODO: remove, deprecated in 2.3
1589
2102
  base_template = "dcim/device/base.html"
1590
2103
 
1591
2104
 
@@ -1626,6 +2139,432 @@ class DeviceBulkDeleteView(generic.BulkDeleteView):
1626
2139
  table = tables.DeviceTable
1627
2140
 
1628
2141
 
2142
+ #
2143
+ # Modules
2144
+ #
2145
+
2146
+
2147
+ class BulkComponentCreateUIViewSetMixin:
2148
+ def _bulk_component_create(self, request, component_queryset, bulk_component_form, parent_field=None):
2149
+ parent_model_name = self.queryset.model._meta.verbose_name_plural
2150
+ if parent_field is None:
2151
+ parent_field = self.queryset.model._meta.model_name
2152
+ model_name = component_queryset.model._meta.verbose_name_plural
2153
+ model = component_queryset.model
2154
+ component_create_form = get_form_for_model(model)
2155
+
2156
+ # Are we editing *all* objects in the queryset or just a selected subset?
2157
+ if request.POST.get("_all") and self.filterset is not None:
2158
+ pk_list = [obj.pk for obj in self.filterset(request.GET, self.get_queryset().only("pk")).qs]
2159
+ else:
2160
+ pk_list = request.POST.getlist("pk")
2161
+
2162
+ selected_objects = self.get_queryset().filter(pk__in=pk_list)
2163
+ if not selected_objects:
2164
+ messages.warning(
2165
+ request,
2166
+ f"No {parent_model_name} were selected.",
2167
+ )
2168
+ return redirect(self.get_return_url(request))
2169
+ table = self.table_class(selected_objects)
2170
+
2171
+ if "_create" in request.POST:
2172
+ form = bulk_component_form(model, request.POST)
2173
+
2174
+ if form.is_valid():
2175
+ new_components = []
2176
+ data = deepcopy(form.cleaned_data)
2177
+
2178
+ try:
2179
+ with transaction.atomic():
2180
+ for obj in data["pk"]:
2181
+ names = data["name_pattern"]
2182
+ labels = data["label_pattern"] if "label_pattern" in data else None
2183
+ for i, name in enumerate(names):
2184
+ label = labels[i] if labels else None
2185
+
2186
+ component_data = {
2187
+ parent_field: obj.pk,
2188
+ "name": name,
2189
+ "label": label,
2190
+ }
2191
+ component_data.update(data)
2192
+ component_form = component_create_form(component_data)
2193
+ if component_form.is_valid():
2194
+ instance = component_form.save()
2195
+ new_components.append(instance)
2196
+ else:
2197
+ for (
2198
+ field,
2199
+ errors,
2200
+ ) in component_form.errors.as_data().items():
2201
+ for e in errors:
2202
+ err_str = ", ".join(e)
2203
+ form.add_error(
2204
+ field,
2205
+ f"{obj} {name}: {err_str}",
2206
+ )
2207
+
2208
+ # Enforce object-level permissions
2209
+ if component_queryset.filter(pk__in=[obj.pk for obj in new_components]).count() != len(
2210
+ new_components
2211
+ ):
2212
+ raise ObjectDoesNotExist
2213
+
2214
+ except IntegrityError:
2215
+ pass
2216
+
2217
+ except ObjectDoesNotExist:
2218
+ msg = "Component creation failed due to object-level permissions violation"
2219
+ form.add_error(None, msg)
2220
+
2221
+ if not form.errors:
2222
+ msg = f"Added {len(new_components)} {model_name} to {len(form.cleaned_data['pk'])} {parent_model_name}."
2223
+ messages.success(request, msg)
2224
+
2225
+ return redirect(self.get_return_url(request))
2226
+
2227
+ else:
2228
+ form = bulk_component_form(model, initial={"pk": pk_list})
2229
+
2230
+ return Response(
2231
+ {
2232
+ "template": "generic/object_bulk_add_component.html",
2233
+ "form": form,
2234
+ "parent_model_name": parent_model_name,
2235
+ "model_name": model_name,
2236
+ "table": table,
2237
+ "return_url": self.get_return_url(request),
2238
+ },
2239
+ )
2240
+
2241
+
2242
+ class ModuleUIViewSet(BulkComponentCreateUIViewSetMixin, NautobotUIViewSet):
2243
+ queryset = Module.objects.all()
2244
+ filterset_class = filters.ModuleFilterSet
2245
+ filterset_form_class = forms.ModuleFilterForm
2246
+ form_class = forms.ModuleForm
2247
+ bulk_update_form_class = forms.ModuleBulkEditForm
2248
+ serializer_class = serializers.ModuleSerializer
2249
+ table_class = tables.ModuleTable
2250
+ component_model = None
2251
+
2252
+ def get_action(self):
2253
+ if self.component_model:
2254
+ method = self.request.method.lower()
2255
+ if method == "get":
2256
+ return "view"
2257
+ else:
2258
+ return "change"
2259
+
2260
+ return super().get_action()
2261
+
2262
+ def get_required_permission(self):
2263
+ # TODO: standardize a pattern for permissions enforcement on custom actions
2264
+ if self.component_model:
2265
+ model = self.component_model
2266
+ method = self.request.method.lower()
2267
+ if method == "get":
2268
+ component_action = "view"
2269
+ permissions = [*self.get_permissions_for_model(model, [component_action]), "dcim.view_module"]
2270
+ elif self.action.startswith("bulk_add"):
2271
+ component_action = "add"
2272
+ permissions = [*self.get_permissions_for_model(model, [component_action]), "dcim.change_module"]
2273
+ else:
2274
+ component_action = "change"
2275
+ permissions = [*self.get_permissions_for_model(model, [component_action]), "dcim.change_module"]
2276
+
2277
+ return permissions
2278
+
2279
+ return super().get_required_permission()
2280
+
2281
+ def get_extra_context(self, request, instance):
2282
+ context = super().get_extra_context(request, instance)
2283
+ if instance:
2284
+ context["modulebay_count"] = instance.module_bays.count()
2285
+ populated_module_count = instance.module_bays.filter(installed_module__isnull=False).count()
2286
+ context["module_count"] = f"{populated_module_count}/{context['modulebay_count']}"
2287
+ if self.action in ["create", "update"]:
2288
+ context["active_parent_tab"] = self._get_edit_view_active_parent_tab(request)
2289
+ return context
2290
+
2291
+ def _get_edit_view_active_parent_tab(self, request):
2292
+ active_parent_tab = "device"
2293
+ form_class = self.get_form_class()
2294
+ form = form_class(
2295
+ data=request.POST,
2296
+ files=request.FILES,
2297
+ initial=normalize_querydict(request.GET, form_class=form_class),
2298
+ instance=self.get_object(),
2299
+ )
2300
+ if form["parent_module_bay_module"].initial:
2301
+ active_parent_tab = "module"
2302
+ elif form["location"].initial:
2303
+ active_parent_tab = "location"
2304
+
2305
+ return active_parent_tab
2306
+
2307
+ @action(detail=True, url_path="console-ports", component_model=ConsolePort)
2308
+ def consoleports(self, request, *args, **kwargs):
2309
+ instance = self.get_object()
2310
+ consoleports = (
2311
+ instance.console_ports.restrict(request.user, "view")
2312
+ .select_related("cable")
2313
+ .prefetch_related("_path__destination")
2314
+ )
2315
+ consoleport_table = tables.DeviceModuleConsolePortTable(data=consoleports, user=request.user, orderable=False)
2316
+ if request.user.has_perm("dcim.change_consoleport") or request.user.has_perm("dcim.delete_consoleport"):
2317
+ consoleport_table.columns.show("pk")
2318
+
2319
+ return Response(
2320
+ {
2321
+ "consoleport_table": consoleport_table,
2322
+ "active_tab": "console-ports",
2323
+ }
2324
+ )
2325
+
2326
+ @action(detail=True, url_path="console-server-ports", component_model=ConsoleServerPort)
2327
+ def consoleserverports(self, request, *args, **kwargs):
2328
+ instance = self.get_object()
2329
+ consoleserverports = (
2330
+ instance.console_server_ports.restrict(request.user, "view")
2331
+ .select_related("cable")
2332
+ .prefetch_related("_path__destination")
2333
+ )
2334
+ consoleserverport_table = tables.DeviceModuleConsoleServerPortTable(
2335
+ data=consoleserverports, user=request.user, orderable=False, parent_module=instance
2336
+ )
2337
+ if request.user.has_perm("dcim.change_consoleserverport") or request.user.has_perm(
2338
+ "dcim.delete_consoleserverport"
2339
+ ):
2340
+ consoleserverport_table.columns.show("pk")
2341
+
2342
+ return Response(
2343
+ {
2344
+ "consoleserverport_table": consoleserverport_table,
2345
+ "active_tab": "console-server-ports",
2346
+ }
2347
+ )
2348
+
2349
+ @action(detail=True, url_path="power-ports", component_model=PowerPort)
2350
+ def powerports(self, request, *args, **kwargs):
2351
+ instance = self.get_object()
2352
+ powerports = (
2353
+ instance.power_ports.restrict(request.user, "view")
2354
+ .select_related("cable")
2355
+ .prefetch_related("_path__destination")
2356
+ )
2357
+ powerport_table = tables.DeviceModulePowerPortTable(
2358
+ data=powerports, user=request.user, orderable=False, parent_module=instance
2359
+ )
2360
+ if request.user.has_perm("dcim.change_powerport") or request.user.has_perm("dcim.delete_powerport"):
2361
+ powerport_table.columns.show("pk")
2362
+
2363
+ return Response(
2364
+ {
2365
+ "powerport_table": powerport_table,
2366
+ "active_tab": "power-ports",
2367
+ }
2368
+ )
2369
+
2370
+ @action(detail=True, url_path="power-outlets", component_model=PowerOutlet)
2371
+ def poweroutlets(self, request, *args, **kwargs):
2372
+ instance = self.get_object()
2373
+ poweroutlets = (
2374
+ instance.power_outlets.restrict(request.user, "view")
2375
+ .select_related("cable", "power_port")
2376
+ .prefetch_related("_path__destination")
2377
+ )
2378
+ poweroutlet_table = tables.DeviceModulePowerOutletTable(
2379
+ data=poweroutlets, user=request.user, orderable=False, parent_module=instance
2380
+ )
2381
+ if request.user.has_perm("dcim.change_poweroutlet") or request.user.has_perm("dcim.delete_poweroutlet"):
2382
+ poweroutlet_table.columns.show("pk")
2383
+
2384
+ return Response(
2385
+ {
2386
+ "poweroutlet_table": poweroutlet_table,
2387
+ "active_tab": "power-outlets",
2388
+ }
2389
+ )
2390
+
2391
+ @action(detail=True, component_model=Interface)
2392
+ def interfaces(self, request, *args, **kwargs):
2393
+ instance = self.get_object()
2394
+ interfaces = (
2395
+ instance.interfaces.restrict(request.user, "view")
2396
+ .prefetch_related(
2397
+ Prefetch("ip_addresses", queryset=IPAddress.objects.restrict(request.user)),
2398
+ Prefetch("member_interfaces", queryset=Interface.objects.restrict(request.user)),
2399
+ "_path__destination",
2400
+ "tags",
2401
+ )
2402
+ .select_related("lag", "cable")
2403
+ )
2404
+ interface_table = tables.DeviceModuleInterfaceTable(
2405
+ data=interfaces, user=request.user, orderable=False, parent_module=instance
2406
+ )
2407
+ if request.user.has_perm("dcim.change_interface") or request.user.has_perm("dcim.delete_interface"):
2408
+ interface_table.columns.show("pk")
2409
+
2410
+ return Response(
2411
+ {
2412
+ "interface_table": interface_table,
2413
+ "active_tab": "interfaces",
2414
+ }
2415
+ )
2416
+
2417
+ @action(detail=True, url_path="front-ports", component_model=FrontPort)
2418
+ def frontports(self, request, *args, **kwargs):
2419
+ instance = self.get_object()
2420
+ frontports = instance.front_ports.restrict(request.user, "view").select_related("cable", "rear_port")
2421
+ frontport_table = tables.DeviceModuleFrontPortTable(
2422
+ data=frontports, user=request.user, orderable=False, parent_module=instance
2423
+ )
2424
+ if request.user.has_perm("dcim.change_frontport") or request.user.has_perm("dcim.delete_frontport"):
2425
+ frontport_table.columns.show("pk")
2426
+
2427
+ return Response(
2428
+ {
2429
+ "frontport_table": frontport_table,
2430
+ "active_tab": "front-ports",
2431
+ },
2432
+ )
2433
+
2434
+ @action(detail=True, url_path="rear-ports", component_model=RearPort)
2435
+ def rearports(self, request, *args, **kwargs):
2436
+ instance = self.get_object()
2437
+ rearports = instance.rear_ports.restrict(request.user, "view").select_related("cable")
2438
+ rearport_table = tables.DeviceModuleRearPortTable(
2439
+ data=rearports, user=request.user, orderable=False, parent_module=instance
2440
+ )
2441
+ if request.user.has_perm("dcim.change_rearport") or request.user.has_perm("dcim.delete_rearport"):
2442
+ rearport_table.columns.show("pk")
2443
+
2444
+ return Response(
2445
+ {
2446
+ "rearport_table": rearport_table,
2447
+ "active_tab": "rear-ports",
2448
+ }
2449
+ )
2450
+
2451
+ @action(detail=True, url_path="module-bays", component_model=ModuleBay)
2452
+ def modulebays(self, request, *args, **kwargs):
2453
+ instance = self.get_object()
2454
+ modulebays = instance.module_bays.restrict(request.user, "view").prefetch_related(
2455
+ "installed_module__status", "installed_module"
2456
+ )
2457
+ modulebay_table = tables.ModuleModuleBayTable(data=modulebays, user=request.user, orderable=False)
2458
+ if request.user.has_perm("dcim.change_modulebay") or request.user.has_perm("dcim.delete_modulebay"):
2459
+ modulebay_table.columns.show("pk")
2460
+
2461
+ return Response(
2462
+ {
2463
+ "modulebay_table": modulebay_table,
2464
+ "active_tab": "module-bays",
2465
+ }
2466
+ )
2467
+
2468
+ @action(
2469
+ detail=False,
2470
+ methods=["POST"],
2471
+ url_path="console-ports/add",
2472
+ url_name="bulk_add_consoleport",
2473
+ component_model=ConsolePort,
2474
+ )
2475
+ def bulk_add_consoleport(self, request, *args, **kwargs):
2476
+ return self._bulk_component_create(
2477
+ request=request,
2478
+ component_queryset=ConsolePort.objects.all(),
2479
+ bulk_component_form=forms.ModuleConsolePortBulkCreateForm,
2480
+ )
2481
+
2482
+ @action(
2483
+ detail=False,
2484
+ methods=["POST"],
2485
+ url_path="console-server-ports/add",
2486
+ url_name="bulk_add_consoleserverport",
2487
+ component_model=ConsoleServerPort,
2488
+ )
2489
+ def bulk_add_consoleserverport(self, request, *args, **kwargs):
2490
+ return self._bulk_component_create(
2491
+ request=request,
2492
+ component_queryset=ConsoleServerPort.objects.all(),
2493
+ bulk_component_form=forms.ModuleConsoleServerPortBulkCreateForm,
2494
+ )
2495
+
2496
+ @action(
2497
+ detail=False,
2498
+ methods=["POST"],
2499
+ url_path="power-ports/add",
2500
+ url_name="bulk_add_powerport",
2501
+ component_model=PowerPort,
2502
+ )
2503
+ def bulk_add_powerport(self, request, *args, **kwargs):
2504
+ return self._bulk_component_create(
2505
+ request=request,
2506
+ component_queryset=PowerPort.objects.all(),
2507
+ bulk_component_form=forms.ModulePowerPortBulkCreateForm,
2508
+ )
2509
+
2510
+ @action(
2511
+ detail=False,
2512
+ methods=["POST"],
2513
+ url_path="power-outlets/add",
2514
+ url_name="bulk_add_poweroutlet",
2515
+ component_model=PowerOutlet,
2516
+ )
2517
+ def bulk_add_poweroutlet(self, request, *args, **kwargs):
2518
+ return self._bulk_component_create(
2519
+ request=request,
2520
+ component_queryset=PowerOutlet.objects.all(),
2521
+ bulk_component_form=forms.ModulePowerOutletBulkCreateForm,
2522
+ )
2523
+
2524
+ @action(
2525
+ detail=False,
2526
+ methods=["POST"],
2527
+ url_path="interfaces/add",
2528
+ url_name="bulk_add_interface",
2529
+ component_model=Interface,
2530
+ )
2531
+ def bulk_add_interface(self, request, *args, **kwargs):
2532
+ return self._bulk_component_create(
2533
+ request=request,
2534
+ component_queryset=Interface.objects.all(),
2535
+ bulk_component_form=forms.ModuleInterfaceBulkCreateForm,
2536
+ )
2537
+
2538
+ @action(
2539
+ detail=False,
2540
+ methods=["POST"],
2541
+ url_path="rear-ports/add",
2542
+ url_name="bulk_add_rearport",
2543
+ component_model=RearPort,
2544
+ )
2545
+ def bulk_add_rearport(self, request, *args, **kwargs):
2546
+ return self._bulk_component_create(
2547
+ request=request,
2548
+ component_queryset=RearPort.objects.all(),
2549
+ bulk_component_form=forms.ModuleRearPortBulkCreateForm,
2550
+ )
2551
+
2552
+ @action(
2553
+ detail=False,
2554
+ methods=["POST"],
2555
+ url_path="module-bays/add",
2556
+ url_name="bulk_add_modulebay",
2557
+ component_model=ModuleBay,
2558
+ )
2559
+ def bulk_add_modulebay(self, request, *args, **kwargs):
2560
+ return self._bulk_component_create(
2561
+ request=request,
2562
+ component_queryset=ModuleBay.objects.all(),
2563
+ bulk_component_form=forms.ModuleModuleBayBulkCreateForm,
2564
+ parent_field="parent_module",
2565
+ )
2566
+
2567
+
1629
2568
  #
1630
2569
  # Console ports
1631
2570
  #
@@ -1643,7 +2582,11 @@ class ConsolePortView(generic.ObjectView):
1643
2582
  queryset = ConsolePort.objects.all()
1644
2583
 
1645
2584
  def get_extra_context(self, request, instance):
1646
- return {"breadcrumb_url": "dcim:device_consoleports", **super().get_extra_context(request, instance)}
2585
+ return {
2586
+ "device_breadcrumb_url": "dcim:device_consoleports",
2587
+ "module_breadcrumb_url": "dcim:module_consoleports",
2588
+ **super().get_extra_context(request, instance),
2589
+ }
1647
2590
 
1648
2591
 
1649
2592
  class ConsolePortCreateView(generic.ComponentCreateView):
@@ -1705,7 +2648,11 @@ class ConsoleServerPortView(generic.ObjectView):
1705
2648
  queryset = ConsoleServerPort.objects.all()
1706
2649
 
1707
2650
  def get_extra_context(self, request, instance):
1708
- return {"breadcrumb_url": "dcim:device_consoleserverports", **super().get_extra_context(request, instance)}
2651
+ return {
2652
+ "device_breadcrumb_url": "dcim:device_consoleserverports",
2653
+ "module_breadcrumb_url": "dcim:module_consoleserverports",
2654
+ **super().get_extra_context(request, instance),
2655
+ }
1709
2656
 
1710
2657
 
1711
2658
  class ConsoleServerPortCreateView(generic.ComponentCreateView):
@@ -1767,7 +2714,11 @@ class PowerPortView(generic.ObjectView):
1767
2714
  queryset = PowerPort.objects.all()
1768
2715
 
1769
2716
  def get_extra_context(self, request, instance):
1770
- return {"breadcrumb_url": "dcim:device_powerports", **super().get_extra_context(request, instance)}
2717
+ return {
2718
+ "device_breadcrumb_url": "dcim:device_powerports",
2719
+ "module_breadcrumb_url": "dcim:module_powerports",
2720
+ **super().get_extra_context(request, instance),
2721
+ }
1771
2722
 
1772
2723
 
1773
2724
  class PowerPortCreateView(generic.ComponentCreateView):
@@ -1829,7 +2780,11 @@ class PowerOutletView(generic.ObjectView):
1829
2780
  queryset = PowerOutlet.objects.all()
1830
2781
 
1831
2782
  def get_extra_context(self, request, instance):
1832
- return {"breadcrumb_url": "dcim:device_poweroutlets", **super().get_extra_context(request, instance)}
2783
+ return {
2784
+ "device_breadcrumb_url": "dcim:device_poweroutlets",
2785
+ "module_breadcrumb_url": "dcim:module_poweroutlets",
2786
+ **super().get_extra_context(request, instance),
2787
+ }
1833
2788
 
1834
2789
 
1835
2790
  class PowerOutletCreateView(generic.ComponentCreateView):
@@ -1922,7 +2877,8 @@ class InterfaceView(generic.ObjectView):
1922
2877
  return {
1923
2878
  "ipaddress_table": ipaddress_table,
1924
2879
  "vlan_table": vlan_table,
1925
- "breadcrumb_url": "dcim:device_interfaces",
2880
+ "device_breadcrumb_url": "dcim:device_interfaces",
2881
+ "module_breadcrumb_url": "dcim:module_interfaces",
1926
2882
  "child_interfaces_table": child_interfaces_tables,
1927
2883
  "redundancy_table": redundancy_table,
1928
2884
  **super().get_extra_context(request, instance),
@@ -2012,7 +2968,11 @@ class FrontPortView(generic.ObjectView):
2012
2968
  queryset = FrontPort.objects.all()
2013
2969
 
2014
2970
  def get_extra_context(self, request, instance):
2015
- return {"breadcrumb_url": "dcim:device_frontports", **super().get_extra_context(request, instance)}
2971
+ return {
2972
+ "device_breadcrumb_url": "dcim:device_frontports",
2973
+ "module_breadcrumb_url": "dcim:module_frontports",
2974
+ **super().get_extra_context(request, instance),
2975
+ }
2016
2976
 
2017
2977
 
2018
2978
  class FrontPortCreateView(generic.ComponentCreateView):
@@ -2074,7 +3034,11 @@ class RearPortView(generic.ObjectView):
2074
3034
  queryset = RearPort.objects.all()
2075
3035
 
2076
3036
  def get_extra_context(self, request, instance):
2077
- return {"breadcrumb_url": "dcim:device_rearports", **super().get_extra_context(request, instance)}
3037
+ return {
3038
+ "device_breadcrumb_url": "dcim:device_rearports",
3039
+ "module_breadcrumb_url": "dcim:module_rearports",
3040
+ **super().get_extra_context(request, instance),
3041
+ }
2078
3042
 
2079
3043
 
2080
3044
  class RearPortCreateView(generic.ComponentCreateView):
@@ -2136,7 +3100,7 @@ class DeviceBayView(generic.ObjectView):
2136
3100
  queryset = DeviceBay.objects.all()
2137
3101
 
2138
3102
  def get_extra_context(self, request, instance):
2139
- return {"breadcrumb_url": "dcim:device_devicebays", **super().get_extra_context(request, instance)}
3103
+ return {"device_breadcrumb_url": "dcim:device_devicebays", **super().get_extra_context(request, instance)}
2140
3104
 
2141
3105
 
2142
3106
  class DeviceBayCreateView(generic.ComponentCreateView):
@@ -2262,6 +3226,43 @@ class DeviceBayBulkDeleteView(generic.BulkDeleteView):
2262
3226
  table = tables.DeviceBayTable
2263
3227
 
2264
3228
 
3229
+ #
3230
+ # Module bays
3231
+ #
3232
+
3233
+
3234
+ class ModuleBayUIViewSet(ModuleBayCommonViewSetMixin, NautobotUIViewSet):
3235
+ queryset = ModuleBay.objects.all()
3236
+ filterset_class = filters.ModuleBayFilterSet
3237
+ filterset_form_class = forms.ModuleBayFilterForm
3238
+ bulk_update_form_class = forms.ModuleBayBulkEditForm
3239
+ create_form_class = forms.ModuleBayCreateForm
3240
+ form_class = forms.ModuleBayForm
3241
+ model_form_class = forms.ModuleBayForm
3242
+ serializer_class = serializers.ModuleBaySerializer
3243
+ table_class = tables.ModuleBayTable
3244
+ create_template_name = "dcim/device_component_add.html"
3245
+
3246
+ def get_extra_context(self, request, instance):
3247
+ if instance:
3248
+ return {
3249
+ "device_breadcrumb_url": "dcim:device_modulebays",
3250
+ "module_breadcrumb_url": "dcim:module_modulebays",
3251
+ }
3252
+ return {}
3253
+
3254
+ def get_selected_objects_parents_name(self, selected_objects):
3255
+ selected_object = selected_objects.first()
3256
+ if selected_object:
3257
+ parent = selected_object.parent_device or selected_object.parent_module
3258
+ return parent.display
3259
+ return ""
3260
+
3261
+ @action(detail=False, methods=["GET", "POST"], url_path="rename", url_name="bulk_rename")
3262
+ def bulk_rename(self, request, *args, **kwargs):
3263
+ return self._bulk_rename(request, *args, **kwargs)
3264
+
3265
+
2265
3266
  #
2266
3267
  # Inventory items
2267
3268
  #
@@ -2286,7 +3287,7 @@ class InventoryItemView(generic.ObjectView):
2286
3287
  software_version_images = []
2287
3288
 
2288
3289
  return {
2289
- "breadcrumb_url": "dcim:device_inventory",
3290
+ "device_breadcrumb_url": "dcim:device_inventory",
2290
3291
  "software_version_images": software_version_images,
2291
3292
  **super().get_extra_context(request, instance),
2292
3293
  }
@@ -2425,6 +3426,17 @@ class DeviceBulkAddDeviceBayView(generic.BulkComponentCreateView):
2425
3426
  default_return_url = "dcim:device_list"
2426
3427
 
2427
3428
 
3429
+ class DeviceBulkAddModuleBayView(generic.BulkComponentCreateView):
3430
+ parent_model = Device
3431
+ parent_field = "parent_device"
3432
+ form = forms.ModuleBayBulkCreateForm
3433
+ queryset = ModuleBay.objects.all()
3434
+ model_form = forms.ModuleBayForm
3435
+ filterset = filters.DeviceFilterSet
3436
+ table = tables.DeviceTable
3437
+ default_return_url = "dcim:device_list"
3438
+
3439
+
2428
3440
  class DeviceBulkAddInventoryItemView(generic.BulkComponentCreateView):
2429
3441
  parent_model = Device
2430
3442
  parent_field = "device"
@@ -2695,7 +3707,7 @@ class InterfaceConnectionsListView(ConnectionsListView):
2695
3707
 
2696
3708
 
2697
3709
  class VirtualChassisListView(generic.ObjectListView):
2698
- queryset = VirtualChassis.objects.annotate(member_count=count_related(Device, "virtual_chassis"))
3710
+ queryset = VirtualChassis.objects.all()
2699
3711
  table = tables.VirtualChassisTable
2700
3712
  filterset = filters.VirtualChassisFilterSet
2701
3713
  filterset_form = forms.VirtualChassisFilterForm
@@ -2931,7 +3943,7 @@ class VirtualChassisBulkDeleteView(generic.BulkDeleteView):
2931
3943
 
2932
3944
 
2933
3945
  class PowerPanelListView(generic.ObjectListView):
2934
- queryset = PowerPanel.objects.annotate(power_feed_count=count_related(PowerFeed, "power_panel"))
3946
+ queryset = PowerPanel.objects.all()
2935
3947
  filterset = filters.PowerPanelFilterSet
2936
3948
  filterset_form = forms.PowerPanelFilterForm
2937
3949
  table = tables.PowerPanelTable
@@ -2971,9 +3983,7 @@ class PowerPanelBulkEditView(generic.BulkEditView):
2971
3983
 
2972
3984
 
2973
3985
  class PowerPanelBulkDeleteView(generic.BulkDeleteView):
2974
- queryset = PowerPanel.objects.select_related("location", "rack_group").annotate(
2975
- power_feed_count=count_related(PowerFeed, "power_panel")
2976
- )
3986
+ queryset = PowerPanel.objects.all()
2977
3987
  filterset = filters.PowerPanelFilterSet
2978
3988
  table = tables.PowerPanelTable
2979
3989
 
@@ -3027,14 +4037,7 @@ class DeviceRedundancyGroupUIViewSet(NautobotUIViewSet):
3027
4037
  filterset_class = filters.DeviceRedundancyGroupFilterSet
3028
4038
  filterset_form_class = forms.DeviceRedundancyGroupFilterForm
3029
4039
  form_class = forms.DeviceRedundancyGroupForm
3030
- queryset = (
3031
- DeviceRedundancyGroup.objects.select_related("status")
3032
- .prefetch_related("controllers", "devices")
3033
- .annotate(
3034
- device_count=count_related(Device, "device_redundancy_group"),
3035
- controller_count=count_related(Controller, "controller_device_redundancy_group"),
3036
- )
3037
- )
4040
+ queryset = DeviceRedundancyGroup.objects.all()
3038
4041
  serializer_class = serializers.DeviceRedundancyGroupSerializer
3039
4042
  table_class = tables.DeviceRedundancyGroupTable
3040
4043
 
@@ -3059,11 +4062,7 @@ class InterfaceRedundancyGroupUIViewSet(NautobotUIViewSet):
3059
4062
  filterset_class = filters.InterfaceRedundancyGroupFilterSet
3060
4063
  filterset_form_class = forms.InterfaceRedundancyGroupFilterForm
3061
4064
  form_class = forms.InterfaceRedundancyGroupForm
3062
- queryset = InterfaceRedundancyGroup.objects.select_related("status")
3063
- queryset = queryset.prefetch_related("interfaces")
3064
- queryset = queryset.annotate(
3065
- interface_count=count_related(Interface, "interface_redundancy_groups"),
3066
- )
4065
+ queryset = InterfaceRedundancyGroup.objects.all()
3067
4066
  serializer_class = serializers.InterfaceRedundancyGroupSerializer
3068
4067
  table_class = tables.InterfaceRedundancyGroupTable
3069
4068
  lookup_field = "pk"
@@ -3114,7 +4113,7 @@ class DeviceFamilyUIViewSet(NautobotUIViewSet):
3114
4113
  filterset_form_class = forms.DeviceFamilyFilterForm
3115
4114
  form_class = forms.DeviceFamilyForm
3116
4115
  bulk_update_form_class = forms.DeviceFamilyBulkEditForm
3117
- queryset = DeviceFamily.objects.annotate(device_type_count=count_related(DeviceType, "device_family"))
4116
+ queryset = DeviceFamily.objects.all()
3118
4117
  serializer_class = serializers.DeviceFamilySerializer
3119
4118
  table_class = tables.DeviceFamilyTable
3120
4119
  lookup_field = "pk"
@@ -3157,8 +4156,7 @@ class SoftwareImageFileUIViewSet(NautobotUIViewSet):
3157
4156
  filterset_form_class = forms.SoftwareImageFileFilterForm
3158
4157
  form_class = forms.SoftwareImageFileForm
3159
4158
  bulk_update_form_class = forms.SoftwareImageFileBulkEditForm
3160
- queryset = SoftwareImageFile.objects.annotate(device_type_count=count_related(DeviceType, "software_image_files"))
3161
-
4159
+ queryset = SoftwareImageFile.objects.all()
3162
4160
  serializer_class = serializers.SoftwareImageFileSerializer
3163
4161
  table_class = tables.SoftwareImageFileTable
3164
4162
 
@@ -3168,11 +4166,7 @@ class SoftwareVersionUIViewSet(NautobotUIViewSet):
3168
4166
  filterset_form_class = forms.SoftwareVersionFilterForm
3169
4167
  form_class = forms.SoftwareVersionForm
3170
4168
  bulk_update_form_class = forms.SoftwareVersionBulkEditForm
3171
- queryset = SoftwareVersion.objects.annotate(
3172
- software_image_file_count=count_related(SoftwareImageFile, "software_version"),
3173
- device_count=count_related(Device, "software_version"),
3174
- inventory_item_count=count_related(InventoryItem, "software_version"),
3175
- )
4169
+ queryset = SoftwareVersion.objects.all()
3176
4170
  serializer_class = serializers.SoftwareVersionSerializer
3177
4171
  table_class = tables.SoftwareVersionTable
3178
4172
 
@@ -3215,11 +4209,7 @@ class ControllerManagedDeviceGroupUIViewSet(NautobotUIViewSet):
3215
4209
  filterset_form_class = forms.ControllerManagedDeviceGroupFilterForm
3216
4210
  form_class = forms.ControllerManagedDeviceGroupForm
3217
4211
  bulk_update_form_class = forms.ControllerManagedDeviceGroupBulkEditForm
3218
- queryset = (
3219
- ControllerManagedDeviceGroup.objects.all()
3220
- .prefetch_related("devices")
3221
- .annotate(device_count=count_related(Device, "controller_managed_device_group"))
3222
- )
4212
+ queryset = ControllerManagedDeviceGroup.objects.all()
3223
4213
  serializer_class = serializers.ControllerManagedDeviceGroupSerializer
3224
4214
  table_class = tables.ControllerManagedDeviceGroupTable
3225
4215
  template_name = "dcim/controllermanageddevicegroup_create.html"