nautobot 2.2.8__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 (704) 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 +110 -14
  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/generators.py +2 -2
  61. nautobot/core/graphql/schema.py +28 -7
  62. nautobot/core/jobs/__init__.py +20 -3
  63. nautobot/core/jobs/cleanup.py +100 -0
  64. nautobot/core/jobs/groups.py +38 -0
  65. nautobot/core/management/commands/generate_test_data.py +116 -3
  66. nautobot/core/models/__init__.py +34 -9
  67. nautobot/core/models/generics.py +19 -3
  68. nautobot/core/models/name_color_content_types.py +7 -28
  69. nautobot/core/models/querysets.py +4 -3
  70. nautobot/core/models/tree_queries.py +1 -1
  71. nautobot/core/models/utils.py +21 -5
  72. nautobot/core/settings.py +15 -19
  73. nautobot/core/settings.yaml +48 -13
  74. nautobot/core/settings_funcs.py +103 -0
  75. nautobot/core/tables.py +130 -56
  76. nautobot/core/templates/admin/search_form.html +1 -1
  77. nautobot/core/templates/buttons/add.html +11 -3
  78. nautobot/core/templates/buttons/consolidated_bulk_action_buttons.html +13 -0
  79. nautobot/core/templates/buttons/consolidated_detail_view_action_buttons.html +13 -0
  80. nautobot/core/templates/buttons/export.html +101 -53
  81. nautobot/core/templates/buttons/job_import.html +11 -3
  82. nautobot/core/templates/generic/object_bulk_destroy.html +3 -1
  83. nautobot/core/templates/generic/object_bulk_update.html +3 -1
  84. nautobot/core/templates/generic/object_changelog.html +0 -9
  85. nautobot/core/templates/generic/object_list.html +156 -17
  86. nautobot/core/templates/generic/object_retrieve.html +80 -16
  87. nautobot/core/templates/inc/extras_features_edit_form_fields.html +8 -0
  88. nautobot/core/templates/inc/javascript.html +2 -0
  89. nautobot/core/templates/inc/media.html +2 -2
  90. nautobot/core/templates/inc/nav_menu.html +1 -0
  91. nautobot/core/templates/inc/paginator.html +7 -7
  92. nautobot/core/templates/inc/search_panel.html +2 -2
  93. nautobot/core/templates/inc/table.html +2 -2
  94. nautobot/core/templates/nautobot_config.py.j2 +28 -8
  95. nautobot/core/templates/utilities/templatetags/dynamic_group_assignment_modal.html +37 -0
  96. nautobot/core/templates/utilities/templatetags/filter_form_modal.html +2 -2
  97. nautobot/core/templates/utilities/templatetags/saved_view_modal.html +38 -0
  98. nautobot/core/templates/utilities/theme_preview.html +25 -8
  99. nautobot/core/templates/utilities/worker_status.html +152 -0
  100. nautobot/core/templatetags/buttons.py +335 -38
  101. nautobot/core/templatetags/form_helpers.py +1 -1
  102. nautobot/core/templatetags/helpers.py +181 -11
  103. nautobot/core/testing/api.py +5 -4
  104. nautobot/core/testing/filters.py +63 -14
  105. nautobot/core/testing/mixins.py +46 -0
  106. nautobot/core/testing/models.py +22 -0
  107. nautobot/core/testing/schema.py +4 -8
  108. nautobot/core/testing/views.py +31 -14
  109. nautobot/core/tests/integration/test_general_functionality.py +1 -1
  110. nautobot/core/tests/integration/test_import_objects_ui.py +1 -0
  111. nautobot/core/tests/integration/test_swagger.py +1 -1
  112. nautobot/core/tests/nautobot_config.py +0 -1
  113. nautobot/core/tests/runner.py +2 -2
  114. nautobot/core/tests/test_api.py +1 -0
  115. nautobot/core/tests/test_authentication.py +7 -2
  116. nautobot/core/tests/test_filters.py +11 -9
  117. nautobot/core/tests/test_forms.py +9 -0
  118. nautobot/core/tests/test_graphql.py +27 -16
  119. nautobot/core/tests/test_jobs.py +204 -2
  120. nautobot/core/tests/test_tables.py +3 -1
  121. nautobot/core/tests/test_templatetags_helpers.py +12 -5
  122. nautobot/core/tests/test_templatetags_netutils.py +3 -3
  123. nautobot/core/tests/test_utils.py +31 -20
  124. nautobot/core/tests/test_views.py +6 -6
  125. nautobot/core/urls.py +8 -3
  126. nautobot/core/utils/deprecation.py +29 -0
  127. nautobot/core/utils/filtering.py +12 -9
  128. nautobot/core/utils/lookup.py +37 -2
  129. nautobot/core/utils/requests.py +4 -1
  130. nautobot/core/views/__init__.py +137 -24
  131. nautobot/core/views/generic.py +119 -67
  132. nautobot/core/views/mixins.py +105 -36
  133. nautobot/core/views/paginator.py +9 -3
  134. nautobot/core/views/renderers.py +121 -56
  135. nautobot/core/views/utils.py +81 -1
  136. nautobot/dcim/__init__.py +0 -1
  137. nautobot/dcim/api/serializers.py +180 -44
  138. nautobot/dcim/api/urls.py +7 -3
  139. nautobot/dcim/api/views.py +53 -7
  140. nautobot/dcim/apps.py +3 -0
  141. nautobot/dcim/choices.py +25 -0
  142. nautobot/dcim/constants.py +7 -0
  143. nautobot/dcim/factory.py +252 -18
  144. nautobot/dcim/filters/__init__.py +373 -193
  145. nautobot/dcim/filters/mixins.py +274 -1
  146. nautobot/dcim/forms.py +834 -121
  147. nautobot/dcim/graphql/types.py +2 -2
  148. nautobot/dcim/homepage.py +1 -1
  149. nautobot/dcim/migrations/0059_add_role_field_to_interface_models.py +27 -0
  150. nautobot/dcim/migrations/0060_alter_cable_status_alter_consoleport__path_and_more.py +303 -0
  151. nautobot/dcim/migrations/0061_module_models.py +862 -0
  152. nautobot/dcim/migrations/0062_module_data_migration.py +25 -0
  153. nautobot/dcim/models/__init__.py +8 -0
  154. nautobot/dcim/models/cables.py +15 -0
  155. nautobot/dcim/models/device_component_templates.py +207 -53
  156. nautobot/dcim/models/device_components.py +282 -99
  157. nautobot/dcim/models/devices.py +472 -13
  158. nautobot/dcim/models/racks.py +0 -1
  159. nautobot/dcim/navigation.py +47 -0
  160. nautobot/dcim/signals.py +3 -3
  161. nautobot/dcim/tables/__init__.py +35 -23
  162. nautobot/dcim/tables/devices.py +248 -47
  163. nautobot/dcim/tables/devicetypes.py +65 -9
  164. nautobot/dcim/tables/racks.py +5 -1
  165. nautobot/dcim/tables/template_code.py +46 -26
  166. nautobot/dcim/templates/dcim/cable_connect.html +76 -3
  167. nautobot/dcim/templates/dcim/console_port_connection_list.html +7 -5
  168. nautobot/dcim/templates/dcim/device/base.html +14 -6
  169. nautobot/dcim/templates/dcim/device/consoleports.html +2 -3
  170. nautobot/dcim/templates/dcim/device/consoleserverports.html +2 -3
  171. nautobot/dcim/templates/dcim/device/devicebays.html +6 -7
  172. nautobot/dcim/templates/dcim/device/frontports.html +2 -3
  173. nautobot/dcim/templates/dcim/device/interfaces.html +2 -3
  174. nautobot/dcim/templates/dcim/device/inventory.html +2 -3
  175. nautobot/dcim/templates/dcim/device/modulebays.html +49 -0
  176. nautobot/dcim/templates/dcim/device/poweroutlets.html +2 -3
  177. nautobot/dcim/templates/dcim/device/powerports.html +2 -3
  178. nautobot/dcim/templates/dcim/device/rearports.html +2 -3
  179. nautobot/dcim/templates/dcim/device.html +45 -1
  180. nautobot/dcim/templates/dcim/device_component.html +13 -5
  181. nautobot/dcim/templates/dcim/device_list.html +2 -1
  182. nautobot/dcim/templates/dcim/deviceredundancygroup_retrieve.html +6 -0
  183. nautobot/dcim/templates/dcim/devicetype.html +99 -98
  184. nautobot/dcim/templates/dcim/devicetype_list.html +8 -16
  185. nautobot/dcim/templates/dcim/inc/devicetype_component_table.html +1 -1
  186. nautobot/dcim/templates/dcim/inc/moduletype_component_table.html +39 -0
  187. nautobot/dcim/templates/dcim/interface.html +17 -2
  188. nautobot/dcim/templates/dcim/interface_connection_list.html +7 -5
  189. nautobot/dcim/templates/dcim/interface_edit.html +1 -0
  190. nautobot/dcim/templates/dcim/manufacturer.html +24 -0
  191. nautobot/dcim/templates/dcim/module/base.html +97 -0
  192. nautobot/dcim/templates/dcim/module_bulk_destroy.html +5 -0
  193. nautobot/dcim/templates/dcim/module_consoleports.html +53 -0
  194. nautobot/dcim/templates/dcim/module_consoleserverports.html +53 -0
  195. nautobot/dcim/templates/dcim/module_destroy.html +5 -0
  196. nautobot/dcim/templates/dcim/module_frontports.html +53 -0
  197. nautobot/dcim/templates/dcim/module_interfaces.html +57 -0
  198. nautobot/dcim/templates/dcim/module_list.html +20 -0
  199. nautobot/dcim/templates/dcim/module_modulebays.html +49 -0
  200. nautobot/dcim/templates/dcim/module_poweroutlets.html +53 -0
  201. nautobot/dcim/templates/dcim/module_powerports.html +53 -0
  202. nautobot/dcim/templates/dcim/module_rearports.html +53 -0
  203. nautobot/dcim/templates/dcim/module_retrieve.html +63 -0
  204. nautobot/dcim/templates/dcim/module_update.html +71 -0
  205. nautobot/dcim/templates/dcim/modulebay_bulk_destroy.html +5 -0
  206. nautobot/dcim/templates/dcim/modulebay_destroy.html +8 -0
  207. nautobot/dcim/templates/dcim/modulebay_retrieve.html +101 -0
  208. nautobot/dcim/templates/dcim/moduletype_list.html +11 -0
  209. nautobot/dcim/templates/dcim/moduletype_retrieve.html +159 -0
  210. nautobot/dcim/templates/dcim/power_port_connection_list.html +7 -5
  211. nautobot/dcim/templates/dcim/softwareimagefile_retrieve.html +65 -19
  212. nautobot/dcim/tests/integration/test_cable_connect_form.py +4 -4
  213. nautobot/dcim/tests/test_api.py +693 -208
  214. nautobot/dcim/tests/test_filters.py +843 -217
  215. nautobot/dcim/tests/test_models.py +1103 -8
  216. nautobot/dcim/tests/test_views.py +1525 -343
  217. nautobot/dcim/urls.py +17 -2
  218. nautobot/dcim/utils.py +2 -3
  219. nautobot/dcim/views.py +1109 -113
  220. nautobot/extras/__init__.py +0 -1
  221. nautobot/extras/api/serializers.py +115 -3
  222. nautobot/extras/api/urls.py +12 -0
  223. nautobot/extras/api/views.py +73 -59
  224. nautobot/extras/apps.py +2 -2
  225. nautobot/extras/choices.py +43 -0
  226. nautobot/extras/context_managers.py +13 -8
  227. nautobot/extras/datasources/git.py +2 -0
  228. nautobot/extras/factory.py +460 -9
  229. nautobot/extras/filters/__init__.py +174 -3
  230. nautobot/extras/filters/mixins.py +46 -43
  231. nautobot/extras/forms/base.py +24 -5
  232. nautobot/extras/forms/forms.py +227 -8
  233. nautobot/extras/forms/mixins.py +93 -0
  234. nautobot/extras/graphql/types.py +23 -10
  235. nautobot/extras/homepage.py +26 -3
  236. nautobot/extras/jobs.py +2 -2
  237. nautobot/extras/management/__init__.py +1 -0
  238. nautobot/extras/management/commands/refresh_dynamic_group_member_caches.py +1 -16
  239. nautobot/extras/migrations/0021_customfield_changelog_data.py +1 -0
  240. nautobot/extras/migrations/0109_dynamicgroup_group_type_dynamicgroup_tags_and_more.py +108 -0
  241. nautobot/extras/migrations/0110_alter_configcontext_cluster_groups_and_more.py +111 -0
  242. nautobot/extras/migrations/0111_metadata.py +162 -0
  243. nautobot/extras/migrations/0112_dynamic_group_group_type_data_migration.py +28 -0
  244. nautobot/extras/migrations/0113_saved_views.py +77 -0
  245. nautobot/extras/models/__init__.py +15 -1
  246. nautobot/extras/models/change_logging.py +3 -3
  247. nautobot/extras/models/contacts.py +4 -0
  248. nautobot/extras/models/customfields.py +18 -3
  249. nautobot/extras/models/groups.py +389 -225
  250. nautobot/extras/models/jobs.py +87 -3
  251. nautobot/extras/models/metadata.py +441 -0
  252. nautobot/extras/models/mixins.py +72 -62
  253. nautobot/extras/models/models.py +118 -9
  254. nautobot/extras/models/relationships.py +9 -2
  255. nautobot/extras/models/tags.py +13 -2
  256. nautobot/extras/navigation.py +57 -0
  257. nautobot/extras/plugins/__init__.py +3 -1
  258. nautobot/extras/querysets.py +30 -66
  259. nautobot/extras/signals.py +109 -101
  260. nautobot/extras/tables.py +201 -17
  261. nautobot/extras/templates/extras/dynamicgroup.html +44 -15
  262. nautobot/extras/templates/extras/dynamicgroup_edit.html +2 -0
  263. nautobot/extras/templates/extras/job.html +1 -1
  264. nautobot/extras/templates/extras/job_detail.html +11 -0
  265. nautobot/extras/templates/extras/jobresult.html +61 -74
  266. nautobot/extras/templates/extras/metadatatype_create.html +89 -0
  267. nautobot/extras/templates/extras/metadatatype_retrieve.html +67 -0
  268. nautobot/extras/templates/extras/object_dynamicgroups.html +7 -0
  269. nautobot/extras/templates/extras/objectchange_list.html +0 -12
  270. nautobot/extras/templates/extras/plugins_list.html +1 -3
  271. nautobot/extras/templates/extras/role_retrieve.html +48 -0
  272. nautobot/extras/templates/extras/staticgroupassociation_retrieve.html +20 -0
  273. nautobot/extras/tests/integration/test_customfields.py +1 -0
  274. nautobot/extras/tests/test_api.py +509 -23
  275. nautobot/extras/tests/test_changelog.py +20 -9
  276. nautobot/extras/tests/test_context_managers.py +22 -15
  277. nautobot/extras/tests/test_datasources.py +13 -1
  278. nautobot/extras/tests/test_dynamicgroups.py +201 -171
  279. nautobot/extras/tests/test_filters.py +211 -12
  280. nautobot/extras/tests/test_jobs.py +6 -6
  281. nautobot/extras/tests/test_models.py +501 -4
  282. nautobot/extras/tests/test_relationships.py +1 -0
  283. nautobot/extras/tests/test_views.py +586 -8
  284. nautobot/extras/tests/test_webhooks.py +1 -1
  285. nautobot/extras/urls.py +5 -0
  286. nautobot/extras/utils.py +85 -16
  287. nautobot/extras/views.py +562 -122
  288. nautobot/ipam/__init__.py +0 -1
  289. nautobot/ipam/apps.py +1 -0
  290. nautobot/ipam/factory.py +17 -19
  291. nautobot/ipam/filters.py +13 -0
  292. nautobot/ipam/forms.py +8 -4
  293. nautobot/ipam/graphql/types.py +2 -2
  294. nautobot/ipam/migrations/0047_alter_ipaddress_role_alter_ipaddress_status_and_more.py +59 -0
  295. nautobot/ipam/models.py +20 -20
  296. nautobot/ipam/querysets.py +1 -1
  297. nautobot/ipam/signals.py +4 -2
  298. nautobot/ipam/tables.py +5 -0
  299. nautobot/ipam/templates/ipam/ipaddress_interfaces.html +1 -1
  300. nautobot/ipam/templates/ipam/ipaddress_vm_interfaces.html +1 -1
  301. nautobot/ipam/templates/ipam/prefix.html +1 -0
  302. nautobot/ipam/tests/test_api.py +37 -18
  303. nautobot/ipam/tests/test_filters.py +26 -2
  304. nautobot/ipam/tests/test_models.py +9 -2
  305. nautobot/ipam/tests/test_querysets.py +1 -1
  306. nautobot/ipam/tests/test_views.py +3 -2
  307. nautobot/ipam/urls.py +2 -2
  308. nautobot/ipam/views.py +20 -34
  309. nautobot/project-static/css/base.css +21 -0
  310. nautobot/project-static/css/dark.css +11 -0
  311. nautobot/project-static/docs/404.html +894 -90
  312. nautobot/project-static/docs/apps/index.html +894 -90
  313. nautobot/project-static/docs/apps/nautobot-apps.html +894 -90
  314. nautobot/project-static/docs/assets/_mkdocstrings.css +5 -0
  315. nautobot/project-static/docs/assets/stylesheets/main.3cba04c6.min.css +1 -0
  316. nautobot/project-static/docs/assets/stylesheets/main.3cba04c6.min.css.map +1 -0
  317. nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +921 -122
  318. nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +906 -103
  319. nautobot/project-static/docs/code-reference/nautobot/apps/api.html +1620 -905
  320. nautobot/project-static/docs/code-reference/nautobot/apps/change_logging.html +937 -146
  321. nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +979 -190
  322. nautobot/project-static/docs/code-reference/nautobot/apps/config.html +903 -101
  323. nautobot/project-static/docs/code-reference/nautobot/apps/constants.html +899 -95
  324. nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +993 -195
  325. nautobot/project-static/docs/code-reference/nautobot/apps/exceptions.html +976 -133
  326. nautobot/project-static/docs/code-reference/nautobot/apps/factory.html +1080 -274
  327. nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +1244 -336
  328. nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +1729 -877
  329. nautobot/project-static/docs/code-reference/nautobot/apps/graphql.html +1166 -383
  330. nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +2090 -1376
  331. nautobot/project-static/docs/code-reference/nautobot/apps/models.html +2248 -1424
  332. nautobot/project-static/docs/code-reference/nautobot/apps/querysets.html +914 -113
  333. nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +965 -165
  334. nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +1012 -225
  335. nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +1915 -1279
  336. nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +1848 -1104
  337. nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +906 -103
  338. nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +2335 -1701
  339. nautobot/project-static/docs/code-reference/nautobot/apps/views.html +1804 -1026
  340. nautobot/project-static/docs/development/apps/api/configuration-view.html +894 -90
  341. nautobot/project-static/docs/development/apps/api/database-backend-config.html +894 -90
  342. nautobot/project-static/docs/development/apps/api/models/django-admin.html +894 -90
  343. nautobot/project-static/docs/development/apps/api/models/global-search.html +894 -90
  344. nautobot/project-static/docs/development/apps/api/models/graphql.html +894 -90
  345. nautobot/project-static/docs/development/apps/api/models/index.html +944 -92
  346. nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +894 -90
  347. nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +894 -90
  348. nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +894 -90
  349. nautobot/project-static/docs/development/apps/api/platform-features/git-repository-content.html +894 -90
  350. nautobot/project-static/docs/development/apps/api/platform-features/index.html +894 -90
  351. nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +894 -90
  352. nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +894 -90
  353. nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +894 -90
  354. nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +894 -90
  355. nautobot/project-static/docs/development/apps/api/platform-features/uniquely-identify-objects.html +894 -90
  356. nautobot/project-static/docs/development/apps/api/prometheus.html +894 -90
  357. nautobot/project-static/docs/development/apps/api/setup.html +894 -90
  358. nautobot/project-static/docs/development/apps/api/testing.html +894 -90
  359. nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +894 -90
  360. nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +894 -90
  361. nautobot/project-static/docs/development/apps/api/ui-extensions/index.html +894 -90
  362. nautobot/project-static/docs/development/apps/api/ui-extensions/navigation.html +894 -90
  363. nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +894 -90
  364. nautobot/project-static/docs/development/apps/api/views/base-template.html +894 -90
  365. nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +894 -90
  366. nautobot/project-static/docs/development/apps/api/views/django-generic-views.html +894 -90
  367. nautobot/project-static/docs/development/apps/api/views/help-documentation.html +894 -90
  368. nautobot/project-static/docs/development/apps/api/views/index.html +894 -90
  369. nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +894 -90
  370. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +894 -90
  371. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +894 -90
  372. nautobot/project-static/docs/development/apps/api/views/notes.html +894 -90
  373. nautobot/project-static/docs/development/apps/api/views/rest-api.html +894 -90
  374. nautobot/project-static/docs/development/apps/api/views/urls.html +894 -90
  375. nautobot/project-static/docs/development/apps/index.html +894 -90
  376. nautobot/project-static/docs/development/apps/migration/code-updates.html +894 -90
  377. nautobot/project-static/docs/development/apps/migration/dependency-updates.html +894 -90
  378. nautobot/project-static/docs/development/apps/migration/from-v1.html +894 -90
  379. nautobot/project-static/docs/development/apps/migration/model-updates/dcim.html +894 -90
  380. nautobot/project-static/docs/development/apps/migration/model-updates/extras.html +894 -90
  381. nautobot/project-static/docs/development/apps/migration/model-updates/global.html +894 -90
  382. nautobot/project-static/docs/development/apps/migration/model-updates/ipam.html +894 -90
  383. nautobot/project-static/docs/development/apps/porting-from-netbox.html +894 -90
  384. nautobot/project-static/docs/development/core/application-registry.html +894 -90
  385. nautobot/project-static/docs/development/core/best-practices.html +895 -90
  386. nautobot/project-static/docs/development/core/bootstrap-ui.html +894 -90
  387. nautobot/project-static/docs/development/core/caching.html +894 -90
  388. nautobot/project-static/docs/development/core/controllers.html +894 -90
  389. nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +894 -90
  390. nautobot/project-static/docs/development/core/generic-views.html +894 -90
  391. nautobot/project-static/docs/development/core/getting-started.html +894 -90
  392. nautobot/project-static/docs/development/core/homepage.html +894 -90
  393. nautobot/project-static/docs/development/core/index.html +905 -90
  394. nautobot/project-static/docs/development/core/model-checklist.html +903 -91
  395. nautobot/project-static/docs/development/core/model-features.html +894 -90
  396. nautobot/project-static/docs/development/core/natural-keys.html +894 -90
  397. nautobot/project-static/docs/development/core/navigation-menu.html +894 -90
  398. nautobot/project-static/docs/development/core/release-checklist.html +897 -93
  399. nautobot/project-static/docs/development/core/role-internals.html +894 -90
  400. nautobot/project-static/docs/development/core/settings.html +894 -90
  401. nautobot/project-static/docs/development/core/style-guide.html +895 -91
  402. nautobot/project-static/docs/development/core/templates.html +906 -91
  403. nautobot/project-static/docs/development/core/testing.html +894 -90
  404. nautobot/project-static/docs/development/core/user-preferences.html +894 -90
  405. nautobot/project-static/docs/development/index.html +894 -90
  406. nautobot/project-static/docs/development/jobs/index.html +1271 -453
  407. nautobot/project-static/docs/development/jobs/migration/from-v1.html +894 -90
  408. nautobot/project-static/docs/index.html +9032 -13
  409. nautobot/project-static/docs/media/models/cloud_aws_direct_connect_dark.png +0 -0
  410. nautobot/project-static/docs/media/models/cloud_aws_direct_connect_light.png +0 -0
  411. nautobot/project-static/docs/models/cloud/cloudaccount.html +15 -0
  412. nautobot/project-static/docs/models/cloud/cloudnetwork.html +15 -0
  413. nautobot/project-static/docs/models/cloud/cloudnetworkprefixassignment.html +15 -0
  414. nautobot/project-static/docs/models/cloud/cloudresourcetype.html +15 -0
  415. nautobot/project-static/docs/models/cloud/cloudservice.html +15 -0
  416. nautobot/project-static/docs/models/cloud/cloudservicenetworkassignment.html +15 -0
  417. nautobot/project-static/docs/models/dcim/module.html +15 -0
  418. nautobot/project-static/docs/models/dcim/modulebay.html +15 -0
  419. nautobot/project-static/docs/models/dcim/modulebaytemplate.html +15 -0
  420. nautobot/project-static/docs/models/dcim/moduletype.html +15 -0
  421. nautobot/project-static/docs/models/extras/metadatachoice.html +15 -0
  422. nautobot/project-static/docs/models/extras/metadatatype.html +15 -0
  423. nautobot/project-static/docs/models/extras/objectmetadata.html +15 -0
  424. nautobot/project-static/docs/models/extras/role.html +15 -0
  425. nautobot/project-static/docs/models/extras/savedview.html +15 -0
  426. nautobot/project-static/docs/models/extras/staticgroupassociation.html +15 -0
  427. nautobot/project-static/docs/models/extras/status.html +15 -0
  428. nautobot/project-static/docs/objects.inv +0 -0
  429. nautobot/project-static/docs/overview/application_stack.html +902 -91
  430. nautobot/project-static/docs/overview/design_philosophy.html +896 -92
  431. nautobot/project-static/docs/overview/index.html +13 -8228
  432. nautobot/project-static/docs/release-notes/index.html +1131 -94
  433. nautobot/project-static/docs/release-notes/version-1.0.html +894 -90
  434. nautobot/project-static/docs/release-notes/version-1.1.html +894 -90
  435. nautobot/project-static/docs/release-notes/version-1.2.html +894 -90
  436. nautobot/project-static/docs/release-notes/version-1.3.html +894 -90
  437. nautobot/project-static/docs/release-notes/version-1.4.html +894 -90
  438. nautobot/project-static/docs/release-notes/version-1.5.html +895 -91
  439. nautobot/project-static/docs/release-notes/version-1.6.html +895 -91
  440. nautobot/project-static/docs/release-notes/version-2.0.html +894 -90
  441. nautobot/project-static/docs/release-notes/version-2.1.html +894 -90
  442. nautobot/project-static/docs/release-notes/version-2.2.html +1137 -196
  443. nautobot/project-static/docs/release-notes/version-2.3.html +9954 -0
  444. nautobot/project-static/docs/requirements.txt +5 -5
  445. nautobot/project-static/docs/search/search_index.json +1 -1
  446. nautobot/project-static/docs/sitemap.xml +335 -260
  447. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  448. nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +894 -90
  449. nautobot/project-static/docs/user-guide/administration/configuration/authentication/remote.html +894 -90
  450. nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +894 -90
  451. nautobot/project-static/docs/user-guide/administration/configuration/index.html +894 -90
  452. nautobot/project-static/docs/user-guide/administration/configuration/optional-settings.html +1025 -175
  453. nautobot/project-static/docs/user-guide/administration/configuration/required-settings.html +894 -90
  454. nautobot/project-static/docs/user-guide/administration/configuration/time-zones.html +894 -90
  455. nautobot/project-static/docs/user-guide/administration/guides/caching.html +894 -90
  456. nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +902 -90
  457. nautobot/project-static/docs/user-guide/administration/guides/healthcheck.html +894 -90
  458. nautobot/project-static/docs/user-guide/administration/guides/permissions.html +894 -90
  459. nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +894 -90
  460. nautobot/project-static/docs/user-guide/administration/guides/replicating-nautobot.html +894 -90
  461. nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +894 -90
  462. nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +894 -90
  463. nautobot/project-static/docs/user-guide/administration/installation/app-install.html +894 -90
  464. nautobot/project-static/docs/user-guide/administration/installation/external-authentication.html +894 -90
  465. nautobot/project-static/docs/user-guide/administration/installation/http-server.html +946 -155
  466. nautobot/project-static/docs/user-guide/administration/installation/index.html +903 -95
  467. nautobot/project-static/docs/user-guide/administration/installation/install_system.html +936 -124
  468. nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +956 -159
  469. nautobot/project-static/docs/user-guide/administration/installation/services.html +915 -114
  470. nautobot/project-static/docs/user-guide/administration/installation-extras/docker.html +910 -101
  471. nautobot/project-static/docs/user-guide/administration/installation-extras/health-checks.html +894 -90
  472. nautobot/project-static/docs/user-guide/administration/installation-extras/selinux-troubleshooting.html +894 -90
  473. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-netbox.html +894 -90
  474. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +894 -90
  475. nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +977 -121
  476. nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +894 -90
  477. nautobot/project-static/docs/user-guide/administration/upgrading/database-backup.html +894 -90
  478. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/after-you-upgrade.html +894 -90
  479. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/before-you-upgrade.html +894 -90
  480. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/for-developers.html +894 -90
  481. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/index.html +894 -90
  482. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/whats-changed.html +894 -90
  483. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/region-and-site-data-migration-guide.html +894 -90
  484. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +894 -90
  485. nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +894 -90
  486. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuit.html +894 -90
  487. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittermination.html +895 -91
  488. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittype.html +894 -90
  489. nautobot/project-static/docs/user-guide/core-data-model/circuits/provider.html +898 -90
  490. nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +897 -93
  491. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloud.html +8984 -0
  492. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudaccount.html +8828 -0
  493. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetwork.html +8829 -0
  494. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetworkprefixassignment.html +8828 -0
  495. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudresourcetype.html +8829 -0
  496. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservice.html +8833 -0
  497. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservicenetworkassignment.html +8828 -0
  498. nautobot/project-static/docs/user-guide/core-data-model/dcim/cable.html +908 -104
  499. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +925 -107
  500. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +925 -107
  501. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +920 -102
  502. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +925 -107
  503. nautobot/project-static/docs/user-guide/core-data-model/dcim/controller.html +908 -104
  504. nautobot/project-static/docs/user-guide/core-data-model/dcim/controllermanageddevicegroup.html +908 -104
  505. nautobot/project-static/docs/user-guide/core-data-model/dcim/device.html +915 -107
  506. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +922 -118
  507. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +923 -119
  508. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicefamily.html +920 -116
  509. nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +908 -104
  510. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +916 -107
  511. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +928 -110
  512. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +938 -120
  513. nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +930 -108
  514. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +908 -104
  515. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +939 -121
  516. nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +930 -112
  517. nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +920 -116
  518. nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +923 -119
  519. nautobot/project-static/docs/user-guide/core-data-model/dcim/manufacturer.html +925 -117
  520. nautobot/project-static/docs/user-guide/core-data-model/dcim/module.html +8828 -0
  521. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebay.html +8846 -0
  522. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebaytemplate.html +8843 -0
  523. nautobot/project-static/docs/user-guide/core-data-model/dcim/moduletype.html +8823 -0
  524. nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +918 -114
  525. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerfeed.html +908 -104
  526. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +942 -85
  527. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +926 -108
  528. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerpanel.html +908 -104
  529. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +945 -88
  530. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +923 -105
  531. nautobot/project-static/docs/user-guide/core-data-model/dcim/rack.html +931 -127
  532. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackgroup.html +920 -116
  533. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackreservation.html +908 -104
  534. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +924 -106
  535. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +926 -108
  536. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +908 -104
  537. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareversion.html +908 -104
  538. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualchassis.html +908 -104
  539. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +938 -90
  540. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +894 -90
  541. nautobot/project-static/docs/user-guide/core-data-model/extras/contact.html +899 -91
  542. nautobot/project-static/docs/user-guide/core-data-model/extras/team.html +899 -91
  543. nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +894 -90
  544. nautobot/project-static/docs/user-guide/core-data-model/ipam/namespace.html +894 -90
  545. nautobot/project-static/docs/user-guide/core-data-model/ipam/prefix.html +894 -90
  546. nautobot/project-static/docs/user-guide/core-data-model/ipam/rir.html +894 -90
  547. nautobot/project-static/docs/user-guide/core-data-model/ipam/routetarget.html +894 -90
  548. nautobot/project-static/docs/user-guide/core-data-model/ipam/service.html +894 -90
  549. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +894 -90
  550. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlangroup.html +894 -90
  551. nautobot/project-static/docs/user-guide/core-data-model/ipam/vrf.html +894 -90
  552. nautobot/project-static/docs/user-guide/core-data-model/overview/introduction.html +894 -90
  553. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenant.html +903 -98
  554. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenantgroup.html +894 -90
  555. nautobot/project-static/docs/user-guide/core-data-model/virtualization/cluster.html +894 -90
  556. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustergroup.html +894 -90
  557. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustertype.html +894 -90
  558. nautobot/project-static/docs/user-guide/core-data-model/virtualization/virtualmachine.html +894 -90
  559. nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +899 -91
  560. nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +894 -90
  561. nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +894 -90
  562. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +894 -90
  563. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +894 -90
  564. nautobot/project-static/docs/user-guide/feature-guides/getting-started/index.html +894 -90
  565. nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +894 -90
  566. nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +894 -90
  567. nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +894 -90
  568. nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +894 -90
  569. nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +894 -90
  570. nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +894 -90
  571. nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +894 -90
  572. nautobot/project-static/docs/user-guide/feature-guides/graphql.html +894 -90
  573. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/clear-view-button.png +0 -0
  574. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/cleared-view.png +0 -0
  575. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/config-table-columns-to-locations.png +0 -0
  576. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/configure-button.png +0 -0
  577. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/create-saved-view-success.png +0 -0
  578. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/current-saved-view-drop-down-menu.png +0 -0
  579. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/default-location-list-view.png +0 -0
  580. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/dropdown-button-after-new-saved-view.png +0 -0
  581. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/filter-application-to-locations.png +0 -0
  582. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/filter-button.png +0 -0
  583. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/global-default-location-list-view.png +0 -0
  584. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/location-list-view-with-saved-views.png +0 -0
  585. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/navigation-menu.png +0 -0
  586. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/save-as-new-view-drop-down.png +0 -0
  587. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/save-view-modal.png +0 -0
  588. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/saved-view-admin-edit-buttons.png +0 -0
  589. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/saved-view-admin-edit-success.png +0 -0
  590. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/saved-view-admin-edit-view-unchecked.png +0 -0
  591. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/saved-view-admin-edit-view.png +0 -0
  592. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/saved-view-different-user.png +0 -0
  593. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/saved-view-modal-unchecked.png +0 -0
  594. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/set-as-my-default-button.png +0 -0
  595. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/set-as-my-default-success.png +0 -0
  596. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/unsaved-saved-view.png +0 -0
  597. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/updated-saved-view.png +0 -0
  598. nautobot/project-static/docs/user-guide/feature-guides/ip-address-merge-tool.html +894 -90
  599. nautobot/project-static/docs/user-guide/feature-guides/relationships.html +894 -90
  600. nautobot/project-static/docs/user-guide/feature-guides/software-image-files-and-versions.html +894 -90
  601. nautobot/project-static/docs/user-guide/index.html +894 -90
  602. nautobot/project-static/docs/user-guide/platform-functionality/change-logging.html +894 -90
  603. nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +894 -90
  604. nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +894 -90
  605. nautobot/project-static/docs/user-guide/platform-functionality/customlink.html +894 -90
  606. nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +1260 -787
  607. nautobot/project-static/docs/user-guide/platform-functionality/exporttemplate.html +897 -93
  608. nautobot/project-static/docs/user-guide/platform-functionality/externalintegration.html +894 -90
  609. nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +894 -90
  610. nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +894 -90
  611. nautobot/project-static/docs/user-guide/platform-functionality/graphqlquery.html +894 -90
  612. nautobot/project-static/docs/user-guide/platform-functionality/imageattachment.html +894 -90
  613. nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +894 -90
  614. nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +894 -90
  615. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +894 -90
  616. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +894 -90
  617. nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +898 -90
  618. nautobot/project-static/docs/user-guide/platform-functionality/napalm.html +894 -90
  619. nautobot/project-static/docs/user-guide/platform-functionality/note.html +897 -93
  620. nautobot/project-static/docs/user-guide/platform-functionality/objectmetadata.html +9061 -0
  621. nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +897 -93
  622. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +894 -90
  623. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +894 -90
  624. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +894 -90
  625. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/ui-related-endpoints.html +894 -90
  626. nautobot/project-static/docs/user-guide/platform-functionality/role.html +897 -93
  627. nautobot/project-static/docs/user-guide/platform-functionality/savedview.html +9137 -0
  628. nautobot/project-static/docs/user-guide/platform-functionality/secret.html +897 -93
  629. nautobot/project-static/docs/user-guide/platform-functionality/staticgroupassociation.html +8933 -0
  630. nautobot/project-static/docs/user-guide/platform-functionality/status.html +894 -90
  631. nautobot/project-static/docs/user-guide/platform-functionality/tag.html +894 -90
  632. nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +952 -123
  633. nautobot/project-static/docs/user-guide/platform-functionality/users/objectpermission.html +894 -90
  634. nautobot/project-static/docs/user-guide/platform-functionality/users/token.html +894 -90
  635. nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +894 -90
  636. nautobot/project-static/js/forms.js +71 -0
  637. nautobot/project-static/js/table_sorting_indicator.js +46 -0
  638. nautobot/project-static/js/tableconfig.js +6 -1
  639. nautobot/project-static/materialdesignicons-7.4.47/css/materialdesignicons.min.css +3 -0
  640. nautobot/project-static/{materialdesignicons-6.5.95 → materialdesignicons-7.4.47}/fonts/materialdesignicons-webfont.eot +0 -0
  641. nautobot/project-static/{materialdesignicons-6.5.95 → materialdesignicons-7.4.47}/fonts/materialdesignicons-webfont.ttf +0 -0
  642. nautobot/project-static/materialdesignicons-7.4.47/fonts/materialdesignicons-webfont.woff +0 -0
  643. nautobot/project-static/materialdesignicons-7.4.47/fonts/materialdesignicons-webfont.woff2 +0 -0
  644. nautobot/tenancy/__init__.py +0 -1
  645. nautobot/tenancy/apps.py +1 -0
  646. nautobot/tenancy/factory.py +3 -2
  647. nautobot/tenancy/filters/__init__.py +1 -0
  648. nautobot/tenancy/forms.py +1 -1
  649. nautobot/tenancy/templates/tenancy/tenant.html +24 -20
  650. nautobot/tenancy/views.py +11 -10
  651. nautobot/users/__init__.py +0 -1
  652. nautobot/users/api/serializers.py +1 -1
  653. nautobot/users/api/views.py +4 -2
  654. nautobot/users/apps.py +3 -2
  655. nautobot/users/factory.py +3 -3
  656. nautobot/users/migrations/0010_user_default_saved_views.py +20 -0
  657. nautobot/users/models.py +12 -0
  658. nautobot/users/tests/test_filters.py +6 -3
  659. nautobot/users/urls.py +8 -0
  660. nautobot/virtualization/__init__.py +0 -1
  661. nautobot/virtualization/apps.py +1 -0
  662. nautobot/virtualization/filters.py +6 -1
  663. nautobot/virtualization/forms.py +11 -3
  664. nautobot/virtualization/graphql/types.py +2 -2
  665. nautobot/virtualization/migrations/0029_add_role_field_to_interface_models.py +27 -0
  666. nautobot/virtualization/migrations/0030_alter_virtualmachine_local_config_context_data_owner_content_type_and_more.py +67 -0
  667. nautobot/virtualization/models.py +0 -2
  668. nautobot/virtualization/tables.py +12 -8
  669. nautobot/virtualization/templates/virtualization/virtualmachine.html +1 -1
  670. nautobot/virtualization/templates/virtualization/vminterface.html +7 -1
  671. nautobot/virtualization/templates/virtualization/vminterface_edit.html +1 -0
  672. nautobot/virtualization/tests/test_api.py +9 -4
  673. nautobot/virtualization/tests/test_filters.py +22 -0
  674. nautobot/virtualization/tests/test_models.py +7 -3
  675. nautobot/virtualization/tests/test_views.py +19 -3
  676. nautobot/virtualization/urls.py +2 -2
  677. nautobot/virtualization/views.py +10 -32
  678. {nautobot-2.2.8.dist-info → nautobot-2.3.0.dist-info}/METADATA +21 -19
  679. {nautobot-2.2.8.dist-info → nautobot-2.3.0.dist-info}/RECORD +684 -564
  680. nautobot/project-static/docs/assets/stylesheets/main.76a95c52.min.css +0 -1
  681. nautobot/project-static/docs/assets/stylesheets/main.76a95c52.min.css.map +0 -1
  682. nautobot/project-static/materialdesignicons-6.5.95/.github/ISSUE_TEMPLATE.md +0 -3
  683. nautobot/project-static/materialdesignicons-6.5.95/README.md +0 -25
  684. nautobot/project-static/materialdesignicons-6.5.95/css/materialdesignicons.css +0 -26654
  685. nautobot/project-static/materialdesignicons-6.5.95/css/materialdesignicons.css.map +0 -16
  686. nautobot/project-static/materialdesignicons-6.5.95/css/materialdesignicons.min.css +0 -3
  687. nautobot/project-static/materialdesignicons-6.5.95/css/materialdesignicons.min.css.map +0 -16
  688. nautobot/project-static/materialdesignicons-6.5.95/fonts/materialdesignicons-webfont.woff +0 -0
  689. nautobot/project-static/materialdesignicons-6.5.95/fonts/materialdesignicons-webfont.woff2 +0 -0
  690. nautobot/project-static/materialdesignicons-6.5.95/package.json +0 -28
  691. nautobot/project-static/materialdesignicons-6.5.95/preview.html +0 -717
  692. nautobot/project-static/materialdesignicons-6.5.95/scss/_animated.scss +0 -27
  693. nautobot/project-static/materialdesignicons-6.5.95/scss/_core.scss +0 -10
  694. nautobot/project-static/materialdesignicons-6.5.95/scss/_extras.scss +0 -65
  695. nautobot/project-static/materialdesignicons-6.5.95/scss/_functions.scss +0 -20
  696. nautobot/project-static/materialdesignicons-6.5.95/scss/_icons.scss +0 -10
  697. nautobot/project-static/materialdesignicons-6.5.95/scss/_path.scss +0 -10
  698. nautobot/project-static/materialdesignicons-6.5.95/scss/_variables.scss +0 -6606
  699. nautobot/project-static/materialdesignicons-6.5.95/scss/materialdesignicons.scss +0 -8
  700. /nautobot/project-static/{materialdesignicons-6.5.95 → materialdesignicons-7.4.47}/LICENSE +0 -0
  701. {nautobot-2.2.8.dist-info → nautobot-2.3.0.dist-info}/LICENSE.txt +0 -0
  702. {nautobot-2.2.8.dist-info → nautobot-2.3.0.dist-info}/NOTICE +0 -0
  703. {nautobot-2.2.8.dist-info → nautobot-2.3.0.dist-info}/WHEEL +0 -0
  704. {nautobot-2.2.8.dist-info → nautobot-2.3.0.dist-info}/entry_points.txt +0 -0
@@ -3,6 +3,7 @@ from decimal import Decimal
3
3
  from constance.test import override_config
4
4
  from django.contrib.contenttypes.models import ContentType
5
5
  from django.core.exceptions import ValidationError
6
+ from django.db import IntegrityError
6
7
  from django.test import TestCase
7
8
  from django.test.utils import override_settings
8
9
 
@@ -11,11 +12,14 @@ from nautobot.core.testing.models import ModelTestCases
11
12
  from nautobot.dcim.choices import (
12
13
  CableStatusChoices,
13
14
  CableTypeChoices,
15
+ ConsolePortTypeChoices,
14
16
  DeviceFaceChoices,
15
17
  InterfaceModeChoices,
16
18
  InterfaceTypeChoices,
17
19
  PortTypeChoices,
18
20
  PowerOutletFeedLegChoices,
21
+ PowerOutletTypeChoices,
22
+ PowerPortTypeChoices,
19
23
  SubdeviceRoleChoices,
20
24
  )
21
25
  from nautobot.dcim.models import (
@@ -41,6 +45,10 @@ from nautobot.dcim.models import (
41
45
  Location,
42
46
  LocationType,
43
47
  Manufacturer,
48
+ Module,
49
+ ModuleBay,
50
+ ModuleBayTemplate,
51
+ ModuleType,
44
52
  Platform,
45
53
  PowerOutlet,
46
54
  PowerOutletTemplate,
@@ -64,6 +72,500 @@ from nautobot.users.models import User
64
72
  from nautobot.virtualization.models import Cluster, ClusterType, VirtualMachine
65
73
 
66
74
 
75
+ class ModularDeviceComponentTestCaseMixin:
76
+ """Generic test for modular device components. Also used for testing modular component templates."""
77
+
78
+ # fields required to create instances of the model, with the exception of name, device_field and module_field
79
+ modular_component_create_data = {}
80
+ model = None
81
+ device_field = "device" # field name for the parent device
82
+ module_field = "module" # field name for the parent module
83
+
84
+ @classmethod
85
+ def setUpTestData(cls):
86
+ cls.device = Device.objects.first()
87
+ cls.module = Module.objects.first()
88
+
89
+ def test_parent_validation_device_and_module(self):
90
+ """Assert that a modular component must have a parent device or parent module but not both."""
91
+ instance = self.model(
92
+ name=f"test {self.model._meta.model_name} 1",
93
+ **{self.device_field: self.device, self.module_field: self.module},
94
+ **self.modular_component_create_data,
95
+ )
96
+
97
+ with self.assertRaises(ValidationError):
98
+ instance.full_clean()
99
+
100
+ def test_parent_validation_no_device_or_module(self):
101
+ """Assert that a modular component must have a parent device or parent module but not both."""
102
+ instance = self.model(
103
+ name=f"test {self.model._meta.model_name} 1",
104
+ **self.modular_component_create_data,
105
+ )
106
+
107
+ with self.assertRaises(ValidationError):
108
+ instance.full_clean()
109
+
110
+ def test_parent_validation_succeeds(self):
111
+ """Assert that a modular component must have a parent device or parent module but not both."""
112
+ with self.subTest(f"{self.model._meta.model_name} with a parent device"):
113
+ instance = self.model(
114
+ name=f"test {self.model._meta.model_name} 1",
115
+ **{self.device_field: self.device},
116
+ **self.modular_component_create_data,
117
+ )
118
+
119
+ instance.full_clean()
120
+ instance.save()
121
+
122
+ with self.subTest(f"{self.model._meta.model_name} with a parent module"):
123
+ instance = self.model(
124
+ name=f"test {self.model._meta.model_name} 1",
125
+ **{self.module_field: self.module},
126
+ **self.modular_component_create_data,
127
+ )
128
+
129
+ instance.full_clean()
130
+ instance.save()
131
+
132
+ def test_uniqueness_device(self):
133
+ """Assert that the combination of device and name is unique."""
134
+ instance = self.model(
135
+ name=f"test {self.model._meta.model_name} 1",
136
+ **{self.device_field: self.device},
137
+ **self.modular_component_create_data,
138
+ )
139
+
140
+ instance.full_clean()
141
+ instance.save()
142
+
143
+ # same device, different name works
144
+ instance = self.model(
145
+ name=f"test {self.model._meta.model_name} 2",
146
+ **{self.device_field: self.device},
147
+ **self.modular_component_create_data,
148
+ )
149
+
150
+ instance.full_clean()
151
+ instance.save()
152
+
153
+ instance = self.model(
154
+ name=f"test {self.model._meta.model_name} 1",
155
+ **{self.device_field: self.device},
156
+ **self.modular_component_create_data,
157
+ )
158
+
159
+ with self.assertRaises(ValidationError):
160
+ instance.full_clean()
161
+
162
+ with self.assertRaises(IntegrityError):
163
+ instance.save()
164
+
165
+ def test_uniqueness_module(self):
166
+ """Assert that the combination of module and name is unique."""
167
+ instance = self.model(
168
+ name=f"test {self.model._meta.model_name} 1",
169
+ **{self.module_field: self.module},
170
+ **self.modular_component_create_data,
171
+ )
172
+
173
+ instance.full_clean()
174
+ instance.save()
175
+
176
+ # same module, different name works
177
+ instance = self.model(
178
+ name=f"test {self.model._meta.model_name} 2",
179
+ **{self.module_field: self.module},
180
+ **self.modular_component_create_data,
181
+ )
182
+
183
+ instance.full_clean()
184
+ instance.save()
185
+
186
+ instance = self.model(
187
+ name=f"test {self.model._meta.model_name} 1",
188
+ **{self.module_field: self.module},
189
+ **self.modular_component_create_data,
190
+ )
191
+
192
+ with self.assertRaises(ValidationError):
193
+ instance.full_clean()
194
+
195
+ with self.assertRaises(IntegrityError):
196
+ instance.save()
197
+
198
+
199
+ class ConsolePortTestCase(ModularDeviceComponentTestCaseMixin, ModelTestCases.BaseModelTestCase):
200
+ model = ConsolePort
201
+ modular_component_create_data = {"type": ConsolePortTypeChoices.TYPE_RJ45}
202
+
203
+
204
+ class ConsoleServerPortTestCase(ModularDeviceComponentTestCaseMixin, ModelTestCases.BaseModelTestCase):
205
+ model = ConsoleServerPort
206
+ modular_component_create_data = {"type": ConsolePortTypeChoices.TYPE_RJ45}
207
+
208
+
209
+ class PowerPortTestCase(ModularDeviceComponentTestCaseMixin, ModelTestCases.BaseModelTestCase):
210
+ model = PowerPort
211
+ modular_component_create_data = {"type": PowerPortTypeChoices.TYPE_NEMA_1030P}
212
+
213
+
214
+ class PowerOutletTestCase(ModularDeviceComponentTestCaseMixin, ModelTestCases.BaseModelTestCase):
215
+ model = PowerOutlet
216
+ modular_component_create_data = {"type": PowerOutletTypeChoices.TYPE_IEC_C13}
217
+
218
+
219
+ class RearPortTestCase(ModularDeviceComponentTestCaseMixin, ModelTestCases.BaseModelTestCase):
220
+ model = RearPort
221
+ modular_component_create_data = {"type": PortTypeChoices.TYPE_8P8C}
222
+
223
+
224
+ class FrontPortTestCase(ModelTestCases.BaseModelTestCase):
225
+ model = FrontPort
226
+
227
+ @classmethod
228
+ def setUpTestData(cls):
229
+ cls.module = Module.objects.filter(rear_ports__isnull=False).first()
230
+ cls.module_rear_port = cls.module.rear_ports.first()
231
+ module_used_positions = set(cls.module_rear_port.front_ports.values_list("rear_port_position", flat=True))
232
+ cls.module_available_positions = set(range(1, cls.module_rear_port.positions + 1)).difference(
233
+ module_used_positions
234
+ )
235
+
236
+ cls.device = Device.objects.filter(rear_ports__isnull=False).first()
237
+ cls.device_rear_port = cls.device.rear_ports.first()
238
+ device_used_positions = set(cls.device_rear_port.front_ports.values_list("rear_port_position", flat=True))
239
+ cls.device_available_positions = set(range(1, cls.device_rear_port.positions + 1)).difference(
240
+ device_used_positions
241
+ )
242
+
243
+ def test_parent_validation_device_and_module(self):
244
+ """Assert that a modular component must have a parent device or parent module but not both."""
245
+ instance = self.model(
246
+ device=self.device,
247
+ module=self.module,
248
+ name=f"test {self.model._meta.model_name} 1",
249
+ type=PortTypeChoices.TYPE_8P8C,
250
+ rear_port=self.module_rear_port,
251
+ rear_port_position=self.module_available_positions.copy().pop(),
252
+ )
253
+
254
+ with self.assertRaises(ValidationError):
255
+ instance.full_clean()
256
+
257
+ def test_parent_validation_no_device_or_module(self):
258
+ """Assert that a modular component must have a parent device or parent module but not both."""
259
+ instance = self.model(
260
+ name=f"test {self.model._meta.model_name} 1",
261
+ type=PortTypeChoices.TYPE_8P8C,
262
+ rear_port=self.module_rear_port,
263
+ rear_port_position=self.module_available_positions.copy().pop(),
264
+ )
265
+
266
+ with self.assertRaises(ValidationError):
267
+ instance.full_clean()
268
+
269
+ def test_parent_validation_succeeds(self):
270
+ """Assert that a modular component must have a parent device or parent module but not both."""
271
+ with self.subTest(f"{self.model._meta.model_name} with a parent device"):
272
+ instance = self.model(
273
+ device=self.device,
274
+ name=f"test {self.model._meta.model_name} 1",
275
+ type=PortTypeChoices.TYPE_8P8C,
276
+ rear_port=self.device_rear_port,
277
+ rear_port_position=self.device_available_positions.copy().pop(),
278
+ )
279
+
280
+ instance.full_clean()
281
+ instance.save()
282
+
283
+ with self.subTest(f"{self.model._meta.model_name} with a parent module"):
284
+ instance = self.model(
285
+ module=self.module,
286
+ name=f"test {self.model._meta.model_name} 1",
287
+ type=PortTypeChoices.TYPE_8P8C,
288
+ rear_port=self.module_rear_port,
289
+ rear_port_position=self.module_available_positions.copy().pop(),
290
+ )
291
+
292
+ instance.full_clean()
293
+ instance.save()
294
+
295
+ def test_uniqueness_device(self):
296
+ """Assert that the combination of device and name is unique."""
297
+ device_available_positions = self.device_available_positions.copy()
298
+ instance = self.model(
299
+ device=self.device,
300
+ name=f"test {self.model._meta.model_name} 1",
301
+ type=PortTypeChoices.TYPE_8P8C,
302
+ rear_port=self.device_rear_port,
303
+ rear_port_position=device_available_positions.pop(),
304
+ )
305
+
306
+ instance.full_clean()
307
+ instance.save()
308
+
309
+ # same device, different name works
310
+ instance = self.model(
311
+ device=self.device,
312
+ name=f"test {self.model._meta.model_name} 2",
313
+ type=PortTypeChoices.TYPE_8P8C,
314
+ rear_port=self.device_rear_port,
315
+ rear_port_position=device_available_positions.pop(),
316
+ )
317
+
318
+ instance.full_clean()
319
+ instance.save()
320
+
321
+ instance = self.model(
322
+ device=self.device,
323
+ name=f"test {self.model._meta.model_name} 1",
324
+ type=PortTypeChoices.TYPE_8P8C,
325
+ rear_port=self.device_rear_port,
326
+ rear_port_position=device_available_positions.pop(),
327
+ )
328
+
329
+ with self.assertRaises(ValidationError):
330
+ instance.full_clean()
331
+
332
+ with self.assertRaises(IntegrityError):
333
+ instance.save()
334
+
335
+ def test_uniqueness_module(self):
336
+ """Assert that the combination of module and name is unique."""
337
+ module_available_positions = self.module_available_positions.copy()
338
+ instance = self.model(
339
+ module=self.module,
340
+ name=f"test {self.model._meta.model_name} 1",
341
+ type=PortTypeChoices.TYPE_8P8C,
342
+ rear_port=self.module_rear_port,
343
+ rear_port_position=module_available_positions.pop(),
344
+ )
345
+
346
+ instance.full_clean()
347
+ instance.save()
348
+
349
+ # same module, different name works
350
+ instance = self.model(
351
+ module=self.module,
352
+ name=f"test {self.model._meta.model_name} 2",
353
+ type=PortTypeChoices.TYPE_8P8C,
354
+ rear_port=self.module_rear_port,
355
+ rear_port_position=module_available_positions.pop(),
356
+ )
357
+
358
+ instance.full_clean()
359
+ instance.save()
360
+
361
+ instance = self.model(
362
+ module=self.module,
363
+ name=f"test {self.model._meta.model_name} 1",
364
+ type=PortTypeChoices.TYPE_8P8C,
365
+ rear_port=self.module_rear_port,
366
+ rear_port_position=module_available_positions.pop(),
367
+ )
368
+
369
+ with self.assertRaises(ValidationError):
370
+ instance.full_clean()
371
+
372
+ with self.assertRaises(IntegrityError):
373
+ instance.save()
374
+
375
+
376
+ class ModularDeviceComponentTemplateTestCaseMixin(ModularDeviceComponentTestCaseMixin):
377
+ """Generic test for modular device component templates."""
378
+
379
+ device_field = "device_type" # field name for the parent device_type
380
+ module_field = "module_type" # field name for the parent module_type
381
+
382
+ @classmethod
383
+ def setUpTestData(cls):
384
+ cls.device = DeviceType.objects.first()
385
+ cls.module = ModuleType.objects.first()
386
+
387
+
388
+ class ConsolePortTemplateTestCase(ModularDeviceComponentTemplateTestCaseMixin, ModelTestCases.BaseModelTestCase):
389
+ model = ConsolePortTemplate
390
+ modular_component_create_data = {"type": ConsolePortTypeChoices.TYPE_RJ45}
391
+
392
+
393
+ class ConsoleServerPortTemplateTestCase(ModularDeviceComponentTemplateTestCaseMixin, ModelTestCases.BaseModelTestCase):
394
+ model = ConsoleServerPortTemplate
395
+ modular_component_create_data = {"type": ConsolePortTypeChoices.TYPE_RJ45}
396
+
397
+
398
+ class PowerPortTemplateTestCase(ModularDeviceComponentTemplateTestCaseMixin, ModelTestCases.BaseModelTestCase):
399
+ model = PowerPortTemplate
400
+ modular_component_create_data = {"type": PowerPortTypeChoices.TYPE_NEMA_1030P}
401
+
402
+
403
+ class PowerOutletTemplateTestCase(ModularDeviceComponentTemplateTestCaseMixin, ModelTestCases.BaseModelTestCase):
404
+ model = PowerOutletTemplate
405
+ modular_component_create_data = {"type": PowerOutletTypeChoices.TYPE_IEC_C13}
406
+
407
+
408
+ class RearPortTemplateTestCase(ModularDeviceComponentTemplateTestCaseMixin, ModelTestCases.BaseModelTestCase):
409
+ model = RearPortTemplate
410
+ modular_component_create_data = {"type": PortTypeChoices.TYPE_8P8C}
411
+
412
+
413
+ class FrontPortTemplateTestCase(ModelTestCases.BaseModelTestCase):
414
+ model = FrontPortTemplate
415
+
416
+ @classmethod
417
+ def setUpTestData(cls):
418
+ cls.module_type = ModuleType.objects.filter(rear_port_templates__isnull=False).first()
419
+ cls.module_rear_port = cls.module_type.rear_port_templates.first()
420
+ module_used_positions = set(
421
+ cls.module_rear_port.front_port_templates.values_list("rear_port_position", flat=True)
422
+ )
423
+ cls.module_available_positions = set(range(1, cls.module_rear_port.positions + 1)).difference(
424
+ module_used_positions
425
+ )
426
+
427
+ cls.device_type = DeviceType.objects.filter(rear_port_templates__isnull=False).first()
428
+ cls.device_rear_port = cls.device_type.rear_port_templates.first()
429
+ device_used_positions = set(
430
+ cls.device_rear_port.front_port_templates.values_list("rear_port_position", flat=True)
431
+ )
432
+ cls.device_available_positions = set(range(1, cls.device_rear_port.positions + 1)).difference(
433
+ device_used_positions
434
+ )
435
+
436
+ def test_parent_validation_device_and_module(self):
437
+ """Assert that a modular component must have a parent device or parent module but not both."""
438
+ instance = self.model(
439
+ device_type=self.device_type,
440
+ module_type=self.module_type,
441
+ name=f"test {self.model._meta.model_name} 1",
442
+ type=PortTypeChoices.TYPE_8P8C,
443
+ rear_port_template=self.module_rear_port,
444
+ rear_port_position=self.module_available_positions.copy().pop(),
445
+ )
446
+
447
+ with self.assertRaises(ValidationError):
448
+ instance.full_clean()
449
+
450
+ def test_parent_validation_no_device_or_module(self):
451
+ """Assert that a modular component must have a parent device or parent module but not both."""
452
+ instance = self.model(
453
+ name=f"test {self.model._meta.model_name} 1",
454
+ type=PortTypeChoices.TYPE_8P8C,
455
+ rear_port_template=self.module_rear_port,
456
+ rear_port_position=self.module_available_positions.copy().pop(),
457
+ )
458
+
459
+ with self.assertRaises(ValidationError):
460
+ instance.full_clean()
461
+
462
+ def test_parent_validation_succeeds(self):
463
+ """Assert that a modular component must have a parent device or parent module but not both."""
464
+ with self.subTest(f"{self.model._meta.model_name} with a parent device"):
465
+ instance = self.model(
466
+ device_type=self.device_type,
467
+ name=f"test {self.model._meta.model_name} 1",
468
+ type=PortTypeChoices.TYPE_8P8C,
469
+ rear_port_template=self.device_rear_port,
470
+ rear_port_position=self.device_available_positions.copy().pop(),
471
+ )
472
+
473
+ instance.full_clean()
474
+ instance.save()
475
+
476
+ with self.subTest(f"{self.model._meta.model_name} with a parent module"):
477
+ instance = self.model(
478
+ module_type=self.module_type,
479
+ name=f"test {self.model._meta.model_name} 1",
480
+ type=PortTypeChoices.TYPE_8P8C,
481
+ rear_port_template=self.module_rear_port,
482
+ rear_port_position=self.module_available_positions.copy().pop(),
483
+ )
484
+
485
+ instance.full_clean()
486
+ instance.save()
487
+
488
+ def test_uniqueness_device(self):
489
+ """Assert that the combination of device and name is unique."""
490
+ device_available_positions = self.device_available_positions.copy()
491
+ instance = self.model(
492
+ device_type=self.device_type,
493
+ name=f"test {self.model._meta.model_name} 1",
494
+ type=PortTypeChoices.TYPE_8P8C,
495
+ rear_port_template=self.device_rear_port,
496
+ rear_port_position=device_available_positions.pop(),
497
+ )
498
+
499
+ instance.full_clean()
500
+ instance.save()
501
+
502
+ # same device, different name works
503
+ instance = self.model(
504
+ device_type=self.device_type,
505
+ name=f"test {self.model._meta.model_name} 2",
506
+ type=PortTypeChoices.TYPE_8P8C,
507
+ rear_port_template=self.device_rear_port,
508
+ rear_port_position=device_available_positions.pop(),
509
+ )
510
+
511
+ instance.full_clean()
512
+ instance.save()
513
+
514
+ instance = self.model(
515
+ device_type=self.device_type,
516
+ name=f"test {self.model._meta.model_name} 1",
517
+ type=PortTypeChoices.TYPE_8P8C,
518
+ rear_port_template=self.device_rear_port,
519
+ rear_port_position=device_available_positions.pop(),
520
+ )
521
+
522
+ with self.assertRaises(ValidationError):
523
+ instance.full_clean()
524
+
525
+ with self.assertRaises(IntegrityError):
526
+ instance.save()
527
+
528
+ def test_uniqueness_module(self):
529
+ """Assert that the combination of module and name is unique."""
530
+ module_available_positions = self.module_available_positions.copy()
531
+ instance = self.model(
532
+ module_type=self.module_type,
533
+ name=f"test {self.model._meta.model_name} 1",
534
+ type=PortTypeChoices.TYPE_8P8C,
535
+ rear_port_template=self.module_rear_port,
536
+ rear_port_position=module_available_positions.pop(),
537
+ )
538
+
539
+ instance.full_clean()
540
+ instance.save()
541
+
542
+ # same module, different name works
543
+ instance = self.model(
544
+ module_type=self.module_type,
545
+ name=f"test {self.model._meta.model_name} 2",
546
+ type=PortTypeChoices.TYPE_8P8C,
547
+ rear_port_template=self.module_rear_port,
548
+ rear_port_position=module_available_positions.pop(),
549
+ )
550
+
551
+ instance.full_clean()
552
+ instance.save()
553
+
554
+ instance = self.model(
555
+ module_type=self.module_type,
556
+ name=f"test {self.model._meta.model_name} 1",
557
+ type=PortTypeChoices.TYPE_8P8C,
558
+ rear_port_template=self.module_rear_port,
559
+ rear_port_position=module_available_positions.pop(),
560
+ )
561
+
562
+ with self.assertRaises(ValidationError):
563
+ instance.full_clean()
564
+
565
+ with self.assertRaises(IntegrityError):
566
+ instance.save()
567
+
568
+
67
569
  class CableLengthTestCase(TestCase):
68
570
  @classmethod
69
571
  def setUpTestData(cls):
@@ -169,7 +671,10 @@ class InterfaceTemplateCustomFieldTestCase(TestCase):
169
671
  self.assertEqual(Interface.objects.get(pk=interfaces[1].pk).cf["field_3"], "value_3")
170
672
 
171
673
 
172
- class InterfaceTemplateTestCase(TestCase):
674
+ class InterfaceTemplateTestCase(ModularDeviceComponentTemplateTestCaseMixin, TestCase):
675
+ modular_component_create_data = {"type": InterfaceTypeChoices.TYPE_1GE_FIXED}
676
+ model = InterfaceTemplate
677
+
173
678
  def test_interface_template_sets_interface_status(self):
174
679
  """
175
680
  When a device is created with a device type associated with the template,
@@ -277,31 +782,31 @@ class InterfaceRedundancyGroupTestCase(ModelTestCases.BaseModelTestCase):
277
782
  cls.interfaces = (
278
783
  Interface.objects.create(
279
784
  device=cls.device,
280
- name="Interface 1",
785
+ name="Test Interface 1",
281
786
  type="1000base-t",
282
787
  status=non_default_status,
283
788
  ),
284
789
  Interface.objects.create(
285
790
  device=cls.device,
286
- name="Interface 2",
791
+ name="Test Interface 2",
287
792
  type="1000base-t",
288
793
  status=non_default_status,
289
794
  ),
290
795
  Interface.objects.create(
291
796
  device=cls.device,
292
- name="Interface 3",
797
+ name="Test Interface 3",
293
798
  type=InterfaceTypeChoices.TYPE_BRIDGE,
294
799
  status=non_default_status,
295
800
  ),
296
801
  Interface.objects.create(
297
802
  device=cls.device,
298
- name="Interface 4",
803
+ name="Test Interface 4",
299
804
  type=InterfaceTypeChoices.TYPE_1GE_GBIC,
300
805
  status=non_default_status,
301
806
  ),
302
807
  Interface.objects.create(
303
808
  device=cls.device,
304
- name="Interface 5",
809
+ name="Test Interface 5",
305
810
  type=InterfaceTypeChoices.TYPE_LAG,
306
811
  status=non_default_status,
307
812
  ),
@@ -897,6 +1402,7 @@ class DeviceTestCase(ModelTestCases.BaseModelTestCase):
897
1402
  )
898
1403
  self.device_role = Role.objects.get_for_model(Device).first()
899
1404
  self.device_status = Status.objects.get_for_model(Device).first()
1405
+ self.intf_role = Role.objects.get_for_model(Interface).first()
900
1406
  self.location_type_1 = LocationType.objects.get(name="Building")
901
1407
  self.location_type_2 = LocationType.objects.get(name="Floor")
902
1408
  self.location_type_3 = LocationType.objects.get(name="Campus")
@@ -959,6 +1465,7 @@ class DeviceTestCase(ModelTestCases.BaseModelTestCase):
959
1465
  ).save()
960
1466
 
961
1467
  DeviceBayTemplate(device_type=self.device_type, name="Device Bay 1").save()
1468
+ ModuleBayTemplate.objects.create(device_type=self.device_type, position="1111")
962
1469
 
963
1470
  self.device = Device(
964
1471
  location=self.location_3,
@@ -1036,6 +1543,7 @@ class DeviceTestCase(ModelTestCases.BaseModelTestCase):
1036
1543
  )
1037
1544
 
1038
1545
  DeviceBay.objects.get(device=self.device, name="Device Bay 1")
1546
+ ModuleBay.objects.get(parent_device=self.device, position="1111")
1039
1547
 
1040
1548
  def test_multiple_unnamed_devices(self):
1041
1549
  device1 = Device(
@@ -1148,10 +1656,11 @@ class DeviceTestCase(ModelTestCases.BaseModelTestCase):
1148
1656
  location=self.location_3,
1149
1657
  )
1150
1658
  device.validated_save()
1151
- interface = Interface.objects.create(name="Int1", device=device, status=self.device_status)
1659
+ interface = Interface.objects.create(name="Int1", device=device, status=self.device_status, role=self.intf_role)
1152
1660
  ips = list(IPAddress.objects.filter(ip_version=4)[:5]) + list(IPAddress.objects.filter(ip_version=6)[:5])
1153
1661
  interface.add_ip_addresses(ips)
1154
1662
  device.primary_ip4 = interface.ip_addresses.all().filter(ip_version=6).first()
1663
+ self.assertIsNotNone(device.primary_ip4)
1155
1664
  with self.assertRaises(ValidationError) as cm:
1156
1665
  device.validated_save()
1157
1666
  self.assertIn(
@@ -1160,6 +1669,7 @@ class DeviceTestCase(ModelTestCases.BaseModelTestCase):
1160
1669
  )
1161
1670
  device.primary_ip4 = None
1162
1671
  device.primary_ip6 = interface.ip_addresses.all().filter(ip_version=4).first()
1672
+ self.assertIsNotNone(device.primary_ip6)
1163
1673
  with self.assertRaises(ValidationError) as cm:
1164
1674
  device.validated_save()
1165
1675
  self.assertIn(
@@ -1170,6 +1680,40 @@ class DeviceTestCase(ModelTestCases.BaseModelTestCase):
1170
1680
  device.primary_ip6 = interface.ip_addresses.all().filter(ip_version=6).first()
1171
1681
  device.validated_save()
1172
1682
 
1683
+ def test_primary_ip_validation_logic_modules(self):
1684
+ device = Device(
1685
+ name="Test IP Device",
1686
+ device_type=self.device_type,
1687
+ role=self.device_role,
1688
+ status=self.device_status,
1689
+ location=self.location_3,
1690
+ )
1691
+ device.validated_save()
1692
+ manufacturer = Manufacturer.objects.first()
1693
+ module_type = ModuleType.objects.create(manufacturer=manufacturer, model="module model tests")
1694
+
1695
+ status = Status.objects.get_for_model(Module).first()
1696
+ module_bay = ModuleBay.objects.create(
1697
+ parent_device=device,
1698
+ name="1111",
1699
+ position="1111",
1700
+ )
1701
+
1702
+ module = Module.objects.create(
1703
+ module_type=module_type,
1704
+ parent_module_bay=module_bay,
1705
+ status=status,
1706
+ )
1707
+
1708
+ interface = Interface.objects.create(name="Int1", module=module, status=self.device_status, role=self.intf_role)
1709
+ ips = list(IPAddress.objects.filter(ip_version=4)[:5]) + list(IPAddress.objects.filter(ip_version=6)[:5])
1710
+ interface.add_ip_addresses(ips)
1711
+ device.primary_ip4 = interface.ip_addresses.all().filter(ip_version=4).first()
1712
+ self.assertIsNotNone(device.primary_ip4)
1713
+ device.primary_ip6 = interface.ip_addresses.all().filter(ip_version=6).first()
1714
+ self.assertIsNotNone(device.primary_ip6)
1715
+ device.validated_save()
1716
+
1173
1717
  def test_software_version_device_type_validation(self):
1174
1718
  """
1175
1719
  Assert that device's software version contains a software image file that matches the device's device type or a default image.
@@ -1196,6 +1740,101 @@ class DeviceTestCase(ModelTestCases.BaseModelTestCase):
1196
1740
  self.device_type.software_image_files.add(software_image_file)
1197
1741
  self.device.validated_save()
1198
1742
 
1743
+ def test_all_x_properties(self):
1744
+ self.assertEqual(self.device.all_modules.count(), 0)
1745
+ self.assertEqual(self.device.all_module_bays.count(), 1)
1746
+ self.assertEqual(self.device.all_console_server_ports.count(), 1)
1747
+ self.assertEqual(self.device.all_console_ports.count(), 1)
1748
+ self.assertEqual(self.device.all_front_ports.count(), 1)
1749
+ self.assertEqual(self.device.all_interfaces.count(), 1)
1750
+ self.assertEqual(self.device.all_rear_ports.count(), 1)
1751
+ self.assertEqual(self.device.all_power_ports.count(), 1)
1752
+ self.assertEqual(self.device.all_power_outlets.count(), 1)
1753
+
1754
+ parent_module_bay = self.device.all_module_bays.first()
1755
+
1756
+ manufacturer = Manufacturer.objects.first()
1757
+ module_type = ModuleType.objects.create(manufacturer=manufacturer, model="module model tests")
1758
+ status = Status.objects.get_for_model(Module).first()
1759
+
1760
+ # Create ModuleType components
1761
+ ConsolePortTemplate.objects.create(module_type=module_type, name="Console Port 1")
1762
+ ConsoleServerPortTemplate.objects.create(module_type=module_type, name="Console Server Port 1")
1763
+
1764
+ ppt = PowerPortTemplate.objects.create(
1765
+ module_type=module_type,
1766
+ name="Power Port 1",
1767
+ maximum_draw=1000,
1768
+ allocated_draw=500,
1769
+ )
1770
+
1771
+ PowerOutletTemplate.objects.create(
1772
+ module_type=module_type,
1773
+ name="Power Outlet 1",
1774
+ power_port_template=ppt,
1775
+ feed_leg=PowerOutletFeedLegChoices.FEED_LEG_A,
1776
+ )
1777
+
1778
+ InterfaceTemplate.objects.create(
1779
+ module_type=module_type,
1780
+ name="Interface {module.parent.parent}/{module.parent}/{module}",
1781
+ type=InterfaceTypeChoices.TYPE_1GE_FIXED,
1782
+ mgmt_only=True,
1783
+ )
1784
+
1785
+ rpt = RearPortTemplate.objects.create(
1786
+ module_type=module_type,
1787
+ name="Rear Port 1",
1788
+ type=PortTypeChoices.TYPE_8P8C,
1789
+ positions=8,
1790
+ )
1791
+
1792
+ FrontPortTemplate.objects.create(
1793
+ module_type=module_type,
1794
+ name="Front Port 1",
1795
+ type=PortTypeChoices.TYPE_8P8C,
1796
+ rear_port_template=rpt,
1797
+ rear_port_position=2,
1798
+ )
1799
+
1800
+ ModuleBayTemplate.objects.create(
1801
+ module_type=module_type,
1802
+ position="1111",
1803
+ )
1804
+
1805
+ module = Module.objects.create(
1806
+ module_type=module_type,
1807
+ status=status,
1808
+ parent_module_bay=parent_module_bay,
1809
+ )
1810
+
1811
+ self.assertEqual(self.device.all_modules.count(), 1)
1812
+ self.assertEqual(self.device.all_module_bays.count(), 2)
1813
+ self.assertEqual(self.device.all_console_server_ports.count(), 2)
1814
+ self.assertEqual(self.device.all_console_ports.count(), 2)
1815
+ self.assertEqual(self.device.all_front_ports.count(), 2)
1816
+ self.assertEqual(self.device.all_interfaces.count(), 2)
1817
+ self.assertEqual(self.device.all_rear_ports.count(), 2)
1818
+ self.assertEqual(self.device.all_power_ports.count(), 2)
1819
+ self.assertEqual(self.device.all_power_outlets.count(), 2)
1820
+
1821
+ child_module_bay = module.module_bays.first()
1822
+ Module.objects.create(
1823
+ module_type=module_type,
1824
+ status=status,
1825
+ parent_module_bay=child_module_bay,
1826
+ )
1827
+
1828
+ self.assertEqual(self.device.all_modules.count(), 2)
1829
+ self.assertEqual(self.device.all_module_bays.count(), 3)
1830
+ self.assertEqual(self.device.all_console_server_ports.count(), 3)
1831
+ self.assertEqual(self.device.all_console_ports.count(), 3)
1832
+ self.assertEqual(self.device.all_front_ports.count(), 3)
1833
+ self.assertEqual(self.device.all_interfaces.count(), 3)
1834
+ self.assertEqual(self.device.all_rear_ports.count(), 3)
1835
+ self.assertEqual(self.device.all_power_ports.count(), 3)
1836
+ self.assertEqual(self.device.all_power_outlets.count(), 3)
1837
+
1199
1838
  def test_child_devices_are_not_saved_when_unnecessary(self):
1200
1839
  parent_device = Device.objects.create(
1201
1840
  name="Parent Device 1",
@@ -1264,6 +1903,37 @@ class DeviceTestCase(ModelTestCases.BaseModelTestCase):
1264
1903
  self.assertNotEqual(child_mtime_after_parent_rack_update_save, child_mtime_after_parent_site_update_save)
1265
1904
 
1266
1905
 
1906
+ class DeviceBayTestCase(ModelTestCases.BaseModelTestCase):
1907
+ model = DeviceBay
1908
+
1909
+ def setUp(self):
1910
+ self.devices = Device.objects.filter(device_type__subdevice_role=SubdeviceRoleChoices.ROLE_PARENT)
1911
+ devicetype = DeviceType.objects.create(
1912
+ manufacturer=self.devices[0].device_type.manufacturer,
1913
+ model="TestDeviceType1",
1914
+ u_height=0,
1915
+ subdevice_role=SubdeviceRoleChoices.ROLE_CHILD,
1916
+ )
1917
+ child_device = Device.objects.create(
1918
+ device_type=devicetype,
1919
+ role=self.devices[0].role,
1920
+ name="TestDevice1",
1921
+ status=self.devices[0].status,
1922
+ location=self.devices[0].location,
1923
+ )
1924
+ DeviceBay.objects.create(device=self.devices[0], name="Device Bay 1", installed_device=child_device)
1925
+
1926
+ def test_assigning_installed_device(self):
1927
+ server = Device.objects.exclude(device_type__subdevice_role=SubdeviceRoleChoices.ROLE_CHILD).last()
1928
+ bay = DeviceBay(device=self.devices[1], name="Device Bay Err", installed_device=server)
1929
+ with self.assertRaises(ValidationError) as err:
1930
+ bay.validated_save()
1931
+ self.assertIn(
1932
+ f'Cannot install device "{server}"; device-type "{server.device_type}" subdevice_role is not "child".',
1933
+ str(err.exception),
1934
+ )
1935
+
1936
+
1267
1937
  class DeviceTypeToSoftwareImageFileTestCase(ModelTestCases.BaseModelTestCase):
1268
1938
  model = DeviceTypeToSoftwareImageFile
1269
1939
 
@@ -1636,9 +2306,16 @@ class PowerPanelTestCase(TestCase): # TODO: change to BaseModelTestCase once we
1636
2306
  )
1637
2307
 
1638
2308
 
1639
- class InterfaceTestCase(TestCase): # TODO: change to BaseModelTestCase once we have an InterfaceFactory
2309
+ class InterfaceTestCase(ModularDeviceComponentTestCaseMixin, ModelTestCases.BaseModelTestCase):
2310
+ model = Interface
2311
+
1640
2312
  @classmethod
1641
2313
  def setUpTestData(cls):
2314
+ super().setUpTestData()
2315
+ cls.modular_component_create_data = {
2316
+ "type": InterfaceTypeChoices.TYPE_1GE_FIXED,
2317
+ "status": Status.objects.get_for_model(Interface).first(),
2318
+ }
1642
2319
  manufacturer = Manufacturer.objects.first()
1643
2320
  devicetype = DeviceType.objects.create(manufacturer=manufacturer, model="Device Type 1")
1644
2321
  devicerole = Role.objects.get_for_model(Device).first()
@@ -1684,6 +2361,7 @@ class InterfaceTestCase(TestCase): # TODO: change to BaseModelTestCase once we
1684
2361
  type=InterfaceTypeChoices.TYPE_VIRTUAL,
1685
2362
  device=self.device,
1686
2363
  status=Status.objects.get_for_model(Interface).first(),
2364
+ role=Role.objects.get_for_model(Interface).first(),
1687
2365
  )
1688
2366
  with self.assertRaises(ValidationError) as err:
1689
2367
  interface.tagged_vlans.add(self.vlan)
@@ -1698,6 +2376,7 @@ class InterfaceTestCase(TestCase): # TODO: change to BaseModelTestCase once we
1698
2376
  mode=InterfaceModeChoices.MODE_TAGGED,
1699
2377
  device=self.device,
1700
2378
  status=Status.objects.get_for_model(Interface).first(),
2379
+ role=Role.objects.get_for_model(Interface).first(),
1701
2380
  )
1702
2381
  interface.tagged_vlans.add(self.other_location_vlan)
1703
2382
  self.assertEqual(
@@ -1713,6 +2392,7 @@ class InterfaceTestCase(TestCase): # TODO: change to BaseModelTestCase once we
1713
2392
  type=InterfaceTypeChoices.TYPE_VIRTUAL,
1714
2393
  device=self.device,
1715
2394
  status=Status.objects.get_for_model(Interface).first(),
2395
+ role=Role.objects.get_for_model(Interface).first(),
1716
2396
  )
1717
2397
  ips = list(IPAddress.objects.filter(parent__namespace=self.namespace))
1718
2398
 
@@ -1738,6 +2418,7 @@ class InterfaceTestCase(TestCase): # TODO: change to BaseModelTestCase once we
1738
2418
  type=InterfaceTypeChoices.TYPE_VIRTUAL,
1739
2419
  device=self.device,
1740
2420
  status=Status.objects.get_for_model(Interface).first(),
2421
+ role=Role.objects.get_for_model(Interface).first(),
1741
2422
  )
1742
2423
  ips = list(IPAddress.objects.filter(parent__namespace=self.namespace))
1743
2424
 
@@ -2066,3 +2747,417 @@ class ControllerManagedDeviceGroupTestCase(ModelTestCases.BaseModelTestCase):
2066
2747
  controller2,
2067
2748
  "Child group 2 controller should have been updated",
2068
2749
  )
2750
+
2751
+
2752
+ class ModuleBayTestCase(ModularDeviceComponentTestCaseMixin, ModelTestCases.BaseModelTestCase):
2753
+ model = ModuleBay
2754
+ device_field = "parent_device" # field name for the parent device
2755
+ module_field = "parent_module" # field name for the parent module
2756
+
2757
+ @classmethod
2758
+ def setUpTestData(cls):
2759
+ cls.device = Device.objects.first()
2760
+ cls.device.module_bays.all().delete()
2761
+ cls.module = Module.objects.first()
2762
+ cls.module.module_bays.all().delete()
2763
+
2764
+ def test_parent_property(self):
2765
+ """Assert that the parent property walks up the inheritance tree of Device -> ModuleBay -> Module -> ModuleBay."""
2766
+ module_type = ModuleType.objects.first()
2767
+ status = Status.objects.get_for_model(Module).first()
2768
+
2769
+ parent_module_bay = ModuleBay.objects.create(
2770
+ parent_device=self.device,
2771
+ name="1111",
2772
+ position="1111",
2773
+ )
2774
+ module = Module.objects.create(
2775
+ module_type=module_type,
2776
+ parent_module_bay=parent_module_bay,
2777
+ status=status,
2778
+ )
2779
+ child_module_bay = ModuleBay.objects.create(
2780
+ parent_module=module,
2781
+ name="1111",
2782
+ position="1111",
2783
+ )
2784
+ child_module = Module.objects.create(
2785
+ module_type=module_type,
2786
+ parent_module_bay=child_module_bay,
2787
+ status=status,
2788
+ )
2789
+ grandchild_module_bay = ModuleBay.objects.create(
2790
+ parent_module=child_module,
2791
+ name="1111",
2792
+ position="1111",
2793
+ )
2794
+
2795
+ self.assertEqual(parent_module_bay.parent, self.device)
2796
+ self.assertEqual(child_module_bay.parent, self.device)
2797
+ self.assertEqual(grandchild_module_bay.parent, self.device)
2798
+
2799
+ # Remove the module from the module bay and put it in storage
2800
+ module.parent_module_bay = None
2801
+ module.location = Location.objects.get_for_model(Module).first()
2802
+ module.save()
2803
+
2804
+ self.assertEqual(parent_module_bay.parent, self.device)
2805
+ self.assertIsNone(child_module_bay.parent)
2806
+ self.assertIsNone(grandchild_module_bay.parent)
2807
+
2808
+
2809
+ class ModuleBayTemplateTestCase(ModularDeviceComponentTemplateTestCaseMixin, ModelTestCases.BaseModelTestCase):
2810
+ model = ModuleBayTemplate
2811
+
2812
+ @classmethod
2813
+ def setUpTestData(cls):
2814
+ manufacturer = Manufacturer.objects.first()
2815
+ cls.device_type = cls.device = DeviceType.objects.create(
2816
+ manufacturer=manufacturer, model="Test ModuleBayTemplate DT1"
2817
+ )
2818
+ cls.module_type = cls.module = ModuleType.objects.create(
2819
+ manufacturer=manufacturer, model="Test ModuleBayTemplate MT1"
2820
+ )
2821
+
2822
+ # Create some instances for the generic natural key tests to use
2823
+ ModuleBayTemplate.objects.create(
2824
+ device_type=cls.device_type,
2825
+ name="2222",
2826
+ position="2222",
2827
+ )
2828
+ ModuleBayTemplate.objects.create(
2829
+ device_type=cls.device_type,
2830
+ name="3333",
2831
+ position="3333",
2832
+ )
2833
+ ModuleBayTemplate.objects.create(
2834
+ module_type=cls.module_type,
2835
+ name="3333",
2836
+ position="3333",
2837
+ )
2838
+ ModuleBayTemplate.objects.create(
2839
+ module_type=cls.module_type,
2840
+ name="4444",
2841
+ position="4444",
2842
+ )
2843
+
2844
+ def test_parent_property(self):
2845
+ module_bay_template = ModuleBayTemplate.objects.create(
2846
+ device_type=self.device_type,
2847
+ name="1111",
2848
+ position="1111",
2849
+ )
2850
+ self.assertEqual(module_bay_template.parent, self.device_type)
2851
+
2852
+ module_bay_template = ModuleBayTemplate.objects.create(
2853
+ module_type=self.module_type,
2854
+ name="1111",
2855
+ position="1111",
2856
+ )
2857
+ self.assertEqual(module_bay_template.parent, self.module_type)
2858
+
2859
+
2860
+ class ModuleTestCase(ModelTestCases.BaseModelTestCase):
2861
+ model = Module
2862
+
2863
+ @classmethod
2864
+ def setUpTestData(cls):
2865
+ cls.device = Device.objects.filter(module_bays__isnull=True).first()
2866
+ manufacturer = Manufacturer.objects.first()
2867
+ cls.module_type = ModuleType.objects.create(manufacturer=manufacturer, model="module model tests")
2868
+ cls.location = Location.objects.get_for_model(Module).first()
2869
+ cls.status = Status.objects.get_for_model(Module).first()
2870
+
2871
+ # Create ModuleType components
2872
+ ConsolePortTemplate.objects.create(module_type=cls.module_type, name="Console Port 1")
2873
+ ConsoleServerPortTemplate.objects.create(module_type=cls.module_type, name="Console Server Port 1")
2874
+
2875
+ ppt = PowerPortTemplate.objects.create(
2876
+ module_type=cls.module_type,
2877
+ name="Power Port 1",
2878
+ maximum_draw=1000,
2879
+ allocated_draw=500,
2880
+ )
2881
+
2882
+ PowerOutletTemplate.objects.create(
2883
+ module_type=cls.module_type,
2884
+ name="Power Outlet 1",
2885
+ power_port_template=ppt,
2886
+ feed_leg=PowerOutletFeedLegChoices.FEED_LEG_A,
2887
+ )
2888
+
2889
+ InterfaceTemplate.objects.create(
2890
+ module_type=cls.module_type,
2891
+ name="Interface {module.parent.parent}/{module.parent}/{module}",
2892
+ type=InterfaceTypeChoices.TYPE_1GE_FIXED,
2893
+ mgmt_only=True,
2894
+ )
2895
+
2896
+ rpt = RearPortTemplate.objects.create(
2897
+ module_type=cls.module_type,
2898
+ name="Rear Port 1",
2899
+ type=PortTypeChoices.TYPE_8P8C,
2900
+ positions=8,
2901
+ )
2902
+
2903
+ FrontPortTemplate.objects.create(
2904
+ module_type=cls.module_type,
2905
+ name="Front Port 1",
2906
+ type=PortTypeChoices.TYPE_8P8C,
2907
+ rear_port_template=rpt,
2908
+ rear_port_position=2,
2909
+ )
2910
+
2911
+ ModuleBayTemplate.objects.create(
2912
+ module_type=cls.module_type,
2913
+ position="1111",
2914
+ )
2915
+
2916
+ cls.module = Module.objects.create(
2917
+ module_type=cls.module_type,
2918
+ location=cls.location,
2919
+ status=cls.status,
2920
+ )
2921
+ cls.module_bay = cls.module.module_bays.first()
2922
+
2923
+ def test_parent_validation_module_bay_and_location(self):
2924
+ """Assert that a module must have a parent module bay or location but not both."""
2925
+ module = Module(
2926
+ module_type=self.module_type,
2927
+ parent_module_bay=self.module_bay,
2928
+ location=self.location,
2929
+ status=self.status,
2930
+ )
2931
+
2932
+ with self.assertRaises(ValidationError):
2933
+ module.full_clean()
2934
+
2935
+ def test_parent_validation_no_module_bay_or_location(self):
2936
+ """Assert that a module must have a parent module bay or location but not both."""
2937
+ module = Module(
2938
+ module_type=self.module_type,
2939
+ status=self.status,
2940
+ )
2941
+
2942
+ with self.assertRaises(ValidationError):
2943
+ module.full_clean()
2944
+
2945
+ def test_parent_validation_succeeds(self):
2946
+ """Assert that a module must have a parent module bay or location but not both."""
2947
+ with self.subTest("Module with a parent module bay"):
2948
+ module = Module(
2949
+ module_type=self.module_type,
2950
+ parent_module_bay=self.module_bay,
2951
+ status=self.status,
2952
+ )
2953
+
2954
+ module.full_clean()
2955
+ module.save()
2956
+
2957
+ with self.subTest("Module with a parent location"):
2958
+ module = Module(
2959
+ module_type=self.module_type,
2960
+ location=self.location,
2961
+ status=self.status,
2962
+ )
2963
+
2964
+ module.full_clean()
2965
+ module.save()
2966
+
2967
+ def test_device_property(self):
2968
+ """Assert that the device property walks up the inheritance tree of Device -> ModuleBay -> Module -> ModuleBay."""
2969
+ parent_module_bay = ModuleBay.objects.create(
2970
+ parent_device=self.device,
2971
+ position="1111",
2972
+ )
2973
+ parent_module = Module.objects.create(
2974
+ module_type=self.module_type,
2975
+ parent_module_bay=parent_module_bay,
2976
+ status=self.status,
2977
+ )
2978
+ child_module_bay = parent_module.module_bays.first()
2979
+ child_module = Module.objects.create(
2980
+ module_type=self.module_type,
2981
+ parent_module_bay=child_module_bay,
2982
+ status=self.status,
2983
+ )
2984
+ grandchild_module_bay = child_module.module_bays.first()
2985
+ grandchild_module = Module.objects.create(
2986
+ module_type=self.module_type,
2987
+ parent_module_bay=grandchild_module_bay,
2988
+ status=self.status,
2989
+ )
2990
+
2991
+ self.assertEqual(parent_module.device, self.device)
2992
+ self.assertEqual(child_module.device, self.device)
2993
+ self.assertEqual(grandchild_module.device, self.device)
2994
+
2995
+ # Remove the module from the module bay and put it in storage
2996
+ parent_module.parent_module_bay = None
2997
+ parent_module.location = self.location
2998
+ parent_module.save()
2999
+
3000
+ self.assertIsNone(parent_module.device)
3001
+ self.assertIsNone(child_module.device)
3002
+ self.assertIsNone(grandchild_module.device)
3003
+
3004
+ def test_null_serial_asset_tag(self):
3005
+ """Assert that the serial and asset_tag fields are converted to None if a blank string is supplied."""
3006
+ module = Module.objects.create(
3007
+ module_type=self.module_type,
3008
+ status=self.status,
3009
+ parent_module_bay=self.module_bay,
3010
+ serial="",
3011
+ asset_tag="",
3012
+ )
3013
+
3014
+ module.refresh_from_db()
3015
+
3016
+ self.assertIsNone(module.serial)
3017
+ self.assertIsNone(module.asset_tag)
3018
+
3019
+ module.delete()
3020
+ module = Module.objects.create(
3021
+ module_type=self.module_type,
3022
+ status=self.status,
3023
+ parent_module_bay=self.module_bay,
3024
+ )
3025
+
3026
+ module.refresh_from_db()
3027
+
3028
+ self.assertIsNone(module.serial)
3029
+ self.assertIsNone(module.asset_tag)
3030
+
3031
+ def test_module_components_created(self):
3032
+ module = Module(
3033
+ location=self.location,
3034
+ module_type=self.module_type,
3035
+ status=self.status,
3036
+ )
3037
+ module.validated_save()
3038
+ self.assertEqual(module.console_ports.count(), self.module_type.console_port_templates.count())
3039
+ self.assertEqual(module.console_server_ports.count(), self.module_type.console_server_port_templates.count())
3040
+ self.assertEqual(module.power_ports.count(), self.module_type.power_port_templates.count())
3041
+ self.assertEqual(module.power_outlets.count(), self.module_type.power_outlet_templates.count())
3042
+ self.assertEqual(module.interfaces.count(), self.module_type.interface_templates.count())
3043
+ self.assertEqual(module.front_ports.count(), self.module_type.front_port_templates.count())
3044
+ self.assertEqual(module.rear_ports.count(), self.module_type.rear_port_templates.count())
3045
+ self.assertEqual(module.module_bays.count(), self.module_type.module_bay_templates.count())
3046
+
3047
+ ConsolePort.objects.get(module=module, name="Console Port 1")
3048
+
3049
+ ConsoleServerPort.objects.get(module=module, name="Console Server Port 1")
3050
+
3051
+ pp = PowerPort.objects.get(module=module, name="Power Port 1", maximum_draw=1000, allocated_draw=500)
3052
+
3053
+ PowerOutlet.objects.get(
3054
+ module=module,
3055
+ name="Power Outlet 1",
3056
+ power_port=pp,
3057
+ feed_leg=PowerOutletFeedLegChoices.FEED_LEG_A,
3058
+ )
3059
+
3060
+ Interface.objects.get(
3061
+ module=module,
3062
+ name="Interface {module.parent.parent}/{module.parent}/{module}",
3063
+ type=InterfaceTypeChoices.TYPE_1GE_FIXED,
3064
+ mgmt_only=True,
3065
+ )
3066
+
3067
+ rp = RearPort.objects.get(module=module, name="Rear Port 1", type=PortTypeChoices.TYPE_8P8C, positions=8)
3068
+
3069
+ FrontPort.objects.get(
3070
+ module=module,
3071
+ name="Front Port 1",
3072
+ type=PortTypeChoices.TYPE_8P8C,
3073
+ rear_port=rp,
3074
+ rear_port_position=2,
3075
+ )
3076
+
3077
+ ModuleBay.objects.get(parent_module=module, position="1111")
3078
+
3079
+ def test_module_infinite_recursion_self_parent(self):
3080
+ """Assert that a module cannot be its own parent."""
3081
+ module = Module.objects.create(
3082
+ location=self.location,
3083
+ module_type=self.module_type,
3084
+ status=self.status,
3085
+ )
3086
+ module_bay = module.module_bays.first()
3087
+ module.parent_module_bay = module_bay
3088
+
3089
+ with self.assertRaises(ValidationError) as context:
3090
+ module.save()
3091
+ self.assertEqual(context.exception.message, "Creating this instance would cause an infinite loop.")
3092
+
3093
+ def test_module_infinite_recursion_ancestor(self):
3094
+ """Assert that a module cannot be its own ancestor."""
3095
+ parent_module = Module.objects.create(
3096
+ module_type=self.module_type,
3097
+ location=self.location,
3098
+ status=self.status,
3099
+ )
3100
+ parent_module_bay = parent_module.module_bays.first()
3101
+ child_module = Module.objects.create(
3102
+ module_type=self.module_type,
3103
+ parent_module_bay=parent_module_bay,
3104
+ status=self.status,
3105
+ )
3106
+ child_module_bay = child_module.module_bays.first()
3107
+ parent_module.parent_module_bay = child_module_bay
3108
+ parent_module.location = None
3109
+
3110
+ with self.assertRaises(ValidationError) as context:
3111
+ parent_module.save()
3112
+
3113
+ self.assertEqual(context.exception.message, "Creating this instance would cause an infinite loop.")
3114
+
3115
+ def test_render_component_names(self):
3116
+ """Test that creating a Module with components properly renders the {module} and {module.parent} variables."""
3117
+ grandparent_module = Module.objects.create(
3118
+ module_type=self.module_type,
3119
+ location=self.location,
3120
+ status=self.status,
3121
+ )
3122
+ grandparent_module_bay = grandparent_module.module_bays.first()
3123
+ grandparent_module_bay.position = "3"
3124
+ grandparent_module_bay.save()
3125
+ parent_module = Module.objects.create(
3126
+ parent_module_bay=grandparent_module.module_bays.first(),
3127
+ module_type=self.module_type,
3128
+ status=self.status,
3129
+ )
3130
+ parent_module.clean()
3131
+ parent_module_bay = parent_module.module_bays.first()
3132
+ parent_module_bay.position = "2"
3133
+ parent_module_bay.save()
3134
+ child_module = Module.objects.create(
3135
+ parent_module_bay=parent_module.module_bays.first(),
3136
+ module_type=self.module_type,
3137
+ status=self.status,
3138
+ )
3139
+ child_module.clean()
3140
+
3141
+ self.assertEqual(
3142
+ grandparent_module.interfaces.first().name, "Interface {module.parent.parent}/{module.parent}/{module}"
3143
+ )
3144
+ self.assertEqual(parent_module.interfaces.first().name, "Interface {module.parent.parent}/{module.parent}/3")
3145
+ self.assertEqual(child_module.interfaces.first().name, "Interface {module.parent.parent}/3/2")
3146
+
3147
+ # Moving the grandparent module out of inventory populates the template variables on all descendant interfaces
3148
+ grandparent_module.parent_module_bay = self.module_bay
3149
+ grandparent_module.location = None
3150
+ grandparent_module.validated_save()
3151
+ module_bay_position = self.module_bay.position
3152
+ self.assertEqual(
3153
+ grandparent_module.interfaces.first().name,
3154
+ "Interface {module.parent.parent}/{module.parent}/" + module_bay_position,
3155
+ )
3156
+ self.assertEqual(
3157
+ parent_module.interfaces.first().name, "Interface {module.parent.parent}/" + module_bay_position + "/3"
3158
+ )
3159
+ self.assertEqual(child_module.interfaces.first().name, "Interface " + module_bay_position + "/3/2")
3160
+
3161
+
3162
+ class ModuleTypeTestCase(ModelTestCases.BaseModelTestCase):
3163
+ model = ModuleType