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

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

Potentially problematic release.


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

Files changed (697) hide show
  1. nautobot/apps/forms.py +4 -0
  2. nautobot/apps/models.py +10 -1
  3. nautobot/circuits/__init__.py +0 -1
  4. nautobot/circuits/apps.py +1 -0
  5. nautobot/circuits/factory.py +15 -3
  6. nautobot/circuits/filters.py +13 -0
  7. nautobot/circuits/forms.py +13 -0
  8. nautobot/circuits/migrations/0021_alter_circuit_status_alter_circuittermination__path.py +32 -0
  9. nautobot/circuits/migrations/0022_circuittermination_cloud_network.py +25 -0
  10. nautobot/circuits/models.py +16 -3
  11. nautobot/circuits/tables.py +16 -2
  12. nautobot/circuits/templates/circuits/circuittermination_create.html +10 -2
  13. nautobot/circuits/templates/circuits/circuittermination_retrieve.html +6 -0
  14. nautobot/circuits/templates/circuits/inc/circuit_termination.html +6 -1
  15. nautobot/circuits/tests/test_api.py +7 -5
  16. nautobot/circuits/tests/test_filters.py +12 -5
  17. nautobot/circuits/tests/test_models.py +33 -2
  18. nautobot/circuits/views.py +2 -3
  19. nautobot/cloud/__init__.py +0 -0
  20. nautobot/cloud/api/__init__.py +0 -0
  21. nautobot/cloud/api/serializers.py +54 -0
  22. nautobot/cloud/api/urls.py +16 -0
  23. nautobot/cloud/api/views.py +48 -0
  24. nautobot/cloud/apps.py +13 -0
  25. nautobot/cloud/factory.py +113 -0
  26. nautobot/cloud/filters.py +187 -0
  27. nautobot/cloud/forms.py +339 -0
  28. nautobot/cloud/homepage.py +43 -0
  29. nautobot/cloud/migrations/0001_initial.py +304 -0
  30. nautobot/cloud/migrations/__init__.py +0 -0
  31. nautobot/cloud/models.py +246 -0
  32. nautobot/cloud/navigation.py +85 -0
  33. nautobot/cloud/tables.py +157 -0
  34. nautobot/cloud/templates/cloud/cloudaccount_retrieve.html +43 -0
  35. nautobot/cloud/templates/cloud/cloudnetwork_retrieve.html +122 -0
  36. nautobot/cloud/templates/cloud/cloudnetwork_update.html +33 -0
  37. nautobot/cloud/templates/cloud/cloudresourcetype_retrieve.html +111 -0
  38. nautobot/cloud/templates/cloud/cloudservice_retrieve.html +69 -0
  39. nautobot/cloud/templates/cloud/cloudservice_update.html +25 -0
  40. nautobot/cloud/tests/__init__.py +0 -0
  41. nautobot/cloud/tests/test_api.py +248 -0
  42. nautobot/cloud/tests/test_filters.py +125 -0
  43. nautobot/cloud/tests/test_models.py +43 -0
  44. nautobot/cloud/tests/test_views.py +153 -0
  45. nautobot/cloud/urls.py +14 -0
  46. nautobot/cloud/views.py +181 -0
  47. nautobot/core/__init__.py +0 -3
  48. nautobot/core/api/metadata.py +1 -0
  49. nautobot/core/api/parsers.py +7 -1
  50. nautobot/core/api/urls.py +1 -0
  51. nautobot/core/api/utils.py +1 -0
  52. nautobot/core/api/views.py +4 -0
  53. nautobot/core/apps/__init__.py +6 -3
  54. nautobot/core/constants.py +8 -0
  55. nautobot/core/factory.py +32 -1
  56. nautobot/core/filters.py +95 -13
  57. nautobot/core/forms/fields.py +10 -4
  58. nautobot/core/forms/forms.py +11 -3
  59. nautobot/core/forms/widgets.py +18 -1
  60. nautobot/core/graphql/schema.py +26 -4
  61. nautobot/core/jobs/__init__.py +16 -2
  62. nautobot/core/jobs/cleanup.py +100 -0
  63. nautobot/core/jobs/groups.py +38 -0
  64. nautobot/core/management/commands/generate_test_data.py +116 -3
  65. nautobot/core/models/__init__.py +34 -9
  66. nautobot/core/models/generics.py +19 -3
  67. nautobot/core/models/name_color_content_types.py +7 -28
  68. nautobot/core/models/querysets.py +4 -3
  69. nautobot/core/models/tree_queries.py +1 -1
  70. nautobot/core/models/utils.py +21 -5
  71. nautobot/core/settings.py +2 -17
  72. nautobot/core/settings.yaml +34 -13
  73. nautobot/core/settings_funcs.py +103 -0
  74. nautobot/core/tables.py +130 -56
  75. nautobot/core/templates/admin/search_form.html +1 -1
  76. nautobot/core/templates/buttons/add.html +11 -3
  77. nautobot/core/templates/buttons/consolidated_bulk_action_buttons.html +13 -0
  78. nautobot/core/templates/buttons/consolidated_detail_view_action_buttons.html +13 -0
  79. nautobot/core/templates/buttons/export.html +101 -53
  80. nautobot/core/templates/buttons/job_import.html +11 -3
  81. nautobot/core/templates/generic/object_bulk_destroy.html +3 -1
  82. nautobot/core/templates/generic/object_bulk_update.html +3 -1
  83. nautobot/core/templates/generic/object_changelog.html +0 -9
  84. nautobot/core/templates/generic/object_list.html +156 -17
  85. nautobot/core/templates/generic/object_retrieve.html +80 -16
  86. nautobot/core/templates/inc/extras_features_edit_form_fields.html +8 -0
  87. nautobot/core/templates/inc/javascript.html +2 -0
  88. nautobot/core/templates/inc/media.html +2 -2
  89. nautobot/core/templates/inc/nav_menu.html +1 -0
  90. nautobot/core/templates/inc/paginator.html +7 -7
  91. nautobot/core/templates/inc/search_panel.html +2 -2
  92. nautobot/core/templates/inc/table.html +2 -2
  93. nautobot/core/templates/nautobot_config.py.j2 +13 -8
  94. nautobot/core/templates/utilities/templatetags/dynamic_group_assignment_modal.html +37 -0
  95. nautobot/core/templates/utilities/templatetags/filter_form_modal.html +2 -2
  96. nautobot/core/templates/utilities/templatetags/saved_view_modal.html +38 -0
  97. nautobot/core/templates/utilities/theme_preview.html +25 -8
  98. nautobot/core/templates/utilities/worker_status.html +152 -0
  99. nautobot/core/templatetags/buttons.py +335 -38
  100. nautobot/core/templatetags/form_helpers.py +1 -1
  101. nautobot/core/templatetags/helpers.py +181 -11
  102. nautobot/core/testing/api.py +5 -4
  103. nautobot/core/testing/filters.py +63 -14
  104. nautobot/core/testing/mixins.py +46 -0
  105. nautobot/core/testing/models.py +22 -0
  106. nautobot/core/testing/schema.py +4 -8
  107. nautobot/core/testing/views.py +31 -14
  108. nautobot/core/tests/integration/test_import_objects_ui.py +1 -0
  109. nautobot/core/tests/integration/test_swagger.py +1 -1
  110. nautobot/core/tests/nautobot_config.py +0 -1
  111. nautobot/core/tests/runner.py +2 -2
  112. nautobot/core/tests/test_api.py +1 -0
  113. nautobot/core/tests/test_authentication.py +7 -2
  114. nautobot/core/tests/test_filters.py +11 -9
  115. nautobot/core/tests/test_forms.py +9 -0
  116. nautobot/core/tests/test_graphql.py +27 -16
  117. nautobot/core/tests/test_jobs.py +122 -0
  118. nautobot/core/tests/test_tables.py +3 -1
  119. nautobot/core/tests/test_templatetags_helpers.py +12 -5
  120. nautobot/core/tests/test_utils.py +31 -20
  121. nautobot/core/tests/test_views.py +6 -6
  122. nautobot/core/urls.py +8 -3
  123. nautobot/core/utils/deprecation.py +29 -0
  124. nautobot/core/utils/filtering.py +12 -9
  125. nautobot/core/utils/lookup.py +37 -2
  126. nautobot/core/utils/requests.py +4 -1
  127. nautobot/core/views/__init__.py +137 -24
  128. nautobot/core/views/generic.py +119 -67
  129. nautobot/core/views/mixins.py +105 -36
  130. nautobot/core/views/paginator.py +9 -3
  131. nautobot/core/views/renderers.py +121 -56
  132. nautobot/core/views/utils.py +81 -1
  133. nautobot/dcim/__init__.py +0 -1
  134. nautobot/dcim/api/serializers.py +180 -44
  135. nautobot/dcim/api/urls.py +7 -3
  136. nautobot/dcim/api/views.py +53 -7
  137. nautobot/dcim/apps.py +3 -0
  138. nautobot/dcim/choices.py +25 -0
  139. nautobot/dcim/constants.py +7 -0
  140. nautobot/dcim/factory.py +252 -18
  141. nautobot/dcim/filters/__init__.py +373 -193
  142. nautobot/dcim/filters/mixins.py +274 -1
  143. nautobot/dcim/forms.py +834 -121
  144. nautobot/dcim/graphql/types.py +2 -2
  145. nautobot/dcim/homepage.py +1 -1
  146. nautobot/dcim/migrations/0059_add_role_field_to_interface_models.py +27 -0
  147. nautobot/dcim/migrations/0060_alter_cable_status_alter_consoleport__path_and_more.py +303 -0
  148. nautobot/dcim/migrations/0061_module_models.py +862 -0
  149. nautobot/dcim/migrations/0062_module_data_migration.py +25 -0
  150. nautobot/dcim/models/__init__.py +8 -0
  151. nautobot/dcim/models/cables.py +15 -0
  152. nautobot/dcim/models/device_component_templates.py +207 -53
  153. nautobot/dcim/models/device_components.py +275 -99
  154. nautobot/dcim/models/devices.py +468 -13
  155. nautobot/dcim/models/racks.py +0 -1
  156. nautobot/dcim/navigation.py +47 -0
  157. nautobot/dcim/signals.py +3 -3
  158. nautobot/dcim/tables/__init__.py +35 -23
  159. nautobot/dcim/tables/devices.py +229 -43
  160. nautobot/dcim/tables/devicetypes.py +65 -9
  161. nautobot/dcim/tables/racks.py +5 -1
  162. nautobot/dcim/tables/template_code.py +46 -26
  163. nautobot/dcim/templates/dcim/cable_connect.html +76 -3
  164. nautobot/dcim/templates/dcim/console_port_connection_list.html +7 -5
  165. nautobot/dcim/templates/dcim/device/base.html +14 -6
  166. nautobot/dcim/templates/dcim/device/consoleports.html +2 -3
  167. nautobot/dcim/templates/dcim/device/consoleserverports.html +2 -3
  168. nautobot/dcim/templates/dcim/device/devicebays.html +6 -7
  169. nautobot/dcim/templates/dcim/device/frontports.html +2 -3
  170. nautobot/dcim/templates/dcim/device/interfaces.html +2 -3
  171. nautobot/dcim/templates/dcim/device/inventory.html +2 -3
  172. nautobot/dcim/templates/dcim/device/modulebays.html +49 -0
  173. nautobot/dcim/templates/dcim/device/poweroutlets.html +2 -3
  174. nautobot/dcim/templates/dcim/device/powerports.html +2 -3
  175. nautobot/dcim/templates/dcim/device/rearports.html +2 -3
  176. nautobot/dcim/templates/dcim/device.html +45 -1
  177. nautobot/dcim/templates/dcim/device_component.html +13 -5
  178. nautobot/dcim/templates/dcim/device_list.html +2 -1
  179. nautobot/dcim/templates/dcim/devicetype.html +99 -98
  180. nautobot/dcim/templates/dcim/devicetype_list.html +8 -16
  181. nautobot/dcim/templates/dcim/inc/devicetype_component_table.html +1 -1
  182. nautobot/dcim/templates/dcim/inc/moduletype_component_table.html +39 -0
  183. nautobot/dcim/templates/dcim/interface.html +17 -2
  184. nautobot/dcim/templates/dcim/interface_connection_list.html +7 -5
  185. nautobot/dcim/templates/dcim/interface_edit.html +1 -0
  186. nautobot/dcim/templates/dcim/manufacturer.html +24 -0
  187. nautobot/dcim/templates/dcim/module/base.html +97 -0
  188. nautobot/dcim/templates/dcim/module_bulk_destroy.html +5 -0
  189. nautobot/dcim/templates/dcim/module_consoleports.html +53 -0
  190. nautobot/dcim/templates/dcim/module_consoleserverports.html +53 -0
  191. nautobot/dcim/templates/dcim/module_destroy.html +5 -0
  192. nautobot/dcim/templates/dcim/module_frontports.html +53 -0
  193. nautobot/dcim/templates/dcim/module_interfaces.html +57 -0
  194. nautobot/dcim/templates/dcim/module_list.html +20 -0
  195. nautobot/dcim/templates/dcim/module_modulebays.html +49 -0
  196. nautobot/dcim/templates/dcim/module_poweroutlets.html +53 -0
  197. nautobot/dcim/templates/dcim/module_powerports.html +53 -0
  198. nautobot/dcim/templates/dcim/module_rearports.html +53 -0
  199. nautobot/dcim/templates/dcim/module_retrieve.html +63 -0
  200. nautobot/dcim/templates/dcim/module_update.html +71 -0
  201. nautobot/dcim/templates/dcim/modulebay_bulk_destroy.html +5 -0
  202. nautobot/dcim/templates/dcim/modulebay_destroy.html +8 -0
  203. nautobot/dcim/templates/dcim/modulebay_retrieve.html +101 -0
  204. nautobot/dcim/templates/dcim/moduletype_list.html +11 -0
  205. nautobot/dcim/templates/dcim/moduletype_retrieve.html +159 -0
  206. nautobot/dcim/templates/dcim/power_port_connection_list.html +7 -5
  207. nautobot/dcim/templates/dcim/softwareimagefile_retrieve.html +65 -19
  208. nautobot/dcim/tests/integration/test_cable_connect_form.py +4 -4
  209. nautobot/dcim/tests/test_api.py +693 -208
  210. nautobot/dcim/tests/test_filters.py +843 -217
  211. nautobot/dcim/tests/test_models.py +1072 -8
  212. nautobot/dcim/tests/test_views.py +1510 -341
  213. nautobot/dcim/urls.py +17 -2
  214. nautobot/dcim/utils.py +2 -3
  215. nautobot/dcim/views.py +1106 -116
  216. nautobot/extras/__init__.py +0 -1
  217. nautobot/extras/api/serializers.py +115 -3
  218. nautobot/extras/api/urls.py +12 -0
  219. nautobot/extras/api/views.py +66 -0
  220. nautobot/extras/apps.py +2 -2
  221. nautobot/extras/choices.py +43 -0
  222. nautobot/extras/context_managers.py +13 -8
  223. nautobot/extras/datasources/git.py +2 -0
  224. nautobot/extras/factory.py +460 -9
  225. nautobot/extras/filters/__init__.py +174 -3
  226. nautobot/extras/filters/mixins.py +46 -43
  227. nautobot/extras/forms/base.py +24 -5
  228. nautobot/extras/forms/forms.py +227 -8
  229. nautobot/extras/forms/mixins.py +93 -0
  230. nautobot/extras/graphql/types.py +23 -10
  231. nautobot/extras/homepage.py +14 -1
  232. nautobot/extras/management/__init__.py +1 -0
  233. nautobot/extras/management/commands/refresh_dynamic_group_member_caches.py +1 -16
  234. nautobot/extras/migrations/0021_customfield_changelog_data.py +1 -0
  235. nautobot/extras/migrations/0109_dynamicgroup_group_type_dynamicgroup_tags_and_more.py +108 -0
  236. nautobot/extras/migrations/0110_alter_configcontext_cluster_groups_and_more.py +111 -0
  237. nautobot/extras/migrations/0111_metadata.py +162 -0
  238. nautobot/extras/migrations/0112_dynamic_group_group_type_data_migration.py +28 -0
  239. nautobot/extras/migrations/0113_saved_views.py +77 -0
  240. nautobot/extras/models/__init__.py +15 -1
  241. nautobot/extras/models/change_logging.py +3 -3
  242. nautobot/extras/models/contacts.py +4 -0
  243. nautobot/extras/models/customfields.py +18 -3
  244. nautobot/extras/models/groups.py +389 -225
  245. nautobot/extras/models/jobs.py +6 -3
  246. nautobot/extras/models/metadata.py +441 -0
  247. nautobot/extras/models/mixins.py +72 -62
  248. nautobot/extras/models/models.py +118 -9
  249. nautobot/extras/models/relationships.py +9 -2
  250. nautobot/extras/models/tags.py +13 -2
  251. nautobot/extras/navigation.py +57 -0
  252. nautobot/extras/plugins/__init__.py +3 -1
  253. nautobot/extras/querysets.py +30 -66
  254. nautobot/extras/signals.py +95 -100
  255. nautobot/extras/tables.py +165 -12
  256. nautobot/extras/templates/extras/dynamicgroup.html +44 -15
  257. nautobot/extras/templates/extras/dynamicgroup_edit.html +2 -0
  258. nautobot/extras/templates/extras/job.html +1 -1
  259. nautobot/extras/templates/extras/jobresult.html +61 -74
  260. nautobot/extras/templates/extras/metadatatype_create.html +89 -0
  261. nautobot/extras/templates/extras/metadatatype_retrieve.html +67 -0
  262. nautobot/extras/templates/extras/object_dynamicgroups.html +7 -0
  263. nautobot/extras/templates/extras/objectchange_list.html +0 -12
  264. nautobot/extras/templates/extras/plugins_list.html +1 -3
  265. nautobot/extras/templates/extras/role_retrieve.html +48 -0
  266. nautobot/extras/templates/extras/staticgroupassociation_retrieve.html +20 -0
  267. nautobot/extras/tests/integration/test_customfields.py +1 -0
  268. nautobot/extras/tests/test_api.py +509 -23
  269. nautobot/extras/tests/test_changelog.py +20 -9
  270. nautobot/extras/tests/test_context_managers.py +22 -15
  271. nautobot/extras/tests/test_datasources.py +13 -1
  272. nautobot/extras/tests/test_dynamicgroups.py +201 -171
  273. nautobot/extras/tests/test_filters.py +211 -12
  274. nautobot/extras/tests/test_jobs.py +6 -6
  275. nautobot/extras/tests/test_models.py +501 -4
  276. nautobot/extras/tests/test_relationships.py +1 -0
  277. nautobot/extras/tests/test_views.py +565 -8
  278. nautobot/extras/tests/test_webhooks.py +1 -1
  279. nautobot/extras/urls.py +5 -0
  280. nautobot/extras/utils.py +51 -11
  281. nautobot/extras/views.py +542 -76
  282. nautobot/ipam/__init__.py +0 -1
  283. nautobot/ipam/apps.py +1 -0
  284. nautobot/ipam/factory.py +17 -19
  285. nautobot/ipam/filters.py +13 -0
  286. nautobot/ipam/forms.py +8 -4
  287. nautobot/ipam/graphql/types.py +2 -2
  288. nautobot/ipam/migrations/0047_alter_ipaddress_role_alter_ipaddress_status_and_more.py +59 -0
  289. nautobot/ipam/models.py +11 -8
  290. nautobot/ipam/querysets.py +1 -1
  291. nautobot/ipam/signals.py +4 -2
  292. nautobot/ipam/tables.py +5 -0
  293. nautobot/ipam/templates/ipam/ipaddress_interfaces.html +1 -1
  294. nautobot/ipam/templates/ipam/ipaddress_vm_interfaces.html +1 -1
  295. nautobot/ipam/templates/ipam/prefix.html +1 -0
  296. nautobot/ipam/tests/test_api.py +37 -18
  297. nautobot/ipam/tests/test_filters.py +26 -2
  298. nautobot/ipam/tests/test_models.py +6 -0
  299. nautobot/ipam/tests/test_querysets.py +1 -1
  300. nautobot/ipam/tests/test_views.py +3 -2
  301. nautobot/ipam/urls.py +2 -2
  302. nautobot/ipam/views.py +18 -26
  303. nautobot/project-static/css/base.css +20 -0
  304. nautobot/project-static/css/dark.css +11 -0
  305. nautobot/project-static/docs/404.html +892 -88
  306. nautobot/project-static/docs/apps/index.html +892 -88
  307. nautobot/project-static/docs/apps/nautobot-apps.html +892 -88
  308. nautobot/project-static/docs/assets/_mkdocstrings.css +5 -0
  309. nautobot/project-static/docs/assets/stylesheets/main.3cba04c6.min.css +1 -0
  310. nautobot/project-static/docs/assets/stylesheets/main.3cba04c6.min.css.map +1 -0
  311. nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +919 -120
  312. nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +904 -101
  313. nautobot/project-static/docs/code-reference/nautobot/apps/api.html +1618 -903
  314. nautobot/project-static/docs/code-reference/nautobot/apps/change_logging.html +935 -144
  315. nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +977 -188
  316. nautobot/project-static/docs/code-reference/nautobot/apps/config.html +901 -99
  317. nautobot/project-static/docs/code-reference/nautobot/apps/constants.html +897 -93
  318. nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +991 -193
  319. nautobot/project-static/docs/code-reference/nautobot/apps/exceptions.html +974 -131
  320. nautobot/project-static/docs/code-reference/nautobot/apps/factory.html +1078 -272
  321. nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +1242 -334
  322. nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +1727 -875
  323. nautobot/project-static/docs/code-reference/nautobot/apps/graphql.html +1164 -381
  324. nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +2088 -1374
  325. nautobot/project-static/docs/code-reference/nautobot/apps/models.html +2246 -1422
  326. nautobot/project-static/docs/code-reference/nautobot/apps/querysets.html +912 -111
  327. nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +963 -163
  328. nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +1010 -223
  329. nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +1913 -1277
  330. nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +1846 -1102
  331. nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +904 -101
  332. nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +2331 -1699
  333. nautobot/project-static/docs/code-reference/nautobot/apps/views.html +1802 -1024
  334. nautobot/project-static/docs/development/apps/api/configuration-view.html +892 -88
  335. nautobot/project-static/docs/development/apps/api/database-backend-config.html +892 -88
  336. nautobot/project-static/docs/development/apps/api/models/django-admin.html +892 -88
  337. nautobot/project-static/docs/development/apps/api/models/global-search.html +892 -88
  338. nautobot/project-static/docs/development/apps/api/models/graphql.html +892 -88
  339. nautobot/project-static/docs/development/apps/api/models/index.html +942 -90
  340. nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +892 -88
  341. nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +892 -88
  342. nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +892 -88
  343. nautobot/project-static/docs/development/apps/api/platform-features/git-repository-content.html +892 -88
  344. nautobot/project-static/docs/development/apps/api/platform-features/index.html +892 -88
  345. nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +892 -88
  346. nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +892 -88
  347. nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +892 -88
  348. nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +892 -88
  349. nautobot/project-static/docs/development/apps/api/platform-features/uniquely-identify-objects.html +892 -88
  350. nautobot/project-static/docs/development/apps/api/prometheus.html +892 -88
  351. nautobot/project-static/docs/development/apps/api/setup.html +892 -88
  352. nautobot/project-static/docs/development/apps/api/testing.html +892 -88
  353. nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +892 -88
  354. nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +892 -88
  355. nautobot/project-static/docs/development/apps/api/ui-extensions/index.html +892 -88
  356. nautobot/project-static/docs/development/apps/api/ui-extensions/navigation.html +892 -88
  357. nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +892 -88
  358. nautobot/project-static/docs/development/apps/api/views/base-template.html +892 -88
  359. nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +892 -88
  360. nautobot/project-static/docs/development/apps/api/views/django-generic-views.html +892 -88
  361. nautobot/project-static/docs/development/apps/api/views/help-documentation.html +892 -88
  362. nautobot/project-static/docs/development/apps/api/views/index.html +892 -88
  363. nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +892 -88
  364. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +892 -88
  365. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +892 -88
  366. nautobot/project-static/docs/development/apps/api/views/notes.html +892 -88
  367. nautobot/project-static/docs/development/apps/api/views/rest-api.html +892 -88
  368. nautobot/project-static/docs/development/apps/api/views/urls.html +892 -88
  369. nautobot/project-static/docs/development/apps/index.html +892 -88
  370. nautobot/project-static/docs/development/apps/migration/code-updates.html +892 -88
  371. nautobot/project-static/docs/development/apps/migration/dependency-updates.html +892 -88
  372. nautobot/project-static/docs/development/apps/migration/from-v1.html +892 -88
  373. nautobot/project-static/docs/development/apps/migration/model-updates/dcim.html +892 -88
  374. nautobot/project-static/docs/development/apps/migration/model-updates/extras.html +892 -88
  375. nautobot/project-static/docs/development/apps/migration/model-updates/global.html +892 -88
  376. nautobot/project-static/docs/development/apps/migration/model-updates/ipam.html +892 -88
  377. nautobot/project-static/docs/development/apps/porting-from-netbox.html +892 -88
  378. nautobot/project-static/docs/development/core/application-registry.html +892 -88
  379. nautobot/project-static/docs/development/core/best-practices.html +893 -88
  380. nautobot/project-static/docs/development/core/bootstrap-ui.html +892 -88
  381. nautobot/project-static/docs/development/core/caching.html +892 -88
  382. nautobot/project-static/docs/development/core/controllers.html +892 -88
  383. nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +892 -88
  384. nautobot/project-static/docs/development/core/generic-views.html +892 -88
  385. nautobot/project-static/docs/development/core/getting-started.html +892 -88
  386. nautobot/project-static/docs/development/core/homepage.html +892 -88
  387. nautobot/project-static/docs/development/core/index.html +892 -88
  388. nautobot/project-static/docs/development/core/model-checklist.html +901 -89
  389. nautobot/project-static/docs/development/core/model-features.html +892 -88
  390. nautobot/project-static/docs/development/core/natural-keys.html +892 -88
  391. nautobot/project-static/docs/development/core/navigation-menu.html +892 -88
  392. nautobot/project-static/docs/development/core/release-checklist.html +895 -91
  393. nautobot/project-static/docs/development/core/role-internals.html +892 -88
  394. nautobot/project-static/docs/development/core/settings.html +892 -88
  395. nautobot/project-static/docs/development/core/style-guide.html +893 -89
  396. nautobot/project-static/docs/development/core/templates.html +904 -89
  397. nautobot/project-static/docs/development/core/testing.html +892 -88
  398. nautobot/project-static/docs/development/core/user-preferences.html +892 -88
  399. nautobot/project-static/docs/development/index.html +892 -88
  400. nautobot/project-static/docs/development/jobs/index.html +893 -89
  401. nautobot/project-static/docs/development/jobs/migration/from-v1.html +892 -88
  402. nautobot/project-static/docs/index.html +892 -88
  403. nautobot/project-static/docs/media/models/cloud_aws_direct_connect_dark.png +0 -0
  404. nautobot/project-static/docs/media/models/cloud_aws_direct_connect_light.png +0 -0
  405. nautobot/project-static/docs/models/cloud/cloudaccount.html +15 -0
  406. nautobot/project-static/docs/models/cloud/cloudnetwork.html +15 -0
  407. nautobot/project-static/docs/models/cloud/cloudnetworkprefixassignment.html +15 -0
  408. nautobot/project-static/docs/models/cloud/cloudresourcetype.html +15 -0
  409. nautobot/project-static/docs/models/cloud/cloudservice.html +15 -0
  410. nautobot/project-static/docs/models/cloud/cloudservicenetworkassignment.html +15 -0
  411. nautobot/project-static/docs/models/dcim/module.html +15 -0
  412. nautobot/project-static/docs/models/dcim/modulebay.html +15 -0
  413. nautobot/project-static/docs/models/dcim/modulebaytemplate.html +15 -0
  414. nautobot/project-static/docs/models/dcim/moduletype.html +15 -0
  415. nautobot/project-static/docs/models/extras/metadatachoice.html +15 -0
  416. nautobot/project-static/docs/models/extras/metadatatype.html +15 -0
  417. nautobot/project-static/docs/models/extras/objectmetadata.html +15 -0
  418. nautobot/project-static/docs/models/extras/role.html +15 -0
  419. nautobot/project-static/docs/models/extras/savedview.html +15 -0
  420. nautobot/project-static/docs/models/extras/staticgroupassociation.html +15 -0
  421. nautobot/project-static/docs/models/extras/status.html +15 -0
  422. nautobot/project-static/docs/objects.inv +0 -0
  423. nautobot/project-static/docs/overview/application_stack.html +900 -89
  424. nautobot/project-static/docs/overview/design_philosophy.html +892 -88
  425. nautobot/project-static/docs/release-notes/index.html +1129 -92
  426. nautobot/project-static/docs/release-notes/version-1.0.html +892 -88
  427. nautobot/project-static/docs/release-notes/version-1.1.html +892 -88
  428. nautobot/project-static/docs/release-notes/version-1.2.html +892 -88
  429. nautobot/project-static/docs/release-notes/version-1.3.html +892 -88
  430. nautobot/project-static/docs/release-notes/version-1.4.html +892 -88
  431. nautobot/project-static/docs/release-notes/version-1.5.html +893 -89
  432. nautobot/project-static/docs/release-notes/version-1.6.html +893 -89
  433. nautobot/project-static/docs/release-notes/version-2.0.html +892 -88
  434. nautobot/project-static/docs/release-notes/version-2.1.html +892 -88
  435. nautobot/project-static/docs/release-notes/version-2.2.html +895 -91
  436. nautobot/project-static/docs/release-notes/version-2.3.html +9954 -0
  437. nautobot/project-static/docs/requirements.txt +5 -5
  438. nautobot/project-static/docs/search/search_index.json +1 -1
  439. nautobot/project-static/docs/sitemap.xml +331 -256
  440. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  441. nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +892 -88
  442. nautobot/project-static/docs/user-guide/administration/configuration/authentication/remote.html +892 -88
  443. nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +892 -88
  444. nautobot/project-static/docs/user-guide/administration/configuration/index.html +892 -88
  445. nautobot/project-static/docs/user-guide/administration/configuration/optional-settings.html +992 -174
  446. nautobot/project-static/docs/user-guide/administration/configuration/required-settings.html +892 -88
  447. nautobot/project-static/docs/user-guide/administration/configuration/time-zones.html +892 -88
  448. nautobot/project-static/docs/user-guide/administration/guides/caching.html +892 -88
  449. nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +896 -88
  450. nautobot/project-static/docs/user-guide/administration/guides/healthcheck.html +892 -88
  451. nautobot/project-static/docs/user-guide/administration/guides/permissions.html +892 -88
  452. nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +892 -88
  453. nautobot/project-static/docs/user-guide/administration/guides/replicating-nautobot.html +892 -88
  454. nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +892 -88
  455. nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +892 -88
  456. nautobot/project-static/docs/user-guide/administration/installation/app-install.html +892 -88
  457. nautobot/project-static/docs/user-guide/administration/installation/external-authentication.html +892 -88
  458. nautobot/project-static/docs/user-guide/administration/installation/http-server.html +944 -153
  459. nautobot/project-static/docs/user-guide/administration/installation/index.html +901 -93
  460. nautobot/project-static/docs/user-guide/administration/installation/install_system.html +934 -122
  461. nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +954 -157
  462. nautobot/project-static/docs/user-guide/administration/installation/services.html +913 -112
  463. nautobot/project-static/docs/user-guide/administration/installation-extras/docker.html +908 -99
  464. nautobot/project-static/docs/user-guide/administration/installation-extras/health-checks.html +892 -88
  465. nautobot/project-static/docs/user-guide/administration/installation-extras/selinux-troubleshooting.html +892 -88
  466. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-netbox.html +892 -88
  467. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +892 -88
  468. nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +893 -89
  469. nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +892 -88
  470. nautobot/project-static/docs/user-guide/administration/upgrading/database-backup.html +892 -88
  471. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/after-you-upgrade.html +892 -88
  472. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/before-you-upgrade.html +892 -88
  473. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/for-developers.html +892 -88
  474. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/index.html +892 -88
  475. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/whats-changed.html +892 -88
  476. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/region-and-site-data-migration-guide.html +892 -88
  477. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +892 -88
  478. nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +892 -88
  479. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuit.html +892 -88
  480. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittermination.html +893 -89
  481. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittype.html +892 -88
  482. nautobot/project-static/docs/user-guide/core-data-model/circuits/provider.html +896 -88
  483. nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +895 -91
  484. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloud.html +8984 -0
  485. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudaccount.html +8828 -0
  486. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetwork.html +8829 -0
  487. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetworkprefixassignment.html +8828 -0
  488. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudresourcetype.html +8829 -0
  489. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservice.html +8833 -0
  490. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservicenetworkassignment.html +8828 -0
  491. nautobot/project-static/docs/user-guide/core-data-model/dcim/cable.html +906 -102
  492. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +923 -105
  493. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +923 -105
  494. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +918 -100
  495. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +923 -105
  496. nautobot/project-static/docs/user-guide/core-data-model/dcim/controller.html +906 -102
  497. nautobot/project-static/docs/user-guide/core-data-model/dcim/controllermanageddevicegroup.html +906 -102
  498. nautobot/project-static/docs/user-guide/core-data-model/dcim/device.html +913 -105
  499. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +920 -116
  500. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +921 -117
  501. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicefamily.html +918 -114
  502. nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +906 -102
  503. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +914 -105
  504. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +926 -108
  505. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +936 -118
  506. nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +928 -106
  507. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +906 -102
  508. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +937 -119
  509. nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +928 -110
  510. nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +918 -114
  511. nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +921 -117
  512. nautobot/project-static/docs/user-guide/core-data-model/dcim/manufacturer.html +923 -115
  513. nautobot/project-static/docs/user-guide/core-data-model/dcim/module.html +8828 -0
  514. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebay.html +8846 -0
  515. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebaytemplate.html +8843 -0
  516. nautobot/project-static/docs/user-guide/core-data-model/dcim/moduletype.html +8823 -0
  517. nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +916 -112
  518. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerfeed.html +906 -102
  519. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +940 -83
  520. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +924 -106
  521. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerpanel.html +906 -102
  522. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +943 -86
  523. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +921 -103
  524. nautobot/project-static/docs/user-guide/core-data-model/dcim/rack.html +929 -125
  525. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackgroup.html +918 -114
  526. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackreservation.html +906 -102
  527. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +922 -104
  528. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +924 -106
  529. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +906 -102
  530. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareversion.html +906 -102
  531. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualchassis.html +906 -102
  532. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +936 -88
  533. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +892 -88
  534. nautobot/project-static/docs/user-guide/core-data-model/extras/contact.html +897 -89
  535. nautobot/project-static/docs/user-guide/core-data-model/extras/team.html +897 -89
  536. nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +892 -88
  537. nautobot/project-static/docs/user-guide/core-data-model/ipam/namespace.html +892 -88
  538. nautobot/project-static/docs/user-guide/core-data-model/ipam/prefix.html +892 -88
  539. nautobot/project-static/docs/user-guide/core-data-model/ipam/rir.html +892 -88
  540. nautobot/project-static/docs/user-guide/core-data-model/ipam/routetarget.html +892 -88
  541. nautobot/project-static/docs/user-guide/core-data-model/ipam/service.html +892 -88
  542. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +892 -88
  543. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlangroup.html +892 -88
  544. nautobot/project-static/docs/user-guide/core-data-model/ipam/vrf.html +892 -88
  545. nautobot/project-static/docs/user-guide/core-data-model/overview/introduction.html +892 -88
  546. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenant.html +901 -96
  547. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenantgroup.html +892 -88
  548. nautobot/project-static/docs/user-guide/core-data-model/virtualization/cluster.html +892 -88
  549. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustergroup.html +892 -88
  550. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustertype.html +892 -88
  551. nautobot/project-static/docs/user-guide/core-data-model/virtualization/virtualmachine.html +892 -88
  552. nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +897 -89
  553. nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +892 -88
  554. nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +892 -88
  555. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +892 -88
  556. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +892 -88
  557. nautobot/project-static/docs/user-guide/feature-guides/getting-started/index.html +892 -88
  558. nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +892 -88
  559. nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +892 -88
  560. nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +892 -88
  561. nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +892 -88
  562. nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +892 -88
  563. nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +892 -88
  564. nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +892 -88
  565. nautobot/project-static/docs/user-guide/feature-guides/graphql.html +892 -88
  566. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/clear-view-button.png +0 -0
  567. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/cleared-view.png +0 -0
  568. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/config-table-columns-to-locations.png +0 -0
  569. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/configure-button.png +0 -0
  570. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/create-saved-view-success.png +0 -0
  571. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/current-saved-view-drop-down-menu.png +0 -0
  572. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/default-location-list-view.png +0 -0
  573. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/dropdown-button-after-new-saved-view.png +0 -0
  574. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/filter-application-to-locations.png +0 -0
  575. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/filter-button.png +0 -0
  576. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/global-default-location-list-view.png +0 -0
  577. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/location-list-view-with-saved-views.png +0 -0
  578. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/navigation-menu.png +0 -0
  579. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/save-as-new-view-drop-down.png +0 -0
  580. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/save-view-modal.png +0 -0
  581. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/saved-view-admin-edit-buttons.png +0 -0
  582. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/saved-view-admin-edit-success.png +0 -0
  583. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/saved-view-admin-edit-view-unchecked.png +0 -0
  584. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/saved-view-admin-edit-view.png +0 -0
  585. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/saved-view-different-user.png +0 -0
  586. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/saved-view-modal-unchecked.png +0 -0
  587. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/set-as-my-default-button.png +0 -0
  588. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/set-as-my-default-success.png +0 -0
  589. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/unsaved-saved-view.png +0 -0
  590. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/updated-saved-view.png +0 -0
  591. nautobot/project-static/docs/user-guide/feature-guides/ip-address-merge-tool.html +892 -88
  592. nautobot/project-static/docs/user-guide/feature-guides/relationships.html +892 -88
  593. nautobot/project-static/docs/user-guide/feature-guides/software-image-files-and-versions.html +892 -88
  594. nautobot/project-static/docs/user-guide/index.html +892 -88
  595. nautobot/project-static/docs/user-guide/platform-functionality/change-logging.html +892 -88
  596. nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +892 -88
  597. nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +892 -88
  598. nautobot/project-static/docs/user-guide/platform-functionality/customlink.html +892 -88
  599. nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +1258 -785
  600. nautobot/project-static/docs/user-guide/platform-functionality/exporttemplate.html +895 -91
  601. nautobot/project-static/docs/user-guide/platform-functionality/externalintegration.html +892 -88
  602. nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +892 -88
  603. nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +892 -88
  604. nautobot/project-static/docs/user-guide/platform-functionality/graphqlquery.html +892 -88
  605. nautobot/project-static/docs/user-guide/platform-functionality/imageattachment.html +892 -88
  606. nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +892 -88
  607. nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +892 -88
  608. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +892 -88
  609. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +892 -88
  610. nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +896 -88
  611. nautobot/project-static/docs/user-guide/platform-functionality/napalm.html +892 -88
  612. nautobot/project-static/docs/user-guide/platform-functionality/note.html +895 -91
  613. nautobot/project-static/docs/user-guide/platform-functionality/objectmetadata.html +9061 -0
  614. nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +895 -91
  615. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +892 -88
  616. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +892 -88
  617. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +892 -88
  618. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/ui-related-endpoints.html +892 -88
  619. nautobot/project-static/docs/user-guide/platform-functionality/role.html +895 -91
  620. nautobot/project-static/docs/user-guide/platform-functionality/savedview.html +9137 -0
  621. nautobot/project-static/docs/user-guide/platform-functionality/secret.html +895 -91
  622. nautobot/project-static/docs/user-guide/platform-functionality/staticgroupassociation.html +8933 -0
  623. nautobot/project-static/docs/user-guide/platform-functionality/status.html +892 -88
  624. nautobot/project-static/docs/user-guide/platform-functionality/tag.html +892 -88
  625. nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +950 -121
  626. nautobot/project-static/docs/user-guide/platform-functionality/users/objectpermission.html +892 -88
  627. nautobot/project-static/docs/user-guide/platform-functionality/users/token.html +892 -88
  628. nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +892 -88
  629. nautobot/project-static/js/forms.js +71 -0
  630. nautobot/project-static/js/table_sorting_indicator.js +46 -0
  631. nautobot/project-static/js/tableconfig.js +6 -1
  632. nautobot/project-static/materialdesignicons-7.4.47/css/materialdesignicons.min.css +3 -0
  633. nautobot/project-static/{materialdesignicons-6.5.95 → materialdesignicons-7.4.47}/fonts/materialdesignicons-webfont.eot +0 -0
  634. nautobot/project-static/{materialdesignicons-6.5.95 → materialdesignicons-7.4.47}/fonts/materialdesignicons-webfont.ttf +0 -0
  635. nautobot/project-static/materialdesignicons-7.4.47/fonts/materialdesignicons-webfont.woff +0 -0
  636. nautobot/project-static/materialdesignicons-7.4.47/fonts/materialdesignicons-webfont.woff2 +0 -0
  637. nautobot/tenancy/__init__.py +0 -1
  638. nautobot/tenancy/apps.py +1 -0
  639. nautobot/tenancy/factory.py +3 -2
  640. nautobot/tenancy/filters/__init__.py +1 -0
  641. nautobot/tenancy/forms.py +1 -1
  642. nautobot/tenancy/templates/tenancy/tenant.html +24 -20
  643. nautobot/tenancy/views.py +11 -10
  644. nautobot/users/__init__.py +0 -1
  645. nautobot/users/api/serializers.py +1 -1
  646. nautobot/users/api/views.py +4 -2
  647. nautobot/users/apps.py +3 -2
  648. nautobot/users/factory.py +3 -3
  649. nautobot/users/migrations/0010_user_default_saved_views.py +20 -0
  650. nautobot/users/models.py +12 -0
  651. nautobot/users/tests/test_filters.py +6 -3
  652. nautobot/users/urls.py +8 -0
  653. nautobot/virtualization/__init__.py +0 -1
  654. nautobot/virtualization/apps.py +1 -0
  655. nautobot/virtualization/filters.py +6 -1
  656. nautobot/virtualization/forms.py +11 -3
  657. nautobot/virtualization/graphql/types.py +2 -2
  658. nautobot/virtualization/migrations/0029_add_role_field_to_interface_models.py +27 -0
  659. nautobot/virtualization/migrations/0030_alter_virtualmachine_local_config_context_data_owner_content_type_and_more.py +67 -0
  660. nautobot/virtualization/models.py +0 -2
  661. nautobot/virtualization/tables.py +10 -3
  662. nautobot/virtualization/templates/virtualization/virtualmachine.html +1 -1
  663. nautobot/virtualization/templates/virtualization/vminterface.html +7 -1
  664. nautobot/virtualization/templates/virtualization/vminterface_edit.html +1 -0
  665. nautobot/virtualization/tests/test_api.py +9 -4
  666. nautobot/virtualization/tests/test_filters.py +22 -0
  667. nautobot/virtualization/tests/test_models.py +7 -3
  668. nautobot/virtualization/tests/test_views.py +19 -3
  669. nautobot/virtualization/urls.py +2 -2
  670. nautobot/virtualization/views.py +10 -32
  671. {nautobot-2.2.9.dist-info → nautobot-2.3.0.dist-info}/METADATA +20 -18
  672. {nautobot-2.2.9.dist-info → nautobot-2.3.0.dist-info}/RECORD +677 -557
  673. nautobot/project-static/docs/assets/stylesheets/main.76a95c52.min.css +0 -1
  674. nautobot/project-static/docs/assets/stylesheets/main.76a95c52.min.css.map +0 -1
  675. nautobot/project-static/materialdesignicons-6.5.95/.github/ISSUE_TEMPLATE.md +0 -3
  676. nautobot/project-static/materialdesignicons-6.5.95/README.md +0 -25
  677. nautobot/project-static/materialdesignicons-6.5.95/css/materialdesignicons.css +0 -26654
  678. nautobot/project-static/materialdesignicons-6.5.95/css/materialdesignicons.css.map +0 -16
  679. nautobot/project-static/materialdesignicons-6.5.95/css/materialdesignicons.min.css +0 -3
  680. nautobot/project-static/materialdesignicons-6.5.95/css/materialdesignicons.min.css.map +0 -16
  681. nautobot/project-static/materialdesignicons-6.5.95/fonts/materialdesignicons-webfont.woff +0 -0
  682. nautobot/project-static/materialdesignicons-6.5.95/fonts/materialdesignicons-webfont.woff2 +0 -0
  683. nautobot/project-static/materialdesignicons-6.5.95/package.json +0 -28
  684. nautobot/project-static/materialdesignicons-6.5.95/preview.html +0 -717
  685. nautobot/project-static/materialdesignicons-6.5.95/scss/_animated.scss +0 -27
  686. nautobot/project-static/materialdesignicons-6.5.95/scss/_core.scss +0 -10
  687. nautobot/project-static/materialdesignicons-6.5.95/scss/_extras.scss +0 -65
  688. nautobot/project-static/materialdesignicons-6.5.95/scss/_functions.scss +0 -20
  689. nautobot/project-static/materialdesignicons-6.5.95/scss/_icons.scss +0 -10
  690. nautobot/project-static/materialdesignicons-6.5.95/scss/_path.scss +0 -10
  691. nautobot/project-static/materialdesignicons-6.5.95/scss/_variables.scss +0 -6606
  692. nautobot/project-static/materialdesignicons-6.5.95/scss/materialdesignicons.scss +0 -8
  693. /nautobot/project-static/{materialdesignicons-6.5.95 → materialdesignicons-7.4.47}/LICENSE +0 -0
  694. {nautobot-2.2.9.dist-info → nautobot-2.3.0.dist-info}/LICENSE.txt +0 -0
  695. {nautobot-2.2.9.dist-info → nautobot-2.3.0.dist-info}/NOTICE +0 -0
  696. {nautobot-2.2.9.dist-info → nautobot-2.3.0.dist-info}/WHEEL +0 -0
  697. {nautobot-2.2.9.dist-info → nautobot-2.3.0.dist-info}/entry_points.txt +0 -0
nautobot/dcim/forms.py CHANGED
@@ -49,11 +49,21 @@ from nautobot.extras.forms import (
49
49
  NoteModelFormMixin,
50
50
  RoleModelBulkEditFormMixin,
51
51
  RoleModelFilterFormMixin,
52
+ RoleNotRequiredModelFormMixin,
52
53
  StatusModelBulkEditFormMixin,
53
54
  StatusModelFilterFormMixin,
54
55
  TagsBulkEditFormMixin,
55
56
  )
56
- from nautobot.extras.models import Contact, ContactAssociation, ExternalIntegration, Role, SecretsGroup, Status, Team
57
+ from nautobot.extras.models import (
58
+ Contact,
59
+ ContactAssociation,
60
+ ExternalIntegration,
61
+ Role,
62
+ SecretsGroup,
63
+ Status,
64
+ Tag,
65
+ Team,
66
+ )
57
67
  from nautobot.ipam.constants import BGP_ASN_MAX, BGP_ASN_MIN
58
68
  from nautobot.ipam.models import IPAddress, IPAddressToInterface, VLAN, VLANLocationAssignment, VRF
59
69
  from nautobot.tenancy.forms import TenancyFilterForm, TenancyForm
@@ -113,6 +123,10 @@ from .models import (
113
123
  Location,
114
124
  LocationType,
115
125
  Manufacturer,
126
+ Module,
127
+ ModuleBay,
128
+ ModuleBayTemplate,
129
+ ModuleType,
116
130
  Platform,
117
131
  PowerFeed,
118
132
  PowerOutlet,
@@ -172,6 +186,14 @@ class DeviceComponentFilterForm(NautobotFilterForm):
172
186
  )
173
187
 
174
188
 
189
+ class ModularDeviceComponentFilterForm(DeviceComponentFilterForm):
190
+ module = DynamicModelMultipleChoiceField(
191
+ queryset=Module.objects.all(),
192
+ required=False,
193
+ label="Module",
194
+ )
195
+
196
+
175
197
  class InterfaceCommonForm(forms.Form):
176
198
  def clean(self):
177
199
  super().clean()
@@ -242,6 +264,26 @@ class ComponentForm(BootstrapMixin, forms.Form):
242
264
  )
243
265
 
244
266
 
267
+ class ModularComponentForm(ComponentForm):
268
+ name_pattern = ExpandableNameField(
269
+ label="Name",
270
+ help_text="""
271
+ Alphanumeric ranges are supported for bulk creation. Mixed cases and types within a single range
272
+ are not supported. Examples:
273
+ <ul>
274
+ <li><code>[ge,xe]-0/0/[0-9]</code></li>
275
+ <li><code>e[0-3][a-d,f]</code></li>
276
+ </ul>
277
+
278
+ The variables <code>{module}</code>, <code>{module.parent}</code>, <code>{module.parent.parent}</code>, etc.
279
+ may be used in the name field and will be replaced by the <code>position</code> of the module bay that the
280
+ module occupies (skipping over any bays with a blank <code>position</code>). These variables can be used
281
+ multiple times in the component name and there is no limit to the depth of parent levels.
282
+ Any variables that cannot be replaced by a suitable position value will remain unchanged.
283
+ """,
284
+ )
285
+
286
+
245
287
  #
246
288
  # Fields
247
289
  #
@@ -844,6 +886,7 @@ class DeviceTypeBulkEditForm(TagsBulkEditFormMixin, NautobotBulkEditForm):
844
886
  software_image_files = DynamicModelMultipleChoiceField(queryset=SoftwareImageFile.objects.all(), required=False)
845
887
  u_height = forms.IntegerField(required=False)
846
888
  is_full_depth = forms.NullBooleanField(required=False, widget=BulkEditNullBooleanSelect(), label="Is full depth")
889
+ comments = CommentField(label="Comments", required=False)
847
890
 
848
891
  class Meta:
849
892
  nullable_fields = ["device_family", "software_image_files"]
@@ -897,48 +940,160 @@ class DeviceTypeFilterForm(NautobotFilterForm):
897
940
  tags = TagFilterField(model)
898
941
 
899
942
 
943
+ #
944
+ # Module types
945
+ #
946
+
947
+
948
+ class ModuleTypeForm(NautobotModelForm):
949
+ manufacturer = DynamicModelChoiceField(queryset=Manufacturer.objects.all())
950
+ comments = CommentField(label="Comments")
951
+
952
+ class Meta:
953
+ model = ModuleType
954
+ fields = [
955
+ "manufacturer",
956
+ "model",
957
+ "part_number",
958
+ "comments",
959
+ "tags",
960
+ ]
961
+
962
+
963
+ class ModuleTypeImportForm(BootstrapMixin, forms.ModelForm):
964
+ """
965
+ Form for JSON/YAML import of ModuleType objects.
966
+
967
+ TODO: at some point we'll want to add general-purpose YAML serialization/deserialization,
968
+ similar to what we've done for CSV in 2.0, but for the moment we're leaving this as-is so that we can remain
969
+ at least nominally compatible with the netbox-community/devicetype-library repo.
970
+ """
971
+
972
+ manufacturer = forms.ModelChoiceField(queryset=Manufacturer.objects.all(), to_field_name="name")
973
+
974
+ class Meta:
975
+ model = ModuleType
976
+ fields = [
977
+ "manufacturer",
978
+ "model",
979
+ "part_number",
980
+ "comments",
981
+ ]
982
+
983
+
984
+ class ModuleTypeBulkEditForm(TagsBulkEditFormMixin, NautobotBulkEditForm):
985
+ pk = forms.ModelMultipleChoiceField(queryset=ModuleType.objects.all(), widget=forms.MultipleHiddenInput())
986
+ manufacturer = DynamicModelChoiceField(queryset=Manufacturer.objects.all(), required=False)
987
+ part_number = forms.CharField(required=False)
988
+ comments = CommentField(label="Comments", required=False)
989
+
990
+ class Meta:
991
+ nullable_fields = []
992
+
993
+
994
+ class ModuleTypeFilterForm(NautobotFilterForm):
995
+ model = ModuleType
996
+ q = forms.CharField(required=False, label="Search")
997
+ manufacturer = DynamicModelMultipleChoiceField(
998
+ queryset=Manufacturer.objects.all(), to_field_name="name", required=False
999
+ )
1000
+ has_console_ports = forms.NullBooleanField(
1001
+ required=False,
1002
+ label="Has console ports",
1003
+ widget=StaticSelect2(choices=BOOLEAN_WITH_BLANK_CHOICES),
1004
+ )
1005
+ has_console_server_ports = forms.NullBooleanField(
1006
+ required=False,
1007
+ label="Has console server ports",
1008
+ widget=StaticSelect2(choices=BOOLEAN_WITH_BLANK_CHOICES),
1009
+ )
1010
+ has_power_ports = forms.NullBooleanField(
1011
+ required=False,
1012
+ label="Has power ports",
1013
+ widget=StaticSelect2(choices=BOOLEAN_WITH_BLANK_CHOICES),
1014
+ )
1015
+ has_power_outlets = forms.NullBooleanField(
1016
+ required=False,
1017
+ label="Has power outlets",
1018
+ widget=StaticSelect2(choices=BOOLEAN_WITH_BLANK_CHOICES),
1019
+ )
1020
+ has_interfaces = forms.NullBooleanField(
1021
+ required=False,
1022
+ label="Has interfaces",
1023
+ widget=StaticSelect2(choices=BOOLEAN_WITH_BLANK_CHOICES),
1024
+ )
1025
+ tags = TagFilterField(model)
1026
+
1027
+
900
1028
  #
901
1029
  # Device component templates
902
1030
  #
903
1031
 
904
1032
 
1033
+ class ComponentTemplateForm(NautobotModelForm):
1034
+ # TODO: placeholder values shouldn't be form controls, instead use:
1035
+ # <p class="form-control-static">{{ obj|hyperlinked_object_target_new_tab }}</p>
1036
+ device_type = DynamicModelChoiceField(
1037
+ queryset=DeviceType.objects.all(),
1038
+ )
1039
+
1040
+
1041
+ class ModularComponentTemplateForm(ComponentTemplateForm):
1042
+ device_type = DynamicModelChoiceField(
1043
+ queryset=DeviceType.objects.all(),
1044
+ required=False,
1045
+ )
1046
+ module_type = DynamicModelChoiceField(
1047
+ queryset=ModuleType.objects.all(),
1048
+ required=False,
1049
+ )
1050
+
1051
+
905
1052
  class ComponentTemplateCreateForm(ComponentForm):
906
1053
  """
907
1054
  Base form for the creation of device component templates (subclassed from ComponentTemplateModel).
908
1055
  """
909
1056
 
910
- manufacturer = DynamicModelChoiceField(
911
- queryset=Manufacturer.objects.all(),
912
- required=False,
913
- initial_params={"device_types": "device_type"},
1057
+ device_type = DynamicModelChoiceField(
1058
+ queryset=DeviceType.objects.all(),
914
1059
  )
1060
+ description = forms.CharField(required=False)
1061
+
1062
+
1063
+ class ModularComponentTemplateCreateForm(ModularComponentForm):
1064
+ """
1065
+ Base form for the creation of modular device component templates (subclassed from ModularComponentTemplateModel).
1066
+ """
1067
+
915
1068
  device_type = DynamicModelChoiceField(
916
1069
  queryset=DeviceType.objects.all(),
917
- query_params={"manufacturer": "$manufacturer"},
1070
+ required=False,
1071
+ )
1072
+ module_type = DynamicModelChoiceField(
1073
+ queryset=ModuleType.objects.all(),
1074
+ required=False,
918
1075
  )
919
1076
  description = forms.CharField(required=False)
920
1077
 
921
1078
 
922
- class ConsolePortTemplateForm(NautobotModelForm):
1079
+ class ConsolePortTemplateForm(ModularComponentTemplateForm):
923
1080
  class Meta:
924
1081
  model = ConsolePortTemplate
925
1082
  fields = [
926
1083
  "device_type",
1084
+ "module_type",
927
1085
  "name",
928
1086
  "label",
929
1087
  "type",
930
1088
  "description",
931
1089
  ]
932
- widgets = {
933
- "device_type": forms.HiddenInput(),
934
- }
935
1090
 
936
1091
 
937
- class ConsolePortTemplateCreateForm(ComponentTemplateCreateForm):
1092
+ class ConsolePortTemplateCreateForm(ModularComponentTemplateCreateForm):
938
1093
  type = forms.ChoiceField(choices=add_blank_choice(ConsolePortTypeChoices), widget=StaticSelect2())
939
1094
  field_order = (
940
- "manufacturer",
941
1095
  "device_type",
1096
+ "module_type",
942
1097
  "name_pattern",
943
1098
  "label_pattern",
944
1099
  "type",
@@ -959,26 +1114,24 @@ class ConsolePortTemplateBulkEditForm(NautobotBulkEditForm):
959
1114
  nullable_fields = ["label", "type", "description"]
960
1115
 
961
1116
 
962
- class ConsoleServerPortTemplateForm(NautobotModelForm):
1117
+ class ConsoleServerPortTemplateForm(ModularComponentTemplateForm):
963
1118
  class Meta:
964
1119
  model = ConsoleServerPortTemplate
965
1120
  fields = [
966
1121
  "device_type",
1122
+ "module_type",
967
1123
  "name",
968
1124
  "label",
969
1125
  "type",
970
1126
  "description",
971
1127
  ]
972
- widgets = {
973
- "device_type": forms.HiddenInput(),
974
- }
975
1128
 
976
1129
 
977
- class ConsoleServerPortTemplateCreateForm(ComponentTemplateCreateForm):
1130
+ class ConsoleServerPortTemplateCreateForm(ModularComponentTemplateCreateForm):
978
1131
  type = forms.ChoiceField(choices=add_blank_choice(ConsolePortTypeChoices), widget=StaticSelect2())
979
1132
  field_order = (
980
- "manufacturer",
981
1133
  "device_type",
1134
+ "module_type",
982
1135
  "name_pattern",
983
1136
  "label_pattern",
984
1137
  "type",
@@ -1003,11 +1156,12 @@ class ConsoleServerPortTemplateBulkEditForm(NautobotBulkEditForm):
1003
1156
  nullable_fields = ["label", "type", "description"]
1004
1157
 
1005
1158
 
1006
- class PowerPortTemplateForm(NautobotModelForm):
1159
+ class PowerPortTemplateForm(ModularComponentTemplateForm):
1007
1160
  class Meta:
1008
1161
  model = PowerPortTemplate
1009
1162
  fields = [
1010
1163
  "device_type",
1164
+ "module_type",
1011
1165
  "name",
1012
1166
  "label",
1013
1167
  "type",
@@ -1015,18 +1169,15 @@ class PowerPortTemplateForm(NautobotModelForm):
1015
1169
  "allocated_draw",
1016
1170
  "description",
1017
1171
  ]
1018
- widgets = {
1019
- "device_type": forms.HiddenInput(),
1020
- }
1021
1172
 
1022
1173
 
1023
- class PowerPortTemplateCreateForm(ComponentTemplateCreateForm):
1174
+ class PowerPortTemplateCreateForm(ModularComponentTemplateCreateForm):
1024
1175
  type = forms.ChoiceField(choices=add_blank_choice(PowerPortTypeChoices), required=False)
1025
1176
  maximum_draw = forms.IntegerField(min_value=1, required=False, help_text="Maximum power draw (watts)")
1026
1177
  allocated_draw = forms.IntegerField(min_value=1, required=False, help_text="Allocated power draw (watts)")
1027
1178
  field_order = (
1028
- "manufacturer",
1029
1179
  "device_type",
1180
+ "module_type",
1030
1181
  "name_pattern",
1031
1182
  "label_pattern",
1032
1183
  "type",
@@ -1058,11 +1209,12 @@ class PowerPortTemplateBulkEditForm(NautobotBulkEditForm):
1058
1209
  ]
1059
1210
 
1060
1211
 
1061
- class PowerOutletTemplateForm(NautobotModelForm):
1212
+ class PowerOutletTemplateForm(ModularComponentTemplateForm):
1062
1213
  class Meta:
1063
1214
  model = PowerOutletTemplate
1064
1215
  fields = [
1065
1216
  "device_type",
1217
+ "module_type",
1066
1218
  "name",
1067
1219
  "label",
1068
1220
  "type",
@@ -1070,21 +1222,22 @@ class PowerOutletTemplateForm(NautobotModelForm):
1070
1222
  "feed_leg",
1071
1223
  "description",
1072
1224
  ]
1073
- widgets = {
1074
- "device_type": forms.HiddenInput(),
1075
- }
1076
1225
 
1077
1226
  def __init__(self, *args, **kwargs):
1078
1227
  super().__init__(*args, **kwargs)
1079
1228
 
1080
- # Limit power_port_template choices to current DeviceType
1081
- if hasattr(self.instance, "device_type"):
1229
+ # Limit power_port_template choices to current DeviceType or ModuleType
1230
+ if getattr(self.instance, "device_type", None):
1082
1231
  self.fields["power_port_template"].queryset = PowerPortTemplate.objects.filter(
1083
1232
  device_type=self.instance.device_type
1084
1233
  )
1234
+ elif getattr(self.instance, "module_type", None):
1235
+ self.fields["power_port_template"].queryset = PowerPortTemplate.objects.filter(
1236
+ module_type=self.instance.module_type
1237
+ )
1085
1238
 
1086
1239
 
1087
- class PowerOutletTemplateCreateForm(ComponentTemplateCreateForm):
1240
+ class PowerOutletTemplateCreateForm(ModularComponentTemplateCreateForm):
1088
1241
  type = forms.ChoiceField(choices=add_blank_choice(PowerOutletTypeChoices), required=False)
1089
1242
  power_port_template = DynamicModelChoiceField(
1090
1243
  queryset=PowerPortTemplate.objects.all(),
@@ -1097,8 +1250,8 @@ class PowerOutletTemplateCreateForm(ComponentTemplateCreateForm):
1097
1250
  widget=StaticSelect2(),
1098
1251
  )
1099
1252
  field_order = (
1100
- "manufacturer",
1101
1253
  "device_type",
1254
+ "module_type",
1102
1255
  "name_pattern",
1103
1256
  "label_pattern",
1104
1257
  "type",
@@ -1113,7 +1266,6 @@ class PowerOutletTemplateBulkEditForm(NautobotBulkEditForm):
1113
1266
  device_type = forms.ModelChoiceField(
1114
1267
  queryset=DeviceType.objects.all(),
1115
1268
  required=False,
1116
- disabled=True,
1117
1269
  widget=forms.HiddenInput(),
1118
1270
  )
1119
1271
  label = forms.CharField(max_length=CHARFIELD_MAX_LENGTH, required=False)
@@ -1145,11 +1297,12 @@ class PowerOutletTemplateBulkEditForm(NautobotBulkEditForm):
1145
1297
  self.fields["power_port_template"].widget.attrs["disabled"] = True
1146
1298
 
1147
1299
 
1148
- class InterfaceTemplateForm(NautobotModelForm):
1300
+ class InterfaceTemplateForm(ModularComponentTemplateForm):
1149
1301
  class Meta:
1150
1302
  model = InterfaceTemplate
1151
1303
  fields = [
1152
1304
  "device_type",
1305
+ "module_type",
1153
1306
  "name",
1154
1307
  "label",
1155
1308
  "type",
@@ -1157,17 +1310,16 @@ class InterfaceTemplateForm(NautobotModelForm):
1157
1310
  "description",
1158
1311
  ]
1159
1312
  widgets = {
1160
- "device_type": forms.HiddenInput(),
1161
1313
  "type": StaticSelect2(),
1162
1314
  }
1163
1315
 
1164
1316
 
1165
- class InterfaceTemplateCreateForm(ComponentTemplateCreateForm):
1317
+ class InterfaceTemplateCreateForm(ModularComponentTemplateCreateForm):
1166
1318
  type = forms.ChoiceField(choices=InterfaceTypeChoices, widget=StaticSelect2())
1167
1319
  mgmt_only = forms.BooleanField(required=False, label="Management only")
1168
1320
  field_order = (
1169
- "manufacturer",
1170
1321
  "device_type",
1322
+ "module_type",
1171
1323
  "name_pattern",
1172
1324
  "label_pattern",
1173
1325
  "type",
@@ -1191,11 +1343,12 @@ class InterfaceTemplateBulkEditForm(NautobotBulkEditForm):
1191
1343
  nullable_fields = ["label", "description"]
1192
1344
 
1193
1345
 
1194
- class FrontPortTemplateForm(NautobotModelForm):
1346
+ class FrontPortTemplateForm(ModularComponentTemplateForm):
1195
1347
  class Meta:
1196
1348
  model = FrontPortTemplate
1197
1349
  fields = [
1198
1350
  "device_type",
1351
+ "module_type",
1199
1352
  "name",
1200
1353
  "label",
1201
1354
  "type",
@@ -1204,21 +1357,24 @@ class FrontPortTemplateForm(NautobotModelForm):
1204
1357
  "description",
1205
1358
  ]
1206
1359
  widgets = {
1207
- "device_type": forms.HiddenInput(),
1208
1360
  "rear_port_template": StaticSelect2(),
1209
1361
  }
1210
1362
 
1211
1363
  def __init__(self, *args, **kwargs):
1212
1364
  super().__init__(*args, **kwargs)
1213
1365
 
1214
- # Limit rear_port_template choices to current DeviceType
1215
- if hasattr(self.instance, "device_type"):
1366
+ # Limit rear_port_template choices to current DeviceType or ModuleType
1367
+ if getattr(self.instance, "device_type", None):
1216
1368
  self.fields["rear_port_template"].queryset = RearPortTemplate.objects.filter(
1217
1369
  device_type=self.instance.device_type
1218
1370
  )
1371
+ elif getattr(self.instance, "module_type", None):
1372
+ self.fields["rear_port_template"].queryset = RearPortTemplate.objects.filter(
1373
+ module_type=self.instance.module_type
1374
+ )
1219
1375
 
1220
1376
 
1221
- class FrontPortTemplateCreateForm(ComponentTemplateCreateForm):
1377
+ class FrontPortTemplateCreateForm(ModularComponentTemplateCreateForm):
1222
1378
  type = forms.ChoiceField(choices=PortTypeChoices, widget=StaticSelect2())
1223
1379
  rear_port_template_set = forms.MultipleChoiceField(
1224
1380
  choices=[],
@@ -1226,8 +1382,8 @@ class FrontPortTemplateCreateForm(ComponentTemplateCreateForm):
1226
1382
  help_text="Select one rear port assignment for each front port being created.",
1227
1383
  )
1228
1384
  field_order = (
1229
- "manufacturer",
1230
1385
  "device_type",
1386
+ "module_type",
1231
1387
  "name_pattern",
1232
1388
  "label_pattern",
1233
1389
  "type",
@@ -1238,17 +1394,25 @@ class FrontPortTemplateCreateForm(ComponentTemplateCreateForm):
1238
1394
  def __init__(self, *args, **kwargs):
1239
1395
  super().__init__(*args, **kwargs)
1240
1396
 
1241
- device_type = DeviceType.objects.get(pk=self.initial.get("device_type") or self.data.get("device_type"))
1397
+ device_type = self.initial.get("device_type") or self.data.get("device_type")
1398
+ module_type = self.initial.get("module_type") or self.data.get("module_type")
1399
+ if device_type:
1400
+ parent = DeviceType.objects.get(pk=device_type)
1401
+ elif module_type:
1402
+ parent = ModuleType.objects.get(pk=module_type)
1403
+ else:
1404
+ return
1242
1405
 
1243
1406
  # Determine which rear port positions are occupied. These will be excluded from the list of available mappings.
1244
1407
  occupied_port_positions = [
1245
1408
  (front_port_template.rear_port_template_id, front_port_template.rear_port_position)
1246
- for front_port_template in device_type.front_port_templates.all()
1409
+ for front_port_template in parent.front_port_templates.all()
1247
1410
  ]
1248
1411
 
1249
1412
  # Populate rear port choices
1250
1413
  choices = []
1251
- rear_port_templates = RearPortTemplate.objects.filter(device_type=device_type)
1414
+ parent_field_name = parent._meta.verbose_name.replace(" ", "_")
1415
+ rear_port_templates = RearPortTemplate.objects.filter(**{parent_field_name: parent})
1252
1416
  for rear_port_template in rear_port_templates:
1253
1417
  for i in range(1, rear_port_template.positions + 1):
1254
1418
  if (rear_port_template.pk, i) not in occupied_port_positions:
@@ -1300,11 +1464,12 @@ class FrontPortTemplateBulkEditForm(NautobotBulkEditForm):
1300
1464
  nullable_fields = ["description"]
1301
1465
 
1302
1466
 
1303
- class RearPortTemplateForm(NautobotModelForm):
1467
+ class RearPortTemplateForm(ModularComponentTemplateForm):
1304
1468
  class Meta:
1305
1469
  model = RearPortTemplate
1306
1470
  fields = [
1307
1471
  "device_type",
1472
+ "module_type",
1308
1473
  "name",
1309
1474
  "label",
1310
1475
  "type",
@@ -1312,12 +1477,11 @@ class RearPortTemplateForm(NautobotModelForm):
1312
1477
  "description",
1313
1478
  ]
1314
1479
  widgets = {
1315
- "device_type": forms.HiddenInput(),
1316
1480
  "type": StaticSelect2(),
1317
1481
  }
1318
1482
 
1319
1483
 
1320
- class RearPortTemplateCreateForm(ComponentTemplateCreateForm):
1484
+ class RearPortTemplateCreateForm(ModularComponentTemplateCreateForm):
1321
1485
  type = forms.ChoiceField(
1322
1486
  choices=PortTypeChoices,
1323
1487
  widget=StaticSelect2(),
@@ -1329,8 +1493,8 @@ class RearPortTemplateCreateForm(ComponentTemplateCreateForm):
1329
1493
  help_text="The number of front ports which may be mapped to each rear port",
1330
1494
  )
1331
1495
  field_order = (
1332
- "manufacturer",
1333
1496
  "device_type",
1497
+ "module_type",
1334
1498
  "name_pattern",
1335
1499
  "label_pattern",
1336
1500
  "type",
@@ -1353,7 +1517,7 @@ class RearPortTemplateBulkEditForm(NautobotBulkEditForm):
1353
1517
  nullable_fields = ["description"]
1354
1518
 
1355
1519
 
1356
- class DeviceBayTemplateForm(NautobotModelForm):
1520
+ class DeviceBayTemplateForm(ComponentTemplateForm):
1357
1521
  class Meta:
1358
1522
  model = DeviceBayTemplate
1359
1523
  fields = [
@@ -1362,14 +1526,10 @@ class DeviceBayTemplateForm(NautobotModelForm):
1362
1526
  "label",
1363
1527
  "description",
1364
1528
  ]
1365
- widgets = {
1366
- "device_type": forms.HiddenInput(),
1367
- }
1368
1529
 
1369
1530
 
1370
1531
  class DeviceBayTemplateCreateForm(ComponentTemplateCreateForm):
1371
1532
  field_order = (
1372
- "manufacturer",
1373
1533
  "device_type",
1374
1534
  "name_pattern",
1375
1535
  "label_pattern",
@@ -1386,6 +1546,92 @@ class DeviceBayTemplateBulkEditForm(NautobotBulkEditForm):
1386
1546
  nullable_fields = ("label", "description")
1387
1547
 
1388
1548
 
1549
+ class ModuleBayTemplateForm(ModularComponentTemplateForm):
1550
+ class Meta:
1551
+ model = ModuleBayTemplate
1552
+ fields = [
1553
+ "device_type",
1554
+ "module_type",
1555
+ "name",
1556
+ "position",
1557
+ "label",
1558
+ "description",
1559
+ ]
1560
+
1561
+
1562
+ class ModuleBayBaseCreateForm(BootstrapMixin, forms.Form):
1563
+ name_pattern = ExpandableNameField(label="Name")
1564
+ label_pattern = ExpandableNameField(
1565
+ label="Label",
1566
+ required=False,
1567
+ help_text="Alphanumeric ranges are supported. (Must match the number of names being created.)",
1568
+ )
1569
+ position_pattern = ExpandableNameField(
1570
+ label="Position",
1571
+ required=False,
1572
+ help_text="Alphanumeric ranges are supported. (Must match the number of names being created.)",
1573
+ )
1574
+ description = forms.CharField(max_length=CHARFIELD_MAX_LENGTH, required=False)
1575
+
1576
+ def clean(self):
1577
+ super().clean()
1578
+
1579
+ # Validate that the number of components being created from both the name_pattern, position_pattern and label_pattern are equal
1580
+ if self.cleaned_data["label_pattern"]:
1581
+ name_pattern_count = len(self.cleaned_data["name_pattern"])
1582
+ label_pattern_count = len(self.cleaned_data["label_pattern"])
1583
+ if name_pattern_count != label_pattern_count:
1584
+ raise forms.ValidationError(
1585
+ {
1586
+ "label_pattern": f"The provided name pattern will create {name_pattern_count} components, however "
1587
+ f"{label_pattern_count} labels will be generated. These counts must match."
1588
+ },
1589
+ code="label_pattern_mismatch",
1590
+ )
1591
+
1592
+ if self.cleaned_data["position_pattern"]:
1593
+ name_pattern_count = len(self.cleaned_data["name_pattern"])
1594
+ position_pattern_count = len(self.cleaned_data["position_pattern"])
1595
+ if name_pattern_count != position_pattern_count:
1596
+ raise forms.ValidationError(
1597
+ {
1598
+ "position_pattern": f"The provided name pattern will create {name_pattern_count} components, however "
1599
+ f"{position_pattern_count} positions will be generated. These counts must match."
1600
+ },
1601
+ code="position_pattern_mismatch",
1602
+ )
1603
+
1604
+
1605
+ class ModuleBayTemplateCreateForm(ModuleBayBaseCreateForm):
1606
+ device_type = DynamicModelChoiceField(
1607
+ queryset=DeviceType.objects.all(),
1608
+ required=False,
1609
+ )
1610
+ module_type = DynamicModelChoiceField(
1611
+ queryset=ModuleType.objects.all(),
1612
+ required=False,
1613
+ )
1614
+
1615
+ field_order = (
1616
+ "device_type",
1617
+ "module_type",
1618
+ "name_pattern",
1619
+ "label_pattern",
1620
+ "position_pattern",
1621
+ "description",
1622
+ )
1623
+
1624
+
1625
+ class ModuleBayTemplateBulkEditForm(NautobotBulkEditForm):
1626
+ pk = forms.ModelMultipleChoiceField(queryset=ModuleBayTemplate.objects.all(), widget=forms.MultipleHiddenInput())
1627
+ label = forms.CharField(max_length=CHARFIELD_MAX_LENGTH, required=False)
1628
+ description = forms.CharField(max_length=CHARFIELD_MAX_LENGTH, required=False)
1629
+ position = forms.CharField(max_length=CHARFIELD_MAX_LENGTH, required=False)
1630
+
1631
+ class Meta:
1632
+ nullable_fields = ("label", "description")
1633
+
1634
+
1389
1635
  #
1390
1636
  # Component template import forms
1391
1637
  #
@@ -1400,14 +1646,7 @@ class ComponentTemplateImportForm(BootstrapMixin, CustomFieldModelCSVForm):
1400
1646
  netbox-community/devicetype-library repository.
1401
1647
  """
1402
1648
 
1403
- def __init__(self, device_type, data=None, *args, **kwargs):
1404
- # Must pass the parent DeviceType on form initialization
1405
- data.update(
1406
- {
1407
- "device_type": device_type.pk,
1408
- }
1409
- )
1410
-
1649
+ def __init__(self, data=None, *args, **kwargs):
1411
1650
  super().__init__(data, *args, **kwargs)
1412
1651
 
1413
1652
  if "type" in self.fields:
@@ -1419,9 +1658,21 @@ class ComponentTemplateImportForm(BootstrapMixin, CustomFieldModelCSVForm):
1419
1658
  data = self.cleaned_data["device_type"]
1420
1659
 
1421
1660
  # Limit fields referencing other components to the parent DeviceType
1422
- for field_name, field in self.fields.items():
1423
- if isinstance(field, forms.ModelChoiceField) and field_name != "device_type":
1424
- field.queryset = field.queryset.filter(device_type=data)
1661
+ if data:
1662
+ for field_name, field in self.fields.items():
1663
+ if isinstance(field, forms.ModelChoiceField) and field_name not in ["device_type", "module_type"]:
1664
+ field.queryset = field.queryset.filter(device_type=data)
1665
+
1666
+ return data
1667
+
1668
+ def clean_module_type(self):
1669
+ data = self.cleaned_data["module_type"]
1670
+
1671
+ # Limit fields referencing other components to the parent ModuleType
1672
+ if data:
1673
+ for field_name, field in self.fields.items():
1674
+ if isinstance(field, forms.ModelChoiceField) and field_name not in ["device_type", "module_type"]:
1675
+ field.queryset = field.queryset.filter(module_type=data)
1425
1676
 
1426
1677
  return data
1427
1678
 
@@ -1454,6 +1705,7 @@ class ConsolePortTemplateImportForm(ComponentTemplateImportForm):
1454
1705
  model = ConsolePortTemplate
1455
1706
  fields = [
1456
1707
  "device_type",
1708
+ "module_type",
1457
1709
  "name",
1458
1710
  "label",
1459
1711
  "type",
@@ -1465,6 +1717,7 @@ class ConsoleServerPortTemplateImportForm(ComponentTemplateImportForm):
1465
1717
  model = ConsoleServerPortTemplate
1466
1718
  fields = [
1467
1719
  "device_type",
1720
+ "module_type",
1468
1721
  "name",
1469
1722
  "label",
1470
1723
  "type",
@@ -1476,6 +1729,7 @@ class PowerPortTemplateImportForm(ComponentTemplateImportForm):
1476
1729
  model = PowerPortTemplate
1477
1730
  fields = [
1478
1731
  "device_type",
1732
+ "module_type",
1479
1733
  "name",
1480
1734
  "label",
1481
1735
  "type",
@@ -1495,6 +1749,7 @@ class PowerOutletTemplateImportForm(ComponentTemplateImportForm):
1495
1749
  model = PowerOutletTemplate
1496
1750
  fields = [
1497
1751
  "device_type",
1752
+ "module_type",
1498
1753
  "name",
1499
1754
  "label",
1500
1755
  "type",
@@ -1517,6 +1772,7 @@ class InterfaceTemplateImportForm(ComponentTemplateImportForm):
1517
1772
  model = InterfaceTemplate
1518
1773
  fields = [
1519
1774
  "device_type",
1775
+ "module_type",
1520
1776
  "name",
1521
1777
  "label",
1522
1778
  "type",
@@ -1535,6 +1791,7 @@ class FrontPortTemplateImportForm(ComponentTemplateImportForm):
1535
1791
  model = FrontPortTemplate
1536
1792
  fields = [
1537
1793
  "device_type",
1794
+ "module_type",
1538
1795
  "name",
1539
1796
  "type",
1540
1797
  "rear_port_template",
@@ -1556,6 +1813,7 @@ class RearPortTemplateImportForm(ComponentTemplateImportForm):
1556
1813
  model = RearPortTemplate
1557
1814
  fields = [
1558
1815
  "device_type",
1816
+ "module_type",
1559
1817
  "name",
1560
1818
  "type",
1561
1819
  "positions",
@@ -1571,6 +1829,17 @@ class DeviceBayTemplateImportForm(ComponentTemplateImportForm):
1571
1829
  ]
1572
1830
 
1573
1831
 
1832
+ class ModuleBayTemplateImportForm(ComponentTemplateImportForm):
1833
+ class Meta:
1834
+ model = ModuleBayTemplate
1835
+ fields = [
1836
+ "device_type",
1837
+ "module_type",
1838
+ "name",
1839
+ "position",
1840
+ ]
1841
+
1842
+
1574
1843
  #
1575
1844
  # Platforms
1576
1845
  #
@@ -1734,7 +2003,7 @@ class DeviceForm(LocatableModelFormMixin, NautobotModelForm, TenancyForm, LocalC
1734
2003
  ip_choices = [(None, "---------")]
1735
2004
 
1736
2005
  # Gather PKs of all interfaces belonging to this Device or a peer VirtualChassis member
1737
- interface_ids = self.instance.vc_interfaces.values_list("pk", flat=True)
2006
+ interface_ids = self.instance.all_interfaces.values_list("pk", flat=True)
1738
2007
 
1739
2008
  # Collect interface IPs
1740
2009
  interface_ip_assignments = IPAddressToInterface.objects.filter(
@@ -2001,36 +2270,247 @@ class DeviceFilterForm(
2001
2270
 
2002
2271
 
2003
2272
  #
2004
- # Device components
2273
+ # Modules
2005
2274
  #
2006
2275
 
2007
2276
 
2008
- class ComponentCreateForm(ComponentForm):
2009
- """
2010
- Base form for the creation of device components (models subclassed from ComponentModel).
2011
- """
2012
-
2013
- device = DynamicModelChoiceField(queryset=Device.objects.all())
2014
- description = forms.CharField(max_length=CHARFIELD_MAX_LENGTH, required=False)
2015
-
2016
-
2017
- class ComponentEditForm(NautobotModelForm):
2018
- """
2019
- Base class for editing device components (models subclassed from ComponentModel).
2020
-
2021
- Distinct from ComponentCreateForm in that it has a name/label instead of a name_pattern/label_pattern.
2022
- """
2023
-
2024
- device = DynamicModelChoiceField(queryset=Device.objects.all())
2277
+ class ModuleForm(LocatableModelFormMixin, NautobotModelForm, TenancyForm):
2278
+ manufacturer = DynamicModelChoiceField(
2279
+ queryset=Manufacturer.objects.all(),
2280
+ required=False,
2281
+ initial_params={"module_types": "$module_type"},
2282
+ )
2283
+ module_type = DynamicModelChoiceField(
2284
+ queryset=ModuleType.objects.all(),
2285
+ query_params={"manufacturer": "$manufacturer"},
2286
+ )
2287
+ parent_module_bay_device_filter = DynamicModelChoiceField(
2288
+ queryset=Device.objects.all(),
2289
+ required=False,
2290
+ label="Parent Device",
2291
+ query_params={"has_empty_module_bays": True},
2292
+ initial_params={"module_bays": "$parent_module_bay"},
2293
+ )
2294
+ parent_module_bay_device = DynamicModelChoiceField(
2295
+ queryset=ModuleBay.objects.all(),
2296
+ label="Parent Module Bay",
2297
+ required=False,
2298
+ query_params={"parent_device": "$parent_module_bay_device_filter", "has_installed_module": False},
2299
+ initial_params={"pk": "$parent_module_bay", "parent_device__module_bays": "$parent_module_bay"},
2300
+ )
2301
+ parent_module_bay_module_filter = DynamicModelChoiceField(
2302
+ queryset=Module.objects.all(),
2303
+ required=False,
2304
+ label="Parent Module",
2305
+ query_params={"has_empty_module_bays": True},
2306
+ initial_params={"module_bays": "$parent_module_bay"},
2307
+ )
2308
+ parent_module_bay_module = DynamicModelChoiceField(
2309
+ queryset=ModuleBay.objects.all(),
2310
+ label="Parent Module Bay",
2311
+ required=False,
2312
+ query_params={"parent_module": "$parent_module_bay_module_filter", "has_installed_module": False},
2313
+ initial_params={"pk": "$parent_module_bay", "parent_module__module_bays": "$parent_module_bay"},
2314
+ )
2315
+ location = DynamicModelChoiceField(
2316
+ queryset=Location.objects.all(),
2317
+ required=False,
2318
+ label="Location",
2319
+ query_params={"content_type": Module._meta.label_lower},
2320
+ )
2321
+ role = DynamicModelChoiceField(
2322
+ queryset=Role.objects.all(),
2323
+ required=False,
2324
+ query_params={"content_types": Module._meta.label_lower},
2325
+ )
2025
2326
 
2026
- def __init__(self, *args, **kwargs):
2327
+ class Meta:
2328
+ model = Module
2329
+ fields = [
2330
+ "manufacturer",
2331
+ "module_type",
2332
+ "parent_module_bay",
2333
+ "location",
2334
+ "serial",
2335
+ "asset_tag",
2336
+ "role",
2337
+ "status",
2338
+ "tenant_group",
2339
+ "tenant",
2340
+ "tags",
2341
+ ]
2342
+ help_texts = {
2343
+ "serial": "Module serial number",
2344
+ }
2345
+
2346
+ def clean(self):
2347
+ cleaned_data = self.cleaned_data
2348
+ if cleaned_data["parent_module_bay_device"] and cleaned_data["parent_module_bay_module"]:
2349
+ raise forms.ValidationError("Multiple parent module bays selected.")
2350
+ elif cleaned_data["parent_module_bay_device"]:
2351
+ cleaned_data["parent_module_bay"] = cleaned_data.pop("parent_module_bay_device")
2352
+ elif cleaned_data["parent_module_bay_module"]:
2353
+ cleaned_data["parent_module_bay"] = cleaned_data.pop("parent_module_bay_module")
2354
+
2355
+ return cleaned_data
2356
+
2357
+
2358
+ class ModuleBulkEditForm(
2359
+ TagsBulkEditFormMixin,
2360
+ LocatableModelBulkEditFormMixin,
2361
+ StatusModelBulkEditFormMixin,
2362
+ RoleModelBulkEditFormMixin,
2363
+ NautobotBulkEditForm,
2364
+ LocalContextModelBulkEditForm,
2365
+ ):
2366
+ pk = forms.ModelMultipleChoiceField(queryset=Module.objects.all(), widget=forms.MultipleHiddenInput())
2367
+ manufacturer = DynamicModelChoiceField(queryset=Manufacturer.objects.all(), required=False)
2368
+ module_type = DynamicModelChoiceField(
2369
+ queryset=ModuleType.objects.all(),
2370
+ required=False,
2371
+ query_params={"manufacturer": "$manufacturer"},
2372
+ )
2373
+ tenant = DynamicModelChoiceField(queryset=Tenant.objects.all(), required=False)
2374
+ serial = forms.CharField(max_length=CHARFIELD_MAX_LENGTH, required=False, label="Serial Number")
2375
+
2376
+ class Meta:
2377
+ model = Module
2378
+ nullable_fields = [
2379
+ "location",
2380
+ "tenant",
2381
+ "serial",
2382
+ ]
2383
+
2384
+
2385
+ class ModuleFilterForm(
2386
+ NautobotFilterForm,
2387
+ LocalContextFilterForm,
2388
+ LocatableModelFilterFormMixin,
2389
+ TenancyFilterForm,
2390
+ StatusModelFilterFormMixin,
2391
+ RoleModelFilterFormMixin,
2392
+ ):
2393
+ model = Module
2394
+ field_order = [
2395
+ "q",
2396
+ "location",
2397
+ "status",
2398
+ "role",
2399
+ "tenant_group",
2400
+ "tenant",
2401
+ "manufacturer",
2402
+ "module_type",
2403
+ "mac_address",
2404
+ ]
2405
+ q = forms.CharField(required=False, label="Search")
2406
+ manufacturer = DynamicModelMultipleChoiceField(
2407
+ queryset=Manufacturer.objects.all(),
2408
+ to_field_name="name",
2409
+ required=False,
2410
+ label="Manufacturer",
2411
+ )
2412
+ module_type = DynamicModelMultipleChoiceField(
2413
+ queryset=ModuleType.objects.all(),
2414
+ required=False,
2415
+ label="Model",
2416
+ query_params={"manufacturer": "$manufacturer"},
2417
+ )
2418
+ mac_address = forms.CharField(required=False, label="MAC address")
2419
+ has_console_ports = forms.NullBooleanField(
2420
+ required=False,
2421
+ label="Has console ports",
2422
+ widget=StaticSelect2(choices=BOOLEAN_WITH_BLANK_CHOICES),
2423
+ )
2424
+ has_console_server_ports = forms.NullBooleanField(
2425
+ required=False,
2426
+ label="Has console server ports",
2427
+ widget=StaticSelect2(choices=BOOLEAN_WITH_BLANK_CHOICES),
2428
+ )
2429
+ has_power_ports = forms.NullBooleanField(
2430
+ required=False,
2431
+ label="Has power ports",
2432
+ widget=StaticSelect2(choices=BOOLEAN_WITH_BLANK_CHOICES),
2433
+ )
2434
+ has_power_outlets = forms.NullBooleanField(
2435
+ required=False,
2436
+ label="Has power outlets",
2437
+ widget=StaticSelect2(choices=BOOLEAN_WITH_BLANK_CHOICES),
2438
+ )
2439
+ has_interfaces = forms.NullBooleanField(
2440
+ required=False,
2441
+ label="Has interfaces",
2442
+ widget=StaticSelect2(choices=BOOLEAN_WITH_BLANK_CHOICES),
2443
+ )
2444
+ has_front_ports = forms.NullBooleanField(
2445
+ required=False,
2446
+ label="Has front ports",
2447
+ widget=StaticSelect2(choices=BOOLEAN_WITH_BLANK_CHOICES),
2448
+ )
2449
+ has_rear_ports = forms.NullBooleanField(
2450
+ required=False,
2451
+ label="Has rear ports",
2452
+ widget=StaticSelect2(choices=BOOLEAN_WITH_BLANK_CHOICES),
2453
+ )
2454
+ tags = TagFilterField(model)
2455
+
2456
+
2457
+ #
2458
+ # Device components
2459
+ #
2460
+
2461
+
2462
+ class ComponentCreateForm(ComponentForm):
2463
+ """
2464
+ Base form for the creation of device components (models subclassed from ComponentModel).
2465
+ """
2466
+
2467
+ device = DynamicModelChoiceField(queryset=Device.objects.all())
2468
+ description = forms.CharField(max_length=CHARFIELD_MAX_LENGTH, required=False)
2469
+
2470
+
2471
+ class ModularComponentCreateForm(ModularComponentForm):
2472
+ """
2473
+ Base form for the creation of modular device components (models subclassed from ModularComponentModel).
2474
+ """
2475
+
2476
+ device = DynamicModelChoiceField(queryset=Device.objects.all(), required=False)
2477
+ module = DynamicModelChoiceField(queryset=Module.objects.all(), required=False)
2478
+
2479
+
2480
+ class ComponentEditForm(NautobotModelForm):
2481
+ """
2482
+ Base class for editing device components (models subclassed from ComponentModel).
2483
+
2484
+ Distinct from ComponentCreateForm in that it has a name/label instead of a name_pattern/label_pattern.
2485
+ """
2486
+
2487
+ device = DynamicModelChoiceField(queryset=Device.objects.all())
2488
+
2489
+ def __init__(self, *args, **kwargs):
2027
2490
  super().__init__(*args, **kwargs)
2028
2491
 
2029
- # Disallow changing the device of an existing component
2030
- if self.instance is not None and self.instance.present_in_database:
2492
+ # Disable the device field if an initial value is provided
2493
+ if "device" in self.initial:
2031
2494
  self.fields["device"].disabled = True
2032
2495
 
2033
2496
 
2497
+ class ModularComponentEditForm(ComponentEditForm):
2498
+ """
2499
+ Base class for editing modular device components (models subclassed from ModularComponentModel).
2500
+ """
2501
+
2502
+ device = DynamicModelChoiceField(queryset=Device.objects.all(), required=False)
2503
+ module = DynamicModelChoiceField(queryset=Module.objects.all(), required=False)
2504
+
2505
+ def __init__(self, *args, **kwargs):
2506
+ super().__init__(*args, **kwargs)
2507
+
2508
+ # Disable the device and module fields if an initial value is provided for either
2509
+ if "device" in self.initial or "module" in self.initial:
2510
+ self.fields["device"].disabled = True
2511
+ self.fields["module"].disabled = True
2512
+
2513
+
2034
2514
  class DeviceBulkAddComponentForm(ComponentForm, CustomFieldModelBulkEditFormMixin):
2035
2515
  pk = forms.ModelMultipleChoiceField(queryset=Device.objects.all(), widget=forms.MultipleHiddenInput())
2036
2516
  description = forms.CharField(max_length=CHARFIELD_MAX_LENGTH, required=False)
@@ -2039,22 +2519,31 @@ class DeviceBulkAddComponentForm(ComponentForm, CustomFieldModelBulkEditFormMixi
2039
2519
  nullable_fields = []
2040
2520
 
2041
2521
 
2522
+ class ModuleBulkAddComponentForm(ModularComponentForm, CustomFieldModelBulkEditFormMixin):
2523
+ pk = forms.ModelMultipleChoiceField(queryset=Module.objects.all(), widget=forms.MultipleHiddenInput())
2524
+ description = forms.CharField(max_length=CHARFIELD_MAX_LENGTH, required=False)
2525
+
2526
+ class Meta:
2527
+ nullable_fields = []
2528
+
2529
+
2042
2530
  #
2043
2531
  # Console ports
2044
2532
  #
2045
2533
 
2046
2534
 
2047
- class ConsolePortFilterForm(DeviceComponentFilterForm):
2535
+ class ConsolePortFilterForm(ModularDeviceComponentFilterForm):
2048
2536
  model = ConsolePort
2049
2537
  type = forms.MultipleChoiceField(choices=ConsolePortTypeChoices, required=False, widget=StaticSelect2Multiple())
2050
2538
  tags = TagFilterField(model)
2051
2539
 
2052
2540
 
2053
- class ConsolePortForm(ComponentEditForm):
2541
+ class ConsolePortForm(ModularComponentEditForm):
2054
2542
  class Meta:
2055
2543
  model = ConsolePort
2056
2544
  fields = [
2057
2545
  "device",
2546
+ "module",
2058
2547
  "name",
2059
2548
  "label",
2060
2549
  "type",
@@ -2063,7 +2552,7 @@ class ConsolePortForm(ComponentEditForm):
2063
2552
  ]
2064
2553
 
2065
2554
 
2066
- class ConsolePortCreateForm(ComponentCreateForm):
2555
+ class ConsolePortCreateForm(ModularComponentCreateForm):
2067
2556
  type = forms.ChoiceField(
2068
2557
  choices=add_blank_choice(ConsolePortTypeChoices),
2069
2558
  required=False,
@@ -2071,6 +2560,7 @@ class ConsolePortCreateForm(ComponentCreateForm):
2071
2560
  )
2072
2561
  field_order = (
2073
2562
  "device",
2563
+ "module",
2074
2564
  "name_pattern",
2075
2565
  "label_pattern",
2076
2566
  "type",
@@ -2083,6 +2573,10 @@ class ConsolePortBulkCreateForm(form_from_model(ConsolePort, ["type", "tags"]),
2083
2573
  field_order = ("name_pattern", "label_pattern", "type", "description", "tags")
2084
2574
 
2085
2575
 
2576
+ class ModuleConsolePortBulkCreateForm(form_from_model(ConsolePort, ["type", "tags"]), ModuleBulkAddComponentForm):
2577
+ field_order = ("name_pattern", "label_pattern", "type", "description", "tags")
2578
+
2579
+
2086
2580
  class ConsolePortBulkEditForm(
2087
2581
  form_from_model(ConsolePort, ["label", "type", "description"]),
2088
2582
  TagsBulkEditFormMixin,
@@ -2099,17 +2593,18 @@ class ConsolePortBulkEditForm(
2099
2593
  #
2100
2594
 
2101
2595
 
2102
- class ConsoleServerPortFilterForm(DeviceComponentFilterForm):
2596
+ class ConsoleServerPortFilterForm(ModularDeviceComponentFilterForm):
2103
2597
  model = ConsoleServerPort
2104
2598
  type = forms.MultipleChoiceField(choices=ConsolePortTypeChoices, required=False, widget=StaticSelect2Multiple())
2105
2599
  tags = TagFilterField(model)
2106
2600
 
2107
2601
 
2108
- class ConsoleServerPortForm(ComponentEditForm):
2602
+ class ConsoleServerPortForm(ModularComponentEditForm):
2109
2603
  class Meta:
2110
2604
  model = ConsoleServerPort
2111
2605
  fields = [
2112
2606
  "device",
2607
+ "module",
2113
2608
  "name",
2114
2609
  "label",
2115
2610
  "type",
@@ -2118,7 +2613,7 @@ class ConsoleServerPortForm(ComponentEditForm):
2118
2613
  ]
2119
2614
 
2120
2615
 
2121
- class ConsoleServerPortCreateForm(ComponentCreateForm):
2616
+ class ConsoleServerPortCreateForm(ModularComponentCreateForm):
2122
2617
  type = forms.ChoiceField(
2123
2618
  choices=add_blank_choice(ConsolePortTypeChoices),
2124
2619
  required=False,
@@ -2126,6 +2621,7 @@ class ConsoleServerPortCreateForm(ComponentCreateForm):
2126
2621
  )
2127
2622
  field_order = (
2128
2623
  "device",
2624
+ "module",
2129
2625
  "name_pattern",
2130
2626
  "label_pattern",
2131
2627
  "type",
@@ -2138,6 +2634,12 @@ class ConsoleServerPortBulkCreateForm(form_from_model(ConsoleServerPort, ["type"
2138
2634
  field_order = ("name_pattern", "label_pattern", "type", "description", "tags")
2139
2635
 
2140
2636
 
2637
+ class ModuleConsoleServerPortBulkCreateForm(
2638
+ form_from_model(ConsoleServerPort, ["type", "tags"]), ModuleBulkAddComponentForm
2639
+ ):
2640
+ field_order = ("name_pattern", "label_pattern", "type", "description", "tags")
2641
+
2642
+
2141
2643
  class ConsoleServerPortBulkEditForm(
2142
2644
  form_from_model(ConsoleServerPort, ["label", "type", "description"]),
2143
2645
  TagsBulkEditFormMixin,
@@ -2154,17 +2656,18 @@ class ConsoleServerPortBulkEditForm(
2154
2656
  #
2155
2657
 
2156
2658
 
2157
- class PowerPortFilterForm(DeviceComponentFilterForm):
2659
+ class PowerPortFilterForm(ModularDeviceComponentFilterForm):
2158
2660
  model = PowerPort
2159
2661
  type = forms.MultipleChoiceField(choices=PowerPortTypeChoices, required=False, widget=StaticSelect2Multiple())
2160
2662
  tags = TagFilterField(model)
2161
2663
 
2162
2664
 
2163
- class PowerPortForm(ComponentEditForm):
2665
+ class PowerPortForm(ModularComponentEditForm):
2164
2666
  class Meta:
2165
2667
  model = PowerPort
2166
2668
  fields = [
2167
2669
  "device",
2670
+ "module",
2168
2671
  "name",
2169
2672
  "label",
2170
2673
  "type",
@@ -2175,7 +2678,7 @@ class PowerPortForm(ComponentEditForm):
2175
2678
  ]
2176
2679
 
2177
2680
 
2178
- class PowerPortCreateForm(ComponentCreateForm):
2681
+ class PowerPortCreateForm(ModularComponentCreateForm):
2179
2682
  type = forms.ChoiceField(
2180
2683
  choices=add_blank_choice(PowerPortTypeChoices),
2181
2684
  required=False,
@@ -2185,6 +2688,7 @@ class PowerPortCreateForm(ComponentCreateForm):
2185
2688
  allocated_draw = forms.IntegerField(min_value=1, required=False, help_text="Allocated draw in watts")
2186
2689
  field_order = (
2187
2690
  "device",
2691
+ "module",
2188
2692
  "name_pattern",
2189
2693
  "label_pattern",
2190
2694
  "type",
@@ -2210,6 +2714,21 @@ class PowerPortBulkCreateForm(
2210
2714
  )
2211
2715
 
2212
2716
 
2717
+ class ModulePowerPortBulkCreateForm(
2718
+ form_from_model(PowerPort, ["type", "maximum_draw", "allocated_draw", "tags"]),
2719
+ ModuleBulkAddComponentForm,
2720
+ ):
2721
+ field_order = (
2722
+ "name_pattern",
2723
+ "label_pattern",
2724
+ "type",
2725
+ "maximum_draw",
2726
+ "allocated_draw",
2727
+ "description",
2728
+ "tags",
2729
+ )
2730
+
2731
+
2213
2732
  class PowerPortBulkEditForm(
2214
2733
  form_from_model(PowerPort, ["label", "type", "maximum_draw", "allocated_draw", "description"]),
2215
2734
  TagsBulkEditFormMixin,
@@ -2226,13 +2745,13 @@ class PowerPortBulkEditForm(
2226
2745
  #
2227
2746
 
2228
2747
 
2229
- class PowerOutletFilterForm(DeviceComponentFilterForm):
2748
+ class PowerOutletFilterForm(ModularDeviceComponentFilterForm):
2230
2749
  model = PowerOutlet
2231
2750
  type = forms.MultipleChoiceField(choices=PowerOutletTypeChoices, required=False, widget=StaticSelect2Multiple())
2232
2751
  tags = TagFilterField(model)
2233
2752
 
2234
2753
 
2235
- class PowerOutletForm(ComponentEditForm):
2754
+ class PowerOutletForm(ModularComponentEditForm):
2236
2755
  power_port = DynamicModelChoiceField(
2237
2756
  queryset=PowerPort.objects.all(),
2238
2757
  required=False,
@@ -2243,6 +2762,7 @@ class PowerOutletForm(ComponentEditForm):
2243
2762
  model = PowerOutlet
2244
2763
  fields = [
2245
2764
  "device",
2765
+ "module",
2246
2766
  "name",
2247
2767
  "label",
2248
2768
  "type",
@@ -2253,7 +2773,7 @@ class PowerOutletForm(ComponentEditForm):
2253
2773
  ]
2254
2774
 
2255
2775
 
2256
- class PowerOutletCreateForm(ComponentCreateForm):
2776
+ class PowerOutletCreateForm(ModularComponentCreateForm):
2257
2777
  type = forms.ChoiceField(
2258
2778
  choices=add_blank_choice(PowerOutletTypeChoices),
2259
2779
  required=False,
@@ -2267,6 +2787,7 @@ class PowerOutletCreateForm(ComponentCreateForm):
2267
2787
  feed_leg = forms.ChoiceField(choices=add_blank_choice(PowerOutletFeedLegChoices), required=False)
2268
2788
  field_order = (
2269
2789
  "device",
2790
+ "module",
2270
2791
  "name_pattern",
2271
2792
  "label_pattern",
2272
2793
  "type",
@@ -2288,6 +2809,19 @@ class PowerOutletBulkCreateForm(form_from_model(PowerOutlet, ["type", "feed_leg"
2288
2809
  )
2289
2810
 
2290
2811
 
2812
+ class ModulePowerOutletBulkCreateForm(
2813
+ form_from_model(PowerOutlet, ["type", "feed_leg", "tags"]), ModuleBulkAddComponentForm
2814
+ ):
2815
+ field_order = (
2816
+ "name_pattern",
2817
+ "label_pattern",
2818
+ "type",
2819
+ "feed_leg",
2820
+ "description",
2821
+ "tags",
2822
+ )
2823
+
2824
+
2291
2825
  class PowerOutletBulkEditForm(
2292
2826
  form_from_model(PowerOutlet, ["label", "type", "feed_leg", "power_port", "description"]),
2293
2827
  TagsBulkEditFormMixin,
@@ -2297,7 +2831,6 @@ class PowerOutletBulkEditForm(
2297
2831
  device = forms.ModelChoiceField(
2298
2832
  queryset=Device.objects.all(),
2299
2833
  required=False,
2300
- disabled=True,
2301
2834
  widget=forms.HiddenInput(),
2302
2835
  )
2303
2836
 
@@ -2321,7 +2854,7 @@ class PowerOutletBulkEditForm(
2321
2854
  #
2322
2855
 
2323
2856
 
2324
- class InterfaceFilterForm(DeviceComponentFilterForm, StatusModelFilterFormMixin):
2857
+ class InterfaceFilterForm(ModularDeviceComponentFilterForm, RoleModelFilterFormMixin, StatusModelFilterFormMixin):
2325
2858
  model = Interface
2326
2859
  type = forms.MultipleChoiceField(choices=InterfaceTypeChoices, required=False, widget=StaticSelect2Multiple())
2327
2860
  enabled = forms.NullBooleanField(required=False, widget=StaticSelect2(choices=BOOLEAN_WITH_BLANK_CHOICES))
@@ -2330,7 +2863,7 @@ class InterfaceFilterForm(DeviceComponentFilterForm, StatusModelFilterFormMixin)
2330
2863
  tags = TagFilterField(model)
2331
2864
 
2332
2865
 
2333
- class InterfaceForm(InterfaceCommonForm, ComponentEditForm):
2866
+ class InterfaceForm(InterfaceCommonForm, ModularComponentEditForm):
2334
2867
  parent_interface = DynamicModelChoiceField(
2335
2868
  queryset=Interface.objects.all(),
2336
2869
  required=False,
@@ -2389,7 +2922,9 @@ class InterfaceForm(InterfaceCommonForm, ComponentEditForm):
2389
2922
  model = Interface
2390
2923
  fields = [
2391
2924
  "device",
2925
+ "module",
2392
2926
  "name",
2927
+ "role",
2393
2928
  "label",
2394
2929
  "type",
2395
2930
  "enabled",
@@ -2420,7 +2955,8 @@ class InterfaceForm(InterfaceCommonForm, ComponentEditForm):
2420
2955
  }
2421
2956
 
2422
2957
 
2423
- class InterfaceCreateForm(ComponentCreateForm, InterfaceCommonForm):
2958
+ class InterfaceCreateForm(ModularComponentCreateForm, InterfaceCommonForm, RoleNotRequiredModelFormMixin):
2959
+ model = Interface
2424
2960
  type = forms.ChoiceField(
2425
2961
  choices=InterfaceTypeChoices,
2426
2962
  widget=StaticSelect2(),
@@ -2503,9 +3039,11 @@ class InterfaceCreateForm(ComponentCreateForm, InterfaceCommonForm):
2503
3039
  )
2504
3040
  field_order = (
2505
3041
  "device",
3042
+ "module",
2506
3043
  "name_pattern",
2507
3044
  "label_pattern",
2508
3045
  "status",
3046
+ "role",
2509
3047
  "type",
2510
3048
  "enabled",
2511
3049
  "parent_interface",
@@ -2527,7 +3065,41 @@ class InterfaceCreateForm(ComponentCreateForm, InterfaceCommonForm):
2527
3065
  class InterfaceBulkCreateForm(
2528
3066
  form_from_model(Interface, ["enabled", "mtu", "vrf", "mgmt_only", "mode", "tags"]),
2529
3067
  DeviceBulkAddComponentForm,
3068
+ RoleNotRequiredModelFormMixin,
3069
+ ):
3070
+ model = Interface
3071
+ type = forms.ChoiceField(
3072
+ choices=InterfaceTypeChoices,
3073
+ widget=StaticSelect2(),
3074
+ )
3075
+ status = DynamicModelChoiceField(
3076
+ required=True,
3077
+ queryset=Status.objects.all(),
3078
+ query_params={"content_types": Interface._meta.label_lower},
3079
+ )
3080
+
3081
+ field_order = (
3082
+ "name_pattern",
3083
+ "label_pattern",
3084
+ "status",
3085
+ "role",
3086
+ "type",
3087
+ "enabled",
3088
+ "mtu",
3089
+ "vrf",
3090
+ "mgmt_only",
3091
+ "description",
3092
+ "mode",
3093
+ "tags",
3094
+ )
3095
+
3096
+
3097
+ class ModuleInterfaceBulkCreateForm(
3098
+ form_from_model(Interface, ["enabled", "mtu", "vrf", "mgmt_only", "mode", "tags"]),
3099
+ ModuleBulkAddComponentForm,
3100
+ RoleNotRequiredModelFormMixin,
2530
3101
  ):
3102
+ model = Interface
2531
3103
  type = forms.ChoiceField(
2532
3104
  choices=InterfaceTypeChoices,
2533
3105
  widget=StaticSelect2(),
@@ -2542,6 +3114,7 @@ class InterfaceBulkCreateForm(
2542
3114
  "name_pattern",
2543
3115
  "label_pattern",
2544
3116
  "status",
3117
+ "role",
2545
3118
  "type",
2546
3119
  "enabled",
2547
3120
  "mtu",
@@ -2559,6 +3132,7 @@ class InterfaceBulkEditForm(
2559
3132
  ),
2560
3133
  TagsBulkEditFormMixin,
2561
3134
  StatusModelBulkEditFormMixin,
3135
+ RoleModelBulkEditFormMixin,
2562
3136
  NautobotBulkEditForm,
2563
3137
  ):
2564
3138
  pk = forms.ModelMultipleChoiceField(queryset=Interface.objects.all(), widget=forms.MultipleHiddenInput())
@@ -2670,13 +3244,13 @@ class InterfaceBulkEditForm(
2670
3244
  #
2671
3245
 
2672
3246
 
2673
- class FrontPortFilterForm(DeviceComponentFilterForm):
3247
+ class FrontPortFilterForm(ModularDeviceComponentFilterForm):
2674
3248
  model = FrontPort
2675
3249
  type = forms.MultipleChoiceField(choices=PortTypeChoices, required=False, widget=StaticSelect2Multiple())
2676
3250
  tags = TagFilterField(model)
2677
3251
 
2678
3252
 
2679
- class FrontPortForm(ComponentEditForm):
3253
+ class FrontPortForm(ModularComponentEditForm):
2680
3254
  rear_port = DynamicModelChoiceField(
2681
3255
  queryset=RearPort.objects.all(),
2682
3256
  query_params={"device": "$device"},
@@ -2686,6 +3260,7 @@ class FrontPortForm(ComponentEditForm):
2686
3260
  model = FrontPort
2687
3261
  fields = [
2688
3262
  "device",
3263
+ "module",
2689
3264
  "name",
2690
3265
  "label",
2691
3266
  "type",
@@ -2700,7 +3275,7 @@ class FrontPortForm(ComponentEditForm):
2700
3275
 
2701
3276
 
2702
3277
  # TODO: Merge with FrontPortTemplateCreateForm to remove duplicate logic
2703
- class FrontPortCreateForm(ComponentCreateForm):
3278
+ class FrontPortCreateForm(ModularComponentCreateForm):
2704
3279
  type = forms.ChoiceField(
2705
3280
  choices=PortTypeChoices,
2706
3281
  widget=StaticSelect2(),
@@ -2712,6 +3287,7 @@ class FrontPortCreateForm(ComponentCreateForm):
2712
3287
  )
2713
3288
  field_order = (
2714
3289
  "device",
3290
+ "module",
2715
3291
  "name_pattern",
2716
3292
  "label_pattern",
2717
3293
  "type",
@@ -2723,17 +3299,24 @@ class FrontPortCreateForm(ComponentCreateForm):
2723
3299
  def __init__(self, *args, **kwargs):
2724
3300
  super().__init__(*args, **kwargs)
2725
3301
 
2726
- device = Device.objects.get(pk=self.initial.get("device") or self.data.get("device"))
3302
+ device = self.initial.get("device") or self.data.get("device")
3303
+ module = self.initial.get("module") or self.data.get("module")
3304
+ if device:
3305
+ parent = Device.objects.get(pk=device)
3306
+ elif module:
3307
+ parent = Module.objects.get(pk=module)
3308
+ else:
3309
+ return
2727
3310
 
2728
3311
  # Determine which rear port positions are occupied. These will be excluded from the list of available
2729
3312
  # mappings.
2730
3313
  occupied_port_positions = [
2731
- (front_port.rear_port_id, front_port.rear_port_position) for front_port in device.front_ports.all()
3314
+ (front_port.rear_port_id, front_port.rear_port_position) for front_port in parent.front_ports.all()
2732
3315
  ]
2733
3316
 
2734
3317
  # Populate rear port choices
2735
3318
  choices = []
2736
- rear_ports = RearPort.objects.filter(device=device)
3319
+ rear_ports = RearPort.objects.filter(**{parent._meta.model_name: parent})
2737
3320
  for rear_port in rear_ports:
2738
3321
  for i in range(1, rear_port.positions + 1):
2739
3322
  if (rear_port.pk, i) not in occupied_port_positions:
@@ -2794,17 +3377,18 @@ class FrontPortBulkEditForm(
2794
3377
  #
2795
3378
 
2796
3379
 
2797
- class RearPortFilterForm(DeviceComponentFilterForm):
3380
+ class RearPortFilterForm(ModularDeviceComponentFilterForm):
2798
3381
  model = RearPort
2799
3382
  type = forms.MultipleChoiceField(choices=PortTypeChoices, required=False, widget=StaticSelect2Multiple())
2800
3383
  tags = TagFilterField(model)
2801
3384
 
2802
3385
 
2803
- class RearPortForm(ComponentEditForm):
3386
+ class RearPortForm(ModularComponentEditForm):
2804
3387
  class Meta:
2805
3388
  model = RearPort
2806
3389
  fields = [
2807
3390
  "device",
3391
+ "module",
2808
3392
  "name",
2809
3393
  "label",
2810
3394
  "type",
@@ -2817,7 +3401,7 @@ class RearPortForm(ComponentEditForm):
2817
3401
  }
2818
3402
 
2819
3403
 
2820
- class RearPortCreateForm(ComponentCreateForm):
3404
+ class RearPortCreateForm(ModularComponentCreateForm):
2821
3405
  type = forms.ChoiceField(
2822
3406
  choices=PortTypeChoices,
2823
3407
  widget=StaticSelect2(),
@@ -2830,6 +3414,7 @@ class RearPortCreateForm(ComponentCreateForm):
2830
3414
  )
2831
3415
  field_order = (
2832
3416
  "device",
3417
+ "module",
2833
3418
  "name_pattern",
2834
3419
  "label_pattern",
2835
3420
  "type",
@@ -2850,6 +3435,19 @@ class RearPortBulkCreateForm(form_from_model(RearPort, ["type", "positions", "ta
2850
3435
  )
2851
3436
 
2852
3437
 
3438
+ class ModuleRearPortBulkCreateForm(
3439
+ form_from_model(RearPort, ["type", "positions", "tags"]), ModuleBulkAddComponentForm
3440
+ ):
3441
+ field_order = (
3442
+ "name_pattern",
3443
+ "label_pattern",
3444
+ "type",
3445
+ "positions",
3446
+ "description",
3447
+ "tags",
3448
+ )
3449
+
3450
+
2853
3451
  class RearPortBulkEditForm(
2854
3452
  form_from_model(RearPort, ["label", "type", "description"]),
2855
3453
  TagsBulkEditFormMixin,
@@ -2922,13 +3520,119 @@ class DeviceBayBulkEditForm(
2922
3520
  nullable_fields = ["label", "description"]
2923
3521
 
2924
3522
 
3523
+ #
3524
+ # Module bays
3525
+ #
3526
+
3527
+
3528
+ class ModuleBayFilterForm(NautobotFilterForm):
3529
+ model = ModuleBay
3530
+
3531
+ field_order = ["q", "parent_device", "parent_module"]
3532
+ q = forms.CharField(required=False, label="Search")
3533
+ parent_device = DynamicModelMultipleChoiceField(
3534
+ queryset=Device.objects.all(),
3535
+ required=False,
3536
+ label="Parent device",
3537
+ )
3538
+ parent_module = DynamicModelMultipleChoiceField(
3539
+ queryset=Module.objects.all(),
3540
+ required=False,
3541
+ label="Parent module",
3542
+ )
3543
+
3544
+
3545
+ class ModuleBayForm(NautobotModelForm):
3546
+ parent_device = DynamicModelChoiceField(
3547
+ queryset=Device.objects.all(),
3548
+ required=False,
3549
+ label="Parent Device",
3550
+ )
3551
+ parent_module = DynamicModelChoiceField(
3552
+ queryset=Module.objects.all(),
3553
+ required=False,
3554
+ label="Parent Module",
3555
+ )
3556
+ # TODO: Installed module field
3557
+
3558
+ class Meta:
3559
+ model = ModuleBay
3560
+ fields = [
3561
+ "parent_device",
3562
+ "parent_module",
3563
+ "name",
3564
+ "position",
3565
+ "label",
3566
+ "description",
3567
+ "tags",
3568
+ ]
3569
+
3570
+ def __init__(self, *args, **kwargs):
3571
+ super().__init__(*args, **kwargs)
3572
+
3573
+ # Disable the parent_device and parent_module fields if an initial value is provided for either
3574
+ if "parent_device" in self.initial or "parent_module" in self.initial:
3575
+ self.fields["parent_device"].disabled = True
3576
+ self.fields["parent_module"].disabled = True
3577
+
3578
+
3579
+ class ModuleBayCreateForm(ModuleBayBaseCreateForm):
3580
+ parent_device = DynamicModelChoiceField(queryset=Device.objects.all(), required=False)
3581
+ parent_module = DynamicModelChoiceField(queryset=Module.objects.all(), required=False)
3582
+ tags = DynamicModelMultipleChoiceField(
3583
+ queryset=Tag.objects.all(),
3584
+ required=False,
3585
+ query_params={"content_types": ModuleBay._meta.label_lower},
3586
+ )
3587
+ field_order = (
3588
+ "parent_device",
3589
+ "parent_module",
3590
+ "name_pattern",
3591
+ "label_pattern",
3592
+ "position_pattern",
3593
+ "description",
3594
+ "tags",
3595
+ )
3596
+
3597
+
3598
+ class ModuleBayBulkCreateForm(
3599
+ form_from_model(ModuleBay, ["tags"]),
3600
+ ModuleBayBaseCreateForm,
3601
+ CustomFieldModelBulkEditFormMixin,
3602
+ ):
3603
+ pk = forms.ModelMultipleChoiceField(queryset=Device.objects.all(), widget=forms.MultipleHiddenInput())
3604
+ description = forms.CharField(max_length=CHARFIELD_MAX_LENGTH, required=False)
3605
+
3606
+ field_order = ("name_pattern", "label_pattern", "position_pattern", "description", "tags")
3607
+
3608
+ class Meta:
3609
+ nullable_fields = []
3610
+
3611
+
3612
+ class ModuleModuleBayBulkCreateForm(ModuleBayBulkCreateForm):
3613
+ pk = forms.ModelMultipleChoiceField(queryset=Module.objects.all(), widget=forms.MultipleHiddenInput())
3614
+
3615
+ class Meta(ModuleBayBulkCreateForm.Meta):
3616
+ pass
3617
+
3618
+
3619
+ class ModuleBayBulkEditForm(
3620
+ form_from_model(ModuleBay, ["label", "description", "position"]),
3621
+ TagsBulkEditFormMixin,
3622
+ NautobotBulkEditForm,
3623
+ ):
3624
+ pk = forms.ModelMultipleChoiceField(queryset=ModuleBay.objects.all(), widget=forms.MultipleHiddenInput())
3625
+
3626
+ class Meta:
3627
+ nullable_fields = ["label", "description"]
3628
+
3629
+
2925
3630
  #
2926
3631
  # Inventory items
2927
3632
  #
2928
3633
 
2929
3634
 
2930
- class InventoryItemForm(NautobotModelForm):
2931
- device = DynamicModelChoiceField(queryset=Device.objects.all())
3635
+ class InventoryItemForm(ComponentEditForm):
2932
3636
  parent = DynamicModelChoiceField(
2933
3637
  queryset=InventoryItem.objects.all(),
2934
3638
  required=False,
@@ -3101,6 +3805,14 @@ class ConnectCableToDeviceForm(ConnectCableExcludeIDMixin, NautobotModelForm):
3101
3805
  "rack": "$termination_b_rack",
3102
3806
  },
3103
3807
  )
3808
+ termination_b_module = DynamicModelChoiceField(
3809
+ queryset=Module.objects.all(),
3810
+ label="Module",
3811
+ required=False,
3812
+ query_params={
3813
+ "device": "$termination_b_device",
3814
+ },
3815
+ )
3104
3816
 
3105
3817
  class Meta:
3106
3818
  model = Cable
@@ -3135,7 +3847,7 @@ class ConnectCableToConsolePortForm(ConnectCableToDeviceForm):
3135
3847
  queryset=ConsolePort.objects.all(),
3136
3848
  label="Name",
3137
3849
  disabled_indicator="cable",
3138
- query_params={"device": "$termination_b_device"},
3850
+ query_params={"device": "$termination_b_device", "module": "$termination_b_module"},
3139
3851
  )
3140
3852
 
3141
3853
 
@@ -3144,7 +3856,7 @@ class ConnectCableToConsoleServerPortForm(ConnectCableToDeviceForm):
3144
3856
  queryset=ConsoleServerPort.objects.all(),
3145
3857
  label="Name",
3146
3858
  disabled_indicator="cable",
3147
- query_params={"device": "$termination_b_device"},
3859
+ query_params={"device": "$termination_b_device", "module": "$termination_b_module"},
3148
3860
  )
3149
3861
 
3150
3862
 
@@ -3153,7 +3865,7 @@ class ConnectCableToPowerPortForm(ConnectCableToDeviceForm):
3153
3865
  queryset=PowerPort.objects.all(),
3154
3866
  label="Name",
3155
3867
  disabled_indicator="cable",
3156
- query_params={"device": "$termination_b_device"},
3868
+ query_params={"device": "$termination_b_device", "module": "$termination_b_module"},
3157
3869
  )
3158
3870
 
3159
3871
 
@@ -3162,7 +3874,7 @@ class ConnectCableToPowerOutletForm(ConnectCableToDeviceForm):
3162
3874
  queryset=PowerOutlet.objects.all(),
3163
3875
  label="Name",
3164
3876
  disabled_indicator="cable",
3165
- query_params={"device": "$termination_b_device"},
3877
+ query_params={"device": "$termination_b_device", "module": "$termination_b_module"},
3166
3878
  )
3167
3879
 
3168
3880
 
@@ -3173,6 +3885,7 @@ class ConnectCableToInterfaceForm(ConnectCableToDeviceForm):
3173
3885
  disabled_indicator="cable",
3174
3886
  query_params={
3175
3887
  "device_id": "$termination_b_device",
3888
+ "module": "$termination_b_module",
3176
3889
  "kind": "physical",
3177
3890
  },
3178
3891
  )
@@ -3183,7 +3896,7 @@ class ConnectCableToFrontPortForm(ConnectCableToDeviceForm):
3183
3896
  queryset=FrontPort.objects.all(),
3184
3897
  label="Name",
3185
3898
  disabled_indicator="cable",
3186
- query_params={"device": "$termination_b_device"},
3899
+ query_params={"device": "$termination_b_device", "module": "$termination_b_module"},
3187
3900
  )
3188
3901
 
3189
3902
 
@@ -3192,7 +3905,7 @@ class ConnectCableToRearPortForm(ConnectCableToDeviceForm):
3192
3905
  queryset=RearPort.objects.all(),
3193
3906
  label="Name",
3194
3907
  disabled_indicator="cable",
3195
- query_params={"device": "$termination_b_device"},
3908
+ query_params={"device": "$termination_b_device", "module": "$termination_b_module"},
3196
3909
  )
3197
3910
 
3198
3911