nautobot 2.4.17__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 (627) hide show
  1. nautobot/apps/ui.py +6 -0
  2. nautobot/apps/views.py +2 -0
  3. nautobot/circuits/tables.py +1 -1
  4. nautobot/circuits/templates/circuits/circuit_create.html +7 -7
  5. nautobot/circuits/templates/circuits/circuit_retrieve.html +1 -5
  6. nautobot/circuits/templates/circuits/circuittermination_create.html +26 -26
  7. nautobot/circuits/templates/circuits/circuittermination_retrieve.html +1 -8
  8. nautobot/circuits/templates/circuits/inc/circuit_termination.html +20 -20
  9. nautobot/circuits/templates/circuits/inc/circuit_termination_header_extra_content.html +3 -3
  10. nautobot/circuits/templates/circuits/inc/circuit_termination_speed_fragment.html +9 -0
  11. nautobot/circuits/templates/circuits/providernetwork_retrieve.html +1 -3
  12. nautobot/circuits/tests/integration/test_circuit.py +2 -2
  13. nautobot/circuits/views.py +49 -15
  14. nautobot/cloud/templates/cloud/cloudaccount_retrieve.html +1 -4
  15. nautobot/cloud/templates/cloud/cloudnetwork_retrieve.html +1 -7
  16. nautobot/cloud/templates/cloud/cloudresourcetype_retrieve.html +1 -3
  17. nautobot/cloud/templates/cloud/cloudservice_retrieve.html +1 -5
  18. nautobot/cloud/views.py +45 -0
  19. nautobot/core/filters.py +2 -2
  20. nautobot/core/graphql/generators.py +5 -2
  21. nautobot/core/jobs/bulk_actions.py +48 -85
  22. nautobot/core/models/querysets.py +2 -1
  23. nautobot/core/settings.py +1 -0
  24. nautobot/core/settings.yaml +9 -0
  25. nautobot/core/tables.py +21 -23
  26. nautobot/core/templates/40x.html +15 -15
  27. nautobot/core/templates/500.html +21 -21
  28. nautobot/core/templates/admin/app_index.html +8 -8
  29. nautobot/core/templates/admin/base.html +104 -104
  30. nautobot/core/templates/admin/change_form.html +65 -65
  31. nautobot/core/templates/admin/change_list.html +60 -60
  32. nautobot/core/templates/admin/change_list_results.html +39 -39
  33. nautobot/core/templates/admin/config/config.html +47 -47
  34. nautobot/core/templates/admin/delete_confirmation.html +47 -47
  35. nautobot/core/templates/admin/edit_inline/stacked.html +124 -124
  36. nautobot/core/templates/admin/edit_inline/tabular.html +60 -60
  37. nautobot/core/templates/admin/includes/fieldset.html +4 -4
  38. nautobot/core/templates/admin/index.html +60 -60
  39. nautobot/core/templates/admin/prepopulated_fields_js.html +18 -18
  40. nautobot/core/templates/admin/submit_line.html +4 -4
  41. nautobot/core/templates/base_django.html +46 -46
  42. nautobot/core/templates/buttons/consolidated_bulk_action_buttons.html +8 -8
  43. nautobot/core/templates/buttons/consolidated_detail_view_action_buttons.html +8 -8
  44. nautobot/core/templates/buttons/export.html +3 -3
  45. nautobot/core/templates/components/breadcrumbs.html +19 -0
  46. nautobot/core/templates/components/button/default.html +3 -3
  47. nautobot/core/templates/components/button/dropdown.html +7 -7
  48. nautobot/core/templates/components/button/formbutton.html +4 -4
  49. nautobot/core/templates/components/panel/body_content_data_table.html +1 -1
  50. nautobot/core/templates/components/panel/body_wrapper_generic_table.html +3 -0
  51. nautobot/core/templates/components/panel/footer_content_table.html +3 -1
  52. nautobot/core/templates/components/panel/header_extra_content_table.html +10 -1
  53. nautobot/core/templates/components/tab/content_wrapper.html +1 -1
  54. nautobot/core/templates/components/tab/label_wrapper.html +1 -1
  55. nautobot/core/templates/components/tab/label_wrapper_distinct_view.html +10 -3
  56. nautobot/core/templates/generic/object_bulk_add_component.html +40 -40
  57. nautobot/core/templates/generic/object_bulk_create.html +3 -3
  58. nautobot/core/templates/generic/object_bulk_destroy.html +6 -6
  59. nautobot/core/templates/generic/object_bulk_update.html +52 -52
  60. nautobot/core/templates/generic/object_changelog.html +0 -2
  61. nautobot/core/templates/generic/object_import.html +33 -33
  62. nautobot/core/templates/generic/object_list.html +271 -268
  63. nautobot/core/templates/generic/object_notes.html +0 -2
  64. nautobot/core/templates/generic/object_retrieve.html +264 -257
  65. nautobot/core/templates/graphene/graphiql.html +127 -127
  66. nautobot/core/templates/home.html +62 -62
  67. nautobot/core/templates/inc/computed_fields/panel_data.html +13 -13
  68. nautobot/core/templates/inc/created_updated.html +8 -8
  69. nautobot/core/templates/inc/custom_fields/panel_data.html +13 -13
  70. nautobot/core/templates/inc/dynamic_groups_panel.html +11 -11
  71. nautobot/core/templates/inc/footer.html +19 -19
  72. nautobot/core/templates/inc/javascript.html +1 -1
  73. nautobot/core/templates/inc/media.html +46 -46
  74. nautobot/core/templates/inc/nav_menu.html +1 -1
  75. nautobot/core/templates/inc/relationships_table_rows.html +22 -22
  76. nautobot/core/templates/inc/tenant_table_row.html +1 -1
  77. nautobot/core/templates/login.html +77 -77
  78. nautobot/core/templates/media_failure.html +38 -38
  79. nautobot/core/templates/panel_table.html +1 -1
  80. nautobot/core/templates/rest_framework/api.html +3 -3
  81. nautobot/core/templates/search.html +1 -1
  82. nautobot/core/templates/swagger_ui.html +9 -9
  83. nautobot/core/templates/utilities/confirmation_form.html +18 -18
  84. nautobot/core/templates/utilities/render_field.html +1 -1
  85. nautobot/core/templates/utilities/render_jinja2.html +43 -43
  86. nautobot/core/templates/utilities/templatetags/filter_form_modal.html +56 -56
  87. nautobot/core/templates/utilities/templatetags/utilization_graph.html +1 -1
  88. nautobot/core/templates/utilities/theme_preview.html +799 -799
  89. nautobot/core/templates/utilities/worker_status.html +122 -122
  90. nautobot/core/templates/widgets/clearable_file.html +3 -3
  91. nautobot/core/templates/widgets/sluginput.html +1 -1
  92. nautobot/core/templatetags/buttons.py +8 -2
  93. nautobot/core/templatetags/helpers.py +24 -0
  94. nautobot/core/templatetags/ui_framework.py +40 -5
  95. nautobot/core/testing/filters.py +37 -21
  96. nautobot/core/testing/integration.py +7 -4
  97. nautobot/core/testing/views.py +49 -5
  98. nautobot/core/tests/test_breadcrumbs.py +78 -4
  99. nautobot/core/tests/test_commands.py +7 -4
  100. nautobot/core/tests/test_graphql.py +20 -5
  101. nautobot/core/tests/test_jobs.py +34 -21
  102. nautobot/core/tests/test_tables.py +43 -6
  103. nautobot/core/tests/test_templatetags_ui_framework.py +146 -0
  104. nautobot/core/tests/test_titles.py +2 -2
  105. nautobot/core/tests/test_ui.py +188 -1
  106. nautobot/core/tests/test_utils.py +35 -0
  107. nautobot/core/tests/test_views.py +45 -0
  108. nautobot/core/tests/test_views_generic.py +43 -0
  109. nautobot/core/tests/test_views_utils.py +239 -5
  110. nautobot/core/ui/breadcrumbs.py +220 -28
  111. nautobot/core/ui/bulk_buttons.py +8 -0
  112. nautobot/core/ui/object_detail.py +181 -60
  113. nautobot/core/ui/titles.py +10 -5
  114. nautobot/core/utils/requests.py +27 -2
  115. nautobot/core/views/__init__.py +24 -3
  116. nautobot/core/views/generic.py +70 -35
  117. nautobot/core/views/mixins.py +226 -122
  118. nautobot/core/views/utils.py +270 -1
  119. nautobot/dcim/api/serializers.py +8 -2
  120. nautobot/dcim/constants.py +1 -0
  121. nautobot/dcim/factory.py +4 -3
  122. nautobot/dcim/filters/mixins.py +1 -2
  123. nautobot/dcim/forms.py +5 -1
  124. nautobot/dcim/migrations/0074_alter_rack_u_height.py +21 -0
  125. nautobot/dcim/models/devices.py +30 -1
  126. nautobot/dcim/models/racks.py +2 -2
  127. nautobot/dcim/tables/__init__.py +2 -0
  128. nautobot/dcim/tables/devices.py +24 -0
  129. nautobot/dcim/tables/power.py +2 -2
  130. nautobot/dcim/templates/dcim/cable.html +53 -53
  131. nautobot/dcim/templates/dcim/cable_connect.html +182 -182
  132. nautobot/dcim/templates/dcim/cable_trace.html +1 -1
  133. nautobot/dcim/templates/dcim/console_port_connection_list.html +5 -5
  134. nautobot/dcim/templates/dcim/consoleport.html +86 -86
  135. nautobot/dcim/templates/dcim/consoleserverport.html +86 -86
  136. nautobot/dcim/templates/dcim/controller_create.html +34 -34
  137. nautobot/dcim/templates/dcim/controllermanageddevicegroup_create.html +68 -68
  138. nautobot/dcim/templates/dcim/device/base.html +1 -114
  139. nautobot/dcim/templates/dcim/device/config.html +17 -17
  140. nautobot/dcim/templates/dcim/device/consoleports.html +1 -52
  141. nautobot/dcim/templates/dcim/device/consoleserverports.html +1 -52
  142. nautobot/dcim/templates/dcim/device/devicebays.html +1 -48
  143. nautobot/dcim/templates/dcim/device/frontports.html +1 -52
  144. nautobot/dcim/templates/dcim/device/interfaces.html +1 -56
  145. nautobot/dcim/templates/dcim/device/inventory.html +1 -48
  146. nautobot/dcim/templates/dcim/device/lldp_neighbors.html +64 -64
  147. nautobot/dcim/templates/dcim/device/modulebays.html +1 -48
  148. nautobot/dcim/templates/dcim/device/poweroutlets.html +1 -52
  149. nautobot/dcim/templates/dcim/device/powerports.html +1 -52
  150. nautobot/dcim/templates/dcim/device/rearports.html +1 -52
  151. nautobot/dcim/templates/dcim/device/status.html +66 -66
  152. nautobot/dcim/templates/dcim/device/wireless.html +1 -72
  153. nautobot/dcim/templates/dcim/device.html +4 -422
  154. nautobot/dcim/templates/dcim/device_component.html +0 -19
  155. nautobot/dcim/templates/dcim/device_component_add.html +25 -25
  156. nautobot/dcim/templates/dcim/device_create.html +229 -0
  157. nautobot/dcim/templates/dcim/device_edit.html +2 -227
  158. nautobot/dcim/templates/dcim/devicebay.html +41 -41
  159. nautobot/dcim/templates/dcim/devicebay_populate.html +32 -32
  160. nautobot/dcim/templates/dcim/devicetype_component_add.html +28 -28
  161. nautobot/dcim/templates/dcim/devicetype_retrieve.html +1 -3
  162. nautobot/dcim/templates/dcim/frontport.html +84 -84
  163. nautobot/dcim/templates/dcim/inc/cable_toggle_buttons.html +1 -1
  164. nautobot/dcim/templates/dcim/inc/device_interface_filter.html +8 -0
  165. nautobot/dcim/templates/dcim/inc/device_napalm_tabs.html +1 -15
  166. nautobot/dcim/templates/dcim/inc/location_hierarchy.html +22 -22
  167. nautobot/dcim/templates/dcim/interface.html +206 -206
  168. nautobot/dcim/templates/dcim/interface_connection_list.html +5 -5
  169. nautobot/dcim/templates/dcim/interfaceredundancygroupassociation_create.html +6 -6
  170. nautobot/dcim/templates/dcim/inventoryitem.html +44 -44
  171. nautobot/dcim/templates/dcim/inventoryitem_add.html +32 -32
  172. nautobot/dcim/templates/dcim/inventoryitem_edit.html +22 -22
  173. nautobot/dcim/templates/dcim/location_migrate_data_to_contact.html +46 -46
  174. nautobot/dcim/templates/dcim/location_retrieve.html +1 -7
  175. nautobot/dcim/templates/dcim/locationtype.html +1 -6
  176. nautobot/dcim/templates/dcim/locationtype_retrieve.html +1 -7
  177. nautobot/dcim/templates/dcim/module/base.html +85 -85
  178. nautobot/dcim/templates/dcim/module_interfaces.html +1 -1
  179. nautobot/dcim/templates/dcim/module_modulebays.html +1 -1
  180. nautobot/dcim/templates/dcim/module_retrieve.html +52 -52
  181. nautobot/dcim/templates/dcim/module_update.html +61 -61
  182. nautobot/dcim/templates/dcim/modulebay_destroy.html +1 -1
  183. nautobot/dcim/templates/dcim/modulebay_retrieve.html +83 -99
  184. nautobot/dcim/templates/dcim/modulebay_update.html +33 -33
  185. nautobot/dcim/templates/dcim/modulefamily_retrieve.html +1 -1
  186. nautobot/dcim/templates/dcim/moduletype_retrieve.html +140 -144
  187. nautobot/dcim/templates/dcim/platform_create.html +38 -38
  188. nautobot/dcim/templates/dcim/power_port_connection_list.html +5 -5
  189. nautobot/dcim/templates/dcim/powerfeed_retrieve.html +1 -8
  190. nautobot/dcim/templates/dcim/poweroutlet.html +85 -85
  191. nautobot/dcim/templates/dcim/powerpanel_retrieve.html +1 -8
  192. nautobot/dcim/templates/dcim/powerport.html +91 -91
  193. nautobot/dcim/templates/dcim/rack_elevation_list.html +18 -18
  194. nautobot/dcim/templates/dcim/rack_retrieve.html +264 -274
  195. nautobot/dcim/templates/dcim/rackreservation_retrieve.html +0 -3
  196. nautobot/dcim/templates/dcim/rearport.html +78 -78
  197. nautobot/dcim/templates/dcim/virtualchassis_retrieve.html +1 -50
  198. nautobot/dcim/templates/dcim/virtualdevicecontext_retrieve.html +1 -5
  199. nautobot/dcim/tests/integration/test_device_bulk_operations.py +3 -2
  200. nautobot/dcim/tests/integration/test_location_bulk_operations.py +6 -2
  201. nautobot/dcim/tests/test_api.py +33 -1
  202. nautobot/dcim/tests/test_views.py +189 -4
  203. nautobot/dcim/ui.py +29 -0
  204. nautobot/dcim/urls.py +1 -109
  205. nautobot/dcim/utils.py +30 -0
  206. nautobot/dcim/views.py +1149 -550
  207. nautobot/extras/filters/mixins.py +1 -1
  208. nautobot/extras/forms/forms.py +15 -0
  209. nautobot/extras/models/groups.py +10 -1
  210. nautobot/extras/models/jobs.py +2 -2
  211. nautobot/extras/plugins/views.py +18 -5
  212. nautobot/extras/tables.py +24 -2
  213. nautobot/extras/templates/extras/computedfield_edit.html +4 -4
  214. nautobot/extras/templates/extras/configcontext_update.html +1 -1
  215. nautobot/extras/templates/extras/configcontextschema_retrieve.html +32 -32
  216. nautobot/extras/templates/extras/customfield_retrieve.html +1 -128
  217. nautobot/extras/templates/extras/customfield_update.html +23 -23
  218. nautobot/extras/templates/extras/dynamicgroup.html +2 -99
  219. nautobot/extras/templates/extras/dynamicgroup_edit.html +2 -199
  220. nautobot/extras/templates/extras/dynamicgroup_retrieve.html +99 -0
  221. nautobot/extras/templates/extras/dynamicgroup_update.html +199 -0
  222. nautobot/extras/templates/extras/gitrepository.html +2 -82
  223. nautobot/extras/templates/extras/gitrepository_list.html +10 -10
  224. nautobot/extras/templates/extras/gitrepository_object_edit.html +2 -13
  225. nautobot/extras/templates/extras/gitrepository_retrieve.html +82 -0
  226. nautobot/extras/templates/extras/gitrepository_update.html +13 -0
  227. nautobot/extras/templates/extras/graphqlquery_retrieve.html +73 -73
  228. nautobot/extras/templates/extras/inc/configcontext_format.html +2 -2
  229. nautobot/extras/templates/extras/inc/job_table.html +10 -10
  230. nautobot/extras/templates/extras/inc/jobresult.html +21 -21
  231. nautobot/extras/templates/extras/inc/jobresult_js.html +6 -6
  232. nautobot/extras/templates/extras/inc/tags_panel.html +10 -10
  233. nautobot/extras/templates/extras/job.html +64 -64
  234. nautobot/extras/templates/extras/job_approval_request.html +9 -9
  235. nautobot/extras/templates/extras/job_bulk_edit.html +13 -13
  236. nautobot/extras/templates/extras/job_edit.html +45 -45
  237. nautobot/extras/templates/extras/job_list.html +4 -4
  238. nautobot/extras/templates/extras/jobresult_retrieve.html +0 -25
  239. nautobot/extras/templates/extras/marketplace.html +101 -101
  240. nautobot/extras/templates/extras/metadatatype_create.html +20 -20
  241. nautobot/extras/templates/extras/note_retrieve.html +0 -52
  242. nautobot/extras/templates/extras/object_assign_contact_or_team.html +18 -18
  243. nautobot/extras/templates/extras/object_configcontext.html +1 -3
  244. nautobot/extras/templates/extras/objectchange.html +2 -165
  245. nautobot/extras/templates/extras/objectchange_retrieve.html +165 -0
  246. nautobot/extras/templates/extras/plugin_detail.html +44 -48
  247. nautobot/extras/templates/extras/plugins_list.html +9 -11
  248. nautobot/extras/templates/extras/plugins_tiles.html +26 -26
  249. nautobot/extras/templates/extras/relationship_edit.html +4 -4
  250. nautobot/extras/templates/extras/role_retrieve.html +13 -13
  251. nautobot/extras/templates/extras/scheduled_jobs_approval_queue_list.html +21 -21
  252. nautobot/extras/templates/extras/scheduledjob.html +128 -128
  253. nautobot/extras/templates/extras/secret_create.html +53 -53
  254. nautobot/extras/templates/extras/secretsgroup_update.html +13 -13
  255. nautobot/extras/templates/extras/templatetags/plugin_object_detail_tabs.html +3 -3
  256. nautobot/extras/templates/extras/webhook.html +79 -79
  257. nautobot/extras/tests/integration/test_relationships.py +6 -6
  258. nautobot/extras/tests/test_dynamicgroups.py +73 -18
  259. nautobot/extras/tests/test_filters.py +1 -1
  260. nautobot/extras/tests/test_jobs.py +2 -0
  261. nautobot/extras/tests/test_views.py +8 -3
  262. nautobot/extras/urls.py +3 -97
  263. nautobot/extras/views.py +524 -456
  264. nautobot/ipam/filters.py +2 -2
  265. nautobot/ipam/migrations/0053_alter_vrfdeviceassignment_options_and_more.py +20 -0
  266. nautobot/ipam/models.py +34 -0
  267. nautobot/ipam/querysets.py +3 -3
  268. nautobot/ipam/signals.py +6 -1
  269. nautobot/ipam/tables.py +3 -1
  270. nautobot/ipam/templates/ipam/inc/prefix_header_extra_content_table.html +4 -0
  271. nautobot/ipam/templates/ipam/inc/toggle_available.html +8 -8
  272. nautobot/ipam/templates/ipam/inc/vlangroup_header.html +4 -4
  273. nautobot/ipam/templates/ipam/ipaddress.html +119 -123
  274. nautobot/ipam/templates/ipam/ipaddress_assign.html +10 -10
  275. nautobot/ipam/templates/ipam/ipaddress_edit.html +1 -1
  276. nautobot/ipam/templates/ipam/ipaddress_merge.html +180 -180
  277. nautobot/ipam/templates/ipam/ipaddresstointerface_retrieve.html +48 -48
  278. nautobot/ipam/templates/ipam/prefix.html +2 -115
  279. nautobot/ipam/templates/ipam/prefix_create.html +34 -0
  280. nautobot/ipam/templates/ipam/prefix_edit.html +1 -34
  281. nautobot/ipam/templates/ipam/prefix_retrieve.html +3 -0
  282. nautobot/ipam/templates/ipam/service_retrieve.html +1 -6
  283. nautobot/ipam/templates/ipam/vlan_retrieve.html +1 -7
  284. nautobot/ipam/templates/ipam/vrf_edit.html +1 -1
  285. nautobot/ipam/tests/test_api.py +5 -0
  286. nautobot/ipam/tests/test_models.py +387 -0
  287. nautobot/ipam/tests/test_querysets.py +46 -0
  288. nautobot/ipam/tests/test_views.py +34 -0
  289. nautobot/ipam/ui.py +145 -0
  290. nautobot/ipam/urls.py +1 -46
  291. nautobot/ipam/utils/__init__.py +26 -0
  292. nautobot/ipam/utils/migrations.py +1 -1
  293. nautobot/ipam/views.py +234 -112
  294. nautobot/project-static/docs/404.html +11 -11
  295. nautobot/project-static/docs/apps/index.html +11 -11
  296. nautobot/project-static/docs/apps/nautobot-apps.html +11 -11
  297. nautobot/project-static/docs/assets/javascripts/{bundle.92b07e13.min.js → bundle.f55a23d4.min.js} +2 -2
  298. nautobot/project-static/docs/assets/javascripts/{bundle.92b07e13.min.js.map → bundle.f55a23d4.min.js.map} +2 -2
  299. nautobot/project-static/docs/assets/stylesheets/{main.7e37652d.min.css → main.e53b48f4.min.css} +1 -1
  300. nautobot/project-static/docs/assets/stylesheets/{main.7e37652d.min.css.map → main.e53b48f4.min.css.map} +1 -1
  301. nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +11 -11
  302. nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +11 -11
  303. nautobot/project-static/docs/code-reference/nautobot/apps/api.html +11 -11
  304. nautobot/project-static/docs/code-reference/nautobot/apps/change_logging.html +11 -11
  305. nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +11 -11
  306. nautobot/project-static/docs/code-reference/nautobot/apps/config.html +11 -11
  307. nautobot/project-static/docs/code-reference/nautobot/apps/constants.html +11 -11
  308. nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +11 -11
  309. nautobot/project-static/docs/code-reference/nautobot/apps/events.html +11 -11
  310. nautobot/project-static/docs/code-reference/nautobot/apps/exceptions.html +11 -11
  311. nautobot/project-static/docs/code-reference/nautobot/apps/factory.html +11 -11
  312. nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +11 -11
  313. nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +11 -11
  314. nautobot/project-static/docs/code-reference/nautobot/apps/graphql.html +11 -11
  315. nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +11 -11
  316. nautobot/project-static/docs/code-reference/nautobot/apps/models.html +11 -11
  317. nautobot/project-static/docs/code-reference/nautobot/apps/querysets.html +11 -11
  318. nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +11 -11
  319. nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +11 -11
  320. nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +83 -11
  321. nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +1265 -281
  322. nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +11 -11
  323. nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +12 -12
  324. nautobot/project-static/docs/code-reference/nautobot/apps/views.html +452 -29
  325. nautobot/project-static/docs/development/apps/api/configuration-view.html +11 -11
  326. nautobot/project-static/docs/development/apps/api/database-backend-config.html +11 -11
  327. nautobot/project-static/docs/development/apps/api/models/django-admin.html +11 -11
  328. nautobot/project-static/docs/development/apps/api/models/global-search.html +11 -11
  329. nautobot/project-static/docs/development/apps/api/models/graphql.html +11 -11
  330. nautobot/project-static/docs/development/apps/api/models/index.html +11 -11
  331. nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +12 -12
  332. nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +11 -11
  333. nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +11 -11
  334. nautobot/project-static/docs/development/apps/api/platform-features/git-repository-content.html +11 -11
  335. nautobot/project-static/docs/development/apps/api/platform-features/index.html +11 -11
  336. nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +11 -11
  337. nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +11 -11
  338. nautobot/project-static/docs/development/apps/api/platform-features/prepopulating-data.html +11 -11
  339. nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +11 -11
  340. nautobot/project-static/docs/development/apps/api/platform-features/table-extensions.html +11 -11
  341. nautobot/project-static/docs/development/apps/api/platform-features/uniquely-identify-objects.html +11 -11
  342. nautobot/project-static/docs/development/apps/api/prometheus.html +11 -11
  343. nautobot/project-static/docs/development/apps/api/setup.html +11 -11
  344. nautobot/project-static/docs/development/apps/api/testing.html +11 -11
  345. nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +11 -11
  346. nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +11 -11
  347. nautobot/project-static/docs/development/apps/api/ui-extensions/index.html +11 -11
  348. nautobot/project-static/docs/development/apps/api/ui-extensions/navigation.html +11 -11
  349. nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +11 -11
  350. nautobot/project-static/docs/development/apps/api/views/base-template.html +11 -11
  351. nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +11 -11
  352. nautobot/project-static/docs/development/apps/api/views/django-generic-views.html +11 -11
  353. nautobot/project-static/docs/development/apps/api/views/help-documentation.html +11 -11
  354. nautobot/project-static/docs/development/apps/api/views/index.html +11 -11
  355. nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +11 -11
  356. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +11 -11
  357. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +11 -11
  358. nautobot/project-static/docs/development/apps/api/views/notes.html +11 -11
  359. nautobot/project-static/docs/development/apps/api/views/rest-api.html +11 -11
  360. nautobot/project-static/docs/development/apps/api/views/urls.html +11 -11
  361. nautobot/project-static/docs/development/apps/index.html +11 -11
  362. nautobot/project-static/docs/development/apps/migration/code-updates.html +11 -11
  363. nautobot/project-static/docs/development/apps/migration/dependency-updates.html +11 -11
  364. nautobot/project-static/docs/development/apps/migration/from-v1.html +11 -11
  365. nautobot/project-static/docs/development/apps/migration/model-updates/dcim.html +11 -11
  366. nautobot/project-static/docs/development/apps/migration/model-updates/extras.html +11 -11
  367. nautobot/project-static/docs/development/apps/migration/model-updates/global.html +11 -11
  368. nautobot/project-static/docs/development/apps/migration/model-updates/ipam.html +11 -11
  369. nautobot/project-static/docs/development/apps/migration/ui-component-framework/best-practices.html +11 -11
  370. nautobot/project-static/docs/development/apps/migration/ui-component-framework/breadcrumbs-titles.html +11 -11
  371. nautobot/project-static/docs/development/apps/migration/ui-component-framework/custom-content.html +11 -11
  372. nautobot/project-static/docs/development/apps/migration/ui-component-framework/index.html +11 -11
  373. nautobot/project-static/docs/development/apps/migration/ui-component-framework/migration-steps.html +11 -11
  374. nautobot/project-static/docs/development/apps/porting-from-netbox.html +11 -11
  375. nautobot/project-static/docs/development/core/application-registry.html +11 -11
  376. nautobot/project-static/docs/development/core/best-practices.html +11 -11
  377. nautobot/project-static/docs/development/core/bootstrap-ui.html +11 -11
  378. nautobot/project-static/docs/development/core/caching.html +11 -11
  379. nautobot/project-static/docs/development/core/controllers.html +11 -11
  380. nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +11 -11
  381. nautobot/project-static/docs/development/core/generic-views.html +11 -11
  382. nautobot/project-static/docs/development/core/getting-started.html +50 -63
  383. nautobot/project-static/docs/development/core/homepage.html +11 -11
  384. nautobot/project-static/docs/development/core/index.html +11 -11
  385. nautobot/project-static/docs/development/core/minikube-dev-environment-for-k8s-jobs.html +11 -11
  386. nautobot/project-static/docs/development/core/model-checklist.html +11 -11
  387. nautobot/project-static/docs/development/core/model-features.html +11 -11
  388. nautobot/project-static/docs/development/core/natural-keys.html +11 -11
  389. nautobot/project-static/docs/development/core/navigation-menu.html +11 -11
  390. nautobot/project-static/docs/development/core/release-checklist.html +11 -11
  391. nautobot/project-static/docs/development/core/role-internals.html +11 -11
  392. nautobot/project-static/docs/development/core/settings.html +11 -11
  393. nautobot/project-static/docs/development/core/style-guide.html +15 -11
  394. nautobot/project-static/docs/development/core/templates.html +11 -11
  395. nautobot/project-static/docs/development/core/testing.html +11 -11
  396. nautobot/project-static/docs/development/core/ui-component-framework.html +17 -22
  397. nautobot/project-static/docs/development/core/user-preferences.html +11 -11
  398. nautobot/project-static/docs/development/index.html +11 -11
  399. nautobot/project-static/docs/development/jobs/getting-started.html +11 -11
  400. nautobot/project-static/docs/development/jobs/index.html +11 -11
  401. nautobot/project-static/docs/development/jobs/installation.html +11 -11
  402. nautobot/project-static/docs/development/jobs/job-extensions.html +11 -11
  403. nautobot/project-static/docs/development/jobs/job-logging.html +11 -11
  404. nautobot/project-static/docs/development/jobs/job-patterns.html +11 -11
  405. nautobot/project-static/docs/development/jobs/job-structure.html +11 -11
  406. nautobot/project-static/docs/development/jobs/migration/from-v1.html +11 -11
  407. nautobot/project-static/docs/development/jobs/testing.html +11 -11
  408. nautobot/project-static/docs/index.html +11 -11
  409. nautobot/project-static/docs/objects.inv +0 -0
  410. nautobot/project-static/docs/overview/application_stack.html +11 -11
  411. nautobot/project-static/docs/overview/design_philosophy.html +11 -11
  412. nautobot/project-static/docs/release-notes/index.html +11 -11
  413. nautobot/project-static/docs/release-notes/version-1.0.html +11 -11
  414. nautobot/project-static/docs/release-notes/version-1.1.html +11 -11
  415. nautobot/project-static/docs/release-notes/version-1.2.html +11 -11
  416. nautobot/project-static/docs/release-notes/version-1.3.html +11 -11
  417. nautobot/project-static/docs/release-notes/version-1.4.html +11 -11
  418. nautobot/project-static/docs/release-notes/version-1.5.html +11 -11
  419. nautobot/project-static/docs/release-notes/version-1.6.html +11 -11
  420. nautobot/project-static/docs/release-notes/version-2.0.html +11 -11
  421. nautobot/project-static/docs/release-notes/version-2.1.html +11 -11
  422. nautobot/project-static/docs/release-notes/version-2.2.html +11 -11
  423. nautobot/project-static/docs/release-notes/version-2.3.html +11 -11
  424. nautobot/project-static/docs/release-notes/version-2.4.html +418 -11
  425. nautobot/project-static/docs/search/search_index.json +1 -1
  426. nautobot/project-static/docs/sitemap.xml +300 -300
  427. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  428. nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +11 -11
  429. nautobot/project-static/docs/user-guide/administration/configuration/authentication/remote.html +11 -11
  430. nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +11 -11
  431. nautobot/project-static/docs/user-guide/administration/configuration/index.html +11 -11
  432. nautobot/project-static/docs/user-guide/administration/configuration/redis.html +11 -11
  433. nautobot/project-static/docs/user-guide/administration/configuration/settings.html +38 -11
  434. nautobot/project-static/docs/user-guide/administration/configuration/time-zones.html +11 -11
  435. nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +89 -14
  436. nautobot/project-static/docs/user-guide/administration/guides/docker.html +11 -11
  437. nautobot/project-static/docs/user-guide/administration/guides/health-checks.html +11 -11
  438. nautobot/project-static/docs/user-guide/administration/guides/permissions.html +11 -11
  439. nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +11 -11
  440. nautobot/project-static/docs/user-guide/administration/guides/replicating-nautobot.html +11 -11
  441. nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +11 -11
  442. nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +11 -11
  443. nautobot/project-static/docs/user-guide/administration/guides/selinux-troubleshooting.html +11 -11
  444. nautobot/project-static/docs/user-guide/administration/installation/app-install.html +11 -11
  445. nautobot/project-static/docs/user-guide/administration/installation/external-authentication.html +11 -11
  446. nautobot/project-static/docs/user-guide/administration/installation/http-server.html +11 -11
  447. nautobot/project-static/docs/user-guide/administration/installation/index.html +11 -11
  448. nautobot/project-static/docs/user-guide/administration/installation/install_system.html +11 -11
  449. nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +11 -11
  450. nautobot/project-static/docs/user-guide/administration/installation/services.html +11 -11
  451. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-netbox.html +11 -11
  452. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +11 -11
  453. nautobot/project-static/docs/user-guide/administration/security/index.html +11 -11
  454. nautobot/project-static/docs/user-guide/administration/security/notices.html +11 -11
  455. nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +11 -11
  456. nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +11 -11
  457. nautobot/project-static/docs/user-guide/administration/upgrading/database-backup.html +11 -11
  458. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/after-you-upgrade.html +11 -11
  459. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/before-you-upgrade.html +11 -11
  460. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/for-developers.html +11 -11
  461. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/index.html +11 -11
  462. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/whats-changed.html +11 -11
  463. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/region-and-site-data-migration-guide.html +11 -11
  464. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +11 -11
  465. nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +11 -11
  466. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuit.html +11 -11
  467. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittermination.html +11 -11
  468. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittype.html +11 -11
  469. nautobot/project-static/docs/user-guide/core-data-model/circuits/provider.html +11 -11
  470. nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +11 -11
  471. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloud.html +11 -11
  472. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudaccount.html +11 -11
  473. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetwork.html +11 -11
  474. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetworkprefixassignment.html +11 -11
  475. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudresourcetype.html +11 -11
  476. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservice.html +11 -11
  477. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservicenetworkassignment.html +11 -11
  478. nautobot/project-static/docs/user-guide/core-data-model/dcim/cable.html +11 -11
  479. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +11 -11
  480. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +11 -11
  481. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +11 -11
  482. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +11 -11
  483. nautobot/project-static/docs/user-guide/core-data-model/dcim/controller.html +11 -11
  484. nautobot/project-static/docs/user-guide/core-data-model/dcim/controllermanageddevicegroup.html +11 -11
  485. nautobot/project-static/docs/user-guide/core-data-model/dcim/device.html +11 -11
  486. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +11 -11
  487. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +11 -11
  488. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicefamily.html +11 -11
  489. nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +11 -11
  490. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +11 -11
  491. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +11 -11
  492. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +11 -11
  493. nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +11 -11
  494. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +11 -11
  495. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +11 -11
  496. nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +11 -11
  497. nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +11 -11
  498. nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +11 -11
  499. nautobot/project-static/docs/user-guide/core-data-model/dcim/manufacturer.html +11 -11
  500. nautobot/project-static/docs/user-guide/core-data-model/dcim/module.html +11 -11
  501. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebay.html +11 -11
  502. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebaytemplate.html +11 -11
  503. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulefamily.html +11 -11
  504. nautobot/project-static/docs/user-guide/core-data-model/dcim/moduletype.html +11 -11
  505. nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +11 -11
  506. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerfeed.html +11 -11
  507. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +11 -11
  508. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +11 -11
  509. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerpanel.html +11 -11
  510. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +11 -11
  511. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +11 -11
  512. nautobot/project-static/docs/user-guide/core-data-model/dcim/rack.html +11 -11
  513. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackgroup.html +11 -11
  514. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackreservation.html +11 -11
  515. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +11 -11
  516. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +11 -11
  517. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +11 -11
  518. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareversion.html +11 -11
  519. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualchassis.html +11 -11
  520. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualdevicecontext.html +11 -11
  521. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +11 -11
  522. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +11 -11
  523. nautobot/project-static/docs/user-guide/core-data-model/extras/contact.html +11 -11
  524. nautobot/project-static/docs/user-guide/core-data-model/extras/team.html +11 -11
  525. nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +11 -11
  526. nautobot/project-static/docs/user-guide/core-data-model/ipam/namespace.html +11 -11
  527. nautobot/project-static/docs/user-guide/core-data-model/ipam/prefix.html +11 -11
  528. nautobot/project-static/docs/user-guide/core-data-model/ipam/rir.html +11 -11
  529. nautobot/project-static/docs/user-guide/core-data-model/ipam/routetarget.html +11 -11
  530. nautobot/project-static/docs/user-guide/core-data-model/ipam/service.html +11 -11
  531. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +11 -11
  532. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlangroup.html +11 -11
  533. nautobot/project-static/docs/user-guide/core-data-model/ipam/vrf.html +11 -11
  534. nautobot/project-static/docs/user-guide/core-data-model/overview/introduction.html +11 -11
  535. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenant.html +11 -11
  536. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenantgroup.html +11 -11
  537. nautobot/project-static/docs/user-guide/core-data-model/virtualization/cluster.html +11 -11
  538. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustergroup.html +11 -11
  539. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustertype.html +11 -11
  540. nautobot/project-static/docs/user-guide/core-data-model/virtualization/virtualmachine.html +11 -11
  541. nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +11 -11
  542. nautobot/project-static/docs/user-guide/core-data-model/wireless/index.html +11 -11
  543. nautobot/project-static/docs/user-guide/core-data-model/wireless/radioprofile.html +11 -11
  544. nautobot/project-static/docs/user-guide/core-data-model/wireless/supporteddatarate.html +11 -11
  545. nautobot/project-static/docs/user-guide/core-data-model/wireless/wirelessnetwork.html +11 -11
  546. nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +11 -11
  547. nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +11 -11
  548. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +11 -11
  549. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +11 -11
  550. nautobot/project-static/docs/user-guide/feature-guides/getting-started/index.html +11 -11
  551. nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +11 -11
  552. nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +11 -11
  553. nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +11 -11
  554. nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +11 -11
  555. nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +11 -11
  556. nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +11 -11
  557. nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +11 -11
  558. nautobot/project-static/docs/user-guide/feature-guides/graphql.html +11 -11
  559. nautobot/project-static/docs/user-guide/feature-guides/ip-address-merge-tool.html +11 -11
  560. nautobot/project-static/docs/user-guide/feature-guides/relationships.html +11 -11
  561. nautobot/project-static/docs/user-guide/feature-guides/software-image-files-and-versions.html +11 -11
  562. nautobot/project-static/docs/user-guide/feature-guides/wireless-networks-and-controllers.html +11 -11
  563. nautobot/project-static/docs/user-guide/index.html +11 -11
  564. nautobot/project-static/docs/user-guide/platform-functionality/change-logging.html +11 -11
  565. nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +11 -11
  566. nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +11 -11
  567. nautobot/project-static/docs/user-guide/platform-functionality/customlink.html +11 -11
  568. nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +11 -11
  569. nautobot/project-static/docs/user-guide/platform-functionality/events.html +11 -11
  570. nautobot/project-static/docs/user-guide/platform-functionality/exporttemplate.html +11 -11
  571. nautobot/project-static/docs/user-guide/platform-functionality/externalintegration.html +11 -11
  572. nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +11 -11
  573. nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +11 -11
  574. nautobot/project-static/docs/user-guide/platform-functionality/graphqlquery.html +11 -11
  575. nautobot/project-static/docs/user-guide/platform-functionality/imageattachment.html +11 -11
  576. nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +11 -11
  577. nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +11 -11
  578. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +11 -11
  579. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +11 -11
  580. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobqueue.html +11 -11
  581. nautobot/project-static/docs/user-guide/platform-functionality/jobs/kubernetes-job-support.html +11 -11
  582. nautobot/project-static/docs/user-guide/platform-functionality/jobs/managing-jobs.html +11 -11
  583. nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +11 -11
  584. nautobot/project-static/docs/user-guide/platform-functionality/napalm.html +11 -11
  585. nautobot/project-static/docs/user-guide/platform-functionality/note.html +11 -11
  586. nautobot/project-static/docs/user-guide/platform-functionality/objectmetadata.html +11 -11
  587. nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +11 -11
  588. nautobot/project-static/docs/user-guide/platform-functionality/rendering-jinja-templates.html +11 -11
  589. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +11 -11
  590. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +11 -11
  591. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +11 -11
  592. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/ui-related-endpoints.html +11 -11
  593. nautobot/project-static/docs/user-guide/platform-functionality/role.html +11 -11
  594. nautobot/project-static/docs/user-guide/platform-functionality/savedview.html +11 -11
  595. nautobot/project-static/docs/user-guide/platform-functionality/secret.html +11 -11
  596. nautobot/project-static/docs/user-guide/platform-functionality/staticgroupassociation.html +11 -11
  597. nautobot/project-static/docs/user-guide/platform-functionality/status.html +11 -11
  598. nautobot/project-static/docs/user-guide/platform-functionality/tag.html +11 -11
  599. nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +11 -11
  600. nautobot/project-static/docs/user-guide/platform-functionality/users/objectpermission.html +11 -11
  601. nautobot/project-static/docs/user-guide/platform-functionality/users/token.html +11 -11
  602. nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +11 -11
  603. nautobot/project-static/img/nautobot_icon.svg +32 -34
  604. nautobot/project-static/js/table_sorting_indicator.js +0 -2
  605. nautobot/tenancy/templates/tenancy/tenant.html +1 -7
  606. nautobot/tenancy/views.py +13 -0
  607. nautobot/users/templates/users/api_tokens.html +4 -4
  608. nautobot/users/templates/users/base.html +28 -28
  609. nautobot/virtualization/templates/virtualization/cluster.html +64 -64
  610. nautobot/virtualization/templates/virtualization/inc/virtualmachine_vminterface_filter.html +8 -0
  611. nautobot/virtualization/templates/virtualization/virtualmachine_component_add.html +25 -25
  612. nautobot/virtualization/templates/virtualization/virtualmachine_retrieve.html +1 -251
  613. nautobot/virtualization/templates/virtualization/vminterface.html +70 -70
  614. nautobot/virtualization/urls.py +0 -12
  615. nautobot/virtualization/views.py +158 -54
  616. nautobot/wireless/templates/wireless/wirelessnetwork_create.html +13 -13
  617. nautobot/wireless/tests/integration/test_radio_profile.py +1 -1
  618. {nautobot-2.4.17.dist-info → nautobot-2.4.19.dist-info}/METADATA +4 -4
  619. {nautobot-2.4.17.dist-info → nautobot-2.4.19.dist-info}/RECORD +623 -607
  620. nautobot/core/templates/inc/breadcrumbs.html +0 -14
  621. nautobot/ipam/templates/ipam/prefix_ipaddresses.html +0 -11
  622. nautobot/ipam/templates/ipam/prefix_prefixes.html +0 -11
  623. nautobot/project-static/docs/requirements.txt +0 -14
  624. {nautobot-2.4.17.dist-info → nautobot-2.4.19.dist-info}/LICENSE.txt +0 -0
  625. {nautobot-2.4.17.dist-info → nautobot-2.4.19.dist-info}/NOTICE +0 -0
  626. {nautobot-2.4.17.dist-info → nautobot-2.4.19.dist-info}/WHEEL +0 -0
  627. {nautobot-2.4.17.dist-info → nautobot-2.4.19.dist-info}/entry_points.txt +0 -0
nautobot/dcim/views.py CHANGED
@@ -16,12 +16,12 @@ 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
24
- from django.utils.http import url_has_allowed_host_and_scheme
23
+ from django.utils.html import format_html, mark_safe
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
27
27
  from rest_framework.decorators import action
@@ -38,17 +38,23 @@ 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,
48
+ ViewNameBreadcrumbItem,
45
49
  )
46
50
  from nautobot.core.ui.bulk_buttons import (
47
51
  BulkDeleteButton,
52
+ BulkDisconnectButton,
48
53
  BulkEditButton,
49
54
  BulkRenameButton,
50
55
  )
51
56
  from nautobot.core.ui.choices import SectionChoices
57
+ from nautobot.core.ui.titles import Titles
52
58
  from nautobot.core.utils.lookup import get_form_for_model
53
59
  from nautobot.core.utils.permissions import get_permission_for_model
54
60
  from nautobot.core.utils.requests import normalize_querydict
@@ -66,23 +72,26 @@ from nautobot.core.views.mixins import (
66
72
  ObjectPermissionRequiredMixin,
67
73
  )
68
74
  from nautobot.core.views.paginator import EnhancedPaginator, get_paginate_count
69
- 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
70
76
  from nautobot.core.views.viewsets import NautobotUIViewSet
71
77
  from nautobot.dcim.choices import LocationDataToContactActionChoices
72
78
  from nautobot.dcim.forms import LocationMigrateDataToContactForm
73
- from nautobot.dcim.utils import get_all_network_driver_mappings
74
- from nautobot.extras.models import Contact, ContactAssociation, Role, Status, Team
75
- from nautobot.extras.tables import DynamicGroupTable
76
- from nautobot.extras.views import ObjectChangeLogView, ObjectConfigContextView, ObjectDynamicGroupsView
77
- from nautobot.ipam.models import IPAddress, Prefix, Service, VLAN
78
- 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
79
92
  from nautobot.virtualization.models import VirtualMachine
80
93
  from nautobot.virtualization.tables import VirtualMachineTable
81
94
  from nautobot.wireless.forms import ControllerManagedDeviceGroupWirelessNetworkFormSet
82
- from nautobot.wireless.models import (
83
- ControllerManagedDeviceGroupRadioProfileAssignment,
84
- ControllerManagedDeviceGroupWirelessNetworkAssignment,
85
- )
86
95
  from nautobot.wireless.tables import (
87
96
  BaseControllerManagedDeviceGroupWirelessNetworkAssignmentTable,
88
97
  ControllerManagedDeviceGroupRadioProfileAssignmentTable,
@@ -238,6 +247,7 @@ class LocationTypeUIViewSet(NautobotUIViewSet):
238
247
  form_class = forms.LocationTypeForm
239
248
  bulk_update_form_class = forms.LocationTypeBulkEditForm
240
249
  serializer_class = serializers.LocationSerializer
250
+ breadcrumbs = AncestorsBreadcrumbs(detail_item_label=context_object_attr("name"))
241
251
 
242
252
  object_detail_content = object_detail.ObjectDetailContent(
243
253
  panels=(
@@ -281,28 +291,36 @@ class LocationUIViewSet(NautobotUIViewSet):
281
291
  form_class = forms.LocationForm
282
292
  bulk_update_form_class = forms.LocationBulkEditForm
283
293
  serializer_class = serializers.LocationSerializer
294
+ breadcrumbs = AncestorsBreadcrumbs(detail_item_label=context_object_attr("name"))
284
295
 
285
296
  def get_extra_context(self, request, instance):
286
297
  if instance is None:
287
298
  return super().get_extra_context(request, instance)
288
- 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(
289
302
  instance.descendants(include_self=True).restrict(request.user, "view").values_list("pk", flat=True)
290
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()
291
316
  stats = {
317
+ "prefix_count": prefix_count_queryset.count(),
318
+ "vlan_count": vlan_count_queryset.count(),
319
+ "circuit_count": circuit_count_queryset.count(),
292
320
  "rack_count": Rack.objects.restrict(request.user, "view").filter(location__in=related_locations).count(),
293
321
  "device_count": Device.objects.restrict(request.user, "view")
294
322
  .filter(location__in=related_locations)
295
323
  .count(),
296
- "prefix_count": Prefix.objects.restrict(request.user, "view")
297
- .filter(locations__in=related_locations)
298
- .count(),
299
- "vlan_count": VLAN.objects.restrict(request.user, "view")
300
- .filter(locations__in=related_locations)
301
- .distinct()
302
- .count(),
303
- "circuit_count": Circuit.objects.restrict(request.user, "view")
304
- .filter(circuit_terminations__location__in=related_locations)
305
- .count(),
306
324
  "vm_count": VirtualMachine.objects.restrict(request.user, "view")
307
325
  .filter(cluster__location__in=related_locations)
308
326
  .count(),
@@ -515,6 +533,7 @@ class RackGroupUIViewSet(NautobotUIViewSet):
515
533
  if self.action == "retrieve" and instance:
516
534
  racks = (
517
535
  Rack.objects.restrict(request.user, "view")
536
+ # Note this filter - we want the table to include racks assigned to child rack groups as well
518
537
  .filter(rack_group__in=instance.descendants(include_self=True))
519
538
  .select_related("role", "location", "tenant")
520
539
  )
@@ -546,6 +565,7 @@ class RackUIViewSet(NautobotUIViewSet):
546
565
  serializer_class = serializers.RackSerializer
547
566
  table_class = tables.RackDetailTable
548
567
  queryset = Rack.objects.select_related("location", "tenant__tenant_group", "rack_group", "role")
568
+ breadcrumbs = RackBreadcrumbs()
549
569
 
550
570
  def get_extra_context(self, request, instance):
551
571
  context = super().get_extra_context(request, instance)
@@ -590,6 +610,10 @@ class RackElevationListView(generic.ObjectListView):
590
610
  filterset_form = forms.RackFilterForm
591
611
  action_buttons = []
592
612
  template_name = "dcim/rack_elevation_list.html"
613
+ view_titles = Titles(titles={"list": "Rack Elevation"})
614
+ breadcrumbs = Breadcrumbs(
615
+ items={"list": [ViewNameBreadcrumbItem(view_name="dcim:rack_elevation_list", label="Rack Elevation")]}
616
+ )
593
617
 
594
618
  def extra_context(self):
595
619
  racks = self.queryset
@@ -641,14 +665,26 @@ class RackReservationUIViewSet(NautobotUIViewSet):
641
665
  serializer_class = serializers.RackReservationSerializer
642
666
  table_class = tables.RackReservationTable
643
667
  queryset = RackReservation.objects.all()
668
+ breadcrumbs = Breadcrumbs(
669
+ items={
670
+ "detail": [
671
+ ModelBreadcrumbItem(),
672
+ InstanceBreadcrumbItem(instance=context_object_attr("rack")),
673
+ ]
674
+ }
675
+ )
644
676
 
645
677
  object_detail_content = object_detail.ObjectDetailContent(
646
678
  panels=(
647
- object_detail.KeyValueTablePanel(
679
+ object_detail.ObjectFieldsPanel(
648
680
  section=SectionChoices.LEFT_HALF,
649
681
  weight=100,
650
682
  label="Rack",
651
- context_data_key="rack_data",
683
+ fields=["rack__location", "rack__rack_group", "rack"],
684
+ key_transforms={
685
+ "rack__location": "Location",
686
+ "rack__rack_group": "Rack Group",
687
+ },
652
688
  ),
653
689
  object_detail.ObjectFieldsPanel(
654
690
  section=SectionChoices.LEFT_HALF,
@@ -664,23 +700,6 @@ class RackReservationUIViewSet(NautobotUIViewSet):
664
700
  ),
665
701
  )
666
702
 
667
- def get_extra_context(self, request, instance):
668
- context = super().get_extra_context(request, instance)
669
- if self.action == "retrieve":
670
- context["rack_data"] = self.get_rack_context(instance)
671
- return context
672
-
673
- def get_rack_context(self, instance):
674
- rack = getattr(instance, "rack", None)
675
- if not rack:
676
- return {}
677
-
678
- return {
679
- "location": rack.location,
680
- "rack_group": rack.rack_group,
681
- "rack": rack,
682
- }
683
-
684
703
  def get_object(self):
685
704
  obj = super().get_object()
686
705
 
@@ -773,6 +792,15 @@ def bulk_footer_buttons(form_id: str, model):
773
792
  ]
774
793
 
775
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
+
776
804
  # --- Tab Configuration ---
777
805
  TAB_CONFIGS = [
778
806
  (
@@ -888,6 +916,7 @@ def make_bulk_tab(weight, tab_name, label, url_name, related_attr, table_class,
888
916
  label=label,
889
917
  url_name=url_name,
890
918
  related_object_attribute=related_attr,
919
+ hide_if_empty=True,
891
920
  panels=(
892
921
  object_detail.ObjectsTablePanel(
893
922
  section=SectionChoices.FULL_WIDTH,
@@ -914,6 +943,14 @@ class DeviceTypeUIViewSet(NautobotUIViewSet):
914
943
  serializer_class = serializers.DeviceTypeSerializer
915
944
  table_class = tables.DeviceTypeTable
916
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
+ )
917
954
 
918
955
  object_detail_content = object_detail.ObjectDetailContent(
919
956
  panels=(
@@ -927,6 +964,7 @@ class DeviceTypeUIViewSet(NautobotUIViewSet):
927
964
  weight=200,
928
965
  table_class=tables.SoftwareImageFileTable,
929
966
  table_filter="device_types",
967
+ order_by_fields=["image_file_name"],
930
968
  select_related_fields=["software_version", "status"],
931
969
  exclude_columns=["actions", "tags"],
932
970
  related_field_name="device_types",
@@ -980,7 +1018,7 @@ class DeviceTypeUIViewSet(NautobotUIViewSet):
980
1018
  @action(
981
1019
  detail=True,
982
1020
  methods=["get"],
983
- url_path="frontports",
1021
+ url_path="front-ports",
984
1022
  custom_view_base_action="view",
985
1023
  custom_view_additional_permissions=["dcim.view_frontporttemplate"],
986
1024
  )
@@ -990,7 +1028,7 @@ class DeviceTypeUIViewSet(NautobotUIViewSet):
990
1028
  @action(
991
1029
  detail=True,
992
1030
  methods=["get"],
993
- url_path="rearports",
1031
+ url_path="rear-ports",
994
1032
  custom_view_base_action="view",
995
1033
  custom_view_additional_permissions=["dcim.view_rearporttemplate"],
996
1034
  )
@@ -1000,7 +1038,7 @@ class DeviceTypeUIViewSet(NautobotUIViewSet):
1000
1038
  @action(
1001
1039
  detail=True,
1002
1040
  methods=["get"],
1003
- url_path="consoleports",
1041
+ url_path="console-ports",
1004
1042
  custom_view_base_action="view",
1005
1043
  custom_view_additional_permissions=["dcim.view_consoleporttemplate"],
1006
1044
  )
@@ -1010,7 +1048,7 @@ class DeviceTypeUIViewSet(NautobotUIViewSet):
1010
1048
  @action(
1011
1049
  detail=True,
1012
1050
  methods=["get"],
1013
- url_path="consoleserverports",
1051
+ url_path="console-server-ports",
1014
1052
  custom_view_base_action="view",
1015
1053
  custom_view_additional_permissions=["dcim.view_consoleserverporttemplate"],
1016
1054
  )
@@ -1020,7 +1058,7 @@ class DeviceTypeUIViewSet(NautobotUIViewSet):
1020
1058
  @action(
1021
1059
  detail=True,
1022
1060
  methods=["get"],
1023
- url_path="powerports",
1061
+ url_path="power-ports",
1024
1062
  custom_view_base_action="view",
1025
1063
  custom_view_additional_permissions=["dcim.view_powerporttemplate"],
1026
1064
  )
@@ -1030,7 +1068,7 @@ class DeviceTypeUIViewSet(NautobotUIViewSet):
1030
1068
  @action(
1031
1069
  detail=True,
1032
1070
  methods=["get"],
1033
- url_path="poweroutlets",
1071
+ url_path="power-outlets",
1034
1072
  custom_view_base_action="view",
1035
1073
  custom_view_additional_permissions=["dcim.view_poweroutlettemplate"],
1036
1074
  )
@@ -1040,7 +1078,7 @@ class DeviceTypeUIViewSet(NautobotUIViewSet):
1040
1078
  @action(
1041
1079
  detail=True,
1042
1080
  methods=["get"],
1043
- url_path="devicebays",
1081
+ url_path="device-bays",
1044
1082
  custom_view_base_action="view",
1045
1083
  custom_view_additional_permissions=["dcim.view_devicebaytemplate"],
1046
1084
  )
@@ -1050,7 +1088,7 @@ class DeviceTypeUIViewSet(NautobotUIViewSet):
1050
1088
  @action(
1051
1089
  detail=True,
1052
1090
  methods=["get"],
1053
- url_path="modulebays",
1091
+ url_path="module-bays",
1054
1092
  custom_view_base_action="view",
1055
1093
  custom_view_additional_permissions=["dcim.view_modulebaytemplate"],
1056
1094
  )
@@ -1121,6 +1159,14 @@ class ModuleTypeUIViewSet(
1121
1159
  "front-ports": forms.FrontPortTemplateImportForm,
1122
1160
  "module-bays": forms.ModuleBayTemplateImportForm,
1123
1161
  }
1162
+ breadcrumbs = Breadcrumbs(
1163
+ items={
1164
+ "detail": [
1165
+ ModelBreadcrumbItem(),
1166
+ InstanceParentBreadcrumbItem(parent_key="manufacturer", parent_lookup_key="name"),
1167
+ ]
1168
+ }
1169
+ )
1124
1170
 
1125
1171
  def get_required_permission(self):
1126
1172
  view_action = self.get_action()
@@ -1890,60 +1936,133 @@ class PlatformUIViewSet(NautobotUIViewSet):
1890
1936
  #
1891
1937
 
1892
1938
 
1893
- class DeviceBreadcrumbsMixin:
1939
+ class DeviceComponentPageMixin:
1940
+ """
1941
+ This class hold the breadcrumbs paths for Device Components Pages like console ports.
1942
+ Depending on whether the component is associated with a device or a module, the appropriate breadcrumb path will be rendered.
1943
+
1944
+ For example:
1945
+ - Console Port assigned to the module: Modules / <Module name and link to details> / Console Ports (dcim/modules/<id>/console-ports/) / <Console Port name>
1946
+ - Console Port assigned to the device: Devices / <Device name and link to details> / Console Ports (dcim/devices/<id>/console-ports/) / <Console Port name>
1947
+ """
1948
+
1894
1949
  breadcrumbs = Breadcrumbs(
1895
1950
  items={
1896
1951
  "detail": [
1897
- ModelBreadcrumbItem(model=Device),
1898
- ModelBreadcrumbItem(
1899
- model=Device,
1900
- reverse_query_params=lambda c: {"location": c["object"].location.pk},
1952
+ ModelBreadcrumbItem(model=Device, should_render=lambda c: c["object"].device is not None),
1953
+ InstanceBreadcrumbItem(
1954
+ instance=lambda c: c["object"].device, should_render=lambda c: c["object"].device is not None
1955
+ ),
1956
+ ViewNameBreadcrumbItem(
1957
+ view_name_key="device_breadcrumb_url",
1958
+ should_render=lambda c: c["object"].device is not None and c.get("device_breadcrumb_url"),
1959
+ reverse_kwargs=lambda c: {"pk": c["object"].device.pk},
1960
+ label=lambda c: c["object"]._meta.verbose_name_plural,
1901
1961
  ),
1962
+ ModelBreadcrumbItem(model=Module, should_render=lambda c: c["object"].device is None),
1902
1963
  InstanceBreadcrumbItem(
1903
- instance=lambda c: c["object"].parent_bay.device,
1904
- should_render=lambda c: hasattr(c["object"], "parent_bay"),
1964
+ instance=lambda c: c["object"].module, should_render=lambda c: c["object"].device is None
1905
1965
  ),
1906
- BaseBreadcrumbItem(
1907
- label=lambda c: c["object"].parent_bay, should_render=lambda c: hasattr(c["object"], "parent_bay")
1966
+ ViewNameBreadcrumbItem(
1967
+ view_name_key="module_breadcrumb_url",
1968
+ should_render=lambda c: c["object"].device is None and c.get("module_breadcrumb_url"),
1969
+ reverse_kwargs=lambda c: {"pk": c["object"].module.pk},
1970
+ label=lambda c: c["object"]._meta.verbose_name_plural,
1908
1971
  ),
1909
1972
  ]
1910
1973
  }
1911
1974
  )
1975
+ view_titles = Titles(
1976
+ titles={
1977
+ "detail": "{% if object.device %}{{ object.device }}{% else %}{{ object.module.display }}{% endif %} / {{ object }}"
1978
+ }
1979
+ )
1912
1980
 
1913
1981
 
1914
- class DeviceListView(generic.ObjectListView):
1982
+ class DeviceUIViewSet(NautobotUIViewSet):
1915
1983
  queryset = Device.objects.select_related(
1916
1984
  "device_type__manufacturer", # Needed for __str__() on device_type
1917
1985
  )
1918
- filterset = filters.DeviceFilterSet
1919
- filterset_form = forms.DeviceFilterForm
1920
- table = tables.DeviceTable
1921
- 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
1922
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
+ )
1923
2008
 
1924
- class DeviceView(generic.ObjectView):
1925
- queryset = Device.objects.select_related(
1926
- "cluster__cluster_group",
1927
- "controller_managed_device_group__controller",
1928
- "device_redundancy_group",
1929
- "device_type__device_family",
1930
- "location",
1931
- "platform",
1932
- "primary_ip4",
1933
- "primary_ip6",
1934
- "rack__rack_group",
1935
- "role",
1936
- "secrets_group",
1937
- "software_version",
1938
- "status",
1939
- "tenant__tenant_group",
1940
- ).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
1941
2032
 
1942
2033
  class DeviceDetailContent(object_detail.ObjectDetailContent):
1943
2034
  """
1944
2035
  Override base ObjectDetailContent to render dynamic-groups table as a separate view/tab instead of inline.
1945
2036
  """
1946
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
+
1947
2066
  def __init__(self, **kwargs):
1948
2067
  super().__init__(**kwargs)
1949
2068
  # Remove inline tab definition
@@ -1959,8 +2078,9 @@ class DeviceView(generic.ObjectView):
1959
2078
  url_name="dcim:device_dynamicgroups",
1960
2079
  related_object_attribute="dynamic_groups",
1961
2080
  panels=(
2081
+ self.DeviceDynamicGroupsTextPanel(weight=100),
1962
2082
  object_detail.ObjectsTablePanel(
1963
- weight=100,
2083
+ weight=200,
1964
2084
  table_class=DynamicGroupTable,
1965
2085
  table_attribute="dynamic_groups",
1966
2086
  exclude_columns=["content_type"],
@@ -1972,6 +2092,191 @@ class DeviceView(generic.ObjectView):
1972
2092
  )
1973
2093
  )
1974
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
+
1975
2280
  object_detail_content = DeviceDetailContent(
1976
2281
  extra_buttons=(
1977
2282
  object_detail.DropdownButton(
@@ -2055,486 +2360,665 @@ class DeviceView(generic.ObjectView):
2055
2360
  ),
2056
2361
  ),
2057
2362
  ),
2058
- panels=(), # not yet ported over due to complexity of this template
2059
- # TODO
2060
- # ObjectFieldsPanel(
2061
- # weight=100,
2062
- # section=SectionChoices.LEFT_HALF,
2063
- # fields=["location", "rack", "position", "face", "tenant", "device_type", "serial", "asset_tag"],
2064
- # # TODO add device_type.device_family, device_type.u_height,
2065
- # ),
2066
- # TODO: Virtual Chassis panel
2067
- # ObjectFieldsPanel(
2068
- # weight=110,
2069
- # section=SectionChoices.LEFT_HALF,
2070
- # label="Virtual Chassis",
2071
- # ),
2072
- # ObjectFieldsPanel(
2073
- # weight=120,
2074
- # section=SectionChoices.LEFT_HALF,
2075
- # label="Management",
2076
- # fields=["role", "platform", "status", "primary_ip4", "primary_ip6", "secrets_group", "device_redundancy_group", "controller_managed_device_group", "software_version"],
2077
- # ),
2078
- # TODO: power utilization panel
2079
- # ObjectsTablePanel(
2080
- # weight=100,
2081
- # section=SectionChoices.RIGHT_HALF,
2082
- # table_title="Power Utilization",
2083
- # table_class=???,
2084
- # table_filter="device",
2085
- # ),
2086
- # ObjectsTablePanel(
2087
- # weight=100,
2088
- # section=SectionChoices.RIGHT_HALF,
2089
- # table_title="Assigned VRFs",
2090
- # table_class=VRFDeviceAssignmentTable,
2091
- # table_filter="device",
2092
- # exclude_columns=["virtual_machine", "device"],
2093
- # ),
2094
- # TODO: services panel
2095
- # ObjectsTablePanel(
2096
- # weight=200,
2097
- # section=SectionChoices.RIGHT_HALF,
2098
- # table_class=???,
2099
- # table_filter="device",
2100
- # ),
2101
- # TODO: images panel
2102
- # ObjectsTablePanel(
2103
- # weight=300,
2104
- # section=SectionChoices.RIGHT_HALF,
2105
- # table_class=???,
2106
- # table_filter="device",
2107
- # ),
2108
- # ObjectsTablePanel(
2109
- # weight=100,
2110
- # section=SectionChoices.FULL_WIDTH,
2111
- # table_class=tables.VirtualDeviceContextTable,
2112
- # table_filter="device",
2113
- # select_related_fields=["tenant", "primary_ip4", "primary_ip6"],
2114
- # exclude_columns=["device"],
2115
- # ),
2116
- # ),
2117
- )
2118
-
2119
- def get_extra_context(self, request, instance):
2120
- # VirtualChassis members
2121
- if instance.virtual_chassis is not None:
2122
- vc_members = (
2123
- Device.objects.restrict(request.user, "view")
2124
- .filter(virtual_chassis=instance.virtual_chassis)
2125
- .order_by("vc_position")
2126
- )
2127
- else:
2128
- vc_members = []
2129
-
2130
- # Services
2131
- services = Service.objects.restrict(request.user, "view").filter(device=instance)
2132
-
2133
- # VRF assignments
2134
- vrf_assignments = instance.vrf_assignments.restrict(request.user, "view")
2135
- vrf_table = VRFDeviceAssignmentTable(vrf_assignments)
2136
-
2137
- # Software images
2138
- if instance.software_version is not None:
2139
- software_version_images = instance.software_version.software_image_files.restrict(
2140
- request.user, "view"
2141
- ).filter(device_types=instance.device_type)
2142
- if not software_version_images.exists():
2143
- software_version_images = instance.software_version.software_image_files.restrict(
2144
- request.user, "view"
2145
- ).filter(default_image=True)
2146
- else:
2147
- software_version_images = []
2148
-
2149
- modulebay_count = instance.module_bays.count()
2150
- module_count = instance.module_bays.filter(installed_module__isnull=False).count()
2151
-
2152
- vdcs = instance.virtual_device_contexts.restrict(request.user).select_related(
2153
- "tenant", "primary_ip4", "primary_ip6"
2154
- )
2155
- vdcs_table = tables.VirtualDeviceContextTable(vdcs, orderable=False, exclude=("device",))
2156
- vdc_url = reverse("dcim:virtualdevicecontext_add")
2157
- return_url = instance.get_absolute_url()
2158
- vdcs_table_add_url = f"{vdc_url}?device={instance.id}&return_url={return_url}"
2159
-
2160
- paginate = {
2161
- "paginator_class": EnhancedPaginator,
2162
- "per_page": get_paginate_count(request),
2163
- }
2164
- RequestConfig(request, paginate).configure(vdcs_table)
2165
-
2166
- return {
2167
- **super().get_extra_context(request, instance),
2168
- "services": services,
2169
- "software_version_images": software_version_images,
2170
- "vc_members": vc_members,
2171
- "vrf_table": vrf_table,
2172
- "active_tab": "device",
2173
- "modulebay_count": modulebay_count,
2174
- "module_count": f"{module_count}/{modulebay_count}",
2175
- "vdcs_table": vdcs_table,
2176
- "vdcs_table_add_url": vdcs_table_add_url,
2177
- }
2178
-
2179
-
2180
- class DeviceComponentTabView(generic.ObjectView):
2181
- queryset = Device.objects.all()
2182
-
2183
- def get_extra_context(self, request, instance):
2184
- modulebay_count = instance.module_bays.count()
2185
- module_count = instance.module_bays.filter(installed_module__isnull=False).count()
2186
-
2187
- return {
2188
- "modulebay_count": modulebay_count,
2189
- "module_count": f"{module_count}/{modulebay_count}",
2190
- }
2191
-
2192
-
2193
- class DeviceConsolePortsView(DeviceComponentTabView):
2194
- queryset = Device.objects.all()
2195
- template_name = "dcim/device/consoleports.html"
2196
-
2197
- def get_extra_context(self, request, instance):
2198
- consoleports = (
2199
- instance.all_console_ports.restrict(request.user, "view")
2200
- .select_related("cable")
2201
- .prefetch_related("_path__destination")
2202
- )
2203
- consoleport_table = tables.DeviceModuleConsolePortTable(data=consoleports, user=request.user, orderable=False)
2204
- if request.user.has_perm("dcim.change_consoleport") or request.user.has_perm("dcim.delete_consoleport"):
2205
- consoleport_table.columns.show("pk")
2206
-
2207
- return {
2208
- **super().get_extra_context(request, instance),
2209
- "consoleport_table": consoleport_table,
2210
- "active_tab": "console-ports",
2211
- }
2212
-
2213
-
2214
- class DeviceConsoleServerPortsView(DeviceComponentTabView):
2215
- queryset = Device.objects.all()
2216
- template_name = "dcim/device/consoleserverports.html"
2217
-
2218
- def get_extra_context(self, request, instance):
2219
- consoleserverports = (
2220
- instance.all_console_server_ports.restrict(request.user, "view")
2221
- .select_related("cable")
2222
- .prefetch_related("_path__destination")
2223
- )
2224
- consoleserverport_table = tables.DeviceModuleConsoleServerPortTable(
2225
- data=consoleserverports, user=request.user, orderable=False
2226
- )
2227
- if request.user.has_perm("dcim.change_consoleserverport") or request.user.has_perm(
2228
- "dcim.delete_consoleserverport"
2229
- ):
2230
- consoleserverport_table.columns.show("pk")
2231
-
2232
- return {
2233
- **super().get_extra_context(request, instance),
2234
- "consoleserverport_table": consoleserverport_table,
2235
- "active_tab": "console-server-ports",
2236
- }
2237
-
2238
-
2239
- class DevicePowerPortsView(DeviceComponentTabView):
2240
- queryset = Device.objects.all()
2241
- template_name = "dcim/device/powerports.html"
2242
-
2243
- def get_extra_context(self, request, instance):
2244
- powerports = (
2245
- instance.all_power_ports.restrict(request.user, "view")
2246
- .select_related("cable")
2247
- .prefetch_related("_path__destination")
2248
- )
2249
- powerport_table = tables.DeviceModulePowerPortTable(data=powerports, user=request.user, orderable=False)
2250
- if request.user.has_perm("dcim.change_powerport") or request.user.has_perm("dcim.delete_powerport"):
2251
- powerport_table.columns.show("pk")
2252
-
2253
- return {
2254
- **super().get_extra_context(request, instance),
2255
- "powerport_table": powerport_table,
2256
- "active_tab": "power-ports",
2257
- }
2258
-
2259
-
2260
- class DevicePowerOutletsView(DeviceComponentTabView):
2261
- queryset = Device.objects.all()
2262
- template_name = "dcim/device/poweroutlets.html"
2263
-
2264
- def get_extra_context(self, request, instance):
2265
- poweroutlets = (
2266
- instance.all_power_outlets.restrict(request.user, "view")
2267
- .select_related("cable", "power_port")
2268
- .prefetch_related("_path__destination")
2269
- )
2270
- poweroutlet_table = tables.DeviceModulePowerOutletTable(data=poweroutlets, user=request.user, orderable=False)
2271
- if request.user.has_perm("dcim.change_poweroutlet") or request.user.has_perm("dcim.delete_poweroutlet"):
2272
- poweroutlet_table.columns.show("pk")
2273
-
2274
- return {
2275
- **super().get_extra_context(request, instance),
2276
- "poweroutlet_table": poweroutlet_table,
2277
- "active_tab": "power-outlets",
2278
- }
2279
-
2280
-
2281
- class DeviceInterfacesView(DeviceComponentTabView):
2282
- queryset = Device.objects.all()
2283
- template_name = "dcim/device/interfaces.html"
2284
-
2285
- def get_extra_context(self, request, instance):
2286
- interfaces = (
2287
- instance.vc_interfaces.restrict(request.user, "view")
2288
- .prefetch_related(
2289
- Prefetch("ip_addresses", queryset=IPAddress.objects.restrict(request.user)),
2290
- Prefetch("member_interfaces", queryset=Interface.objects.restrict(request.user)),
2291
- "_path__destination",
2292
- "tags",
2293
- )
2294
- .select_related("lag", "cable")
2295
- .order_by("_name")
2296
- )
2297
- interface_table = tables.DeviceModuleInterfaceTable(data=interfaces, user=request.user, orderable=False)
2298
- if VirtualChassis.objects.filter(master=instance).exists():
2299
- interface_table.columns.show("device")
2300
- if request.user.has_perm("dcim.change_interface") or request.user.has_perm("dcim.delete_interface"):
2301
- interface_table.columns.show("pk")
2302
-
2303
- return {
2304
- **super().get_extra_context(request, instance),
2305
- "interface_table": interface_table,
2306
- "active_tab": "interfaces",
2307
- }
2308
-
2309
-
2310
- class DeviceFrontPortsView(DeviceComponentTabView):
2311
- queryset = Device.objects.all()
2312
- template_name = "dcim/device/frontports.html"
2313
-
2314
- def get_extra_context(self, request, instance):
2315
- frontports = instance.all_front_ports.restrict(request.user, "view").select_related("cable", "rear_port")
2316
- frontport_table = tables.DeviceModuleFrontPortTable(data=frontports, user=request.user, orderable=False)
2317
- if request.user.has_perm("dcim.change_frontport") or request.user.has_perm("dcim.delete_frontport"):
2318
- frontport_table.columns.show("pk")
2319
-
2320
- return {
2321
- **super().get_extra_context(request, instance),
2322
- "frontport_table": frontport_table,
2323
- "active_tab": "front-ports",
2324
- }
2325
-
2326
-
2327
- class DeviceRearPortsView(DeviceComponentTabView):
2328
- queryset = Device.objects.all()
2329
- template_name = "dcim/device/rearports.html"
2330
-
2331
- def get_extra_context(self, request, instance):
2332
- rearports = instance.all_rear_ports.restrict(request.user, "view").select_related("cable")
2333
- rearport_table = tables.DeviceModuleRearPortTable(data=rearports, user=request.user, orderable=False)
2334
- if request.user.has_perm("dcim.change_rearport") or request.user.has_perm("dcim.delete_rearport"):
2335
- rearport_table.columns.show("pk")
2336
-
2337
- return {
2338
- **super().get_extra_context(request, instance),
2339
- "rearport_table": rearport_table,
2340
- "active_tab": "rear-ports",
2341
- }
2342
-
2343
-
2344
- class DeviceDeviceBaysView(DeviceComponentTabView):
2345
- queryset = Device.objects.all()
2346
- template_name = "dcim/device/devicebays.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
+ )
2347
2798
 
2348
2799
  def get_extra_context(self, request, instance):
2349
- devicebays = (
2350
- DeviceBay.objects.restrict(request.user, "view")
2351
- .filter(device=instance)
2352
- .select_related(
2353
- "installed_device__device_type__manufacturer",
2354
- )
2355
- )
2356
- devicebay_table = tables.DeviceDeviceBayTable(data=devicebays, user=request.user, orderable=False)
2357
- if request.user.has_perm("dcim.change_devicebay") or request.user.has_perm("dcim.delete_devicebay"):
2358
- devicebay_table.columns.show("pk")
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
2359
2814
 
2360
- return {
2361
- **super().get_extra_context(request, instance),
2362
- "devicebay_table": devicebay_table,
2363
- "active_tab": "device-bays",
2364
- }
2815
+ return extra_context
2365
2816
 
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({})
2366
2826
 
2367
- class DeviceModuleBaysView(DeviceComponentTabView):
2368
- queryset = Device.objects.all()
2369
- template_name = "dcim/device/modulebays.html"
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({})
2370
2836
 
2371
- def get_extra_context(self, request, instance):
2372
- # note: Device modules tab shouldn't show descendant modules until a proper tree view is implemented
2373
- modulebays = (
2374
- ModuleBay.objects.restrict(request.user, "view")
2375
- .filter(parent_device=instance)
2376
- .prefetch_related("installed_module__status", "installed_module")
2377
- )
2378
- modulebay_table = tables.DeviceModuleBayTable(data=modulebays, user=request.user, orderable=False)
2379
- if request.user.has_perm("dcim.change_modulebay") or request.user.has_perm("dcim.delete_modulebay"):
2380
- modulebay_table.columns.show("pk")
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({})
2381
2846
 
2382
- return {
2383
- **super().get_extra_context(request, instance),
2384
- "modulebay_table": modulebay_table,
2385
- "active_tab": "module-bays",
2386
- }
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({})
2387
2856
 
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({})
2388
2866
 
2389
- class DeviceInventoryView(generic.ObjectView):
2390
- queryset = Device.objects.all()
2391
- template_name = "dcim/device/inventory.html"
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({})
2392
2876
 
2393
- def get_extra_context(self, request, instance):
2394
- inventoryitems = (
2395
- InventoryItem.objects.restrict(request.user, "view").filter(device=instance).select_related("manufacturer")
2396
- )
2397
- inventoryitem_table = tables.DeviceInventoryItemTable(data=inventoryitems, user=request.user, orderable=False)
2398
- if request.user.has_perm("dcim.change_inventoryitem") or request.user.has_perm("dcim.delete_inventoryitem"):
2399
- inventoryitem_table.columns.show("pk")
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({})
2400
2886
 
2401
- return {
2402
- "inventoryitem_table": inventoryitem_table,
2403
- "active_tab": "inventory",
2404
- }
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({})
2405
2896
 
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({})
2406
2906
 
2407
- class DeviceStatusView(generic.ObjectView):
2408
- additional_permissions = ["dcim.napalm_read_device"]
2409
- queryset = Device.objects.all()
2410
- template_name = "dcim/device/status.html"
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({})
2411
2916
 
2412
- def get_extra_context(self, request, instance):
2413
- return {
2414
- "active_tab": "status",
2415
- }
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({})
2416
2926
 
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({})
2417
2936
 
2418
- class DeviceLLDPNeighborsView(generic.ObjectView):
2419
- additional_permissions = ["dcim.napalm_read_device"]
2420
- queryset = Device.objects.all()
2421
- template_name = "dcim/device/lldp_neighbors.html"
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
+ },
2949
+ )
2422
2950
 
2423
- 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()
2424
2960
  interfaces = (
2425
2961
  instance.all_interfaces.restrict(request.user, "view")
2426
2962
  .prefetch_related("_path__destination")
2427
2963
  .exclude(type__in=NONCONNECTABLE_IFACE_TYPES)
2428
2964
  )
2965
+ return Response(
2966
+ {
2967
+ "template": "dcim/device/lldp_neighbors.html",
2968
+ "interfaces": interfaces,
2969
+ },
2970
+ )
2429
2971
 
2430
- return {
2431
- "interfaces": interfaces,
2432
- "active_tab": "lldp-neighbors",
2433
- }
2434
-
2435
-
2436
- class DeviceConfigView(generic.ObjectView):
2437
- additional_permissions = ["dcim.napalm_read_device"]
2438
- queryset = Device.objects.all()
2439
- template_name = "dcim/device/config.html"
2440
-
2441
- def get_extra_context(self, request, instance):
2442
- return {
2443
- "active_tab": "config",
2444
- }
2445
-
2446
-
2447
- class DeviceConfigContextView(ObjectConfigContextView):
2448
- base_template = "dcim/device/base.html"
2449
-
2450
- @cached_property
2451
- def queryset(self): # pylint: disable=method-hidden
2452
- """
2453
- A cached_property rather than a class attribute because annotate_config_context_data() is unsafe at import time.
2454
- """
2455
- return Device.objects.annotate_config_context_data()
2456
-
2457
-
2458
- class DeviceChangeLogView(ObjectChangeLogView):
2459
- base_template = "dcim/device/base.html"
2460
-
2461
-
2462
- class DeviceDynamicGroupsView(ObjectDynamicGroupsView):
2463
- base_template = "dcim/device/base.html"
2464
-
2465
-
2466
- class DeviceEditView(generic.ObjectEditView):
2467
- queryset = Device.objects.all()
2468
- model_form = forms.DeviceForm
2469
- template_name = "dcim/device_edit.html"
2470
-
2471
-
2472
- class DeviceDeleteView(generic.ObjectDeleteView):
2473
- queryset = Device.objects.all()
2474
-
2475
-
2476
- class DeviceBulkImportView(generic.BulkImportView): # 3.0 TODO: remove, unused
2477
- queryset = Device.objects.all()
2478
- table = tables.DeviceImportTable
2479
-
2480
-
2481
- class DeviceBulkEditView(generic.BulkEditView):
2482
- queryset = Device.objects.select_related(
2483
- "tenant",
2484
- "location",
2485
- "rack",
2486
- "role",
2487
- "device_type__manufacturer",
2488
- "secrets_group",
2489
- "device_redundancy_group",
2490
- "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"],
2491
2978
  )
2492
- filterset = filters.DeviceFilterSet
2493
- table = tables.DeviceTable
2494
- form = forms.DeviceBulkEditForm
2495
-
2496
-
2497
- class DeviceBulkDeleteView(generic.BulkDeleteView):
2498
- queryset = Device.objects.select_related("tenant", "location", "rack", "role", "device_type__manufacturer")
2499
- filterset = filters.DeviceFilterSet
2500
- table = tables.DeviceTable
2501
-
2502
-
2503
- class DeviceWirelessView(generic.ObjectView):
2504
- queryset = Device.objects.all()
2505
- template_name = "dcim/device/wireless.html"
2506
-
2507
- def get_extra_context(self, request, instance):
2508
- controller_managed_device_group = instance.controller_managed_device_group
2509
- wireless_networks = ControllerManagedDeviceGroupWirelessNetworkAssignment.objects.filter(
2510
- controller_managed_device_group=controller_managed_device_group
2511
- ).select_related("wireless_network", "controller_managed_device_group", "vlan")
2512
- wireless_networks_table = BaseControllerManagedDeviceGroupWirelessNetworkAssignmentTable(
2513
- data=wireless_networks, user=request.user, orderable=False
2514
- )
2515
- wireless_networks_table.columns.hide("controller_managed_device_group")
2516
- wireless_networks_table.columns.hide("controller")
2517
- RequestConfig(
2518
- request, paginate={"paginator_class": EnhancedPaginator, "per_page": get_paginate_count(request)}
2519
- ).configure(wireless_networks_table)
2520
-
2521
- radio_profiles = ControllerManagedDeviceGroupRadioProfileAssignment.objects.filter(
2522
- controller_managed_device_group=controller_managed_device_group
2523
- ).select_related("radio_profile", "controller_managed_device_group")
2524
- radio_profiles_table = ControllerManagedDeviceGroupRadioProfileAssignmentTable(
2525
- 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
+ },
2526
2984
  )
2527
- radio_profiles_table.columns.hide("controller_managed_device_group")
2528
- RequestConfig(
2529
- request, paginate={"paginator_class": EnhancedPaginator, "per_page": get_paginate_count(request)}
2530
- ).configure(radio_profiles_table)
2531
2985
 
2532
- return {
2533
- "wireless_networks_table": wireless_networks_table,
2534
- "radio_profiles_table": radio_profiles_table,
2535
- "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",
2536
3018
  }
2537
3019
 
3020
+ return Response(context)
3021
+
2538
3022
 
2539
3023
  #
2540
3024
  # Modules
@@ -3007,7 +3491,7 @@ class ConsolePortListView(generic.ObjectListView):
3007
3491
  action_buttons = ("import", "export")
3008
3492
 
3009
3493
 
3010
- class ConsolePortView(generic.ObjectView):
3494
+ class ConsolePortView(DeviceComponentPageMixin, generic.ObjectView):
3011
3495
  queryset = ConsolePort.objects.all()
3012
3496
 
3013
3497
  def get_extra_context(self, request, instance):
@@ -3073,7 +3557,7 @@ class ConsoleServerPortListView(generic.ObjectListView):
3073
3557
  action_buttons = ("import", "export")
3074
3558
 
3075
3559
 
3076
- class ConsoleServerPortView(generic.ObjectView):
3560
+ class ConsoleServerPortView(DeviceComponentPageMixin, generic.ObjectView):
3077
3561
  queryset = ConsoleServerPort.objects.all()
3078
3562
 
3079
3563
  def get_extra_context(self, request, instance):
@@ -3139,7 +3623,7 @@ class PowerPortListView(generic.ObjectListView):
3139
3623
  action_buttons = ("import", "export")
3140
3624
 
3141
3625
 
3142
- class PowerPortView(generic.ObjectView):
3626
+ class PowerPortView(DeviceComponentPageMixin, generic.ObjectView):
3143
3627
  queryset = PowerPort.objects.all()
3144
3628
 
3145
3629
  def get_extra_context(self, request, instance):
@@ -3205,7 +3689,7 @@ class PowerOutletListView(generic.ObjectListView):
3205
3689
  action_buttons = ("import", "export")
3206
3690
 
3207
3691
 
3208
- class PowerOutletView(generic.ObjectView):
3692
+ class PowerOutletView(DeviceComponentPageMixin, generic.ObjectView):
3209
3693
  queryset = PowerOutlet.objects.all()
3210
3694
 
3211
3695
  def get_extra_context(self, request, instance):
@@ -3271,7 +3755,10 @@ class InterfaceListView(generic.ObjectListView):
3271
3755
  action_buttons = ("import", "export")
3272
3756
 
3273
3757
 
3274
- class InterfaceView(generic.ObjectView):
3758
+ class InterfaceView(
3759
+ DeviceComponentPageMixin,
3760
+ generic.ObjectView,
3761
+ ):
3275
3762
  queryset = Interface.objects.all()
3276
3763
 
3277
3764
  def get_extra_context(self, request, instance):
@@ -3401,7 +3888,7 @@ class FrontPortListView(generic.ObjectListView):
3401
3888
  action_buttons = ("import", "export")
3402
3889
 
3403
3890
 
3404
- class FrontPortView(generic.ObjectView):
3891
+ class FrontPortView(DeviceComponentPageMixin, generic.ObjectView):
3405
3892
  queryset = FrontPort.objects.all()
3406
3893
 
3407
3894
  def get_extra_context(self, request, instance):
@@ -3467,7 +3954,7 @@ class RearPortListView(generic.ObjectListView):
3467
3954
  action_buttons = ("import", "export")
3468
3955
 
3469
3956
 
3470
- class RearPortView(generic.ObjectView):
3957
+ class RearPortView(DeviceComponentPageMixin, generic.ObjectView):
3471
3958
  queryset = RearPort.objects.all()
3472
3959
 
3473
3960
  def get_extra_context(self, request, instance):
@@ -3533,7 +4020,7 @@ class DeviceBayListView(generic.ObjectListView):
3533
4020
  action_buttons = ("import", "export")
3534
4021
 
3535
4022
 
3536
- class DeviceBayView(generic.ObjectView):
4023
+ class DeviceBayView(DeviceComponentPageMixin, generic.ObjectView):
3537
4024
  queryset = DeviceBay.objects.all()
3538
4025
 
3539
4026
  def get_extra_context(self, request, instance):
@@ -3695,6 +4182,36 @@ class ModuleBayUIViewSet(ModuleBayCommonViewSetMixin, NautobotUIViewSet):
3695
4182
  ),
3696
4183
  )
3697
4184
  )
4185
+ breadcrumbs = Breadcrumbs(
4186
+ items={
4187
+ "detail": [
4188
+ # Breadcrumb path if ModuleBay is linked with device
4189
+ ModelBreadcrumbItem(model=Device, should_render=lambda c: c["object"].parent_device),
4190
+ InstanceBreadcrumbItem(
4191
+ instance=context_object_attr("parent_device"),
4192
+ should_render=context_object_attr("parent_device"),
4193
+ ),
4194
+ ViewNameBreadcrumbItem(
4195
+ view_name_key="device_breadcrumb_url",
4196
+ should_render=lambda c: c["object"].parent_device and c.get("device_breadcrumb_url"),
4197
+ reverse_kwargs=lambda c: {"pk": c["object"].parent_device.pk},
4198
+ label=lambda c: c["object"]._meta.verbose_name_plural,
4199
+ ),
4200
+ # Breadcrumb path if ModuleBay is linked with module
4201
+ ModelBreadcrumbItem(model=Module, should_render=lambda c: c["object"].parent_device is None),
4202
+ InstanceBreadcrumbItem(
4203
+ instance=context_object_attr("parent_module"),
4204
+ should_render=lambda c: c["object"].parent_device is None,
4205
+ ),
4206
+ ViewNameBreadcrumbItem(
4207
+ view_name_key="module_breadcrumb_url",
4208
+ should_render=lambda c: c["object"].parent_device is None and c.get("module_breadcrumb_url"),
4209
+ reverse_kwargs=lambda c: {"pk": c["object"].parent_module.pk},
4210
+ label=lambda c: c["object"]._meta.verbose_name_plural,
4211
+ ),
4212
+ ]
4213
+ }
4214
+ )
3698
4215
 
3699
4216
  def get_extra_context(self, request, instance):
3700
4217
  context = super().get_extra_context(request, instance)
@@ -3748,7 +4265,7 @@ class InventoryItemListView(generic.ObjectListView):
3748
4265
  action_buttons = ("import", "export")
3749
4266
 
3750
4267
 
3751
- class InventoryItemView(generic.ObjectView):
4268
+ class InventoryItemView(DeviceComponentPageMixin, generic.ObjectView):
3752
4269
  queryset = InventoryItem.objects.all().select_related("device", "manufacturer", "software_version")
3753
4270
 
3754
4271
  def get_extra_context(self, request, instance):
@@ -4108,6 +4625,10 @@ class ConsoleConnectionsListView(ConnectionsListView):
4108
4625
  table = tables.ConsoleConnectionTable
4109
4626
  template_name = "dcim/console_port_connection_list.html"
4110
4627
  action_buttons = ("export",)
4628
+ view_titles = Titles(titles={"list": "Console Connections"})
4629
+ breadcrumbs = Breadcrumbs(
4630
+ items={"list": [ViewNameBreadcrumbItem(view_name="dcim:console_connections_list", label="Console Connections")]}
4631
+ )
4111
4632
 
4112
4633
  def extra_context(self):
4113
4634
  return {
@@ -4124,6 +4645,10 @@ class PowerConnectionsListView(ConnectionsListView):
4124
4645
  table = tables.PowerConnectionTable
4125
4646
  template_name = "dcim/power_port_connection_list.html"
4126
4647
  action_buttons = ("export",)
4648
+ view_titles = Titles(titles={"list": "Power Connections"})
4649
+ breadcrumbs = Breadcrumbs(
4650
+ items={"list": [ViewNameBreadcrumbItem(view_name="dcim:power_connections_list", label="Power Connections")]}
4651
+ )
4127
4652
 
4128
4653
  def extra_context(self):
4129
4654
  return {
@@ -4140,6 +4665,12 @@ class InterfaceConnectionsListView(ConnectionsListView):
4140
4665
  table = tables.InterfaceConnectionTable
4141
4666
  template_name = "dcim/interface_connection_list.html"
4142
4667
  action_buttons = ("export",)
4668
+ view_titles = Titles(titles={"list": "Interface Connections"})
4669
+ breadcrumbs = Breadcrumbs(
4670
+ items={
4671
+ "list": [ViewNameBreadcrumbItem(view_name="dcim:interface_connections_list", label="Interface Connections")]
4672
+ }
4673
+ )
4143
4674
 
4144
4675
  def __init__(self, *args, **kwargs):
4145
4676
  super().__init__(*args, **kwargs)
@@ -4182,10 +4713,50 @@ class VirtualChassisUIViewSet(NautobotUIViewSet):
4182
4713
  bulk_update_form_class = forms.VirtualChassisBulkEditForm
4183
4714
  filterset_class = filters.VirtualChassisFilterSet
4184
4715
  filterset_form_class = forms.VirtualChassisFilterForm
4185
- form_class = forms.VirtualChassisCreateForm
4186
4716
  serializer_class = serializers.VirtualChassisSerializer
4187
4717
  table_class = tables.VirtualChassisTable
4188
4718
  queryset = VirtualChassis.objects.all()
4719
+ create_form_class = forms.VirtualChassisCreateForm
4720
+ update_form_class = forms.VirtualChassisForm
4721
+
4722
+ class MembersObjectsTablePanel(object_detail.ObjectsTablePanel):
4723
+ def _get_table_add_url(self, context):
4724
+ obj = get_obj_from_context(context)
4725
+ request = context["request"]
4726
+ return_url = context.get("return_url", obj.get_absolute_url())
4727
+
4728
+ if not request.user.has_perm("dcim.change_virtualchassis"):
4729
+ return None
4730
+
4731
+ params = []
4732
+ master = obj.master
4733
+
4734
+ if master is not None:
4735
+ if master.location is not None:
4736
+ params.append(("location", master.location.pk))
4737
+
4738
+ if master.rack is not None:
4739
+ params.append(("rack", master.rack.pk))
4740
+
4741
+ params.append(("return_url", return_url))
4742
+ return reverse("dcim:virtualchassis_add_member", kwargs={"pk": obj.pk}) + "?" + urlencode(params)
4743
+
4744
+ object_detail_content = object_detail.ObjectDetailContent(
4745
+ panels=[
4746
+ object_detail.ObjectFieldsPanel(
4747
+ section=SectionChoices.LEFT_HALF,
4748
+ weight=100,
4749
+ fields="__all__",
4750
+ ),
4751
+ MembersObjectsTablePanel(
4752
+ section=SectionChoices.RIGHT_HALF,
4753
+ weight=100,
4754
+ table_class=tables.VirtualChassisMembersTable,
4755
+ table_filter="virtual_chassis",
4756
+ table_title="Members",
4757
+ ),
4758
+ ]
4759
+ )
4189
4760
 
4190
4761
  def get_extra_context(self, request, instance):
4191
4762
  context = super().get_extra_context(request, instance)
@@ -4215,10 +4786,6 @@ class VirtualChassisUIViewSet(NautobotUIViewSet):
4215
4786
  }
4216
4787
  )
4217
4788
 
4218
- elif self.action == "retrieve":
4219
- members = Device.objects.restrict(request.user).filter(virtual_chassis=instance)
4220
- context.update({"members": members})
4221
-
4222
4789
  return context
4223
4790
 
4224
4791
  def form_save(self, form, **kwargs):
@@ -4355,6 +4922,17 @@ class PowerPanelUIViewSet(NautobotUIViewSet):
4355
4922
  serializer_class = serializers.PowerPanelSerializer
4356
4923
  table_class = tables.PowerPanelTable
4357
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
+ )
4358
4936
 
4359
4937
  object_detail_content = object_detail.ObjectDetailContent(
4360
4938
  panels=(
@@ -4429,6 +5007,19 @@ class PowerFeedUIViewSet(NautobotUIViewSet):
4429
5007
  queryset = PowerFeed.objects.all()
4430
5008
  serializer_class = serializers.PowerFeedSerializer
4431
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
+ )
4432
5023
 
4433
5024
  object_detail_content = object_detail.ObjectDetailContent(
4434
5025
  panels=(
@@ -5145,6 +5736,14 @@ class VirtualDeviceContextUIViewSet(NautobotUIViewSet):
5145
5736
  queryset = VirtualDeviceContext.objects.all()
5146
5737
  serializer_class = serializers.VirtualDeviceContextSerializer
5147
5738
  table_class = tables.VirtualDeviceContextTable
5739
+ breadcrumbs = Breadcrumbs(
5740
+ items={
5741
+ "detail": [
5742
+ ModelBreadcrumbItem(),
5743
+ InstanceBreadcrumbItem(instance=context_object_attr("device")),
5744
+ ]
5745
+ }
5746
+ )
5148
5747
  object_detail_content = object_detail.ObjectDetailContent(
5149
5748
  panels=(
5150
5749
  object_detail.ObjectFieldsPanel(