nautobot 2.4.18__py3-none-any.whl → 2.4.19__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 (583) hide show
  1. nautobot/apps/ui.py +6 -0
  2. nautobot/circuits/tables.py +1 -1
  3. nautobot/circuits/templates/circuits/circuit_create.html +7 -7
  4. nautobot/circuits/templates/circuits/circuit_retrieve.html +1 -5
  5. nautobot/circuits/templates/circuits/circuittermination_create.html +26 -26
  6. nautobot/circuits/templates/circuits/inc/circuit_termination.html +20 -20
  7. nautobot/circuits/templates/circuits/inc/circuit_termination_header_extra_content.html +3 -3
  8. nautobot/circuits/templates/circuits/inc/circuit_termination_speed_fragment.html +7 -7
  9. nautobot/circuits/templates/circuits/providernetwork_retrieve.html +1 -3
  10. nautobot/circuits/views.py +26 -9
  11. nautobot/cloud/templates/cloud/cloudaccount_retrieve.html +1 -4
  12. nautobot/cloud/templates/cloud/cloudnetwork_retrieve.html +1 -7
  13. nautobot/cloud/templates/cloud/cloudresourcetype_retrieve.html +1 -3
  14. nautobot/cloud/templates/cloud/cloudservice_retrieve.html +1 -5
  15. nautobot/cloud/views.py +45 -0
  16. nautobot/core/graphql/generators.py +5 -2
  17. nautobot/core/jobs/bulk_actions.py +48 -85
  18. nautobot/core/models/querysets.py +2 -1
  19. nautobot/core/templates/40x.html +15 -15
  20. nautobot/core/templates/500.html +21 -21
  21. nautobot/core/templates/admin/app_index.html +8 -8
  22. nautobot/core/templates/admin/base.html +104 -104
  23. nautobot/core/templates/admin/change_form.html +65 -65
  24. nautobot/core/templates/admin/change_list.html +60 -60
  25. nautobot/core/templates/admin/change_list_results.html +39 -39
  26. nautobot/core/templates/admin/config/config.html +47 -47
  27. nautobot/core/templates/admin/delete_confirmation.html +47 -47
  28. nautobot/core/templates/admin/edit_inline/stacked.html +124 -124
  29. nautobot/core/templates/admin/edit_inline/tabular.html +60 -60
  30. nautobot/core/templates/admin/includes/fieldset.html +4 -4
  31. nautobot/core/templates/admin/index.html +60 -60
  32. nautobot/core/templates/admin/prepopulated_fields_js.html +18 -18
  33. nautobot/core/templates/admin/submit_line.html +4 -4
  34. nautobot/core/templates/base_django.html +46 -46
  35. nautobot/core/templates/buttons/consolidated_bulk_action_buttons.html +8 -8
  36. nautobot/core/templates/buttons/consolidated_detail_view_action_buttons.html +8 -8
  37. nautobot/core/templates/buttons/export.html +3 -3
  38. nautobot/core/templates/components/breadcrumbs.html +5 -5
  39. nautobot/core/templates/components/button/default.html +3 -3
  40. nautobot/core/templates/components/button/dropdown.html +7 -7
  41. nautobot/core/templates/components/button/formbutton.html +4 -4
  42. nautobot/core/templates/components/panel/body_content_data_table.html +1 -1
  43. nautobot/core/templates/components/panel/body_wrapper_generic_table.html +3 -0
  44. nautobot/core/templates/components/panel/footer_content_table.html +3 -1
  45. nautobot/core/templates/components/panel/header_extra_content_table.html +10 -1
  46. nautobot/core/templates/components/tab/content_wrapper.html +1 -1
  47. nautobot/core/templates/components/tab/label_wrapper.html +1 -1
  48. nautobot/core/templates/components/tab/label_wrapper_distinct_view.html +10 -3
  49. nautobot/core/templates/generic/object_bulk_add_component.html +40 -40
  50. nautobot/core/templates/generic/object_bulk_create.html +3 -3
  51. nautobot/core/templates/generic/object_bulk_destroy.html +6 -6
  52. nautobot/core/templates/generic/object_bulk_update.html +52 -52
  53. nautobot/core/templates/generic/object_import.html +33 -33
  54. nautobot/core/templates/generic/object_list.html +267 -267
  55. nautobot/core/templates/generic/object_retrieve.html +261 -261
  56. nautobot/core/templates/graphene/graphiql.html +127 -127
  57. nautobot/core/templates/home.html +62 -62
  58. nautobot/core/templates/inc/computed_fields/panel_data.html +13 -13
  59. nautobot/core/templates/inc/created_updated.html +8 -8
  60. nautobot/core/templates/inc/custom_fields/panel_data.html +13 -13
  61. nautobot/core/templates/inc/dynamic_groups_panel.html +11 -11
  62. nautobot/core/templates/inc/footer.html +19 -19
  63. nautobot/core/templates/inc/javascript.html +1 -1
  64. nautobot/core/templates/inc/media.html +46 -46
  65. nautobot/core/templates/inc/nav_menu.html +1 -1
  66. nautobot/core/templates/inc/relationships_table_rows.html +22 -22
  67. nautobot/core/templates/inc/tenant_table_row.html +1 -1
  68. nautobot/core/templates/login.html +77 -77
  69. nautobot/core/templates/media_failure.html +38 -38
  70. nautobot/core/templates/panel_table.html +1 -1
  71. nautobot/core/templates/rest_framework/api.html +3 -3
  72. nautobot/core/templates/search.html +1 -1
  73. nautobot/core/templates/swagger_ui.html +9 -9
  74. nautobot/core/templates/utilities/confirmation_form.html +18 -18
  75. nautobot/core/templates/utilities/render_field.html +1 -1
  76. nautobot/core/templates/utilities/render_jinja2.html +43 -43
  77. nautobot/core/templates/utilities/templatetags/filter_form_modal.html +56 -56
  78. nautobot/core/templates/utilities/templatetags/utilization_graph.html +1 -1
  79. nautobot/core/templates/utilities/theme_preview.html +799 -799
  80. nautobot/core/templates/utilities/worker_status.html +122 -122
  81. nautobot/core/templates/widgets/clearable_file.html +3 -3
  82. nautobot/core/templates/widgets/sluginput.html +1 -1
  83. nautobot/core/templatetags/buttons.py +8 -2
  84. nautobot/core/templatetags/helpers.py +1 -1
  85. nautobot/core/testing/integration.py +7 -4
  86. nautobot/core/testing/views.py +24 -5
  87. nautobot/core/tests/test_breadcrumbs.py +78 -4
  88. nautobot/core/tests/test_commands.py +7 -4
  89. nautobot/core/tests/test_graphql.py +20 -5
  90. nautobot/core/tests/test_jobs.py +34 -21
  91. nautobot/core/tests/test_ui.py +175 -1
  92. nautobot/core/tests/test_utils.py +35 -0
  93. nautobot/core/tests/test_views_generic.py +43 -0
  94. nautobot/core/tests/test_views_utils.py +239 -5
  95. nautobot/core/ui/breadcrumbs.py +208 -21
  96. nautobot/core/ui/bulk_buttons.py +8 -0
  97. nautobot/core/ui/object_detail.py +138 -55
  98. nautobot/core/ui/titles.py +1 -0
  99. nautobot/core/utils/requests.py +27 -2
  100. nautobot/core/views/generic.py +41 -31
  101. nautobot/core/views/mixins.py +80 -110
  102. nautobot/core/views/utils.py +153 -1
  103. nautobot/dcim/api/serializers.py +8 -2
  104. nautobot/dcim/constants.py +1 -0
  105. nautobot/dcim/factory.py +4 -3
  106. nautobot/dcim/filters/mixins.py +1 -2
  107. nautobot/dcim/forms.py +5 -1
  108. nautobot/dcim/migrations/0074_alter_rack_u_height.py +21 -0
  109. nautobot/dcim/models/devices.py +26 -1
  110. nautobot/dcim/models/racks.py +2 -2
  111. nautobot/dcim/templates/dcim/cable.html +53 -53
  112. nautobot/dcim/templates/dcim/cable_connect.html +182 -182
  113. nautobot/dcim/templates/dcim/cable_trace.html +1 -1
  114. nautobot/dcim/templates/dcim/console_port_connection_list.html +5 -5
  115. nautobot/dcim/templates/dcim/consoleport.html +86 -86
  116. nautobot/dcim/templates/dcim/consoleserverport.html +86 -86
  117. nautobot/dcim/templates/dcim/controller_create.html +34 -34
  118. nautobot/dcim/templates/dcim/controllermanageddevicegroup_create.html +68 -68
  119. nautobot/dcim/templates/dcim/device/base.html +1 -104
  120. nautobot/dcim/templates/dcim/device/config.html +17 -17
  121. nautobot/dcim/templates/dcim/device/consoleports.html +1 -52
  122. nautobot/dcim/templates/dcim/device/consoleserverports.html +1 -52
  123. nautobot/dcim/templates/dcim/device/devicebays.html +1 -48
  124. nautobot/dcim/templates/dcim/device/frontports.html +1 -52
  125. nautobot/dcim/templates/dcim/device/interfaces.html +1 -56
  126. nautobot/dcim/templates/dcim/device/inventory.html +1 -48
  127. nautobot/dcim/templates/dcim/device/lldp_neighbors.html +64 -64
  128. nautobot/dcim/templates/dcim/device/modulebays.html +1 -48
  129. nautobot/dcim/templates/dcim/device/poweroutlets.html +1 -52
  130. nautobot/dcim/templates/dcim/device/powerports.html +1 -52
  131. nautobot/dcim/templates/dcim/device/rearports.html +1 -52
  132. nautobot/dcim/templates/dcim/device/status.html +66 -66
  133. nautobot/dcim/templates/dcim/device/wireless.html +1 -72
  134. nautobot/dcim/templates/dcim/device.html +4 -422
  135. nautobot/dcim/templates/dcim/device_component_add.html +25 -25
  136. nautobot/dcim/templates/dcim/device_create.html +229 -0
  137. nautobot/dcim/templates/dcim/device_edit.html +2 -227
  138. nautobot/dcim/templates/dcim/devicebay.html +41 -41
  139. nautobot/dcim/templates/dcim/devicebay_populate.html +32 -32
  140. nautobot/dcim/templates/dcim/devicetype_component_add.html +28 -28
  141. nautobot/dcim/templates/dcim/devicetype_retrieve.html +1 -3
  142. nautobot/dcim/templates/dcim/frontport.html +84 -84
  143. nautobot/dcim/templates/dcim/inc/cable_toggle_buttons.html +1 -1
  144. nautobot/dcim/templates/dcim/inc/device_interface_filter.html +8 -0
  145. nautobot/dcim/templates/dcim/inc/device_napalm_tabs.html +1 -15
  146. nautobot/dcim/templates/dcim/inc/location_hierarchy.html +22 -22
  147. nautobot/dcim/templates/dcim/interface.html +206 -206
  148. nautobot/dcim/templates/dcim/interface_connection_list.html +5 -5
  149. nautobot/dcim/templates/dcim/interfaceredundancygroupassociation_create.html +6 -6
  150. nautobot/dcim/templates/dcim/inventoryitem.html +44 -44
  151. nautobot/dcim/templates/dcim/inventoryitem_add.html +32 -32
  152. nautobot/dcim/templates/dcim/inventoryitem_edit.html +22 -22
  153. nautobot/dcim/templates/dcim/location_migrate_data_to_contact.html +46 -46
  154. nautobot/dcim/templates/dcim/location_retrieve.html +1 -7
  155. nautobot/dcim/templates/dcim/locationtype.html +1 -6
  156. nautobot/dcim/templates/dcim/locationtype_retrieve.html +1 -7
  157. nautobot/dcim/templates/dcim/module/base.html +85 -85
  158. nautobot/dcim/templates/dcim/module_interfaces.html +1 -1
  159. nautobot/dcim/templates/dcim/module_modulebays.html +1 -1
  160. nautobot/dcim/templates/dcim/module_retrieve.html +52 -52
  161. nautobot/dcim/templates/dcim/module_update.html +61 -61
  162. nautobot/dcim/templates/dcim/modulebay_destroy.html +1 -1
  163. nautobot/dcim/templates/dcim/modulebay_retrieve.html +85 -85
  164. nautobot/dcim/templates/dcim/modulebay_update.html +33 -33
  165. nautobot/dcim/templates/dcim/modulefamily_retrieve.html +1 -1
  166. nautobot/dcim/templates/dcim/moduletype_retrieve.html +140 -144
  167. nautobot/dcim/templates/dcim/platform_create.html +38 -38
  168. nautobot/dcim/templates/dcim/power_port_connection_list.html +5 -5
  169. nautobot/dcim/templates/dcim/powerfeed_retrieve.html +1 -8
  170. nautobot/dcim/templates/dcim/poweroutlet.html +85 -85
  171. nautobot/dcim/templates/dcim/powerpanel_retrieve.html +1 -8
  172. nautobot/dcim/templates/dcim/powerport.html +91 -91
  173. nautobot/dcim/templates/dcim/rack_elevation_list.html +18 -18
  174. nautobot/dcim/templates/dcim/rack_retrieve.html +264 -274
  175. nautobot/dcim/templates/dcim/rackreservation_retrieve.html +0 -3
  176. nautobot/dcim/templates/dcim/rearport.html +78 -78
  177. nautobot/dcim/templates/dcim/virtualdevicecontext_retrieve.html +1 -5
  178. nautobot/dcim/tests/integration/test_device_bulk_operations.py +3 -2
  179. nautobot/dcim/tests/integration/test_location_bulk_operations.py +6 -2
  180. nautobot/dcim/tests/test_api.py +33 -1
  181. nautobot/dcim/tests/test_views.py +148 -4
  182. nautobot/dcim/ui.py +29 -0
  183. nautobot/dcim/urls.py +1 -109
  184. nautobot/dcim/utils.py +30 -0
  185. nautobot/dcim/views.py +1009 -531
  186. nautobot/extras/tables.py +20 -0
  187. nautobot/extras/templates/extras/computedfield_edit.html +4 -4
  188. nautobot/extras/templates/extras/configcontext_update.html +1 -1
  189. nautobot/extras/templates/extras/configcontextschema_retrieve.html +32 -32
  190. nautobot/extras/templates/extras/customfield_update.html +23 -23
  191. nautobot/extras/templates/extras/dynamicgroup_retrieve.html +38 -38
  192. nautobot/extras/templates/extras/dynamicgroup_update.html +62 -62
  193. nautobot/extras/templates/extras/gitrepository_list.html +10 -10
  194. nautobot/extras/templates/extras/gitrepository_retrieve.html +68 -68
  195. nautobot/extras/templates/extras/graphqlquery_retrieve.html +73 -73
  196. nautobot/extras/templates/extras/inc/configcontext_format.html +2 -2
  197. nautobot/extras/templates/extras/inc/job_table.html +10 -10
  198. nautobot/extras/templates/extras/inc/jobresult.html +21 -21
  199. nautobot/extras/templates/extras/inc/jobresult_js.html +6 -6
  200. nautobot/extras/templates/extras/inc/tags_panel.html +10 -10
  201. nautobot/extras/templates/extras/job.html +64 -64
  202. nautobot/extras/templates/extras/job_approval_request.html +9 -9
  203. nautobot/extras/templates/extras/job_bulk_edit.html +13 -13
  204. nautobot/extras/templates/extras/job_edit.html +45 -45
  205. nautobot/extras/templates/extras/job_list.html +4 -4
  206. nautobot/extras/templates/extras/jobresult_retrieve.html +0 -25
  207. nautobot/extras/templates/extras/marketplace.html +101 -101
  208. nautobot/extras/templates/extras/metadatatype_create.html +20 -20
  209. nautobot/extras/templates/extras/object_assign_contact_or_team.html +18 -18
  210. nautobot/extras/templates/extras/object_configcontext.html +1 -3
  211. nautobot/extras/templates/extras/objectchange.html +2 -165
  212. nautobot/extras/templates/extras/objectchange_retrieve.html +165 -0
  213. nautobot/extras/templates/extras/plugin_detail.html +41 -41
  214. nautobot/extras/templates/extras/plugins_list.html +9 -9
  215. nautobot/extras/templates/extras/plugins_tiles.html +26 -26
  216. nautobot/extras/templates/extras/relationship_edit.html +4 -4
  217. nautobot/extras/templates/extras/role_retrieve.html +13 -13
  218. nautobot/extras/templates/extras/scheduled_jobs_approval_queue_list.html +21 -21
  219. nautobot/extras/templates/extras/scheduledjob.html +128 -128
  220. nautobot/extras/templates/extras/secret_create.html +53 -53
  221. nautobot/extras/templates/extras/secretsgroup_update.html +13 -13
  222. nautobot/extras/templates/extras/templatetags/plugin_object_detail_tabs.html +3 -3
  223. nautobot/extras/templates/extras/webhook.html +79 -79
  224. nautobot/extras/tests/integration/test_relationships.py +6 -6
  225. nautobot/extras/tests/test_filters.py +1 -1
  226. nautobot/extras/tests/test_jobs.py +2 -0
  227. nautobot/extras/tests/test_views.py +3 -3
  228. nautobot/extras/urls.py +1 -3
  229. nautobot/extras/views.py +101 -27
  230. nautobot/ipam/filters.py +2 -2
  231. nautobot/ipam/migrations/0053_alter_vrfdeviceassignment_options_and_more.py +20 -0
  232. nautobot/ipam/models.py +34 -0
  233. nautobot/ipam/tables.py +3 -1
  234. nautobot/ipam/templates/ipam/inc/prefix_header_extra_content_table.html +4 -0
  235. nautobot/ipam/templates/ipam/inc/toggle_available.html +8 -8
  236. nautobot/ipam/templates/ipam/inc/vlangroup_header.html +4 -4
  237. nautobot/ipam/templates/ipam/ipaddress.html +119 -123
  238. nautobot/ipam/templates/ipam/ipaddress_assign.html +10 -10
  239. nautobot/ipam/templates/ipam/ipaddress_edit.html +1 -1
  240. nautobot/ipam/templates/ipam/ipaddress_merge.html +180 -180
  241. nautobot/ipam/templates/ipam/ipaddresstointerface_retrieve.html +48 -48
  242. nautobot/ipam/templates/ipam/prefix.html +2 -107
  243. nautobot/ipam/templates/ipam/prefix_create.html +34 -0
  244. nautobot/ipam/templates/ipam/prefix_edit.html +1 -34
  245. nautobot/ipam/templates/ipam/prefix_retrieve.html +3 -0
  246. nautobot/ipam/templates/ipam/service_retrieve.html +1 -6
  247. nautobot/ipam/templates/ipam/vlan_retrieve.html +1 -7
  248. nautobot/ipam/templates/ipam/vrf_edit.html +1 -1
  249. nautobot/ipam/tests/test_views.py +34 -0
  250. nautobot/ipam/ui.py +145 -0
  251. nautobot/ipam/urls.py +1 -46
  252. nautobot/ipam/utils/__init__.py +26 -0
  253. nautobot/ipam/views.py +219 -106
  254. nautobot/project-static/docs/404.html +11 -11
  255. nautobot/project-static/docs/apps/index.html +11 -11
  256. nautobot/project-static/docs/apps/nautobot-apps.html +11 -11
  257. nautobot/project-static/docs/assets/javascripts/{bundle.92b07e13.min.js → bundle.f55a23d4.min.js} +2 -2
  258. nautobot/project-static/docs/assets/javascripts/{bundle.92b07e13.min.js.map → bundle.f55a23d4.min.js.map} +2 -2
  259. nautobot/project-static/docs/assets/stylesheets/{main.7e37652d.min.css → main.e53b48f4.min.css} +1 -1
  260. nautobot/project-static/docs/assets/stylesheets/{main.7e37652d.min.css.map → main.e53b48f4.min.css.map} +1 -1
  261. nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +11 -11
  262. nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +11 -11
  263. nautobot/project-static/docs/code-reference/nautobot/apps/api.html +11 -11
  264. nautobot/project-static/docs/code-reference/nautobot/apps/change_logging.html +11 -11
  265. nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +11 -11
  266. nautobot/project-static/docs/code-reference/nautobot/apps/config.html +11 -11
  267. nautobot/project-static/docs/code-reference/nautobot/apps/constants.html +11 -11
  268. nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +11 -11
  269. nautobot/project-static/docs/code-reference/nautobot/apps/events.html +11 -11
  270. nautobot/project-static/docs/code-reference/nautobot/apps/exceptions.html +11 -11
  271. nautobot/project-static/docs/code-reference/nautobot/apps/factory.html +11 -11
  272. nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +11 -11
  273. nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +11 -11
  274. nautobot/project-static/docs/code-reference/nautobot/apps/graphql.html +11 -11
  275. nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +11 -11
  276. nautobot/project-static/docs/code-reference/nautobot/apps/models.html +11 -11
  277. nautobot/project-static/docs/code-reference/nautobot/apps/querysets.html +11 -11
  278. nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +11 -11
  279. nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +11 -11
  280. nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +11 -11
  281. nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +2174 -1226
  282. nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +11 -11
  283. nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +12 -12
  284. nautobot/project-static/docs/code-reference/nautobot/apps/views.html +72 -27
  285. nautobot/project-static/docs/development/apps/api/configuration-view.html +11 -11
  286. nautobot/project-static/docs/development/apps/api/database-backend-config.html +11 -11
  287. nautobot/project-static/docs/development/apps/api/models/django-admin.html +11 -11
  288. nautobot/project-static/docs/development/apps/api/models/global-search.html +11 -11
  289. nautobot/project-static/docs/development/apps/api/models/graphql.html +11 -11
  290. nautobot/project-static/docs/development/apps/api/models/index.html +11 -11
  291. nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +11 -11
  292. nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +11 -11
  293. nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +11 -11
  294. nautobot/project-static/docs/development/apps/api/platform-features/git-repository-content.html +11 -11
  295. nautobot/project-static/docs/development/apps/api/platform-features/index.html +11 -11
  296. nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +11 -11
  297. nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +11 -11
  298. nautobot/project-static/docs/development/apps/api/platform-features/prepopulating-data.html +11 -11
  299. nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +11 -11
  300. nautobot/project-static/docs/development/apps/api/platform-features/table-extensions.html +11 -11
  301. nautobot/project-static/docs/development/apps/api/platform-features/uniquely-identify-objects.html +11 -11
  302. nautobot/project-static/docs/development/apps/api/prometheus.html +11 -11
  303. nautobot/project-static/docs/development/apps/api/setup.html +11 -11
  304. nautobot/project-static/docs/development/apps/api/testing.html +11 -11
  305. nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +11 -11
  306. nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +11 -11
  307. nautobot/project-static/docs/development/apps/api/ui-extensions/index.html +11 -11
  308. nautobot/project-static/docs/development/apps/api/ui-extensions/navigation.html +11 -11
  309. nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +11 -11
  310. nautobot/project-static/docs/development/apps/api/views/base-template.html +11 -11
  311. nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +11 -11
  312. nautobot/project-static/docs/development/apps/api/views/django-generic-views.html +11 -11
  313. nautobot/project-static/docs/development/apps/api/views/help-documentation.html +11 -11
  314. nautobot/project-static/docs/development/apps/api/views/index.html +11 -11
  315. nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +11 -11
  316. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +11 -11
  317. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +11 -11
  318. nautobot/project-static/docs/development/apps/api/views/notes.html +11 -11
  319. nautobot/project-static/docs/development/apps/api/views/rest-api.html +11 -11
  320. nautobot/project-static/docs/development/apps/api/views/urls.html +11 -11
  321. nautobot/project-static/docs/development/apps/index.html +11 -11
  322. nautobot/project-static/docs/development/apps/migration/code-updates.html +11 -11
  323. nautobot/project-static/docs/development/apps/migration/dependency-updates.html +11 -11
  324. nautobot/project-static/docs/development/apps/migration/from-v1.html +11 -11
  325. nautobot/project-static/docs/development/apps/migration/model-updates/dcim.html +11 -11
  326. nautobot/project-static/docs/development/apps/migration/model-updates/extras.html +11 -11
  327. nautobot/project-static/docs/development/apps/migration/model-updates/global.html +11 -11
  328. nautobot/project-static/docs/development/apps/migration/model-updates/ipam.html +11 -11
  329. nautobot/project-static/docs/development/apps/migration/ui-component-framework/best-practices.html +11 -11
  330. nautobot/project-static/docs/development/apps/migration/ui-component-framework/breadcrumbs-titles.html +11 -11
  331. nautobot/project-static/docs/development/apps/migration/ui-component-framework/custom-content.html +11 -11
  332. nautobot/project-static/docs/development/apps/migration/ui-component-framework/index.html +11 -11
  333. nautobot/project-static/docs/development/apps/migration/ui-component-framework/migration-steps.html +11 -11
  334. nautobot/project-static/docs/development/apps/porting-from-netbox.html +11 -11
  335. nautobot/project-static/docs/development/core/application-registry.html +11 -11
  336. nautobot/project-static/docs/development/core/best-practices.html +11 -11
  337. nautobot/project-static/docs/development/core/bootstrap-ui.html +11 -11
  338. nautobot/project-static/docs/development/core/caching.html +11 -11
  339. nautobot/project-static/docs/development/core/controllers.html +11 -11
  340. nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +11 -11
  341. nautobot/project-static/docs/development/core/generic-views.html +11 -11
  342. nautobot/project-static/docs/development/core/getting-started.html +50 -48
  343. nautobot/project-static/docs/development/core/homepage.html +11 -11
  344. nautobot/project-static/docs/development/core/index.html +11 -11
  345. nautobot/project-static/docs/development/core/minikube-dev-environment-for-k8s-jobs.html +11 -11
  346. nautobot/project-static/docs/development/core/model-checklist.html +11 -11
  347. nautobot/project-static/docs/development/core/model-features.html +11 -11
  348. nautobot/project-static/docs/development/core/natural-keys.html +11 -11
  349. nautobot/project-static/docs/development/core/navigation-menu.html +11 -11
  350. nautobot/project-static/docs/development/core/release-checklist.html +11 -11
  351. nautobot/project-static/docs/development/core/role-internals.html +11 -11
  352. nautobot/project-static/docs/development/core/settings.html +11 -11
  353. nautobot/project-static/docs/development/core/style-guide.html +15 -11
  354. nautobot/project-static/docs/development/core/templates.html +11 -11
  355. nautobot/project-static/docs/development/core/testing.html +11 -11
  356. nautobot/project-static/docs/development/core/ui-component-framework.html +11 -11
  357. nautobot/project-static/docs/development/core/user-preferences.html +11 -11
  358. nautobot/project-static/docs/development/index.html +11 -11
  359. nautobot/project-static/docs/development/jobs/getting-started.html +11 -11
  360. nautobot/project-static/docs/development/jobs/index.html +11 -11
  361. nautobot/project-static/docs/development/jobs/installation.html +11 -11
  362. nautobot/project-static/docs/development/jobs/job-extensions.html +11 -11
  363. nautobot/project-static/docs/development/jobs/job-logging.html +11 -11
  364. nautobot/project-static/docs/development/jobs/job-patterns.html +11 -11
  365. nautobot/project-static/docs/development/jobs/job-structure.html +11 -11
  366. nautobot/project-static/docs/development/jobs/migration/from-v1.html +11 -11
  367. nautobot/project-static/docs/development/jobs/testing.html +11 -11
  368. nautobot/project-static/docs/index.html +11 -11
  369. nautobot/project-static/docs/objects.inv +0 -0
  370. nautobot/project-static/docs/overview/application_stack.html +11 -11
  371. nautobot/project-static/docs/overview/design_philosophy.html +11 -11
  372. nautobot/project-static/docs/release-notes/index.html +11 -11
  373. nautobot/project-static/docs/release-notes/version-1.0.html +11 -11
  374. nautobot/project-static/docs/release-notes/version-1.1.html +11 -11
  375. nautobot/project-static/docs/release-notes/version-1.2.html +11 -11
  376. nautobot/project-static/docs/release-notes/version-1.3.html +11 -11
  377. nautobot/project-static/docs/release-notes/version-1.4.html +11 -11
  378. nautobot/project-static/docs/release-notes/version-1.5.html +11 -11
  379. nautobot/project-static/docs/release-notes/version-1.6.html +11 -11
  380. nautobot/project-static/docs/release-notes/version-2.0.html +11 -11
  381. nautobot/project-static/docs/release-notes/version-2.1.html +11 -11
  382. nautobot/project-static/docs/release-notes/version-2.2.html +11 -11
  383. nautobot/project-static/docs/release-notes/version-2.3.html +11 -11
  384. nautobot/project-static/docs/release-notes/version-2.4.html +196 -11
  385. nautobot/project-static/docs/search/search_index.json +1 -1
  386. nautobot/project-static/docs/sitemap.xml +300 -300
  387. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  388. nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +11 -11
  389. nautobot/project-static/docs/user-guide/administration/configuration/authentication/remote.html +11 -11
  390. nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +11 -11
  391. nautobot/project-static/docs/user-guide/administration/configuration/index.html +11 -11
  392. nautobot/project-static/docs/user-guide/administration/configuration/redis.html +11 -11
  393. nautobot/project-static/docs/user-guide/administration/configuration/settings.html +11 -11
  394. nautobot/project-static/docs/user-guide/administration/configuration/time-zones.html +11 -11
  395. nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +89 -14
  396. nautobot/project-static/docs/user-guide/administration/guides/docker.html +11 -11
  397. nautobot/project-static/docs/user-guide/administration/guides/health-checks.html +11 -11
  398. nautobot/project-static/docs/user-guide/administration/guides/permissions.html +11 -11
  399. nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +11 -11
  400. nautobot/project-static/docs/user-guide/administration/guides/replicating-nautobot.html +11 -11
  401. nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +11 -11
  402. nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +11 -11
  403. nautobot/project-static/docs/user-guide/administration/guides/selinux-troubleshooting.html +11 -11
  404. nautobot/project-static/docs/user-guide/administration/installation/app-install.html +11 -11
  405. nautobot/project-static/docs/user-guide/administration/installation/external-authentication.html +11 -11
  406. nautobot/project-static/docs/user-guide/administration/installation/http-server.html +11 -11
  407. nautobot/project-static/docs/user-guide/administration/installation/index.html +11 -11
  408. nautobot/project-static/docs/user-guide/administration/installation/install_system.html +11 -11
  409. nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +11 -11
  410. nautobot/project-static/docs/user-guide/administration/installation/services.html +11 -11
  411. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-netbox.html +11 -11
  412. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +11 -11
  413. nautobot/project-static/docs/user-guide/administration/security/index.html +11 -11
  414. nautobot/project-static/docs/user-guide/administration/security/notices.html +11 -11
  415. nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +11 -11
  416. nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +11 -11
  417. nautobot/project-static/docs/user-guide/administration/upgrading/database-backup.html +11 -11
  418. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/after-you-upgrade.html +11 -11
  419. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/before-you-upgrade.html +11 -11
  420. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/for-developers.html +11 -11
  421. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/index.html +11 -11
  422. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/whats-changed.html +11 -11
  423. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/region-and-site-data-migration-guide.html +11 -11
  424. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +11 -11
  425. nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +11 -11
  426. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuit.html +11 -11
  427. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittermination.html +11 -11
  428. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittype.html +11 -11
  429. nautobot/project-static/docs/user-guide/core-data-model/circuits/provider.html +11 -11
  430. nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +11 -11
  431. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloud.html +11 -11
  432. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudaccount.html +11 -11
  433. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetwork.html +11 -11
  434. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetworkprefixassignment.html +11 -11
  435. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudresourcetype.html +11 -11
  436. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservice.html +11 -11
  437. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservicenetworkassignment.html +11 -11
  438. nautobot/project-static/docs/user-guide/core-data-model/dcim/cable.html +11 -11
  439. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +11 -11
  440. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +11 -11
  441. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +11 -11
  442. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +11 -11
  443. nautobot/project-static/docs/user-guide/core-data-model/dcim/controller.html +11 -11
  444. nautobot/project-static/docs/user-guide/core-data-model/dcim/controllermanageddevicegroup.html +11 -11
  445. nautobot/project-static/docs/user-guide/core-data-model/dcim/device.html +11 -11
  446. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +11 -11
  447. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +11 -11
  448. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicefamily.html +11 -11
  449. nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +11 -11
  450. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +11 -11
  451. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +11 -11
  452. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +11 -11
  453. nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +11 -11
  454. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +11 -11
  455. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +11 -11
  456. nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +11 -11
  457. nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +11 -11
  458. nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +11 -11
  459. nautobot/project-static/docs/user-guide/core-data-model/dcim/manufacturer.html +11 -11
  460. nautobot/project-static/docs/user-guide/core-data-model/dcim/module.html +11 -11
  461. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebay.html +11 -11
  462. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebaytemplate.html +11 -11
  463. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulefamily.html +11 -11
  464. nautobot/project-static/docs/user-guide/core-data-model/dcim/moduletype.html +11 -11
  465. nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +11 -11
  466. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerfeed.html +11 -11
  467. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +11 -11
  468. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +11 -11
  469. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerpanel.html +11 -11
  470. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +11 -11
  471. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +11 -11
  472. nautobot/project-static/docs/user-guide/core-data-model/dcim/rack.html +11 -11
  473. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackgroup.html +11 -11
  474. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackreservation.html +11 -11
  475. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +11 -11
  476. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +11 -11
  477. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +11 -11
  478. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareversion.html +11 -11
  479. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualchassis.html +11 -11
  480. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualdevicecontext.html +11 -11
  481. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +11 -11
  482. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +11 -11
  483. nautobot/project-static/docs/user-guide/core-data-model/extras/contact.html +11 -11
  484. nautobot/project-static/docs/user-guide/core-data-model/extras/team.html +11 -11
  485. nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +11 -11
  486. nautobot/project-static/docs/user-guide/core-data-model/ipam/namespace.html +11 -11
  487. nautobot/project-static/docs/user-guide/core-data-model/ipam/prefix.html +11 -11
  488. nautobot/project-static/docs/user-guide/core-data-model/ipam/rir.html +11 -11
  489. nautobot/project-static/docs/user-guide/core-data-model/ipam/routetarget.html +11 -11
  490. nautobot/project-static/docs/user-guide/core-data-model/ipam/service.html +11 -11
  491. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +11 -11
  492. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlangroup.html +11 -11
  493. nautobot/project-static/docs/user-guide/core-data-model/ipam/vrf.html +11 -11
  494. nautobot/project-static/docs/user-guide/core-data-model/overview/introduction.html +11 -11
  495. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenant.html +11 -11
  496. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenantgroup.html +11 -11
  497. nautobot/project-static/docs/user-guide/core-data-model/virtualization/cluster.html +11 -11
  498. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustergroup.html +11 -11
  499. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustertype.html +11 -11
  500. nautobot/project-static/docs/user-guide/core-data-model/virtualization/virtualmachine.html +11 -11
  501. nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +11 -11
  502. nautobot/project-static/docs/user-guide/core-data-model/wireless/index.html +11 -11
  503. nautobot/project-static/docs/user-guide/core-data-model/wireless/radioprofile.html +11 -11
  504. nautobot/project-static/docs/user-guide/core-data-model/wireless/supporteddatarate.html +11 -11
  505. nautobot/project-static/docs/user-guide/core-data-model/wireless/wirelessnetwork.html +11 -11
  506. nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +11 -11
  507. nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +11 -11
  508. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +11 -11
  509. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +11 -11
  510. nautobot/project-static/docs/user-guide/feature-guides/getting-started/index.html +11 -11
  511. nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +11 -11
  512. nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +11 -11
  513. nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +11 -11
  514. nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +11 -11
  515. nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +11 -11
  516. nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +11 -11
  517. nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +11 -11
  518. nautobot/project-static/docs/user-guide/feature-guides/graphql.html +11 -11
  519. nautobot/project-static/docs/user-guide/feature-guides/ip-address-merge-tool.html +11 -11
  520. nautobot/project-static/docs/user-guide/feature-guides/relationships.html +11 -11
  521. nautobot/project-static/docs/user-guide/feature-guides/software-image-files-and-versions.html +11 -11
  522. nautobot/project-static/docs/user-guide/feature-guides/wireless-networks-and-controllers.html +11 -11
  523. nautobot/project-static/docs/user-guide/index.html +11 -11
  524. nautobot/project-static/docs/user-guide/platform-functionality/change-logging.html +11 -11
  525. nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +11 -11
  526. nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +11 -11
  527. nautobot/project-static/docs/user-guide/platform-functionality/customlink.html +11 -11
  528. nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +11 -11
  529. nautobot/project-static/docs/user-guide/platform-functionality/events.html +11 -11
  530. nautobot/project-static/docs/user-guide/platform-functionality/exporttemplate.html +11 -11
  531. nautobot/project-static/docs/user-guide/platform-functionality/externalintegration.html +11 -11
  532. nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +11 -11
  533. nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +11 -11
  534. nautobot/project-static/docs/user-guide/platform-functionality/graphqlquery.html +11 -11
  535. nautobot/project-static/docs/user-guide/platform-functionality/imageattachment.html +11 -11
  536. nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +11 -11
  537. nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +11 -11
  538. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +11 -11
  539. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +11 -11
  540. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobqueue.html +11 -11
  541. nautobot/project-static/docs/user-guide/platform-functionality/jobs/kubernetes-job-support.html +11 -11
  542. nautobot/project-static/docs/user-guide/platform-functionality/jobs/managing-jobs.html +11 -11
  543. nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +11 -11
  544. nautobot/project-static/docs/user-guide/platform-functionality/napalm.html +11 -11
  545. nautobot/project-static/docs/user-guide/platform-functionality/note.html +11 -11
  546. nautobot/project-static/docs/user-guide/platform-functionality/objectmetadata.html +11 -11
  547. nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +11 -11
  548. nautobot/project-static/docs/user-guide/platform-functionality/rendering-jinja-templates.html +11 -11
  549. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +11 -11
  550. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +11 -11
  551. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +11 -11
  552. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/ui-related-endpoints.html +11 -11
  553. nautobot/project-static/docs/user-guide/platform-functionality/role.html +11 -11
  554. nautobot/project-static/docs/user-guide/platform-functionality/savedview.html +11 -11
  555. nautobot/project-static/docs/user-guide/platform-functionality/secret.html +11 -11
  556. nautobot/project-static/docs/user-guide/platform-functionality/staticgroupassociation.html +11 -11
  557. nautobot/project-static/docs/user-guide/platform-functionality/status.html +11 -11
  558. nautobot/project-static/docs/user-guide/platform-functionality/tag.html +11 -11
  559. nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +11 -11
  560. nautobot/project-static/docs/user-guide/platform-functionality/users/objectpermission.html +11 -11
  561. nautobot/project-static/docs/user-guide/platform-functionality/users/token.html +11 -11
  562. nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +11 -11
  563. nautobot/tenancy/templates/tenancy/tenant.html +1 -7
  564. nautobot/tenancy/views.py +13 -0
  565. nautobot/users/templates/users/api_tokens.html +4 -4
  566. nautobot/users/templates/users/base.html +28 -28
  567. nautobot/virtualization/templates/virtualization/cluster.html +64 -64
  568. nautobot/virtualization/templates/virtualization/inc/virtualmachine_vminterface_filter.html +8 -0
  569. nautobot/virtualization/templates/virtualization/virtualmachine_component_add.html +25 -25
  570. nautobot/virtualization/templates/virtualization/virtualmachine_retrieve.html +1 -251
  571. nautobot/virtualization/templates/virtualization/vminterface.html +70 -70
  572. nautobot/virtualization/urls.py +0 -12
  573. nautobot/virtualization/views.py +158 -54
  574. nautobot/wireless/templates/wireless/wirelessnetwork_create.html +13 -13
  575. nautobot/wireless/tests/integration/test_radio_profile.py +1 -1
  576. {nautobot-2.4.18.dist-info → nautobot-2.4.19.dist-info}/METADATA +1 -1
  577. {nautobot-2.4.18.dist-info → nautobot-2.4.19.dist-info}/RECORD +581 -570
  578. nautobot/ipam/templates/ipam/prefix_ipaddresses.html +0 -11
  579. nautobot/ipam/templates/ipam/prefix_prefixes.html +0 -11
  580. {nautobot-2.4.18.dist-info → nautobot-2.4.19.dist-info}/LICENSE.txt +0 -0
  581. {nautobot-2.4.18.dist-info → nautobot-2.4.19.dist-info}/NOTICE +0 -0
  582. {nautobot-2.4.18.dist-info → nautobot-2.4.19.dist-info}/WHEEL +0 -0
  583. {nautobot-2.4.18.dist-info → nautobot-2.4.19.dist-info}/entry_points.txt +0 -0
nautobot/dcim/views.py CHANGED
@@ -16,11 +16,11 @@ from django.forms import (
16
16
  MultipleHiddenInput,
17
17
  )
18
18
  from django.shortcuts import get_object_or_404, HttpResponse, redirect, render
19
+ from django.template import Context
19
20
  from django.template.loader import render_to_string
20
21
  from django.urls import reverse
21
22
  from django.utils.encoding import iri_to_uri
22
- from django.utils.functional import cached_property
23
- from django.utils.html import format_html
23
+ from django.utils.html import format_html, mark_safe
24
24
  from django.utils.http import url_has_allowed_host_and_scheme, urlencode
25
25
  from django.views.generic import View
26
26
  from django_tables2 import RequestConfig
@@ -38,14 +38,18 @@ from nautobot.core.templatetags import helpers
38
38
  from nautobot.core.templatetags.helpers import has_perms
39
39
  from nautobot.core.ui import object_detail
40
40
  from nautobot.core.ui.breadcrumbs import (
41
+ AncestorsBreadcrumbs,
41
42
  BaseBreadcrumbItem,
42
43
  Breadcrumbs,
44
+ context_object_attr,
43
45
  InstanceBreadcrumbItem,
46
+ InstanceParentBreadcrumbItem,
44
47
  ModelBreadcrumbItem,
45
48
  ViewNameBreadcrumbItem,
46
49
  )
47
50
  from nautobot.core.ui.bulk_buttons import (
48
51
  BulkDeleteButton,
52
+ BulkDisconnectButton,
49
53
  BulkEditButton,
50
54
  BulkRenameButton,
51
55
  )
@@ -68,23 +72,26 @@ from nautobot.core.views.mixins import (
68
72
  ObjectPermissionRequiredMixin,
69
73
  )
70
74
  from nautobot.core.views.paginator import EnhancedPaginator, get_paginate_count
71
- from nautobot.core.views.utils import get_obj_from_context
75
+ from nautobot.core.views.utils import common_detail_view_context, get_obj_from_context
72
76
  from nautobot.core.views.viewsets import NautobotUIViewSet
73
77
  from nautobot.dcim.choices import LocationDataToContactActionChoices
74
78
  from nautobot.dcim.forms import LocationMigrateDataToContactForm
75
- from nautobot.dcim.utils import get_all_network_driver_mappings
76
- from nautobot.extras.models import Contact, ContactAssociation, Role, Status, Team
77
- from nautobot.extras.tables import DynamicGroupTable
78
- from nautobot.extras.views import ObjectChangeLogView, ObjectConfigContextView, ObjectDynamicGroupsView
79
- from nautobot.ipam.models import IPAddress, Prefix, Service, VLAN
80
- from nautobot.ipam.tables import InterfaceIPAddressTable, InterfaceVLANTable, VRFDeviceAssignmentTable, VRFTable
79
+ from nautobot.dcim.ui import RackBreadcrumbs
80
+ from nautobot.dcim.utils import get_all_network_driver_mappings, render_software_version_and_image_files
81
+ from nautobot.extras.models import ConfigContext, Contact, ContactAssociation, Role, Status, Team
82
+ from nautobot.extras.tables import DynamicGroupTable, ImageAttachmentTable
83
+ from nautobot.ipam.models import IPAddress, Prefix, VLAN
84
+ from nautobot.ipam.tables import (
85
+ InterfaceIPAddressTable,
86
+ InterfaceVLANTable,
87
+ ServiceTable,
88
+ VRFDeviceAssignmentTable,
89
+ VRFTable,
90
+ )
91
+ from nautobot.ipam.utils import render_ip_with_nat
81
92
  from nautobot.virtualization.models import VirtualMachine
82
93
  from nautobot.virtualization.tables import VirtualMachineTable
83
94
  from nautobot.wireless.forms import ControllerManagedDeviceGroupWirelessNetworkFormSet
84
- from nautobot.wireless.models import (
85
- ControllerManagedDeviceGroupRadioProfileAssignment,
86
- ControllerManagedDeviceGroupWirelessNetworkAssignment,
87
- )
88
95
  from nautobot.wireless.tables import (
89
96
  BaseControllerManagedDeviceGroupWirelessNetworkAssignmentTable,
90
97
  ControllerManagedDeviceGroupRadioProfileAssignmentTable,
@@ -240,6 +247,7 @@ class LocationTypeUIViewSet(NautobotUIViewSet):
240
247
  form_class = forms.LocationTypeForm
241
248
  bulk_update_form_class = forms.LocationTypeBulkEditForm
242
249
  serializer_class = serializers.LocationSerializer
250
+ breadcrumbs = AncestorsBreadcrumbs(detail_item_label=context_object_attr("name"))
243
251
 
244
252
  object_detail_content = object_detail.ObjectDetailContent(
245
253
  panels=(
@@ -283,28 +291,36 @@ class LocationUIViewSet(NautobotUIViewSet):
283
291
  form_class = forms.LocationForm
284
292
  bulk_update_form_class = forms.LocationBulkEditForm
285
293
  serializer_class = serializers.LocationSerializer
294
+ breadcrumbs = AncestorsBreadcrumbs(detail_item_label=context_object_attr("name"))
286
295
 
287
296
  def get_extra_context(self, request, instance):
288
297
  if instance is None:
289
298
  return super().get_extra_context(request, instance)
290
- related_locations = (
299
+ # This query can get really expensive when there are big location trees in the DB. By casting it to a list we
300
+ # ensure it is only performed once rather than as a subquery for each of the different count stats.
301
+ related_locations = list(
291
302
  instance.descendants(include_self=True).restrict(request.user, "view").values_list("pk", flat=True)
292
303
  )
304
+ prefix_count_queryset = Prefix.objects.restrict(request.user, "view").filter(locations__in=related_locations)
305
+ vlan_count_queryset = VLAN.objects.restrict(request.user, "view").filter(locations__in=related_locations)
306
+ circuit_count_queryset = Circuit.objects.restrict(request.user, "view").filter(
307
+ circuit_terminations__location__in=related_locations
308
+ )
309
+ # When there is more than one location, the models that can be assigned to more then one location at the same
310
+ # time need to be queried with `distinct`. We are avoiding `distinct` when this is not the case, as it incurs
311
+ # a performance penalty.
312
+ if len(related_locations) > 1:
313
+ prefix_count_queryset = prefix_count_queryset.distinct()
314
+ vlan_count_queryset = vlan_count_queryset.distinct()
315
+ circuit_count_queryset = circuit_count_queryset.distinct()
293
316
  stats = {
317
+ "prefix_count": prefix_count_queryset.count(),
318
+ "vlan_count": vlan_count_queryset.count(),
319
+ "circuit_count": circuit_count_queryset.count(),
294
320
  "rack_count": Rack.objects.restrict(request.user, "view").filter(location__in=related_locations).count(),
295
321
  "device_count": Device.objects.restrict(request.user, "view")
296
322
  .filter(location__in=related_locations)
297
323
  .count(),
298
- "prefix_count": Prefix.objects.restrict(request.user, "view")
299
- .filter(locations__in=related_locations)
300
- .count(),
301
- "vlan_count": VLAN.objects.restrict(request.user, "view")
302
- .filter(locations__in=related_locations)
303
- .distinct()
304
- .count(),
305
- "circuit_count": Circuit.objects.restrict(request.user, "view")
306
- .filter(circuit_terminations__location__in=related_locations)
307
- .count(),
308
324
  "vm_count": VirtualMachine.objects.restrict(request.user, "view")
309
325
  .filter(cluster__location__in=related_locations)
310
326
  .count(),
@@ -549,6 +565,7 @@ class RackUIViewSet(NautobotUIViewSet):
549
565
  serializer_class = serializers.RackSerializer
550
566
  table_class = tables.RackDetailTable
551
567
  queryset = Rack.objects.select_related("location", "tenant__tenant_group", "rack_group", "role")
568
+ breadcrumbs = RackBreadcrumbs()
552
569
 
553
570
  def get_extra_context(self, request, instance):
554
571
  context = super().get_extra_context(request, instance)
@@ -648,6 +665,14 @@ class RackReservationUIViewSet(NautobotUIViewSet):
648
665
  serializer_class = serializers.RackReservationSerializer
649
666
  table_class = tables.RackReservationTable
650
667
  queryset = RackReservation.objects.all()
668
+ breadcrumbs = Breadcrumbs(
669
+ items={
670
+ "detail": [
671
+ ModelBreadcrumbItem(),
672
+ InstanceBreadcrumbItem(instance=context_object_attr("rack")),
673
+ ]
674
+ }
675
+ )
651
676
 
652
677
  object_detail_content = object_detail.ObjectDetailContent(
653
678
  panels=(
@@ -767,6 +792,15 @@ def bulk_footer_buttons(form_id: str, model):
767
792
  ]
768
793
 
769
794
 
795
+ def bulk_cable_termination_footer_buttons(form_id: str, model):
796
+ return [
797
+ BulkRenameButton(form_id=form_id, model=model),
798
+ BulkEditButton(form_id=form_id, model=model),
799
+ BulkDisconnectButton(form_id=form_id, model=model),
800
+ BulkDeleteButton(form_id=form_id, model=model),
801
+ ]
802
+
803
+
770
804
  # --- Tab Configuration ---
771
805
  TAB_CONFIGS = [
772
806
  (
@@ -882,6 +916,7 @@ def make_bulk_tab(weight, tab_name, label, url_name, related_attr, table_class,
882
916
  label=label,
883
917
  url_name=url_name,
884
918
  related_object_attribute=related_attr,
919
+ hide_if_empty=True,
885
920
  panels=(
886
921
  object_detail.ObjectsTablePanel(
887
922
  section=SectionChoices.FULL_WIDTH,
@@ -908,6 +943,14 @@ class DeviceTypeUIViewSet(NautobotUIViewSet):
908
943
  serializer_class = serializers.DeviceTypeSerializer
909
944
  table_class = tables.DeviceTypeTable
910
945
  queryset = DeviceType.objects.select_related("manufacturer").prefetch_related("software_image_files")
946
+ breadcrumbs = Breadcrumbs(
947
+ items={
948
+ "detail": [
949
+ ModelBreadcrumbItem(),
950
+ InstanceParentBreadcrumbItem(parent_key="manufacturer", parent_lookup_key="name"),
951
+ ]
952
+ }
953
+ )
911
954
 
912
955
  object_detail_content = object_detail.ObjectDetailContent(
913
956
  panels=(
@@ -921,6 +964,7 @@ class DeviceTypeUIViewSet(NautobotUIViewSet):
921
964
  weight=200,
922
965
  table_class=tables.SoftwareImageFileTable,
923
966
  table_filter="device_types",
967
+ order_by_fields=["image_file_name"],
924
968
  select_related_fields=["software_version", "status"],
925
969
  exclude_columns=["actions", "tags"],
926
970
  related_field_name="device_types",
@@ -974,7 +1018,7 @@ class DeviceTypeUIViewSet(NautobotUIViewSet):
974
1018
  @action(
975
1019
  detail=True,
976
1020
  methods=["get"],
977
- url_path="frontports",
1021
+ url_path="front-ports",
978
1022
  custom_view_base_action="view",
979
1023
  custom_view_additional_permissions=["dcim.view_frontporttemplate"],
980
1024
  )
@@ -984,7 +1028,7 @@ class DeviceTypeUIViewSet(NautobotUIViewSet):
984
1028
  @action(
985
1029
  detail=True,
986
1030
  methods=["get"],
987
- url_path="rearports",
1031
+ url_path="rear-ports",
988
1032
  custom_view_base_action="view",
989
1033
  custom_view_additional_permissions=["dcim.view_rearporttemplate"],
990
1034
  )
@@ -994,7 +1038,7 @@ class DeviceTypeUIViewSet(NautobotUIViewSet):
994
1038
  @action(
995
1039
  detail=True,
996
1040
  methods=["get"],
997
- url_path="consoleports",
1041
+ url_path="console-ports",
998
1042
  custom_view_base_action="view",
999
1043
  custom_view_additional_permissions=["dcim.view_consoleporttemplate"],
1000
1044
  )
@@ -1004,7 +1048,7 @@ class DeviceTypeUIViewSet(NautobotUIViewSet):
1004
1048
  @action(
1005
1049
  detail=True,
1006
1050
  methods=["get"],
1007
- url_path="consoleserverports",
1051
+ url_path="console-server-ports",
1008
1052
  custom_view_base_action="view",
1009
1053
  custom_view_additional_permissions=["dcim.view_consoleserverporttemplate"],
1010
1054
  )
@@ -1014,7 +1058,7 @@ class DeviceTypeUIViewSet(NautobotUIViewSet):
1014
1058
  @action(
1015
1059
  detail=True,
1016
1060
  methods=["get"],
1017
- url_path="powerports",
1061
+ url_path="power-ports",
1018
1062
  custom_view_base_action="view",
1019
1063
  custom_view_additional_permissions=["dcim.view_powerporttemplate"],
1020
1064
  )
@@ -1024,7 +1068,7 @@ class DeviceTypeUIViewSet(NautobotUIViewSet):
1024
1068
  @action(
1025
1069
  detail=True,
1026
1070
  methods=["get"],
1027
- url_path="poweroutlets",
1071
+ url_path="power-outlets",
1028
1072
  custom_view_base_action="view",
1029
1073
  custom_view_additional_permissions=["dcim.view_poweroutlettemplate"],
1030
1074
  )
@@ -1034,7 +1078,7 @@ class DeviceTypeUIViewSet(NautobotUIViewSet):
1034
1078
  @action(
1035
1079
  detail=True,
1036
1080
  methods=["get"],
1037
- url_path="devicebays",
1081
+ url_path="device-bays",
1038
1082
  custom_view_base_action="view",
1039
1083
  custom_view_additional_permissions=["dcim.view_devicebaytemplate"],
1040
1084
  )
@@ -1044,7 +1088,7 @@ class DeviceTypeUIViewSet(NautobotUIViewSet):
1044
1088
  @action(
1045
1089
  detail=True,
1046
1090
  methods=["get"],
1047
- url_path="modulebays",
1091
+ url_path="module-bays",
1048
1092
  custom_view_base_action="view",
1049
1093
  custom_view_additional_permissions=["dcim.view_modulebaytemplate"],
1050
1094
  )
@@ -1115,6 +1159,14 @@ class ModuleTypeUIViewSet(
1115
1159
  "front-ports": forms.FrontPortTemplateImportForm,
1116
1160
  "module-bays": forms.ModuleBayTemplateImportForm,
1117
1161
  }
1162
+ breadcrumbs = Breadcrumbs(
1163
+ items={
1164
+ "detail": [
1165
+ ModelBreadcrumbItem(),
1166
+ InstanceParentBreadcrumbItem(parent_key="manufacturer", parent_lookup_key="name"),
1167
+ ]
1168
+ }
1169
+ )
1118
1170
 
1119
1171
  def get_required_permission(self):
1120
1172
  view_action = self.get_action()
@@ -1884,28 +1936,6 @@ class PlatformUIViewSet(NautobotUIViewSet):
1884
1936
  #
1885
1937
 
1886
1938
 
1887
- class DevicePageMixin:
1888
- breadcrumbs = Breadcrumbs(
1889
- items={
1890
- "detail": [
1891
- ModelBreadcrumbItem(model=Device),
1892
- ModelBreadcrumbItem(
1893
- model=Device,
1894
- label=lambda c: c["object"].location,
1895
- reverse_query_params=lambda c: {"location": c["object"].location.pk},
1896
- ),
1897
- InstanceBreadcrumbItem(
1898
- instance=lambda c: c["object"].parent_bay.device,
1899
- should_render=lambda c: hasattr(c["object"], "parent_bay"),
1900
- ),
1901
- BaseBreadcrumbItem(
1902
- label=lambda c: c["object"].parent_bay, should_render=lambda c: hasattr(c["object"], "parent_bay")
1903
- ),
1904
- ]
1905
- }
1906
- )
1907
-
1908
-
1909
1939
  class DeviceComponentPageMixin:
1910
1940
  """
1911
1941
  This class hold the breadcrumbs paths for Device Components Pages like console ports.
@@ -1949,39 +1979,90 @@ class DeviceComponentPageMixin:
1949
1979
  )
1950
1980
 
1951
1981
 
1952
- class DeviceListView(generic.ObjectListView):
1982
+ class DeviceUIViewSet(NautobotUIViewSet):
1953
1983
  queryset = Device.objects.select_related(
1954
1984
  "device_type__manufacturer", # Needed for __str__() on device_type
1955
1985
  )
1956
- filterset = filters.DeviceFilterSet
1957
- filterset_form = forms.DeviceFilterForm
1958
- table = tables.DeviceTable
1959
- template_name = "dcim/device_list.html"
1986
+ filterset_class = filters.DeviceFilterSet
1987
+ filterset_form_class = forms.DeviceFilterForm
1988
+ table_class = tables.DeviceTable
1989
+ form_class = forms.DeviceForm
1990
+ bulk_update_form_class = forms.DeviceBulkEditForm
1991
+ serializer_class = serializers.DeviceSerializer
1960
1992
 
1993
+ breadcrumbs = Breadcrumbs(
1994
+ items={
1995
+ "detail": [
1996
+ ModelBreadcrumbItem(model=Device),
1997
+ InstanceParentBreadcrumbItem(parent_key="location"),
1998
+ InstanceBreadcrumbItem(
1999
+ instance=lambda c: c["object"].parent_bay.device,
2000
+ should_render=lambda c: hasattr(c["object"], "parent_bay"),
2001
+ ),
2002
+ BaseBreadcrumbItem(
2003
+ label=lambda c: c["object"].parent_bay, should_render=lambda c: hasattr(c["object"], "parent_bay")
2004
+ ),
2005
+ ]
2006
+ }
2007
+ )
1961
2008
 
1962
- class DeviceView(DevicePageMixin, generic.ObjectView):
1963
- queryset = Device.objects.select_related(
1964
- "cluster__cluster_group",
1965
- "controller_managed_device_group__controller",
1966
- "device_redundancy_group",
1967
- "device_type__device_family",
1968
- "location",
1969
- "platform",
1970
- "primary_ip4",
1971
- "primary_ip6",
1972
- "rack__rack_group",
1973
- "role",
1974
- "secrets_group",
1975
- "software_version",
1976
- "status",
1977
- "tenant__tenant_group",
1978
- ).prefetch_related("images", "software_image_files")
2009
+ def get_queryset(self):
2010
+ queryset = super().get_queryset()
2011
+ if self.detail: # TODO: change to self.action == "retrieve" as a part of addressing NAUTOBOT-1051
2012
+ queryset = queryset.select_related(
2013
+ "cluster__cluster_group",
2014
+ "controller_managed_device_group__controller",
2015
+ "device_redundancy_group",
2016
+ "device_type__device_family",
2017
+ "location",
2018
+ "platform",
2019
+ "primary_ip4",
2020
+ "primary_ip6",
2021
+ "rack__rack_group",
2022
+ "role",
2023
+ "secrets_group",
2024
+ "software_version",
2025
+ "status",
2026
+ "tenant__tenant_group",
2027
+ "virtual_chassis",
2028
+ ).prefetch_related("images", "software_image_files")
2029
+ if self.action == "config_context":
2030
+ queryset = queryset.annotate_config_context_data()
2031
+ return queryset
1979
2032
 
1980
2033
  class DeviceDetailContent(object_detail.ObjectDetailContent):
1981
2034
  """
1982
2035
  Override base ObjectDetailContent to render dynamic-groups table as a separate view/tab instead of inline.
1983
2036
  """
1984
2037
 
2038
+ class DeviceDynamicGroupsTextPanel(object_detail.BaseTextPanel):
2039
+ """Panel displaying a note about caching of dynamic groups."""
2040
+
2041
+ def __init__(
2042
+ self,
2043
+ *,
2044
+ weight,
2045
+ render_as=object_detail.BaseTextPanel.RenderOptions.MARKDOWN,
2046
+ label="Dynamic Group caching",
2047
+ **kwargs,
2048
+ ):
2049
+ super().__init__(weight=weight, render_as=render_as, label=label, **kwargs)
2050
+
2051
+ def get_value(self, context):
2052
+ dg_list_url = reverse("extras:dynamicgroup_list")
2053
+ job_run_url = reverse(
2054
+ "extras:job_run_by_class_path",
2055
+ kwargs={"class_path": "nautobot.core.jobs.groups.RefreshDynamicGroupCaches"},
2056
+ )
2057
+ return (
2058
+ "Dynamic group membership is cached for performance reasons, "
2059
+ "therefore this page may not always be up-to-date.\n\n"
2060
+ "You can refresh the membership of any specific group by viewing it from the list below or from the "
2061
+ f"[Dynamic Groups list view]({dg_list_url}).\n\n"
2062
+ "You can also refresh the membership of **all** groups by running the "
2063
+ f"[Refresh Dynamic Group Caches job]({job_run_url})."
2064
+ )
2065
+
1985
2066
  def __init__(self, **kwargs):
1986
2067
  super().__init__(**kwargs)
1987
2068
  # Remove inline tab definition
@@ -1997,8 +2078,9 @@ class DeviceView(DevicePageMixin, generic.ObjectView):
1997
2078
  url_name="dcim:device_dynamicgroups",
1998
2079
  related_object_attribute="dynamic_groups",
1999
2080
  panels=(
2081
+ self.DeviceDynamicGroupsTextPanel(weight=100),
2000
2082
  object_detail.ObjectsTablePanel(
2001
- weight=100,
2083
+ weight=200,
2002
2084
  table_class=DynamicGroupTable,
2003
2085
  table_attribute="dynamic_groups",
2004
2086
  exclude_columns=["content_type"],
@@ -2010,6 +2092,191 @@ class DeviceView(DevicePageMixin, generic.ObjectView):
2010
2092
  )
2011
2093
  )
2012
2094
 
2095
+ class DeviceFieldsPanel(object_detail.ObjectFieldsPanel):
2096
+ """
2097
+ ObjectFieldsPanel with context-aware rendering of `position`, `device_redundancy_group`, and `software_version`.
2098
+ """
2099
+
2100
+ def render_value(self, key, value, context):
2101
+ if key == "position":
2102
+ instance = get_obj_from_context(context, self.context_object_key)
2103
+ try:
2104
+ if instance.parent_bay is not None:
2105
+ parent = instance.parent_bay.device
2106
+ display = format_html(
2107
+ "{} / {}",
2108
+ helpers.hyperlinked_object(parent),
2109
+ helpers.hyperlinked_object(instance.parent_bay),
2110
+ )
2111
+ if parent.position is not None:
2112
+ display += format_html(" (U{} / {})", parent.position, parent.get_face_display())
2113
+ return display
2114
+ except DeviceBay.DoesNotExist:
2115
+ pass
2116
+ if instance.rack is not None and value is not None:
2117
+ return format_html("U{} / {}", value, instance.get_face_display())
2118
+ if instance.rack is not None and instance.device_type.u_height:
2119
+ return mark_safe('<span class="label label-warning">Not racked</span>')
2120
+ return helpers.HTML_NONE
2121
+ if key == "device_redundancy_group" and value is not None:
2122
+ instance = get_obj_from_context(context, self.context_object_key)
2123
+ return format_html(
2124
+ '{} <span class="badge badge-default">Priority: {}</span>',
2125
+ helpers.hyperlinked_object(value),
2126
+ instance.device_redundancy_group_priority,
2127
+ )
2128
+ if key == "software_version":
2129
+ instance = get_obj_from_context(context, self.context_object_key)
2130
+ return render_software_version_and_image_files(instance, value, context)
2131
+
2132
+ return super().render_value(key, value, context)
2133
+
2134
+ class DeviceVirtualChassisMembersTablePanel(object_detail.ObjectsTablePanel):
2135
+ """ObjectsTablePanel that only renders if the device belongs to a virtual-chassis."""
2136
+
2137
+ def should_render(self, context):
2138
+ obj = get_obj_from_context(context)
2139
+ return obj.virtual_chassis is not None
2140
+
2141
+ def get_extra_context(self, context):
2142
+ extra_context = super().get_extra_context(context)
2143
+ obj = get_obj_from_context(context)
2144
+ extra_context["virtual_chassis"] = obj.virtual_chassis
2145
+ extra_context["body_content_table_list_url"] = (
2146
+ reverse("dcim:device_list") + "?virtual_chassis=" + str(obj.virtual_chassis.pk)
2147
+ )
2148
+ return extra_context
2149
+
2150
+ class DevicePowerUtilizationPanel(object_detail.Panel):
2151
+ """Panel showing a table of PDU calculated power utilization per power-port on the device."""
2152
+
2153
+ def should_render(self, context):
2154
+ """Only render if the device is a PDU, i.e. has both power-ports and power-outlets."""
2155
+ instance = get_obj_from_context(context)
2156
+ return instance.all_power_ports.exists() and instance.all_power_outlets.exists()
2157
+
2158
+ def render_body_content(self, context):
2159
+ """Render a table with one row per power-port and additional rows per leg for three-phase power."""
2160
+ instance = get_obj_from_context(context)
2161
+ header = mark_safe(
2162
+ "<tr><th>Input</th><th>Outlets</th><th>Allocated</th><th>Available</th><th>Utilization</th></tr>"
2163
+ )
2164
+ body = mark_safe("")
2165
+ for powerport in instance.all_power_ports.all():
2166
+ utilization = powerport.get_power_draw()
2167
+ # Table row for each power-port
2168
+ powerfeed = powerport.connected_endpoint
2169
+ if powerfeed is not None and powerfeed.available_power:
2170
+ available_power = powerfeed.available_power
2171
+ utilization_data = Context(
2172
+ helpers.utilization_graph_raw_data(utilization["allocated"], powerfeed.available_power)
2173
+ )
2174
+ utilization_graph = object_detail.render_component_template(
2175
+ "utilities/templatetags/utilization_graph.html", utilization_data
2176
+ )
2177
+ else:
2178
+ available_power = helpers.HTML_NONE
2179
+ utilization_graph = helpers.HTML_NONE
2180
+ body += format_html(
2181
+ "<tr><td>{}</td><td>{}</td><td>{}</td><td>{}</td><td>{}</td></tr>",
2182
+ helpers.hyperlinked_object(powerport),
2183
+ utilization["outlet_count"],
2184
+ utilization["allocated"],
2185
+ available_power,
2186
+ utilization_graph,
2187
+ )
2188
+
2189
+ # Indented table row for each leg of a three-phase power-port.
2190
+ for leg in utilization["legs"]:
2191
+ if powerfeed is not None and powerfeed.available_power:
2192
+ available_power = powerfeed.available_power / 3
2193
+ utilization_data = Context(
2194
+ helpers.utilization_graph_raw_data(leg["allocated"], powerfeed.available_power / 3)
2195
+ )
2196
+ utilization_graph = object_detail.render_component_template(
2197
+ "utilities/templatetags/utilization_graph.html", utilization_data
2198
+ )
2199
+ else:
2200
+ available_power = helpers.HTML_NONE
2201
+ utilization_graph = helpers.HTML_NONE
2202
+ body += format_html(
2203
+ """<tr><td style="padding-left: 20px">{}</td><td>{}</td><td>{}</td><td>{}</td><td>{}</td></tr>""",
2204
+ f"Leg {leg['name']}",
2205
+ leg["outlet_count"],
2206
+ leg["allocated"],
2207
+ available_power,
2208
+ utilization_graph,
2209
+ )
2210
+ return header + body
2211
+
2212
+ class DeviceImageAttachmentsTablePanel(object_detail.ObjectsTablePanel):
2213
+ """
2214
+ ObjectsTablePanel with a custom _get_table_add_url() implementation.
2215
+
2216
+ Needed because the URL is `/dcim/devices/<pk>/images/add/`, not `extras/image-attachments/add?device=<pk>`.
2217
+ """
2218
+
2219
+ def _get_table_add_url(self, context):
2220
+ obj = get_obj_from_context(context)
2221
+ request = context["request"]
2222
+ return_url = context.get("return_url", obj.get_absolute_url())
2223
+
2224
+ if not request.user.has_perms(["extras.add_imageattachment"]):
2225
+ return None
2226
+ return reverse("dcim:device_add_image", kwargs={"object_id": obj.pk}) + f"?return_url={return_url}"
2227
+
2228
+ class DeviceModuleBaysTab(object_detail.DistinctViewTab):
2229
+ """DistinctViewTab for device module-bays; shows both the module and module-bay count as a badge."""
2230
+
2231
+ def render_label(self, context):
2232
+ obj = get_obj_from_context(context)
2233
+ module_count = obj.module_bays.filter(installed_module__isnull=False).count()
2234
+ return format_html(
2235
+ "{} {}",
2236
+ self.label,
2237
+ render_to_string(
2238
+ "utilities/templatetags/badge.html", helpers.badge(f"{module_count}/{self.related_object_count}")
2239
+ ),
2240
+ )
2241
+
2242
+ class DeviceInterfacesTablePanel(object_detail.ObjectsTablePanel):
2243
+ """ObjectsTablePanel for device interfaces; shows the "Device" column if the device is a VirtualChassis master."""
2244
+
2245
+ def get_extra_context(self, context):
2246
+ extra_context = super().get_extra_context(context)
2247
+ obj = get_obj_from_context(context)
2248
+ if VirtualChassis.objects.filter(master=obj).exists():
2249
+ extra_context["body_content_table"].columns.show("device")
2250
+ return extra_context
2251
+
2252
+ class DeviceWirelessTab(object_detail.DistinctViewTab):
2253
+ """
2254
+ DistinctViewTab that only renders if the device belongs to a wireless-capable controller_managed_device_group.
2255
+ """
2256
+
2257
+ def should_render(self, context):
2258
+ if not super().should_render(context):
2259
+ return False
2260
+ obj = get_obj_from_context(context)
2261
+ return (
2262
+ obj.controller_managed_device_group is not None
2263
+ and isinstance(obj.controller_managed_device_group.capabilities, list) # it's potentially None
2264
+ and "wireless" in obj.controller_managed_device_group.capabilities
2265
+ )
2266
+
2267
+ class DeviceNAPALMTab(object_detail.DistinctViewTab):
2268
+ """DistinctViewTab for device NAPALM getters; disables the tab if the device/platform isn't set appropriately."""
2269
+
2270
+ def render_label_wrapper(self, context):
2271
+ obj = get_obj_from_context(context)
2272
+ if obj.platform is None:
2273
+ with context.update({"disabled_message": "No platform assigned to this device"}):
2274
+ return super().render_label_wrapper(context)
2275
+ if not obj.platform.napalm_driver:
2276
+ with context.update({"disabled_message": "No NAPALM driver assigned for this platform"}):
2277
+ return super().render_label_wrapper(context)
2278
+ return super().render_label_wrapper(context)
2279
+
2013
2280
  object_detail_content = DeviceDetailContent(
2014
2281
  extra_buttons=(
2015
2282
  object_detail.DropdownButton(
@@ -2093,487 +2360,665 @@ class DeviceView(DevicePageMixin, generic.ObjectView):
2093
2360
  ),
2094
2361
  ),
2095
2362
  ),
2096
- panels=(), # not yet ported over due to complexity of this template
2097
- # TODO
2098
- # ObjectFieldsPanel(
2099
- # weight=100,
2100
- # section=SectionChoices.LEFT_HALF,
2101
- # fields=["location", "rack", "position", "face", "tenant", "device_type", "serial", "asset_tag"],
2102
- # # TODO add device_type.device_family, device_type.u_height,
2103
- # ),
2104
- # TODO: Virtual Chassis panel
2105
- # ObjectFieldsPanel(
2106
- # weight=110,
2107
- # section=SectionChoices.LEFT_HALF,
2108
- # label="Virtual Chassis",
2109
- # ),
2110
- # ObjectFieldsPanel(
2111
- # weight=120,
2112
- # section=SectionChoices.LEFT_HALF,
2113
- # label="Management",
2114
- # fields=["role", "platform", "status", "primary_ip4", "primary_ip6", "secrets_group", "device_redundancy_group", "controller_managed_device_group", "software_version"],
2115
- # ),
2116
- # TODO: power utilization panel
2117
- # ObjectsTablePanel(
2118
- # weight=100,
2119
- # section=SectionChoices.RIGHT_HALF,
2120
- # table_title="Power Utilization",
2121
- # table_class=???,
2122
- # table_filter="device",
2123
- # ),
2124
- # ObjectsTablePanel(
2125
- # weight=100,
2126
- # section=SectionChoices.RIGHT_HALF,
2127
- # table_title="Assigned VRFs",
2128
- # table_class=VRFDeviceAssignmentTable,
2129
- # table_filter="device",
2130
- # exclude_columns=["virtual_machine", "device"],
2131
- # ),
2132
- # TODO: services panel
2133
- # ObjectsTablePanel(
2134
- # weight=200,
2135
- # section=SectionChoices.RIGHT_HALF,
2136
- # table_class=???,
2137
- # table_filter="device",
2138
- # ),
2139
- # TODO: images panel
2140
- # ObjectsTablePanel(
2141
- # weight=300,
2142
- # section=SectionChoices.RIGHT_HALF,
2143
- # table_class=???,
2144
- # table_filter="device",
2145
- # ),
2146
- # ObjectsTablePanel(
2147
- # weight=100,
2148
- # section=SectionChoices.FULL_WIDTH,
2149
- # table_class=tables.VirtualDeviceContextTable,
2150
- # table_filter="device",
2151
- # select_related_fields=["tenant", "primary_ip4", "primary_ip6"],
2152
- # exclude_columns=["device"],
2153
- # ),
2154
- # ),
2155
- )
2156
-
2157
- def get_extra_context(self, request, instance):
2158
- # VirtualChassis members
2159
- if instance.virtual_chassis is not None:
2160
- vc_members = (
2161
- Device.objects.restrict(request.user, "view")
2162
- .filter(virtual_chassis=instance.virtual_chassis)
2163
- .order_by("vc_position")
2164
- )
2165
- else:
2166
- vc_members = []
2167
-
2168
- # Services
2169
- services = Service.objects.restrict(request.user, "view").filter(device=instance)
2170
-
2171
- # VRF assignments
2172
- vrf_assignments = instance.vrf_assignments.restrict(request.user, "view")
2173
- vrf_table = VRFDeviceAssignmentTable(vrf_assignments)
2174
-
2175
- # Software images
2176
- if instance.software_version is not None:
2177
- software_version_images = instance.software_version.software_image_files.restrict(
2178
- request.user, "view"
2179
- ).filter(device_types=instance.device_type)
2180
- if not software_version_images.exists():
2181
- software_version_images = instance.software_version.software_image_files.restrict(
2182
- request.user, "view"
2183
- ).filter(default_image=True)
2184
- else:
2185
- software_version_images = []
2186
-
2187
- modulebay_count = instance.module_bays.count()
2188
- module_count = instance.module_bays.filter(installed_module__isnull=False).count()
2189
-
2190
- vdcs = instance.virtual_device_contexts.restrict(request.user).select_related(
2191
- "tenant", "primary_ip4", "primary_ip6"
2192
- )
2193
- vdcs_table = tables.VirtualDeviceContextTable(vdcs, orderable=False, exclude=("device",))
2194
- vdc_url = reverse("dcim:virtualdevicecontext_add")
2195
- return_url = instance.get_absolute_url()
2196
- vdcs_table_add_url = f"{vdc_url}?device={instance.id}&return_url={return_url}"
2197
-
2198
- paginate = {
2199
- "paginator_class": EnhancedPaginator,
2200
- "per_page": get_paginate_count(request),
2201
- }
2202
- RequestConfig(request, paginate).configure(vdcs_table)
2203
-
2204
- return {
2205
- **super().get_extra_context(request, instance),
2206
- "services": services,
2207
- "software_version_images": software_version_images,
2208
- "vc_members": vc_members,
2209
- "vrf_table": vrf_table,
2210
- "active_tab": "device",
2211
- "modulebay_count": modulebay_count,
2212
- "module_count": f"{module_count}/{modulebay_count}",
2213
- "vdcs_table": vdcs_table,
2214
- "vdcs_table_add_url": vdcs_table_add_url,
2215
- }
2216
-
2217
-
2218
- class DeviceComponentTabView(DevicePageMixin, generic.ObjectView):
2219
- queryset = Device.objects.all()
2220
-
2221
- def get_extra_context(self, request, instance):
2222
- modulebay_count = instance.module_bays.count()
2223
- module_count = instance.module_bays.filter(installed_module__isnull=False).count()
2224
-
2225
- return {
2226
- **super().get_extra_context(request, instance),
2227
- "modulebay_count": modulebay_count,
2228
- "module_count": f"{module_count}/{modulebay_count}",
2229
- }
2230
-
2231
-
2232
- class DeviceConsolePortsView(DeviceComponentTabView):
2233
- queryset = Device.objects.all()
2234
- template_name = "dcim/device/consoleports.html"
2235
-
2236
- def get_extra_context(self, request, instance):
2237
- consoleports = (
2238
- instance.all_console_ports.restrict(request.user, "view")
2239
- .select_related("cable")
2240
- .prefetch_related("_path__destination")
2241
- )
2242
- consoleport_table = tables.DeviceModuleConsolePortTable(data=consoleports, user=request.user, orderable=False)
2243
- if request.user.has_perm("dcim.change_consoleport") or request.user.has_perm("dcim.delete_consoleport"):
2244
- consoleport_table.columns.show("pk")
2245
-
2246
- return {
2247
- **super().get_extra_context(request, instance),
2248
- "consoleport_table": consoleport_table,
2249
- "active_tab": "console-ports",
2250
- }
2251
-
2252
-
2253
- class DeviceConsoleServerPortsView(DeviceComponentTabView):
2254
- queryset = Device.objects.all()
2255
- template_name = "dcim/device/consoleserverports.html"
2256
-
2257
- def get_extra_context(self, request, instance):
2258
- consoleserverports = (
2259
- instance.all_console_server_ports.restrict(request.user, "view")
2260
- .select_related("cable")
2261
- .prefetch_related("_path__destination")
2262
- )
2263
- consoleserverport_table = tables.DeviceModuleConsoleServerPortTable(
2264
- data=consoleserverports, user=request.user, orderable=False
2265
- )
2266
- if request.user.has_perm("dcim.change_consoleserverport") or request.user.has_perm(
2267
- "dcim.delete_consoleserverport"
2268
- ):
2269
- consoleserverport_table.columns.show("pk")
2270
-
2271
- return {
2272
- **super().get_extra_context(request, instance),
2273
- "consoleserverport_table": consoleserverport_table,
2274
- "active_tab": "console-server-ports",
2275
- }
2276
-
2277
-
2278
- class DevicePowerPortsView(DeviceComponentTabView):
2279
- queryset = Device.objects.all()
2280
- template_name = "dcim/device/powerports.html"
2281
-
2282
- def get_extra_context(self, request, instance):
2283
- powerports = (
2284
- instance.all_power_ports.restrict(request.user, "view")
2285
- .select_related("cable")
2286
- .prefetch_related("_path__destination")
2287
- )
2288
- powerport_table = tables.DeviceModulePowerPortTable(data=powerports, user=request.user, orderable=False)
2289
- if request.user.has_perm("dcim.change_powerport") or request.user.has_perm("dcim.delete_powerport"):
2290
- powerport_table.columns.show("pk")
2291
-
2292
- return {
2293
- **super().get_extra_context(request, instance),
2294
- "powerport_table": powerport_table,
2295
- "active_tab": "power-ports",
2296
- }
2297
-
2298
-
2299
- class DevicePowerOutletsView(DeviceComponentTabView):
2300
- queryset = Device.objects.all()
2301
- template_name = "dcim/device/poweroutlets.html"
2302
-
2303
- def get_extra_context(self, request, instance):
2304
- poweroutlets = (
2305
- instance.all_power_outlets.restrict(request.user, "view")
2306
- .select_related("cable", "power_port")
2307
- .prefetch_related("_path__destination")
2308
- )
2309
- poweroutlet_table = tables.DeviceModulePowerOutletTable(data=poweroutlets, user=request.user, orderable=False)
2310
- if request.user.has_perm("dcim.change_poweroutlet") or request.user.has_perm("dcim.delete_poweroutlet"):
2311
- poweroutlet_table.columns.show("pk")
2312
-
2313
- return {
2314
- **super().get_extra_context(request, instance),
2315
- "poweroutlet_table": poweroutlet_table,
2316
- "active_tab": "power-outlets",
2317
- }
2318
-
2319
-
2320
- class DeviceInterfacesView(DeviceComponentTabView):
2321
- queryset = Device.objects.all()
2322
- template_name = "dcim/device/interfaces.html"
2323
-
2324
- def get_extra_context(self, request, instance):
2325
- interfaces = (
2326
- instance.vc_interfaces.restrict(request.user, "view")
2327
- .prefetch_related(
2328
- Prefetch("ip_addresses", queryset=IPAddress.objects.restrict(request.user)),
2329
- Prefetch("member_interfaces", queryset=Interface.objects.restrict(request.user)),
2330
- "_path__destination",
2331
- "tags",
2332
- )
2333
- .select_related("lag", "cable")
2334
- .order_by("_name")
2335
- )
2336
- interface_table = tables.DeviceModuleInterfaceTable(data=interfaces, user=request.user, orderable=False)
2337
- if VirtualChassis.objects.filter(master=instance).exists():
2338
- interface_table.columns.show("device")
2339
- if request.user.has_perm("dcim.change_interface") or request.user.has_perm("dcim.delete_interface"):
2340
- interface_table.columns.show("pk")
2341
-
2342
- return {
2343
- **super().get_extra_context(request, instance),
2344
- "interface_table": interface_table,
2345
- "active_tab": "interfaces",
2346
- }
2347
-
2348
-
2349
- class DeviceFrontPortsView(DeviceComponentTabView):
2350
- queryset = Device.objects.all()
2351
- template_name = "dcim/device/frontports.html"
2363
+ panels=(
2364
+ DeviceFieldsPanel(
2365
+ weight=100,
2366
+ section=SectionChoices.LEFT_HALF,
2367
+ fields=[
2368
+ "location",
2369
+ "rack",
2370
+ "position",
2371
+ "tenant",
2372
+ "device_type__device_family",
2373
+ "device_type",
2374
+ "serial",
2375
+ "asset_tag",
2376
+ ],
2377
+ key_transforms={"device_type__device_family": "Device Family"},
2378
+ value_transforms={
2379
+ "device_type": [lambda v: format_html("{} ({}U)", helpers.hyperlinked_object(v), v.u_height)],
2380
+ },
2381
+ ),
2382
+ DeviceVirtualChassisMembersTablePanel(
2383
+ weight=110,
2384
+ section=SectionChoices.LEFT_HALF,
2385
+ context_table_key="vc_members_table",
2386
+ table_title="Virtual Chassis",
2387
+ related_field_name="vc_master",
2388
+ show_table_config_button=False,
2389
+ add_button_route=None,
2390
+ footer_buttons=[
2391
+ object_detail.Button(
2392
+ weight=100,
2393
+ label="View Virtual Chassis",
2394
+ icon="mdi-arrow-right-bold",
2395
+ size="xs",
2396
+ link_name="dcim:virtualchassis",
2397
+ context_object_key="virtual_chassis",
2398
+ ),
2399
+ ],
2400
+ ),
2401
+ DeviceFieldsPanel(
2402
+ weight=120,
2403
+ section=SectionChoices.LEFT_HALF,
2404
+ label="Management",
2405
+ fields=[
2406
+ "role",
2407
+ "platform",
2408
+ "status",
2409
+ "primary_ip4",
2410
+ "primary_ip6",
2411
+ "secrets_group",
2412
+ "device_redundancy_group",
2413
+ "controller_managed_device_group",
2414
+ "controller_managed_device_group__controller",
2415
+ "cluster",
2416
+ "software_version",
2417
+ ],
2418
+ key_transforms={"controller_managed_device_group__controller": "Managed By Controller"},
2419
+ value_transforms={
2420
+ "primary_ip4": [render_ip_with_nat],
2421
+ "primary_ip6": [render_ip_with_nat],
2422
+ },
2423
+ hide_if_unset=["controller_managed_device_group__controller", "cluster"],
2424
+ ),
2425
+ DevicePowerUtilizationPanel(
2426
+ weight=100,
2427
+ section=SectionChoices.RIGHT_HALF,
2428
+ label="Power Utilization",
2429
+ body_wrapper_template_path="components/panel/body_wrapper_generic_table.html",
2430
+ ),
2431
+ object_detail.ObjectsTablePanel(
2432
+ weight=200,
2433
+ section=SectionChoices.RIGHT_HALF,
2434
+ table_title="Assigned VRFs",
2435
+ table_class=VRFDeviceAssignmentTable,
2436
+ table_filter="device",
2437
+ exclude_columns=["related_object_type", "related_object_name"],
2438
+ show_table_config_button=False,
2439
+ ),
2440
+ object_detail.ObjectsTablePanel(
2441
+ weight=300,
2442
+ section=SectionChoices.RIGHT_HALF,
2443
+ table_title="Services",
2444
+ table_class=ServiceTable,
2445
+ table_filter="device",
2446
+ exclude_columns=["parent"],
2447
+ include_columns=["ip_addresses"],
2448
+ show_table_config_button=False,
2449
+ ),
2450
+ DeviceImageAttachmentsTablePanel(
2451
+ weight=400,
2452
+ section=SectionChoices.RIGHT_HALF,
2453
+ table_title="Images",
2454
+ table_class=ImageAttachmentTable,
2455
+ table_attribute="images",
2456
+ related_field_name="device",
2457
+ show_table_config_button=False,
2458
+ ),
2459
+ object_detail.ObjectsTablePanel(
2460
+ weight=100,
2461
+ section=SectionChoices.FULL_WIDTH,
2462
+ table_title="Virtual Device Contexts",
2463
+ table_class=tables.VirtualDeviceContextTable,
2464
+ table_filter="device",
2465
+ exclude_columns=["device"],
2466
+ show_table_config_button=False,
2467
+ ),
2468
+ ),
2469
+ extra_tabs=(
2470
+ DeviceModuleBaysTab(
2471
+ weight=object_detail.Tab.WEIGHT_CHANGELOG_TAB + 100,
2472
+ tab_id="module_bays",
2473
+ label="Module Bays",
2474
+ url_name="dcim:device_modulebays",
2475
+ related_object_attribute="module_bays",
2476
+ hide_if_empty=True,
2477
+ panels=(
2478
+ object_detail.ObjectsTablePanel(
2479
+ weight=100,
2480
+ section=SectionChoices.FULL_WIDTH,
2481
+ table_title="Module Bays",
2482
+ table_class=tables.DeviceModuleBayTable,
2483
+ prefetch_related_fields=["installed_module", "installed_module__status"],
2484
+ table_filter="parent_device", # TODO: is this right or should we use table_attribute=module_bays?
2485
+ tab_id="module_bays",
2486
+ enable_bulk_actions=True,
2487
+ form_id="module-bays-form",
2488
+ footer_buttons=bulk_footer_buttons(form_id="module-bays-form", model=ModuleBay),
2489
+ include_paginator=True,
2490
+ ),
2491
+ ),
2492
+ ),
2493
+ object_detail.DistinctViewTab(
2494
+ weight=object_detail.Tab.WEIGHT_CHANGELOG_TAB + 200,
2495
+ tab_id="interfaces",
2496
+ label="Interfaces",
2497
+ url_name="dcim:device_interfaces",
2498
+ related_object_attribute="vc_interfaces",
2499
+ hide_if_empty=True,
2500
+ panels=(
2501
+ DeviceInterfacesTablePanel(
2502
+ # TODO: .prefetch_related(ip_addresses.restrict, member_interfaces.restrict)
2503
+ weight=100,
2504
+ section=SectionChoices.FULL_WIDTH,
2505
+ table_title="Interfaces",
2506
+ table_class=tables.DeviceModuleInterfaceTable,
2507
+ table_attribute="vc_interfaces",
2508
+ order_by_fields=["_name"],
2509
+ prefetch_related_fields=["_path__destination"],
2510
+ select_related_fields=["cable", "lag"],
2511
+ related_field_name="device",
2512
+ tab_id="interfaces",
2513
+ enable_bulk_actions=True,
2514
+ form_id="interfaces-form",
2515
+ footer_buttons=bulk_cable_termination_footer_buttons(
2516
+ form_id="interfaces-form", model=Interface
2517
+ ),
2518
+ include_paginator=True,
2519
+ header_extra_content_template_path="dcim/inc/device_interface_filter.html",
2520
+ ),
2521
+ ),
2522
+ ),
2523
+ object_detail.DistinctViewTab(
2524
+ weight=object_detail.Tab.WEIGHT_CHANGELOG_TAB + 300,
2525
+ tab_id="front_ports",
2526
+ label="Front Ports",
2527
+ url_name="dcim:device_frontports",
2528
+ related_object_attribute="all_front_ports",
2529
+ hide_if_empty=True,
2530
+ panels=(
2531
+ object_detail.ObjectsTablePanel(
2532
+ weight=100,
2533
+ section=SectionChoices.FULL_WIDTH,
2534
+ table_title="Front Ports",
2535
+ table_class=tables.DeviceModuleFrontPortTable,
2536
+ table_attribute="all_front_ports",
2537
+ select_related_fields=["cable", "rear_port"],
2538
+ related_field_name="device",
2539
+ tab_id="front_ports",
2540
+ enable_bulk_actions=True,
2541
+ form_id="front-ports-form",
2542
+ footer_buttons=bulk_cable_termination_footer_buttons(
2543
+ form_id="front-ports-form", model=FrontPort
2544
+ ),
2545
+ include_paginator=True,
2546
+ ),
2547
+ ),
2548
+ ),
2549
+ object_detail.DistinctViewTab(
2550
+ weight=object_detail.Tab.WEIGHT_CHANGELOG_TAB + 400,
2551
+ tab_id="rear_ports",
2552
+ label="Rear Ports",
2553
+ url_name="dcim:device_rearports",
2554
+ related_object_attribute="all_rear_ports",
2555
+ hide_if_empty=True,
2556
+ panels=(
2557
+ object_detail.ObjectsTablePanel(
2558
+ weight=100,
2559
+ section=SectionChoices.FULL_WIDTH,
2560
+ table_title="Rear Ports",
2561
+ table_class=tables.DeviceModuleRearPortTable,
2562
+ table_attribute="all_rear_ports",
2563
+ select_related_fields=["cable"],
2564
+ related_field_name="device",
2565
+ tab_id="rear_ports",
2566
+ enable_bulk_actions=True,
2567
+ form_id="rear-ports-form",
2568
+ footer_buttons=bulk_cable_termination_footer_buttons(form_id="rear-ports-form", model=RearPort),
2569
+ include_paginator=True,
2570
+ ),
2571
+ ),
2572
+ ),
2573
+ object_detail.DistinctViewTab(
2574
+ weight=object_detail.Tab.WEIGHT_CHANGELOG_TAB + 500,
2575
+ tab_id="console_ports",
2576
+ label="Console Ports",
2577
+ url_name="dcim:device_consoleports",
2578
+ related_object_attribute="all_console_ports",
2579
+ hide_if_empty=True,
2580
+ panels=(
2581
+ object_detail.ObjectsTablePanel(
2582
+ weight=100,
2583
+ section=SectionChoices.FULL_WIDTH,
2584
+ table_title="Console Ports",
2585
+ table_class=tables.DeviceModuleConsolePortTable,
2586
+ table_attribute="all_console_ports",
2587
+ select_related_fields=["cable"],
2588
+ prefetch_related_fields=["_path__destination"],
2589
+ related_field_name="device",
2590
+ tab_id="console_ports",
2591
+ enable_bulk_actions=True,
2592
+ form_id="console-ports-form",
2593
+ footer_buttons=bulk_cable_termination_footer_buttons(
2594
+ form_id="console-ports-form", model=ConsolePort
2595
+ ),
2596
+ include_paginator=True,
2597
+ ),
2598
+ ),
2599
+ ),
2600
+ object_detail.DistinctViewTab(
2601
+ weight=object_detail.Tab.WEIGHT_CHANGELOG_TAB + 600,
2602
+ tab_id="console_server_ports",
2603
+ label="Console Server Ports",
2604
+ url_name="dcim:device_consoleserverports",
2605
+ related_object_attribute="all_console_server_ports",
2606
+ hide_if_empty=True,
2607
+ panels=(
2608
+ object_detail.ObjectsTablePanel(
2609
+ weight=100,
2610
+ section=SectionChoices.FULL_WIDTH,
2611
+ table_title="Console Server Ports",
2612
+ table_class=tables.DeviceModuleConsoleServerPortTable,
2613
+ table_attribute="all_console_server_ports",
2614
+ select_related_fields=["cable"],
2615
+ prefetch_related_fields=["_path__destination"],
2616
+ related_field_name="device",
2617
+ tab_id="console_server_ports",
2618
+ enable_bulk_actions=True,
2619
+ form_id="console-server-ports-form",
2620
+ footer_buttons=bulk_cable_termination_footer_buttons(
2621
+ form_id="console-server-ports-form", model=ConsoleServerPort
2622
+ ),
2623
+ include_paginator=True,
2624
+ ),
2625
+ ),
2626
+ ),
2627
+ object_detail.DistinctViewTab(
2628
+ weight=object_detail.Tab.WEIGHT_CHANGELOG_TAB + 700,
2629
+ tab_id="power_ports",
2630
+ label="Power Ports",
2631
+ url_name="dcim:device_powerports",
2632
+ related_object_attribute="all_power_ports",
2633
+ hide_if_empty=True,
2634
+ panels=(
2635
+ object_detail.ObjectsTablePanel(
2636
+ weight=100,
2637
+ section=SectionChoices.FULL_WIDTH,
2638
+ table_title="Power Ports",
2639
+ table_class=tables.DeviceModulePowerPortTable,
2640
+ table_attribute="all_power_ports",
2641
+ select_related_fields=["cable"],
2642
+ prefetch_related_fields=["_path__destination"],
2643
+ related_field_name="device",
2644
+ tab_id="power_ports",
2645
+ enable_bulk_actions=True,
2646
+ form_id="power-ports-form",
2647
+ footer_buttons=bulk_cable_termination_footer_buttons(
2648
+ form_id="power-ports-form", model=PowerPort
2649
+ ),
2650
+ include_paginator=True,
2651
+ ),
2652
+ ),
2653
+ ),
2654
+ object_detail.DistinctViewTab(
2655
+ weight=object_detail.Tab.WEIGHT_CHANGELOG_TAB + 800,
2656
+ tab_id="power_outlets",
2657
+ label="Power Outlets",
2658
+ url_name="dcim:device_poweroutlets",
2659
+ related_object_attribute="all_power_outlets",
2660
+ hide_if_empty=True,
2661
+ panels=(
2662
+ object_detail.ObjectsTablePanel(
2663
+ weight=100,
2664
+ section=SectionChoices.FULL_WIDTH,
2665
+ table_title="Power Outlets",
2666
+ table_class=tables.DeviceModulePowerOutletTable,
2667
+ table_attribute="all_power_outlets",
2668
+ select_related_fields=["cable", "power_port"],
2669
+ prefetch_related_fields=["_path__destination"],
2670
+ related_field_name="device",
2671
+ tab_id="power_outlets",
2672
+ enable_bulk_actions=True,
2673
+ form_id="power-outlets-form",
2674
+ footer_buttons=bulk_cable_termination_footer_buttons(
2675
+ form_id="power-outlets-form", model=PowerOutlet
2676
+ ),
2677
+ include_paginator=True,
2678
+ ),
2679
+ ),
2680
+ ),
2681
+ object_detail.DistinctViewTab(
2682
+ weight=object_detail.Tab.WEIGHT_CHANGELOG_TAB + 900,
2683
+ tab_id="device_bays",
2684
+ label="Device Bays",
2685
+ url_name="dcim:device_devicebays",
2686
+ related_object_attribute="device_bays",
2687
+ hide_if_empty=True,
2688
+ panels=(
2689
+ object_detail.ObjectsTablePanel(
2690
+ weight=100,
2691
+ section=SectionChoices.FULL_WIDTH,
2692
+ table_title="Device Bays",
2693
+ table_class=tables.DeviceDeviceBayTable,
2694
+ select_related_fields=["installed_device__device_type__manufacturer"],
2695
+ table_filter="device",
2696
+ tab_id="device_bays",
2697
+ enable_bulk_actions=True,
2698
+ form_id="device-bays-form",
2699
+ footer_buttons=bulk_footer_buttons(form_id="device-bays-form", model=DeviceBay),
2700
+ include_paginator=True,
2701
+ ),
2702
+ ),
2703
+ ),
2704
+ object_detail.DistinctViewTab(
2705
+ weight=object_detail.Tab.WEIGHT_CHANGELOG_TAB + 1000,
2706
+ tab_id="inventory",
2707
+ label="Inventory",
2708
+ url_name="dcim:device_inventory",
2709
+ related_object_attribute="inventory_items",
2710
+ hide_if_empty=True,
2711
+ panels=(
2712
+ object_detail.ObjectsTablePanel(
2713
+ weight=100,
2714
+ section=SectionChoices.FULL_WIDTH,
2715
+ table_title="Inventory Items",
2716
+ table_class=tables.DeviceInventoryItemTable,
2717
+ select_related_fields=["manufacturer"],
2718
+ table_filter="device",
2719
+ tab_id="inventory",
2720
+ enable_bulk_actions=True,
2721
+ form_id="inventory-form",
2722
+ footer_buttons=bulk_footer_buttons(form_id="inventory-form", model=InventoryItem),
2723
+ include_paginator=True,
2724
+ ),
2725
+ ),
2726
+ ),
2727
+ DeviceWirelessTab(
2728
+ weight=object_detail.Tab.WEIGHT_CHANGELOG_TAB + 1100,
2729
+ tab_id="wireless",
2730
+ label="Wireless",
2731
+ url_name="dcim:device_wireless",
2732
+ related_object_attribute="controller_managed_device_group",
2733
+ panels=(
2734
+ object_detail.ObjectFieldsPanel(
2735
+ weight=100,
2736
+ section=SectionChoices.LEFT_HALF,
2737
+ label="Controller Managed Device Group",
2738
+ fields=["controller_managed_device_group", "controller_managed_device_group__controller"],
2739
+ key_transforms={
2740
+ "controller_managed_device_group": "Name",
2741
+ "controller_managed_device_group__controller": "Controller",
2742
+ },
2743
+ ),
2744
+ object_detail.ObjectsTablePanel(
2745
+ weight=200,
2746
+ section=SectionChoices.FULL_WIDTH,
2747
+ table_title="Wireless Networks",
2748
+ table_class=BaseControllerManagedDeviceGroupWirelessNetworkAssignmentTable,
2749
+ table_attribute="wireless_network_assignments",
2750
+ related_field_name="controller_device_redundancy_group",
2751
+ tab_id="wireless",
2752
+ include_paginator=True,
2753
+ exclude_columns=["controller_managed_device_group", "controller"],
2754
+ ),
2755
+ object_detail.ObjectsTablePanel(
2756
+ weight=300,
2757
+ section=SectionChoices.FULL_WIDTH,
2758
+ table_title="Radio Profiles",
2759
+ table_class=ControllerManagedDeviceGroupRadioProfileAssignmentTable,
2760
+ table_attribute="radio_profile_assignments",
2761
+ related_field_name="controller_device_redundancy_group",
2762
+ tab_id="wireless",
2763
+ include_paginator=True,
2764
+ exclude_columns=["controller_managed_device_group"],
2765
+ ),
2766
+ ),
2767
+ ),
2768
+ DeviceNAPALMTab(
2769
+ weight=object_detail.Tab.WEIGHT_CHANGELOG_TAB + 1200,
2770
+ tab_id="status",
2771
+ label="Status",
2772
+ url_name="dcim:device_status",
2773
+ required_permissions=["dcim.napalm_read_device"],
2774
+ ),
2775
+ DeviceNAPALMTab(
2776
+ weight=object_detail.Tab.WEIGHT_CHANGELOG_TAB + 1300,
2777
+ tab_id="lldp_neighbors",
2778
+ label="LLDP Neighbors",
2779
+ url_name="dcim:device_lldp_neighbors",
2780
+ required_permissions=["dcim.napalm_read_device"],
2781
+ ),
2782
+ DeviceNAPALMTab(
2783
+ weight=object_detail.Tab.WEIGHT_CHANGELOG_TAB + 1400,
2784
+ tab_id="config",
2785
+ label="Configuration",
2786
+ url_name="dcim:device_config",
2787
+ required_permissions=["dcim.napalm_read_device"],
2788
+ ),
2789
+ object_detail.DistinctViewTab(
2790
+ weight=object_detail.Tab.WEIGHT_CHANGELOG_TAB + 1500,
2791
+ tab_id="config_context",
2792
+ label="Config Context",
2793
+ url_name="dcim:device_configcontext",
2794
+ required_permissions=["extras.view_configcontext"],
2795
+ ),
2796
+ ),
2797
+ )
2352
2798
 
2353
2799
  def get_extra_context(self, request, instance):
2354
- frontports = instance.all_front_ports.restrict(request.user, "view").select_related("cable", "rear_port")
2355
- frontport_table = tables.DeviceModuleFrontPortTable(data=frontports, user=request.user, orderable=False)
2356
- if request.user.has_perm("dcim.change_frontport") or request.user.has_perm("dcim.delete_frontport"):
2357
- frontport_table.columns.show("pk")
2358
-
2359
- return {
2360
- **super().get_extra_context(request, instance),
2361
- "frontport_table": frontport_table,
2362
- "active_tab": "front-ports",
2363
- }
2364
-
2800
+ extra_context = super().get_extra_context(request, instance)
2801
+
2802
+ if self.detail: # TODO: change to `if self.action == "retrieve"` as a part of addressing NAUTOBOT-1051
2803
+ # VirtualChassis members
2804
+ if instance.virtual_chassis is not None:
2805
+ vc_members = (
2806
+ Device.objects.restrict(request.user, "view")
2807
+ .filter(virtual_chassis=instance.virtual_chassis)
2808
+ .order_by("vc_position")
2809
+ )
2810
+ vc_members_table = tables.VirtualChassisMembersTable(vc_members)
2811
+ else:
2812
+ vc_members_table = None
2813
+ extra_context["vc_members_table"] = vc_members_table
2365
2814
 
2366
- class DeviceRearPortsView(DeviceComponentTabView):
2367
- queryset = Device.objects.all()
2368
- template_name = "dcim/device/rearports.html"
2815
+ return extra_context
2369
2816
 
2370
- def get_extra_context(self, request, instance):
2371
- rearports = instance.all_rear_ports.restrict(request.user, "view").select_related("cable")
2372
- rearport_table = tables.DeviceModuleRearPortTable(data=rearports, user=request.user, orderable=False)
2373
- if request.user.has_perm("dcim.change_rearport") or request.user.has_perm("dcim.delete_rearport"):
2374
- rearport_table.columns.show("pk")
2817
+ @action(
2818
+ detail=True,
2819
+ url_path="dynamic-groups",
2820
+ url_name="dynamicgroups",
2821
+ custom_view_base_action="view",
2822
+ custom_view_additional_permissions=["extras.view_dynamicgroup"],
2823
+ )
2824
+ def dynamic_groups(self, request, *args, **kwargs):
2825
+ return Response({})
2375
2826
 
2376
- return {
2377
- **super().get_extra_context(request, instance),
2378
- "rearport_table": rearport_table,
2379
- "active_tab": "rear-ports",
2380
- }
2827
+ @action(
2828
+ detail=True,
2829
+ url_path="console-ports",
2830
+ url_name="consoleports",
2831
+ custom_view_base_action="view",
2832
+ custom_view_additional_permissions=["dcim.view_consoleport"],
2833
+ )
2834
+ def console_ports(self, request, *args, **kwargs):
2835
+ return Response({})
2381
2836
 
2837
+ @action(
2838
+ detail=True,
2839
+ url_path="console-server-ports",
2840
+ url_name="consoleserverports",
2841
+ custom_view_base_action="view",
2842
+ custom_view_additional_permissions=["dcim.view_consoleserverport"],
2843
+ )
2844
+ def console_server_ports(self, request, *args, **kwargs):
2845
+ return Response({})
2382
2846
 
2383
- class DeviceDeviceBaysView(DeviceComponentTabView):
2384
- queryset = Device.objects.all()
2385
- template_name = "dcim/device/devicebays.html"
2847
+ @action(
2848
+ detail=True,
2849
+ url_path="device-bays",
2850
+ url_name="devicebays",
2851
+ custom_view_base_action="view",
2852
+ custom_view_additional_permissions=["dcim.view_devicebay"],
2853
+ )
2854
+ def device_bays(self, request, *args, **kwargs):
2855
+ return Response({})
2386
2856
 
2387
- def get_extra_context(self, request, instance):
2388
- devicebays = (
2389
- DeviceBay.objects.restrict(request.user, "view")
2390
- .filter(device=instance)
2391
- .select_related(
2392
- "installed_device__device_type__manufacturer",
2393
- )
2394
- )
2395
- devicebay_table = tables.DeviceDeviceBayTable(data=devicebays, user=request.user, orderable=False)
2396
- if request.user.has_perm("dcim.change_devicebay") or request.user.has_perm("dcim.delete_devicebay"):
2397
- devicebay_table.columns.show("pk")
2857
+ @action(
2858
+ detail=True,
2859
+ url_path="front-ports",
2860
+ url_name="frontports",
2861
+ custom_view_base_action="view",
2862
+ custom_view_additional_permissions=["dcim.view_frontport"],
2863
+ )
2864
+ def front_ports(self, request, *args, **kwargs):
2865
+ return Response({})
2398
2866
 
2399
- return {
2400
- **super().get_extra_context(request, instance),
2401
- "devicebay_table": devicebay_table,
2402
- "active_tab": "device-bays",
2403
- }
2867
+ @action(
2868
+ detail=True,
2869
+ url_path="interfaces",
2870
+ url_name="interfaces",
2871
+ custom_view_base_action="view",
2872
+ custom_view_additional_permissions=["dcim.view_interface"],
2873
+ )
2874
+ def interfaces(self, request, *args, **kwargs):
2875
+ return Response({})
2404
2876
 
2877
+ @action(
2878
+ detail=True,
2879
+ url_path="inventory",
2880
+ url_name="inventory",
2881
+ custom_view_base_action="view",
2882
+ custom_view_additional_permissions=["dcim.view_inventoryitem"],
2883
+ )
2884
+ def inventory(self, request, *args, **kwargs):
2885
+ return Response({})
2405
2886
 
2406
- class DeviceModuleBaysView(DeviceComponentTabView):
2407
- queryset = Device.objects.all()
2408
- template_name = "dcim/device/modulebays.html"
2887
+ @action(
2888
+ detail=True,
2889
+ url_path="module-bays",
2890
+ url_name="modulebays",
2891
+ custom_view_base_action="view",
2892
+ custom_view_additional_permissions=["dcim.view_modulebay"],
2893
+ )
2894
+ def module_bays(self, request, *args, **kwargs):
2895
+ return Response({})
2409
2896
 
2410
- def get_extra_context(self, request, instance):
2411
- # note: Device modules tab shouldn't show descendant modules until a proper tree view is implemented
2412
- modulebays = (
2413
- ModuleBay.objects.restrict(request.user, "view")
2414
- .filter(parent_device=instance)
2415
- .prefetch_related("installed_module__status", "installed_module")
2416
- )
2417
- modulebay_table = tables.DeviceModuleBayTable(data=modulebays, user=request.user, orderable=False)
2418
- if request.user.has_perm("dcim.change_modulebay") or request.user.has_perm("dcim.delete_modulebay"):
2419
- modulebay_table.columns.show("pk")
2897
+ @action(
2898
+ detail=True,
2899
+ url_path="power-outlets",
2900
+ url_name="poweroutlets",
2901
+ custom_view_base_action="view",
2902
+ custom_view_additional_permissions=["dcim.view_poweroutlet"],
2903
+ )
2904
+ def power_outlets(self, request, *args, **kwargs):
2905
+ return Response({})
2420
2906
 
2421
- return {
2422
- **super().get_extra_context(request, instance),
2423
- "modulebay_table": modulebay_table,
2424
- "active_tab": "module-bays",
2425
- }
2907
+ @action(
2908
+ detail=True,
2909
+ url_path="power-ports",
2910
+ url_name="powerports",
2911
+ custom_view_base_action="view",
2912
+ custom_view_additional_permissions=["dcim.view_powerport"],
2913
+ )
2914
+ def power_ports(self, request, *args, **kwargs):
2915
+ return Response({})
2426
2916
 
2917
+ @action(
2918
+ detail=True,
2919
+ url_path="rear-ports",
2920
+ url_name="rearports",
2921
+ custom_view_base_action="view",
2922
+ custom_view_additional_permissions=["dcim.view_rearport"],
2923
+ )
2924
+ def rear_ports(self, request, *args, **kwargs):
2925
+ return Response({})
2427
2926
 
2428
- class DeviceInventoryView(generic.ObjectView):
2429
- queryset = Device.objects.all()
2430
- template_name = "dcim/device/inventory.html"
2927
+ @action(
2928
+ detail=True,
2929
+ url_path="wireless",
2930
+ url_name="wireless",
2931
+ custom_view_base_action="view",
2932
+ custom_view_additional_permissions=["dcim.view_controllermanageddevicegroup"],
2933
+ )
2934
+ def wireless(self, request, *args, **kwargs):
2935
+ return Response({})
2431
2936
 
2432
- def get_extra_context(self, request, instance):
2433
- inventoryitems = (
2434
- InventoryItem.objects.restrict(request.user, "view").filter(device=instance).select_related("manufacturer")
2937
+ @action(
2938
+ detail=True,
2939
+ url_path="status",
2940
+ url_name="status",
2941
+ custom_view_base_action="view",
2942
+ custom_view_additional_permissions=["dcim.napalm_read_device"],
2943
+ )
2944
+ def status(self, request, *args, **kwargs):
2945
+ return Response(
2946
+ {
2947
+ "template": "dcim/device/status.html",
2948
+ },
2435
2949
  )
2436
- inventoryitem_table = tables.DeviceInventoryItemTable(data=inventoryitems, user=request.user, orderable=False)
2437
- if request.user.has_perm("dcim.change_inventoryitem") or request.user.has_perm("dcim.delete_inventoryitem"):
2438
- inventoryitem_table.columns.show("pk")
2439
-
2440
- return {
2441
- "inventoryitem_table": inventoryitem_table,
2442
- "active_tab": "inventory",
2443
- }
2444
-
2445
-
2446
- class DeviceStatusView(generic.ObjectView):
2447
- additional_permissions = ["dcim.napalm_read_device"]
2448
- queryset = Device.objects.all()
2449
- template_name = "dcim/device/status.html"
2450
2950
 
2451
- def get_extra_context(self, request, instance):
2452
- return {
2453
- "active_tab": "status",
2454
- }
2455
-
2456
-
2457
- class DeviceLLDPNeighborsView(generic.ObjectView):
2458
- additional_permissions = ["dcim.napalm_read_device"]
2459
- queryset = Device.objects.all()
2460
- template_name = "dcim/device/lldp_neighbors.html"
2461
-
2462
- def get_extra_context(self, request, instance):
2951
+ @action(
2952
+ detail=True,
2953
+ url_path="lldp-neighbors",
2954
+ url_name="lldp_neighbors",
2955
+ custom_view_base_action="view",
2956
+ custom_view_additional_permissions=["dcim.napalm_read_device"],
2957
+ )
2958
+ def lldp_neighbors(self, request, *args, **kwargs):
2959
+ instance = self.get_object()
2463
2960
  interfaces = (
2464
2961
  instance.all_interfaces.restrict(request.user, "view")
2465
2962
  .prefetch_related("_path__destination")
2466
2963
  .exclude(type__in=NONCONNECTABLE_IFACE_TYPES)
2467
2964
  )
2965
+ return Response(
2966
+ {
2967
+ "template": "dcim/device/lldp_neighbors.html",
2968
+ "interfaces": interfaces,
2969
+ },
2970
+ )
2468
2971
 
2469
- return {
2470
- "interfaces": interfaces,
2471
- "active_tab": "lldp-neighbors",
2472
- }
2473
-
2474
-
2475
- class DeviceConfigView(generic.ObjectView):
2476
- additional_permissions = ["dcim.napalm_read_device"]
2477
- queryset = Device.objects.all()
2478
- template_name = "dcim/device/config.html"
2479
-
2480
- def get_extra_context(self, request, instance):
2481
- return {
2482
- "active_tab": "config",
2483
- }
2484
-
2485
-
2486
- class DeviceConfigContextView(DevicePageMixin, ObjectConfigContextView):
2487
- base_template = "dcim/device/base.html"
2488
-
2489
- @cached_property
2490
- def queryset(self): # pylint: disable=method-hidden
2491
- """
2492
- A cached_property rather than a class attribute because annotate_config_context_data() is unsafe at import time.
2493
- """
2494
- return Device.objects.annotate_config_context_data()
2495
-
2496
-
2497
- class DeviceChangeLogView(DevicePageMixin, ObjectChangeLogView):
2498
- base_template = "dcim/device/base.html"
2499
-
2500
-
2501
- class DeviceDynamicGroupsView(ObjectDynamicGroupsView):
2502
- base_template = "dcim/device/base.html"
2503
-
2504
-
2505
- class DeviceEditView(generic.ObjectEditView):
2506
- queryset = Device.objects.all()
2507
- model_form = forms.DeviceForm
2508
- template_name = "dcim/device_edit.html"
2509
-
2510
-
2511
- class DeviceDeleteView(generic.ObjectDeleteView):
2512
- queryset = Device.objects.all()
2513
-
2514
-
2515
- class DeviceBulkImportView(generic.BulkImportView): # 3.0 TODO: remove, unused
2516
- queryset = Device.objects.all()
2517
- table = tables.DeviceImportTable
2518
-
2519
-
2520
- class DeviceBulkEditView(generic.BulkEditView):
2521
- queryset = Device.objects.select_related(
2522
- "tenant",
2523
- "location",
2524
- "rack",
2525
- "role",
2526
- "device_type__manufacturer",
2527
- "secrets_group",
2528
- "device_redundancy_group",
2529
- "controller_managed_device_group",
2972
+ @action(
2973
+ detail=True,
2974
+ url_path="config",
2975
+ url_name="config",
2976
+ custom_view_base_action="view",
2977
+ custom_view_additional_permissions=["dcim.napalm_read_device"],
2530
2978
  )
2531
- filterset = filters.DeviceFilterSet
2532
- table = tables.DeviceTable
2533
- form = forms.DeviceBulkEditForm
2534
-
2535
-
2536
- class DeviceBulkDeleteView(generic.BulkDeleteView):
2537
- queryset = Device.objects.select_related("tenant", "location", "rack", "role", "device_type__manufacturer")
2538
- filterset = filters.DeviceFilterSet
2539
- table = tables.DeviceTable
2540
-
2541
-
2542
- class DeviceWirelessView(generic.ObjectView):
2543
- queryset = Device.objects.all()
2544
- template_name = "dcim/device/wireless.html"
2545
-
2546
- def get_extra_context(self, request, instance):
2547
- controller_managed_device_group = instance.controller_managed_device_group
2548
- wireless_networks = ControllerManagedDeviceGroupWirelessNetworkAssignment.objects.filter(
2549
- controller_managed_device_group=controller_managed_device_group
2550
- ).select_related("wireless_network", "controller_managed_device_group", "vlan")
2551
- wireless_networks_table = BaseControllerManagedDeviceGroupWirelessNetworkAssignmentTable(
2552
- data=wireless_networks, user=request.user, orderable=False
2553
- )
2554
- wireless_networks_table.columns.hide("controller_managed_device_group")
2555
- wireless_networks_table.columns.hide("controller")
2556
- RequestConfig(
2557
- request, paginate={"paginator_class": EnhancedPaginator, "per_page": get_paginate_count(request)}
2558
- ).configure(wireless_networks_table)
2559
-
2560
- radio_profiles = ControllerManagedDeviceGroupRadioProfileAssignment.objects.filter(
2561
- controller_managed_device_group=controller_managed_device_group
2562
- ).select_related("radio_profile", "controller_managed_device_group")
2563
- radio_profiles_table = ControllerManagedDeviceGroupRadioProfileAssignmentTable(
2564
- data=radio_profiles, user=request.user, orderable=False
2979
+ def config(self, request, *args, **kwargs):
2980
+ return Response(
2981
+ {
2982
+ "template": "dcim/device/config.html",
2983
+ },
2565
2984
  )
2566
- radio_profiles_table.columns.hide("controller_managed_device_group")
2567
- RequestConfig(
2568
- request, paginate={"paginator_class": EnhancedPaginator, "per_page": get_paginate_count(request)}
2569
- ).configure(radio_profiles_table)
2570
2985
 
2571
- return {
2572
- "wireless_networks_table": wireless_networks_table,
2573
- "radio_profiles_table": radio_profiles_table,
2574
- "active_tab": "wireless",
2986
+ @action(
2987
+ detail=True,
2988
+ url_path="config-context",
2989
+ url_name="configcontext",
2990
+ custom_view_base_action="view",
2991
+ custom_view_additional_permissions=["extras.view_configcontext"],
2992
+ )
2993
+ def config_context(self, request, *args, **kwargs):
2994
+ instance = self.get_object()
2995
+
2996
+ # Determine user's preferred output format
2997
+ if request.GET.get("data_format") in ["json", "yaml"]:
2998
+ data_format = request.GET.get("data_format")
2999
+ if request.user.is_authenticated:
3000
+ request.user.set_config("extras.configcontext.format", data_format, commit=True)
3001
+ elif request.user.is_authenticated:
3002
+ data_format = request.user.get_config("extras.configcontext.format", "json")
3003
+ else:
3004
+ data_format = "json"
3005
+
3006
+ context = {
3007
+ "object": instance,
3008
+ "content_type": ContentType.objects.get_for_model(self.queryset.model),
3009
+ "verbose_name": self.queryset.model._meta.verbose_name,
3010
+ "verbose_name_plural": self.queryset.model._meta.verbose_name_plural,
3011
+ "object_detail_content": self.object_detail_content,
3012
+ **common_detail_view_context(request, instance),
3013
+ "rendered_context": instance.get_config_context(),
3014
+ "source_contexts": ConfigContext.objects.restrict(request.user, "view").get_for_object(instance),
3015
+ "format": data_format,
3016
+ "template": "extras/object_configcontext.html",
3017
+ "base_template": "dcim/device.html",
2575
3018
  }
2576
3019
 
3020
+ return Response(context)
3021
+
2577
3022
 
2578
3023
  #
2579
3024
  # Modules
@@ -3743,7 +4188,8 @@ class ModuleBayUIViewSet(ModuleBayCommonViewSetMixin, NautobotUIViewSet):
3743
4188
  # Breadcrumb path if ModuleBay is linked with device
3744
4189
  ModelBreadcrumbItem(model=Device, should_render=lambda c: c["object"].parent_device),
3745
4190
  InstanceBreadcrumbItem(
3746
- instance=lambda c: c["object"].parent_device, should_render=lambda c: c["object"].parent_device
4191
+ instance=context_object_attr("parent_device"),
4192
+ should_render=context_object_attr("parent_device"),
3747
4193
  ),
3748
4194
  ViewNameBreadcrumbItem(
3749
4195
  view_name_key="device_breadcrumb_url",
@@ -3754,7 +4200,7 @@ class ModuleBayUIViewSet(ModuleBayCommonViewSetMixin, NautobotUIViewSet):
3754
4200
  # Breadcrumb path if ModuleBay is linked with module
3755
4201
  ModelBreadcrumbItem(model=Module, should_render=lambda c: c["object"].parent_device is None),
3756
4202
  InstanceBreadcrumbItem(
3757
- instance=lambda c: c["object"].parent_module,
4203
+ instance=context_object_attr("parent_module"),
3758
4204
  should_render=lambda c: c["object"].parent_device is None,
3759
4205
  ),
3760
4206
  ViewNameBreadcrumbItem(
@@ -4476,6 +4922,17 @@ class PowerPanelUIViewSet(NautobotUIViewSet):
4476
4922
  serializer_class = serializers.PowerPanelSerializer
4477
4923
  table_class = tables.PowerPanelTable
4478
4924
  queryset = PowerPanel.objects.all()
4925
+ breadcrumbs = Breadcrumbs(
4926
+ items={
4927
+ "detail": [
4928
+ ModelBreadcrumbItem(),
4929
+ InstanceBreadcrumbItem(instance=context_object_attr("location")),
4930
+ InstanceBreadcrumbItem(
4931
+ instance=context_object_attr("rack_group"), should_render=context_object_attr("rack_group")
4932
+ ),
4933
+ ]
4934
+ }
4935
+ )
4479
4936
 
4480
4937
  object_detail_content = object_detail.ObjectDetailContent(
4481
4938
  panels=(
@@ -4550,6 +5007,19 @@ class PowerFeedUIViewSet(NautobotUIViewSet):
4550
5007
  queryset = PowerFeed.objects.all()
4551
5008
  serializer_class = serializers.PowerFeedSerializer
4552
5009
  table_class = tables.PowerFeedTable
5010
+ breadcrumbs = Breadcrumbs(
5011
+ items={
5012
+ "detail": [
5013
+ ModelBreadcrumbItem(),
5014
+ InstanceBreadcrumbItem(instance=context_object_attr("power_panel.location")),
5015
+ InstanceBreadcrumbItem(instance=context_object_attr("power_panel")),
5016
+ InstanceBreadcrumbItem(
5017
+ instance=context_object_attr("rack"),
5018
+ should_render=context_object_attr("rack"),
5019
+ ),
5020
+ ]
5021
+ }
5022
+ )
4553
5023
 
4554
5024
  object_detail_content = object_detail.ObjectDetailContent(
4555
5025
  panels=(
@@ -5266,6 +5736,14 @@ class VirtualDeviceContextUIViewSet(NautobotUIViewSet):
5266
5736
  queryset = VirtualDeviceContext.objects.all()
5267
5737
  serializer_class = serializers.VirtualDeviceContextSerializer
5268
5738
  table_class = tables.VirtualDeviceContextTable
5739
+ breadcrumbs = Breadcrumbs(
5740
+ items={
5741
+ "detail": [
5742
+ ModelBreadcrumbItem(),
5743
+ InstanceBreadcrumbItem(instance=context_object_attr("device")),
5744
+ ]
5745
+ }
5746
+ )
5269
5747
  object_detail_content = object_detail.ObjectDetailContent(
5270
5748
  panels=(
5271
5749
  object_detail.ObjectFieldsPanel(