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

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

Potentially problematic release.


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

Files changed (697) hide show
  1. nautobot/apps/forms.py +4 -0
  2. nautobot/apps/models.py +10 -1
  3. nautobot/circuits/__init__.py +0 -1
  4. nautobot/circuits/apps.py +1 -0
  5. nautobot/circuits/factory.py +15 -3
  6. nautobot/circuits/filters.py +13 -0
  7. nautobot/circuits/forms.py +13 -0
  8. nautobot/circuits/migrations/0021_alter_circuit_status_alter_circuittermination__path.py +32 -0
  9. nautobot/circuits/migrations/0022_circuittermination_cloud_network.py +25 -0
  10. nautobot/circuits/models.py +16 -3
  11. nautobot/circuits/tables.py +16 -2
  12. nautobot/circuits/templates/circuits/circuittermination_create.html +10 -2
  13. nautobot/circuits/templates/circuits/circuittermination_retrieve.html +6 -0
  14. nautobot/circuits/templates/circuits/inc/circuit_termination.html +6 -1
  15. nautobot/circuits/tests/test_api.py +7 -5
  16. nautobot/circuits/tests/test_filters.py +12 -5
  17. nautobot/circuits/tests/test_models.py +33 -2
  18. nautobot/circuits/views.py +2 -3
  19. nautobot/cloud/__init__.py +0 -0
  20. nautobot/cloud/api/__init__.py +0 -0
  21. nautobot/cloud/api/serializers.py +54 -0
  22. nautobot/cloud/api/urls.py +16 -0
  23. nautobot/cloud/api/views.py +48 -0
  24. nautobot/cloud/apps.py +13 -0
  25. nautobot/cloud/factory.py +111 -0
  26. nautobot/cloud/filters.py +184 -0
  27. nautobot/cloud/forms.py +333 -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 +247 -0
  32. nautobot/cloud/navigation.py +85 -0
  33. nautobot/cloud/tables.py +173 -0
  34. nautobot/cloud/templates/cloud/cloudaccount_retrieve.html +43 -0
  35. nautobot/cloud/templates/cloud/cloudnetwork_retrieve.html +128 -0
  36. nautobot/cloud/templates/cloud/cloudnetwork_update.html +33 -0
  37. nautobot/cloud/templates/cloud/cloudresourcetype_retrieve.html +100 -0
  38. nautobot/cloud/templates/cloud/cloudservice_retrieve.html +65 -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 +113 -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 +96 -28
  57. nautobot/core/forms/fields.py +10 -4
  58. nautobot/core/forms/forms.py +1 -1
  59. nautobot/core/forms/widgets.py +18 -1
  60. nautobot/core/graphql/generators.py +2 -2
  61. nautobot/core/graphql/schema.py +34 -4
  62. nautobot/core/jobs/__init__.py +17 -6
  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 +4 -30
  73. nautobot/core/settings.yaml +34 -27
  74. nautobot/core/settings_funcs.py +103 -0
  75. nautobot/core/tables.py +127 -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 +13 -23
  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 +51 -13
  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 +123 -74
  120. nautobot/core/tests/test_tables.py +3 -1
  121. nautobot/core/tests/test_templatetags_helpers.py +12 -5
  122. nautobot/core/tests/test_utils.py +31 -20
  123. nautobot/core/tests/test_views.py +6 -6
  124. nautobot/core/urls.py +8 -3
  125. nautobot/core/utils/deprecation.py +29 -0
  126. nautobot/core/utils/filtering.py +12 -9
  127. nautobot/core/utils/lookup.py +37 -2
  128. nautobot/core/utils/requests.py +4 -1
  129. nautobot/core/views/__init__.py +137 -24
  130. nautobot/core/views/generic.py +118 -66
  131. nautobot/core/views/mixins.py +104 -35
  132. nautobot/core/views/paginator.py +9 -3
  133. nautobot/core/views/renderers.py +121 -56
  134. nautobot/core/views/utils.py +79 -1
  135. nautobot/dcim/__init__.py +0 -1
  136. nautobot/dcim/api/serializers.py +180 -44
  137. nautobot/dcim/api/urls.py +7 -3
  138. nautobot/dcim/api/views.py +53 -7
  139. nautobot/dcim/apps.py +3 -0
  140. nautobot/dcim/choices.py +25 -0
  141. nautobot/dcim/constants.py +7 -0
  142. nautobot/dcim/factory.py +249 -18
  143. nautobot/dcim/filters/__init__.py +369 -193
  144. nautobot/dcim/filters/mixins.py +274 -1
  145. nautobot/dcim/forms.py +817 -109
  146. nautobot/dcim/graphql/types.py +2 -2
  147. nautobot/dcim/homepage.py +1 -1
  148. nautobot/dcim/migrations/0059_add_role_field_to_interface_models.py +27 -0
  149. nautobot/dcim/migrations/0060_alter_cable_status_alter_consoleport__path_and_more.py +303 -0
  150. nautobot/dcim/migrations/0061_module_models.py +861 -0
  151. nautobot/dcim/migrations/0062_module_data_migration.py +25 -0
  152. nautobot/dcim/models/__init__.py +8 -0
  153. nautobot/dcim/models/cables.py +15 -0
  154. nautobot/dcim/models/device_component_templates.py +207 -53
  155. nautobot/dcim/models/device_components.py +275 -106
  156. nautobot/dcim/models/devices.py +466 -13
  157. nautobot/dcim/navigation.py +47 -0
  158. nautobot/dcim/signals.py +3 -3
  159. nautobot/dcim/tables/__init__.py +35 -23
  160. nautobot/dcim/tables/devices.py +231 -59
  161. nautobot/dcim/tables/devicetypes.py +65 -9
  162. nautobot/dcim/tables/racks.py +5 -1
  163. nautobot/dcim/tables/template_code.py +46 -26
  164. nautobot/dcim/templates/dcim/cable_connect.html +76 -3
  165. nautobot/dcim/templates/dcim/console_port_connection_list.html +7 -5
  166. nautobot/dcim/templates/dcim/device/base.html +15 -7
  167. nautobot/dcim/templates/dcim/device/consoleports.html +2 -3
  168. nautobot/dcim/templates/dcim/device/consoleserverports.html +2 -3
  169. nautobot/dcim/templates/dcim/device/devicebays.html +6 -7
  170. nautobot/dcim/templates/dcim/device/frontports.html +2 -3
  171. nautobot/dcim/templates/dcim/device/interfaces.html +2 -3
  172. nautobot/dcim/templates/dcim/device/inventory.html +2 -3
  173. nautobot/dcim/templates/dcim/device/modulebays.html +49 -0
  174. nautobot/dcim/templates/dcim/device/poweroutlets.html +2 -3
  175. nautobot/dcim/templates/dcim/device/powerports.html +2 -3
  176. nautobot/dcim/templates/dcim/device/rearports.html +2 -3
  177. nautobot/dcim/templates/dcim/device.html +45 -1
  178. nautobot/dcim/templates/dcim/device_component.html +13 -5
  179. nautobot/dcim/templates/dcim/device_list.html +2 -1
  180. nautobot/dcim/templates/dcim/deviceredundancygroup_retrieve.html +0 -6
  181. nautobot/dcim/templates/dcim/devicetype.html +99 -98
  182. nautobot/dcim/templates/dcim/devicetype_list.html +8 -16
  183. nautobot/dcim/templates/dcim/inc/devicetype_component_table.html +1 -1
  184. nautobot/dcim/templates/dcim/inc/moduletype_component_table.html +39 -0
  185. nautobot/dcim/templates/dcim/interface.html +17 -2
  186. nautobot/dcim/templates/dcim/interface_connection_list.html +7 -5
  187. nautobot/dcim/templates/dcim/interface_edit.html +1 -0
  188. nautobot/dcim/templates/dcim/manufacturer.html +24 -0
  189. nautobot/dcim/templates/dcim/module/base.html +97 -0
  190. nautobot/dcim/templates/dcim/module_bulk_destroy.html +5 -0
  191. nautobot/dcim/templates/dcim/module_consoleports.html +53 -0
  192. nautobot/dcim/templates/dcim/module_consoleserverports.html +53 -0
  193. nautobot/dcim/templates/dcim/module_destroy.html +5 -0
  194. nautobot/dcim/templates/dcim/module_frontports.html +53 -0
  195. nautobot/dcim/templates/dcim/module_interfaces.html +57 -0
  196. nautobot/dcim/templates/dcim/module_list.html +20 -0
  197. nautobot/dcim/templates/dcim/module_modulebays.html +49 -0
  198. nautobot/dcim/templates/dcim/module_poweroutlets.html +53 -0
  199. nautobot/dcim/templates/dcim/module_powerports.html +53 -0
  200. nautobot/dcim/templates/dcim/module_rearports.html +53 -0
  201. nautobot/dcim/templates/dcim/module_retrieve.html +63 -0
  202. nautobot/dcim/templates/dcim/module_update.html +71 -0
  203. nautobot/dcim/templates/dcim/modulebay_bulk_destroy.html +5 -0
  204. nautobot/dcim/templates/dcim/modulebay_destroy.html +8 -0
  205. nautobot/dcim/templates/dcim/modulebay_retrieve.html +101 -0
  206. nautobot/dcim/templates/dcim/moduletype_list.html +11 -0
  207. nautobot/dcim/templates/dcim/moduletype_retrieve.html +142 -0
  208. nautobot/dcim/templates/dcim/power_port_connection_list.html +7 -5
  209. nautobot/dcim/templates/dcim/softwareimagefile_retrieve.html +65 -19
  210. nautobot/dcim/tests/integration/test_cable_connect_form.py +4 -4
  211. nautobot/dcim/tests/test_api.py +691 -208
  212. nautobot/dcim/tests/test_filters.py +836 -217
  213. nautobot/dcim/tests/test_models.py +1072 -39
  214. nautobot/dcim/tests/test_views.py +1488 -358
  215. nautobot/dcim/urls.py +17 -2
  216. nautobot/dcim/utils.py +2 -3
  217. nautobot/dcim/views.py +1107 -120
  218. nautobot/extras/__init__.py +0 -1
  219. nautobot/extras/api/serializers.py +115 -3
  220. nautobot/extras/api/urls.py +12 -0
  221. nautobot/extras/api/views.py +125 -7
  222. nautobot/extras/apps.py +2 -2
  223. nautobot/extras/choices.py +43 -0
  224. nautobot/extras/context_managers.py +13 -8
  225. nautobot/extras/datasources/git.py +2 -0
  226. nautobot/extras/factory.py +422 -9
  227. nautobot/extras/filters/__init__.py +174 -3
  228. nautobot/extras/filters/mixins.py +46 -43
  229. nautobot/extras/forms/base.py +17 -4
  230. nautobot/extras/forms/forms.py +227 -8
  231. nautobot/extras/forms/mixins.py +93 -0
  232. nautobot/extras/graphql/types.py +23 -10
  233. nautobot/extras/homepage.py +16 -13
  234. nautobot/extras/jobs.py +2 -2
  235. nautobot/extras/management/__init__.py +1 -0
  236. nautobot/extras/management/commands/refresh_dynamic_group_member_caches.py +1 -16
  237. nautobot/extras/migrations/0021_customfield_changelog_data.py +1 -0
  238. nautobot/extras/migrations/0109_dynamicgroup_group_type_dynamicgroup_tags_and_more.py +108 -0
  239. nautobot/extras/migrations/0110_alter_configcontext_cluster_groups_and_more.py +111 -0
  240. nautobot/extras/migrations/0111_metadata.py +162 -0
  241. nautobot/extras/migrations/0112_dynamic_group_group_type_data_migration.py +28 -0
  242. nautobot/extras/migrations/0113_saved_views.py +77 -0
  243. nautobot/extras/models/__init__.py +15 -1
  244. nautobot/extras/models/change_logging.py +3 -3
  245. nautobot/extras/models/contacts.py +4 -0
  246. nautobot/extras/models/customfields.py +18 -3
  247. nautobot/extras/models/groups.py +389 -225
  248. nautobot/extras/models/jobs.py +4 -84
  249. nautobot/extras/models/metadata.py +441 -0
  250. nautobot/extras/models/mixins.py +72 -62
  251. nautobot/extras/models/models.py +116 -9
  252. nautobot/extras/models/relationships.py +9 -2
  253. nautobot/extras/models/tags.py +13 -2
  254. nautobot/extras/navigation.py +57 -0
  255. nautobot/extras/plugins/__init__.py +3 -1
  256. nautobot/extras/querysets.py +30 -66
  257. nautobot/extras/signals.py +96 -114
  258. nautobot/extras/tables.py +171 -47
  259. nautobot/extras/templates/extras/dynamicgroup.html +44 -15
  260. nautobot/extras/templates/extras/dynamicgroup_edit.html +2 -0
  261. nautobot/extras/templates/extras/job.html +1 -1
  262. nautobot/extras/templates/extras/job_detail.html +0 -11
  263. nautobot/extras/templates/extras/jobresult.html +61 -74
  264. nautobot/extras/templates/extras/metadatatype_create.html +89 -0
  265. nautobot/extras/templates/extras/metadatatype_retrieve.html +67 -0
  266. nautobot/extras/templates/extras/object_dynamicgroups.html +7 -0
  267. nautobot/extras/templates/extras/objectchange_list.html +0 -12
  268. nautobot/extras/templates/extras/plugins_list.html +1 -3
  269. nautobot/extras/templates/extras/role_retrieve.html +48 -0
  270. nautobot/extras/templates/extras/staticgroupassociation_retrieve.html +20 -0
  271. nautobot/extras/tests/integration/test_customfields.py +1 -0
  272. nautobot/extras/tests/test_api.py +501 -22
  273. nautobot/extras/tests/test_changelog.py +20 -9
  274. nautobot/extras/tests/test_context_managers.py +22 -15
  275. nautobot/extras/tests/test_datasources.py +13 -1
  276. nautobot/extras/tests/test_dynamicgroups.py +201 -171
  277. nautobot/extras/tests/test_filters.py +211 -12
  278. nautobot/extras/tests/test_jobs.py +4 -4
  279. nautobot/extras/tests/test_models.py +499 -4
  280. nautobot/extras/tests/test_relationships.py +1 -0
  281. nautobot/extras/tests/test_views.py +565 -28
  282. nautobot/extras/tests/test_webhooks.py +1 -1
  283. nautobot/extras/urls.py +5 -0
  284. nautobot/extras/utils.py +56 -45
  285. nautobot/extras/views.py +585 -96
  286. nautobot/ipam/__init__.py +0 -1
  287. nautobot/ipam/apps.py +1 -0
  288. nautobot/ipam/factory.py +17 -19
  289. nautobot/ipam/filters.py +14 -1
  290. nautobot/ipam/forms.py +9 -5
  291. nautobot/ipam/graphql/types.py +2 -2
  292. nautobot/ipam/migrations/0047_alter_ipaddress_role_alter_ipaddress_status_and_more.py +59 -0
  293. nautobot/ipam/models.py +23 -9
  294. nautobot/ipam/querysets.py +1 -1
  295. nautobot/ipam/signals.py +4 -2
  296. nautobot/ipam/tables.py +1 -0
  297. nautobot/ipam/templates/ipam/ipaddress_interfaces.html +1 -1
  298. nautobot/ipam/templates/ipam/ipaddress_vm_interfaces.html +1 -1
  299. nautobot/ipam/templates/ipam/prefix.html +1 -0
  300. nautobot/ipam/tests/test_api.py +37 -18
  301. nautobot/ipam/tests/test_filters.py +26 -2
  302. nautobot/ipam/tests/test_models.py +8 -3
  303. nautobot/ipam/tests/test_querysets.py +1 -1
  304. nautobot/ipam/tests/test_views.py +3 -2
  305. nautobot/ipam/urls.py +2 -2
  306. nautobot/ipam/views.py +25 -28
  307. nautobot/project-static/css/base.css +20 -1
  308. nautobot/project-static/css/dark.css +11 -0
  309. nautobot/project-static/docs/404.html +884 -80
  310. nautobot/project-static/docs/apps/index.html +884 -80
  311. nautobot/project-static/docs/apps/nautobot-apps.html +884 -80
  312. nautobot/project-static/docs/assets/_mkdocstrings.css +5 -0
  313. nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +911 -112
  314. nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +896 -93
  315. nautobot/project-static/docs/code-reference/nautobot/apps/api.html +1457 -790
  316. nautobot/project-static/docs/code-reference/nautobot/apps/change_logging.html +927 -136
  317. nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +969 -180
  318. nautobot/project-static/docs/code-reference/nautobot/apps/config.html +893 -91
  319. nautobot/project-static/docs/code-reference/nautobot/apps/constants.html +889 -85
  320. nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +983 -185
  321. nautobot/project-static/docs/code-reference/nautobot/apps/exceptions.html +938 -143
  322. nautobot/project-static/docs/code-reference/nautobot/apps/factory.html +1064 -274
  323. nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +1190 -346
  324. nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +1663 -865
  325. nautobot/project-static/docs/code-reference/nautobot/apps/graphql.html +1156 -373
  326. nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +2200 -1502
  327. nautobot/project-static/docs/code-reference/nautobot/apps/models.html +2229 -1421
  328. nautobot/project-static/docs/code-reference/nautobot/apps/querysets.html +904 -103
  329. nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +955 -155
  330. nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +1002 -215
  331. nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +1911 -1275
  332. nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +1835 -1091
  333. nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +896 -93
  334. nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +2323 -1693
  335. nautobot/project-static/docs/code-reference/nautobot/apps/views.html +1785 -1023
  336. nautobot/project-static/docs/development/apps/api/configuration-view.html +884 -80
  337. nautobot/project-static/docs/development/apps/api/database-backend-config.html +884 -80
  338. nautobot/project-static/docs/development/apps/api/models/django-admin.html +884 -80
  339. nautobot/project-static/docs/development/apps/api/models/global-search.html +884 -80
  340. nautobot/project-static/docs/development/apps/api/models/graphql.html +884 -80
  341. nautobot/project-static/docs/development/apps/api/models/index.html +922 -81
  342. nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +884 -80
  343. nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +884 -80
  344. nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +884 -80
  345. nautobot/project-static/docs/development/apps/api/platform-features/git-repository-content.html +884 -80
  346. nautobot/project-static/docs/development/apps/api/platform-features/index.html +884 -80
  347. nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +884 -80
  348. nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +884 -80
  349. nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +884 -80
  350. nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +884 -80
  351. nautobot/project-static/docs/development/apps/api/platform-features/uniquely-identify-objects.html +884 -80
  352. nautobot/project-static/docs/development/apps/api/prometheus.html +884 -80
  353. nautobot/project-static/docs/development/apps/api/setup.html +884 -80
  354. nautobot/project-static/docs/development/apps/api/testing.html +884 -80
  355. nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +884 -80
  356. nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +884 -80
  357. nautobot/project-static/docs/development/apps/api/ui-extensions/index.html +884 -80
  358. nautobot/project-static/docs/development/apps/api/ui-extensions/navigation.html +884 -80
  359. nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +884 -80
  360. nautobot/project-static/docs/development/apps/api/views/base-template.html +884 -80
  361. nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +884 -80
  362. nautobot/project-static/docs/development/apps/api/views/django-generic-views.html +884 -80
  363. nautobot/project-static/docs/development/apps/api/views/help-documentation.html +884 -80
  364. nautobot/project-static/docs/development/apps/api/views/index.html +884 -80
  365. nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +884 -80
  366. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +884 -80
  367. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +884 -80
  368. nautobot/project-static/docs/development/apps/api/views/notes.html +884 -80
  369. nautobot/project-static/docs/development/apps/api/views/rest-api.html +884 -80
  370. nautobot/project-static/docs/development/apps/api/views/urls.html +884 -80
  371. nautobot/project-static/docs/development/apps/index.html +884 -80
  372. nautobot/project-static/docs/development/apps/migration/code-updates.html +884 -80
  373. nautobot/project-static/docs/development/apps/migration/dependency-updates.html +884 -80
  374. nautobot/project-static/docs/development/apps/migration/from-v1.html +884 -80
  375. nautobot/project-static/docs/development/apps/migration/model-updates/dcim.html +884 -80
  376. nautobot/project-static/docs/development/apps/migration/model-updates/extras.html +884 -80
  377. nautobot/project-static/docs/development/apps/migration/model-updates/global.html +884 -80
  378. nautobot/project-static/docs/development/apps/migration/model-updates/ipam.html +884 -80
  379. nautobot/project-static/docs/development/apps/porting-from-netbox.html +884 -80
  380. nautobot/project-static/docs/development/core/application-registry.html +884 -80
  381. nautobot/project-static/docs/development/core/best-practices.html +885 -80
  382. nautobot/project-static/docs/development/core/bootstrap-ui.html +884 -80
  383. nautobot/project-static/docs/development/core/caching.html +884 -80
  384. nautobot/project-static/docs/development/core/controllers.html +884 -80
  385. nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +884 -80
  386. nautobot/project-static/docs/development/core/generic-views.html +884 -80
  387. nautobot/project-static/docs/development/core/getting-started.html +884 -80
  388. nautobot/project-static/docs/development/core/homepage.html +884 -80
  389. nautobot/project-static/docs/development/core/index.html +884 -91
  390. nautobot/project-static/docs/development/core/model-checklist.html +887 -81
  391. nautobot/project-static/docs/development/core/model-features.html +884 -80
  392. nautobot/project-static/docs/development/core/natural-keys.html +884 -80
  393. nautobot/project-static/docs/development/core/navigation-menu.html +884 -80
  394. nautobot/project-static/docs/development/core/release-checklist.html +887 -83
  395. nautobot/project-static/docs/development/core/role-internals.html +884 -80
  396. nautobot/project-static/docs/development/core/settings.html +884 -80
  397. nautobot/project-static/docs/development/core/style-guide.html +885 -81
  398. nautobot/project-static/docs/development/core/templates.html +896 -81
  399. nautobot/project-static/docs/development/core/testing.html +884 -80
  400. nautobot/project-static/docs/development/core/user-preferences.html +884 -80
  401. nautobot/project-static/docs/development/index.html +884 -80
  402. nautobot/project-static/docs/development/jobs/index.html +1247 -457
  403. nautobot/project-static/docs/development/jobs/migration/from-v1.html +884 -80
  404. nautobot/project-static/docs/index.html +13 -8228
  405. nautobot/project-static/docs/media/models/cloud_aws_direct_connect_dark.png +0 -0
  406. nautobot/project-static/docs/media/models/cloud_aws_direct_connect_light.png +0 -0
  407. nautobot/project-static/docs/models/cloud/cloudaccount.html +15 -0
  408. nautobot/project-static/docs/models/cloud/cloudnetwork.html +15 -0
  409. nautobot/project-static/docs/models/cloud/cloudnetworkprefixassignment.html +15 -0
  410. nautobot/project-static/docs/models/cloud/cloudresourcetype.html +15 -0
  411. nautobot/project-static/docs/models/cloud/cloudservice.html +15 -0
  412. nautobot/project-static/docs/models/cloud/cloudservicenetworkassignment.html +15 -0
  413. nautobot/project-static/docs/models/dcim/module.html +15 -0
  414. nautobot/project-static/docs/models/dcim/modulebay.html +15 -0
  415. nautobot/project-static/docs/models/dcim/modulebaytemplate.html +15 -0
  416. nautobot/project-static/docs/models/dcim/moduletype.html +15 -0
  417. nautobot/project-static/docs/models/extras/metadatachoice.html +15 -0
  418. nautobot/project-static/docs/models/extras/metadatatype.html +15 -0
  419. nautobot/project-static/docs/models/extras/objectmetadata.html +15 -0
  420. nautobot/project-static/docs/models/extras/role.html +15 -0
  421. nautobot/project-static/docs/models/extras/savedview.html +15 -0
  422. nautobot/project-static/docs/models/extras/staticgroupassociation.html +15 -0
  423. nautobot/project-static/docs/models/extras/status.html +15 -0
  424. nautobot/project-static/docs/objects.inv +0 -0
  425. nautobot/project-static/docs/overview/application_stack.html +892 -81
  426. nautobot/project-static/docs/overview/design_philosophy.html +886 -82
  427. nautobot/project-static/docs/overview/index.html +9032 -13
  428. nautobot/project-static/docs/release-notes/index.html +887 -83
  429. nautobot/project-static/docs/release-notes/version-1.0.html +884 -80
  430. nautobot/project-static/docs/release-notes/version-1.1.html +884 -80
  431. nautobot/project-static/docs/release-notes/version-1.2.html +884 -80
  432. nautobot/project-static/docs/release-notes/version-1.3.html +884 -80
  433. nautobot/project-static/docs/release-notes/version-1.4.html +884 -80
  434. nautobot/project-static/docs/release-notes/version-1.5.html +885 -81
  435. nautobot/project-static/docs/release-notes/version-1.6.html +885 -81
  436. nautobot/project-static/docs/release-notes/version-2.0.html +884 -80
  437. nautobot/project-static/docs/release-notes/version-2.1.html +884 -80
  438. nautobot/project-static/docs/release-notes/version-2.2.html +990 -323
  439. nautobot/project-static/docs/release-notes/version-2.3.html +9524 -0
  440. nautobot/project-static/docs/requirements.txt +4 -4
  441. nautobot/project-static/docs/search/search_index.json +1 -1
  442. nautobot/project-static/docs/sitemap.xml +335 -260
  443. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  444. nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +884 -80
  445. nautobot/project-static/docs/user-guide/administration/configuration/authentication/remote.html +884 -80
  446. nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +884 -80
  447. nautobot/project-static/docs/user-guide/administration/configuration/index.html +884 -80
  448. nautobot/project-static/docs/user-guide/administration/configuration/optional-settings.html +983 -197
  449. nautobot/project-static/docs/user-guide/administration/configuration/required-settings.html +884 -80
  450. nautobot/project-static/docs/user-guide/administration/configuration/time-zones.html +884 -80
  451. nautobot/project-static/docs/user-guide/administration/guides/caching.html +884 -80
  452. nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +888 -84
  453. nautobot/project-static/docs/user-guide/administration/guides/healthcheck.html +884 -80
  454. nautobot/project-static/docs/user-guide/administration/guides/permissions.html +884 -80
  455. nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +884 -80
  456. nautobot/project-static/docs/user-guide/administration/guides/replicating-nautobot.html +884 -80
  457. nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +884 -80
  458. nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +884 -80
  459. nautobot/project-static/docs/user-guide/administration/installation/app-install.html +884 -80
  460. nautobot/project-static/docs/user-guide/administration/installation/external-authentication.html +884 -80
  461. nautobot/project-static/docs/user-guide/administration/installation/http-server.html +884 -80
  462. nautobot/project-static/docs/user-guide/administration/installation/index.html +888 -80
  463. nautobot/project-static/docs/user-guide/administration/installation/install_system.html +884 -80
  464. nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +884 -80
  465. nautobot/project-static/docs/user-guide/administration/installation/services.html +888 -80
  466. nautobot/project-static/docs/user-guide/administration/installation-extras/docker.html +900 -91
  467. nautobot/project-static/docs/user-guide/administration/installation-extras/health-checks.html +884 -80
  468. nautobot/project-static/docs/user-guide/administration/installation-extras/selinux-troubleshooting.html +884 -80
  469. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-netbox.html +884 -80
  470. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +884 -80
  471. nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +915 -163
  472. nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +884 -80
  473. nautobot/project-static/docs/user-guide/administration/upgrading/database-backup.html +884 -80
  474. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/after-you-upgrade.html +884 -80
  475. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/before-you-upgrade.html +884 -80
  476. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/for-developers.html +884 -80
  477. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/index.html +884 -80
  478. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/whats-changed.html +884 -80
  479. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/region-and-site-data-migration-guide.html +884 -80
  480. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +884 -80
  481. nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +884 -80
  482. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuit.html +884 -80
  483. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittermination.html +885 -81
  484. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittype.html +884 -80
  485. nautobot/project-static/docs/user-guide/core-data-model/circuits/provider.html +888 -80
  486. nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +887 -83
  487. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloud.html +8984 -0
  488. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudaccount.html +8828 -0
  489. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetwork.html +8829 -0
  490. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetworkprefixassignment.html +8828 -0
  491. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudresourcetype.html +8829 -0
  492. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservice.html +8833 -0
  493. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservicenetworkassignment.html +8828 -0
  494. nautobot/project-static/docs/user-guide/core-data-model/dcim/cable.html +898 -94
  495. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +915 -97
  496. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +915 -97
  497. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +910 -92
  498. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +915 -97
  499. nautobot/project-static/docs/user-guide/core-data-model/dcim/controller.html +898 -94
  500. nautobot/project-static/docs/user-guide/core-data-model/dcim/controllermanageddevicegroup.html +898 -94
  501. nautobot/project-static/docs/user-guide/core-data-model/dcim/device.html +905 -97
  502. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +912 -108
  503. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +913 -109
  504. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicefamily.html +910 -106
  505. nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +898 -94
  506. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +906 -97
  507. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +918 -100
  508. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +928 -110
  509. nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +920 -98
  510. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +898 -94
  511. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +929 -111
  512. nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +920 -102
  513. nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +910 -106
  514. nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +913 -109
  515. nautobot/project-static/docs/user-guide/core-data-model/dcim/manufacturer.html +914 -106
  516. nautobot/project-static/docs/user-guide/core-data-model/dcim/module.html +8828 -0
  517. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebay.html +8846 -0
  518. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebaytemplate.html +8843 -0
  519. nautobot/project-static/docs/user-guide/core-data-model/dcim/moduletype.html +8823 -0
  520. nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +908 -104
  521. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerfeed.html +898 -94
  522. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +932 -75
  523. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +916 -98
  524. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerpanel.html +898 -94
  525. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +935 -78
  526. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +913 -95
  527. nautobot/project-static/docs/user-guide/core-data-model/dcim/rack.html +921 -117
  528. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackgroup.html +910 -106
  529. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackreservation.html +898 -94
  530. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +914 -96
  531. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +916 -98
  532. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +898 -94
  533. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareversion.html +898 -94
  534. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualchassis.html +898 -94
  535. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +884 -80
  536. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +884 -80
  537. nautobot/project-static/docs/user-guide/core-data-model/extras/contact.html +889 -81
  538. nautobot/project-static/docs/user-guide/core-data-model/extras/team.html +889 -81
  539. nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +884 -80
  540. nautobot/project-static/docs/user-guide/core-data-model/ipam/namespace.html +884 -80
  541. nautobot/project-static/docs/user-guide/core-data-model/ipam/prefix.html +884 -80
  542. nautobot/project-static/docs/user-guide/core-data-model/ipam/rir.html +884 -80
  543. nautobot/project-static/docs/user-guide/core-data-model/ipam/routetarget.html +884 -80
  544. nautobot/project-static/docs/user-guide/core-data-model/ipam/service.html +884 -80
  545. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +884 -80
  546. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlangroup.html +884 -80
  547. nautobot/project-static/docs/user-guide/core-data-model/ipam/vrf.html +884 -80
  548. nautobot/project-static/docs/user-guide/core-data-model/overview/introduction.html +884 -80
  549. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenant.html +893 -88
  550. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenantgroup.html +884 -80
  551. nautobot/project-static/docs/user-guide/core-data-model/virtualization/cluster.html +884 -80
  552. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustergroup.html +884 -80
  553. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustertype.html +884 -80
  554. nautobot/project-static/docs/user-guide/core-data-model/virtualization/virtualmachine.html +884 -80
  555. nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +889 -81
  556. nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +884 -80
  557. nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +884 -80
  558. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +884 -80
  559. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +884 -80
  560. nautobot/project-static/docs/user-guide/feature-guides/getting-started/index.html +884 -80
  561. nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +884 -80
  562. nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +884 -80
  563. nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +884 -80
  564. nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +884 -80
  565. nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +884 -80
  566. nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +884 -80
  567. nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +884 -80
  568. nautobot/project-static/docs/user-guide/feature-guides/graphql.html +884 -80
  569. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/clear-view-button.png +0 -0
  570. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/cleared-view.png +0 -0
  571. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/config-table-columns-to-locations.png +0 -0
  572. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/configure-button.png +0 -0
  573. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/create-saved-view-success.png +0 -0
  574. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/current-saved-view-drop-down-menu.png +0 -0
  575. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/default-location-list-view.png +0 -0
  576. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/dropdown-button-after-new-saved-view.png +0 -0
  577. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/filter-application-to-locations.png +0 -0
  578. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/filter-button.png +0 -0
  579. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/global-default-location-list-view.png +0 -0
  580. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/location-list-view-with-saved-views.png +0 -0
  581. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/navigation-menu.png +0 -0
  582. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/save-as-new-view-drop-down.png +0 -0
  583. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/save-view-modal.png +0 -0
  584. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/saved-view-admin-edit-buttons.png +0 -0
  585. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/saved-view-admin-edit-success.png +0 -0
  586. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/saved-view-admin-edit-view-unchecked.png +0 -0
  587. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/saved-view-admin-edit-view.png +0 -0
  588. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/saved-view-different-user.png +0 -0
  589. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/saved-view-modal-unchecked.png +0 -0
  590. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/set-as-my-default-button.png +0 -0
  591. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/set-as-my-default-success.png +0 -0
  592. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/unsaved-saved-view.png +0 -0
  593. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/updated-saved-view.png +0 -0
  594. nautobot/project-static/docs/user-guide/feature-guides/ip-address-merge-tool.html +884 -80
  595. nautobot/project-static/docs/user-guide/feature-guides/relationships.html +884 -80
  596. nautobot/project-static/docs/user-guide/feature-guides/software-image-files-and-versions.html +884 -80
  597. nautobot/project-static/docs/user-guide/index.html +884 -80
  598. nautobot/project-static/docs/user-guide/platform-functionality/change-logging.html +884 -80
  599. nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +884 -80
  600. nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +884 -80
  601. nautobot/project-static/docs/user-guide/platform-functionality/customlink.html +884 -80
  602. nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +1250 -777
  603. nautobot/project-static/docs/user-guide/platform-functionality/exporttemplate.html +887 -83
  604. nautobot/project-static/docs/user-guide/platform-functionality/externalintegration.html +884 -80
  605. nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +884 -80
  606. nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +884 -80
  607. nautobot/project-static/docs/user-guide/platform-functionality/graphqlquery.html +884 -80
  608. nautobot/project-static/docs/user-guide/platform-functionality/imageattachment.html +884 -80
  609. nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +884 -80
  610. nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +887 -83
  611. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +884 -80
  612. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +884 -80
  613. nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +888 -80
  614. nautobot/project-static/docs/user-guide/platform-functionality/metadata.html +8948 -0
  615. nautobot/project-static/docs/user-guide/platform-functionality/napalm.html +887 -83
  616. nautobot/project-static/docs/user-guide/platform-functionality/note.html +884 -80
  617. nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +884 -80
  618. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +884 -80
  619. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +884 -80
  620. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +884 -80
  621. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/ui-related-endpoints.html +884 -80
  622. nautobot/project-static/docs/user-guide/platform-functionality/role.html +887 -83
  623. nautobot/project-static/docs/user-guide/platform-functionality/savedview.html +9137 -0
  624. nautobot/project-static/docs/user-guide/platform-functionality/secret.html +887 -83
  625. nautobot/project-static/docs/user-guide/platform-functionality/staticgroupassociation.html +8933 -0
  626. nautobot/project-static/docs/user-guide/platform-functionality/status.html +884 -80
  627. nautobot/project-static/docs/user-guide/platform-functionality/tag.html +884 -80
  628. nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +942 -113
  629. nautobot/project-static/docs/user-guide/platform-functionality/users/objectpermission.html +884 -80
  630. nautobot/project-static/docs/user-guide/platform-functionality/users/token.html +884 -80
  631. nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +884 -80
  632. nautobot/project-static/js/forms.js +71 -0
  633. nautobot/project-static/js/table_sorting_indicator.js +46 -0
  634. nautobot/project-static/js/tableconfig.js +6 -1
  635. nautobot/project-static/materialdesignicons-7.4.47/css/materialdesignicons.min.css +3 -0
  636. nautobot/project-static/{materialdesignicons-6.5.95 → materialdesignicons-7.4.47}/fonts/materialdesignicons-webfont.eot +0 -0
  637. nautobot/project-static/{materialdesignicons-6.5.95 → materialdesignicons-7.4.47}/fonts/materialdesignicons-webfont.ttf +0 -0
  638. nautobot/project-static/materialdesignicons-7.4.47/fonts/materialdesignicons-webfont.woff +0 -0
  639. nautobot/project-static/materialdesignicons-7.4.47/fonts/materialdesignicons-webfont.woff2 +0 -0
  640. nautobot/tenancy/__init__.py +0 -1
  641. nautobot/tenancy/apps.py +1 -0
  642. nautobot/tenancy/factory.py +3 -2
  643. nautobot/tenancy/filters/__init__.py +1 -0
  644. nautobot/tenancy/forms.py +1 -1
  645. nautobot/tenancy/templates/tenancy/tenant.html +22 -18
  646. nautobot/tenancy/views.py +11 -10
  647. nautobot/users/__init__.py +0 -1
  648. nautobot/users/api/serializers.py +1 -1
  649. nautobot/users/api/views.py +4 -2
  650. nautobot/users/apps.py +3 -2
  651. nautobot/users/factory.py +3 -3
  652. nautobot/users/migrations/0010_user_default_saved_views.py +20 -0
  653. nautobot/users/models.py +12 -0
  654. nautobot/users/tests/test_filters.py +6 -3
  655. nautobot/users/urls.py +8 -0
  656. nautobot/virtualization/__init__.py +0 -1
  657. nautobot/virtualization/apps.py +1 -0
  658. nautobot/virtualization/filters.py +6 -1
  659. nautobot/virtualization/forms.py +11 -3
  660. nautobot/virtualization/graphql/types.py +2 -2
  661. nautobot/virtualization/migrations/0029_add_role_field_to_interface_models.py +27 -0
  662. nautobot/virtualization/migrations/0030_alter_virtualmachine_local_config_context_data_owner_content_type_and_more.py +67 -0
  663. nautobot/virtualization/tables.py +15 -5
  664. nautobot/virtualization/templates/virtualization/virtualmachine.html +1 -1
  665. nautobot/virtualization/templates/virtualization/vminterface.html +7 -1
  666. nautobot/virtualization/templates/virtualization/vminterface_edit.html +1 -0
  667. nautobot/virtualization/tests/test_api.py +9 -4
  668. nautobot/virtualization/tests/test_filters.py +22 -0
  669. nautobot/virtualization/tests/test_models.py +7 -3
  670. nautobot/virtualization/tests/test_views.py +19 -3
  671. nautobot/virtualization/urls.py +2 -2
  672. nautobot/virtualization/views.py +10 -32
  673. {nautobot-2.2.9.dist-info → nautobot-2.3.0b1.dist-info}/METADATA +21 -19
  674. {nautobot-2.2.9.dist-info → nautobot-2.3.0b1.dist-info}/RECORD +679 -559
  675. nautobot/project-static/materialdesignicons-6.5.95/.github/ISSUE_TEMPLATE.md +0 -3
  676. nautobot/project-static/materialdesignicons-6.5.95/README.md +0 -25
  677. nautobot/project-static/materialdesignicons-6.5.95/css/materialdesignicons.css +0 -26654
  678. nautobot/project-static/materialdesignicons-6.5.95/css/materialdesignicons.css.map +0 -16
  679. nautobot/project-static/materialdesignicons-6.5.95/css/materialdesignicons.min.css +0 -3
  680. nautobot/project-static/materialdesignicons-6.5.95/css/materialdesignicons.min.css.map +0 -16
  681. nautobot/project-static/materialdesignicons-6.5.95/fonts/materialdesignicons-webfont.woff +0 -0
  682. nautobot/project-static/materialdesignicons-6.5.95/fonts/materialdesignicons-webfont.woff2 +0 -0
  683. nautobot/project-static/materialdesignicons-6.5.95/package.json +0 -28
  684. nautobot/project-static/materialdesignicons-6.5.95/preview.html +0 -717
  685. nautobot/project-static/materialdesignicons-6.5.95/scss/_animated.scss +0 -27
  686. nautobot/project-static/materialdesignicons-6.5.95/scss/_core.scss +0 -10
  687. nautobot/project-static/materialdesignicons-6.5.95/scss/_extras.scss +0 -65
  688. nautobot/project-static/materialdesignicons-6.5.95/scss/_functions.scss +0 -20
  689. nautobot/project-static/materialdesignicons-6.5.95/scss/_icons.scss +0 -10
  690. nautobot/project-static/materialdesignicons-6.5.95/scss/_path.scss +0 -10
  691. nautobot/project-static/materialdesignicons-6.5.95/scss/_variables.scss +0 -6606
  692. nautobot/project-static/materialdesignicons-6.5.95/scss/materialdesignicons.scss +0 -8
  693. /nautobot/project-static/{materialdesignicons-6.5.95 → materialdesignicons-7.4.47}/LICENSE +0 -0
  694. {nautobot-2.2.9.dist-info → nautobot-2.3.0b1.dist-info}/LICENSE.txt +0 -0
  695. {nautobot-2.2.9.dist-info → nautobot-2.3.0b1.dist-info}/NOTICE +0 -0
  696. {nautobot-2.2.9.dist-info → nautobot-2.3.0b1.dist-info}/WHEEL +0 -0
  697. {nautobot-2.2.9.dist-info → nautobot-2.3.0b1.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,37 +1903,6 @@ 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
 
1267
- class DeviceBayTestCase(ModelTestCases.BaseModelTestCase):
1268
- model = DeviceBay
1269
-
1270
- def setUp(self):
1271
- self.devices = Device.objects.filter(device_type__subdevice_role=SubdeviceRoleChoices.ROLE_PARENT)
1272
- devicetype = DeviceType.objects.create(
1273
- manufacturer=self.devices[0].device_type.manufacturer,
1274
- model="TestDeviceType1",
1275
- u_height=0,
1276
- subdevice_role=SubdeviceRoleChoices.ROLE_CHILD,
1277
- )
1278
- child_device = Device.objects.create(
1279
- device_type=devicetype,
1280
- role=self.devices[0].role,
1281
- name="TestDevice1",
1282
- status=self.devices[0].status,
1283
- location=self.devices[0].location,
1284
- )
1285
- DeviceBay.objects.create(device=self.devices[0], name="Device Bay 1", installed_device=child_device)
1286
-
1287
- def test_assigning_installed_device(self):
1288
- server = Device.objects.exclude(device_type__subdevice_role=SubdeviceRoleChoices.ROLE_CHILD).last()
1289
- bay = DeviceBay(device=self.devices[1], name="Device Bay Err", installed_device=server)
1290
- with self.assertRaises(ValidationError) as err:
1291
- bay.validated_save()
1292
- self.assertIn(
1293
- f'Cannot install device "{server}"; device-type "{server.device_type}" subdevice_role is not "child".',
1294
- str(err.exception),
1295
- )
1296
-
1297
-
1298
1906
  class DeviceTypeToSoftwareImageFileTestCase(ModelTestCases.BaseModelTestCase):
1299
1907
  model = DeviceTypeToSoftwareImageFile
1300
1908
 
@@ -1667,9 +2275,16 @@ class PowerPanelTestCase(TestCase): # TODO: change to BaseModelTestCase once we
1667
2275
  )
1668
2276
 
1669
2277
 
1670
- class InterfaceTestCase(TestCase): # TODO: change to BaseModelTestCase once we have an InterfaceFactory
2278
+ class InterfaceTestCase(ModularDeviceComponentTestCaseMixin, ModelTestCases.BaseModelTestCase):
2279
+ model = Interface
2280
+
1671
2281
  @classmethod
1672
2282
  def setUpTestData(cls):
2283
+ super().setUpTestData()
2284
+ cls.modular_component_create_data = {
2285
+ "type": InterfaceTypeChoices.TYPE_1GE_FIXED,
2286
+ "status": Status.objects.get_for_model(Interface).first(),
2287
+ }
1673
2288
  manufacturer = Manufacturer.objects.first()
1674
2289
  devicetype = DeviceType.objects.create(manufacturer=manufacturer, model="Device Type 1")
1675
2290
  devicerole = Role.objects.get_for_model(Device).first()
@@ -1715,6 +2330,7 @@ class InterfaceTestCase(TestCase): # TODO: change to BaseModelTestCase once we
1715
2330
  type=InterfaceTypeChoices.TYPE_VIRTUAL,
1716
2331
  device=self.device,
1717
2332
  status=Status.objects.get_for_model(Interface).first(),
2333
+ role=Role.objects.get_for_model(Interface).first(),
1718
2334
  )
1719
2335
  with self.assertRaises(ValidationError) as err:
1720
2336
  interface.tagged_vlans.add(self.vlan)
@@ -1729,6 +2345,7 @@ class InterfaceTestCase(TestCase): # TODO: change to BaseModelTestCase once we
1729
2345
  mode=InterfaceModeChoices.MODE_TAGGED,
1730
2346
  device=self.device,
1731
2347
  status=Status.objects.get_for_model(Interface).first(),
2348
+ role=Role.objects.get_for_model(Interface).first(),
1732
2349
  )
1733
2350
  interface.tagged_vlans.add(self.other_location_vlan)
1734
2351
  self.assertEqual(
@@ -1744,6 +2361,7 @@ class InterfaceTestCase(TestCase): # TODO: change to BaseModelTestCase once we
1744
2361
  type=InterfaceTypeChoices.TYPE_VIRTUAL,
1745
2362
  device=self.device,
1746
2363
  status=Status.objects.get_for_model(Interface).first(),
2364
+ role=Role.objects.get_for_model(Interface).first(),
1747
2365
  )
1748
2366
  ips = list(IPAddress.objects.filter(parent__namespace=self.namespace))
1749
2367
 
@@ -1769,6 +2387,7 @@ class InterfaceTestCase(TestCase): # TODO: change to BaseModelTestCase once we
1769
2387
  type=InterfaceTypeChoices.TYPE_VIRTUAL,
1770
2388
  device=self.device,
1771
2389
  status=Status.objects.get_for_model(Interface).first(),
2390
+ role=Role.objects.get_for_model(Interface).first(),
1772
2391
  )
1773
2392
  ips = list(IPAddress.objects.filter(parent__namespace=self.namespace))
1774
2393
 
@@ -2097,3 +2716,417 @@ class ControllerManagedDeviceGroupTestCase(ModelTestCases.BaseModelTestCase):
2097
2716
  controller2,
2098
2717
  "Child group 2 controller should have been updated",
2099
2718
  )
2719
+
2720
+
2721
+ class ModuleBayTestCase(ModularDeviceComponentTestCaseMixin, ModelTestCases.BaseModelTestCase):
2722
+ model = ModuleBay
2723
+ device_field = "parent_device" # field name for the parent device
2724
+ module_field = "parent_module" # field name for the parent module
2725
+
2726
+ @classmethod
2727
+ def setUpTestData(cls):
2728
+ cls.device = Device.objects.first()
2729
+ cls.device.module_bays.all().delete()
2730
+ cls.module = Module.objects.first()
2731
+ cls.module.module_bays.all().delete()
2732
+
2733
+ def test_parent_property(self):
2734
+ """Assert that the parent property walks up the inheritance tree of Device -> ModuleBay -> Module -> ModuleBay."""
2735
+ module_type = ModuleType.objects.first()
2736
+ status = Status.objects.get_for_model(Module).first()
2737
+
2738
+ parent_module_bay = ModuleBay.objects.create(
2739
+ parent_device=self.device,
2740
+ name="1111",
2741
+ position="1111",
2742
+ )
2743
+ module = Module.objects.create(
2744
+ module_type=module_type,
2745
+ parent_module_bay=parent_module_bay,
2746
+ status=status,
2747
+ )
2748
+ child_module_bay = ModuleBay.objects.create(
2749
+ parent_module=module,
2750
+ name="1111",
2751
+ position="1111",
2752
+ )
2753
+ child_module = Module.objects.create(
2754
+ module_type=module_type,
2755
+ parent_module_bay=child_module_bay,
2756
+ status=status,
2757
+ )
2758
+ grandchild_module_bay = ModuleBay.objects.create(
2759
+ parent_module=child_module,
2760
+ name="1111",
2761
+ position="1111",
2762
+ )
2763
+
2764
+ self.assertEqual(parent_module_bay.parent, self.device)
2765
+ self.assertEqual(child_module_bay.parent, self.device)
2766
+ self.assertEqual(grandchild_module_bay.parent, self.device)
2767
+
2768
+ # Remove the module from the module bay and put it in storage
2769
+ module.parent_module_bay = None
2770
+ module.location = Location.objects.get_for_model(Module).first()
2771
+ module.save()
2772
+
2773
+ self.assertEqual(parent_module_bay.parent, self.device)
2774
+ self.assertIsNone(child_module_bay.parent)
2775
+ self.assertIsNone(grandchild_module_bay.parent)
2776
+
2777
+
2778
+ class ModuleBayTemplateTestCase(ModularDeviceComponentTemplateTestCaseMixin, ModelTestCases.BaseModelTestCase):
2779
+ model = ModuleBayTemplate
2780
+
2781
+ @classmethod
2782
+ def setUpTestData(cls):
2783
+ manufacturer = Manufacturer.objects.first()
2784
+ cls.device_type = cls.device = DeviceType.objects.create(
2785
+ manufacturer=manufacturer, model="Test ModuleBayTemplate DT1"
2786
+ )
2787
+ cls.module_type = cls.module = ModuleType.objects.create(
2788
+ manufacturer=manufacturer, model="Test ModuleBayTemplate MT1"
2789
+ )
2790
+
2791
+ # Create some instances for the generic natural key tests to use
2792
+ ModuleBayTemplate.objects.create(
2793
+ device_type=cls.device_type,
2794
+ name="2222",
2795
+ position="2222",
2796
+ )
2797
+ ModuleBayTemplate.objects.create(
2798
+ device_type=cls.device_type,
2799
+ name="3333",
2800
+ position="3333",
2801
+ )
2802
+ ModuleBayTemplate.objects.create(
2803
+ module_type=cls.module_type,
2804
+ name="3333",
2805
+ position="3333",
2806
+ )
2807
+ ModuleBayTemplate.objects.create(
2808
+ module_type=cls.module_type,
2809
+ name="4444",
2810
+ position="4444",
2811
+ )
2812
+
2813
+ def test_parent_property(self):
2814
+ module_bay_template = ModuleBayTemplate.objects.create(
2815
+ device_type=self.device_type,
2816
+ name="1111",
2817
+ position="1111",
2818
+ )
2819
+ self.assertEqual(module_bay_template.parent, self.device_type)
2820
+
2821
+ module_bay_template = ModuleBayTemplate.objects.create(
2822
+ module_type=self.module_type,
2823
+ name="1111",
2824
+ position="1111",
2825
+ )
2826
+ self.assertEqual(module_bay_template.parent, self.module_type)
2827
+
2828
+
2829
+ class ModuleTestCase(ModelTestCases.BaseModelTestCase):
2830
+ model = Module
2831
+
2832
+ @classmethod
2833
+ def setUpTestData(cls):
2834
+ cls.device = Device.objects.filter(module_bays__isnull=True).first()
2835
+ manufacturer = Manufacturer.objects.first()
2836
+ cls.module_type = ModuleType.objects.create(manufacturer=manufacturer, model="module model tests")
2837
+ cls.location = Location.objects.get_for_model(Module).first()
2838
+ cls.status = Status.objects.get_for_model(Module).first()
2839
+
2840
+ # Create ModuleType components
2841
+ ConsolePortTemplate.objects.create(module_type=cls.module_type, name="Console Port 1")
2842
+ ConsoleServerPortTemplate.objects.create(module_type=cls.module_type, name="Console Server Port 1")
2843
+
2844
+ ppt = PowerPortTemplate.objects.create(
2845
+ module_type=cls.module_type,
2846
+ name="Power Port 1",
2847
+ maximum_draw=1000,
2848
+ allocated_draw=500,
2849
+ )
2850
+
2851
+ PowerOutletTemplate.objects.create(
2852
+ module_type=cls.module_type,
2853
+ name="Power Outlet 1",
2854
+ power_port_template=ppt,
2855
+ feed_leg=PowerOutletFeedLegChoices.FEED_LEG_A,
2856
+ )
2857
+
2858
+ InterfaceTemplate.objects.create(
2859
+ module_type=cls.module_type,
2860
+ name="Interface {module.parent.parent}/{module.parent}/{module}",
2861
+ type=InterfaceTypeChoices.TYPE_1GE_FIXED,
2862
+ mgmt_only=True,
2863
+ )
2864
+
2865
+ rpt = RearPortTemplate.objects.create(
2866
+ module_type=cls.module_type,
2867
+ name="Rear Port 1",
2868
+ type=PortTypeChoices.TYPE_8P8C,
2869
+ positions=8,
2870
+ )
2871
+
2872
+ FrontPortTemplate.objects.create(
2873
+ module_type=cls.module_type,
2874
+ name="Front Port 1",
2875
+ type=PortTypeChoices.TYPE_8P8C,
2876
+ rear_port_template=rpt,
2877
+ rear_port_position=2,
2878
+ )
2879
+
2880
+ ModuleBayTemplate.objects.create(
2881
+ module_type=cls.module_type,
2882
+ position="1111",
2883
+ )
2884
+
2885
+ cls.module = Module.objects.create(
2886
+ module_type=cls.module_type,
2887
+ location=cls.location,
2888
+ status=cls.status,
2889
+ )
2890
+ cls.module_bay = cls.module.module_bays.first()
2891
+
2892
+ def test_parent_validation_module_bay_and_location(self):
2893
+ """Assert that a module must have a parent module bay or location but not both."""
2894
+ module = Module(
2895
+ module_type=self.module_type,
2896
+ parent_module_bay=self.module_bay,
2897
+ location=self.location,
2898
+ status=self.status,
2899
+ )
2900
+
2901
+ with self.assertRaises(ValidationError):
2902
+ module.full_clean()
2903
+
2904
+ def test_parent_validation_no_module_bay_or_location(self):
2905
+ """Assert that a module must have a parent module bay or location but not both."""
2906
+ module = Module(
2907
+ module_type=self.module_type,
2908
+ status=self.status,
2909
+ )
2910
+
2911
+ with self.assertRaises(ValidationError):
2912
+ module.full_clean()
2913
+
2914
+ def test_parent_validation_succeeds(self):
2915
+ """Assert that a module must have a parent module bay or location but not both."""
2916
+ with self.subTest("Module with a parent module bay"):
2917
+ module = Module(
2918
+ module_type=self.module_type,
2919
+ parent_module_bay=self.module_bay,
2920
+ status=self.status,
2921
+ )
2922
+
2923
+ module.full_clean()
2924
+ module.save()
2925
+
2926
+ with self.subTest("Module with a parent location"):
2927
+ module = Module(
2928
+ module_type=self.module_type,
2929
+ location=self.location,
2930
+ status=self.status,
2931
+ )
2932
+
2933
+ module.full_clean()
2934
+ module.save()
2935
+
2936
+ def test_device_property(self):
2937
+ """Assert that the device property walks up the inheritance tree of Device -> ModuleBay -> Module -> ModuleBay."""
2938
+ parent_module_bay = ModuleBay.objects.create(
2939
+ parent_device=self.device,
2940
+ position="1111",
2941
+ )
2942
+ parent_module = Module.objects.create(
2943
+ module_type=self.module_type,
2944
+ parent_module_bay=parent_module_bay,
2945
+ status=self.status,
2946
+ )
2947
+ child_module_bay = parent_module.module_bays.first()
2948
+ child_module = Module.objects.create(
2949
+ module_type=self.module_type,
2950
+ parent_module_bay=child_module_bay,
2951
+ status=self.status,
2952
+ )
2953
+ grandchild_module_bay = child_module.module_bays.first()
2954
+ grandchild_module = Module.objects.create(
2955
+ module_type=self.module_type,
2956
+ parent_module_bay=grandchild_module_bay,
2957
+ status=self.status,
2958
+ )
2959
+
2960
+ self.assertEqual(parent_module.device, self.device)
2961
+ self.assertEqual(child_module.device, self.device)
2962
+ self.assertEqual(grandchild_module.device, self.device)
2963
+
2964
+ # Remove the module from the module bay and put it in storage
2965
+ parent_module.parent_module_bay = None
2966
+ parent_module.location = self.location
2967
+ parent_module.save()
2968
+
2969
+ self.assertIsNone(parent_module.device)
2970
+ self.assertIsNone(child_module.device)
2971
+ self.assertIsNone(grandchild_module.device)
2972
+
2973
+ def test_null_serial_asset_tag(self):
2974
+ """Assert that the serial and asset_tag fields are converted to None if a blank string is supplied."""
2975
+ module = Module.objects.create(
2976
+ module_type=self.module_type,
2977
+ status=self.status,
2978
+ parent_module_bay=self.module_bay,
2979
+ serial="",
2980
+ asset_tag="",
2981
+ )
2982
+
2983
+ module.refresh_from_db()
2984
+
2985
+ self.assertIsNone(module.serial)
2986
+ self.assertIsNone(module.asset_tag)
2987
+
2988
+ module.delete()
2989
+ module = Module.objects.create(
2990
+ module_type=self.module_type,
2991
+ status=self.status,
2992
+ parent_module_bay=self.module_bay,
2993
+ )
2994
+
2995
+ module.refresh_from_db()
2996
+
2997
+ self.assertIsNone(module.serial)
2998
+ self.assertIsNone(module.asset_tag)
2999
+
3000
+ def test_module_components_created(self):
3001
+ module = Module(
3002
+ location=self.location,
3003
+ module_type=self.module_type,
3004
+ status=self.status,
3005
+ )
3006
+ module.validated_save()
3007
+ self.assertEqual(module.console_ports.count(), self.module_type.console_port_templates.count())
3008
+ self.assertEqual(module.console_server_ports.count(), self.module_type.console_server_port_templates.count())
3009
+ self.assertEqual(module.power_ports.count(), self.module_type.power_port_templates.count())
3010
+ self.assertEqual(module.power_outlets.count(), self.module_type.power_outlet_templates.count())
3011
+ self.assertEqual(module.interfaces.count(), self.module_type.interface_templates.count())
3012
+ self.assertEqual(module.front_ports.count(), self.module_type.front_port_templates.count())
3013
+ self.assertEqual(module.rear_ports.count(), self.module_type.rear_port_templates.count())
3014
+ self.assertEqual(module.module_bays.count(), self.module_type.module_bay_templates.count())
3015
+
3016
+ ConsolePort.objects.get(module=module, name="Console Port 1")
3017
+
3018
+ ConsoleServerPort.objects.get(module=module, name="Console Server Port 1")
3019
+
3020
+ pp = PowerPort.objects.get(module=module, name="Power Port 1", maximum_draw=1000, allocated_draw=500)
3021
+
3022
+ PowerOutlet.objects.get(
3023
+ module=module,
3024
+ name="Power Outlet 1",
3025
+ power_port=pp,
3026
+ feed_leg=PowerOutletFeedLegChoices.FEED_LEG_A,
3027
+ )
3028
+
3029
+ Interface.objects.get(
3030
+ module=module,
3031
+ name="Interface {module.parent.parent}/{module.parent}/{module}",
3032
+ type=InterfaceTypeChoices.TYPE_1GE_FIXED,
3033
+ mgmt_only=True,
3034
+ )
3035
+
3036
+ rp = RearPort.objects.get(module=module, name="Rear Port 1", type=PortTypeChoices.TYPE_8P8C, positions=8)
3037
+
3038
+ FrontPort.objects.get(
3039
+ module=module,
3040
+ name="Front Port 1",
3041
+ type=PortTypeChoices.TYPE_8P8C,
3042
+ rear_port=rp,
3043
+ rear_port_position=2,
3044
+ )
3045
+
3046
+ ModuleBay.objects.get(parent_module=module, position="1111")
3047
+
3048
+ def test_module_infinite_recursion_self_parent(self):
3049
+ """Assert that a module cannot be its own parent."""
3050
+ module = Module.objects.create(
3051
+ location=self.location,
3052
+ module_type=self.module_type,
3053
+ status=self.status,
3054
+ )
3055
+ module_bay = module.module_bays.first()
3056
+ module.parent_module_bay = module_bay
3057
+
3058
+ with self.assertRaises(ValidationError) as context:
3059
+ module.save()
3060
+ self.assertEqual(context.exception.message, "Creating this instance would cause an infinite loop.")
3061
+
3062
+ def test_module_infinite_recursion_ancestor(self):
3063
+ """Assert that a module cannot be its own ancestor."""
3064
+ parent_module = Module.objects.create(
3065
+ module_type=self.module_type,
3066
+ location=self.location,
3067
+ status=self.status,
3068
+ )
3069
+ parent_module_bay = parent_module.module_bays.first()
3070
+ child_module = Module.objects.create(
3071
+ module_type=self.module_type,
3072
+ parent_module_bay=parent_module_bay,
3073
+ status=self.status,
3074
+ )
3075
+ child_module_bay = child_module.module_bays.first()
3076
+ parent_module.parent_module_bay = child_module_bay
3077
+ parent_module.location = None
3078
+
3079
+ with self.assertRaises(ValidationError) as context:
3080
+ parent_module.save()
3081
+
3082
+ self.assertEqual(context.exception.message, "Creating this instance would cause an infinite loop.")
3083
+
3084
+ def test_render_component_names(self):
3085
+ """Test that creating a Module with components properly renders the {module} and {module.parent} variables."""
3086
+ grandparent_module = Module.objects.create(
3087
+ module_type=self.module_type,
3088
+ location=self.location,
3089
+ status=self.status,
3090
+ )
3091
+ grandparent_module_bay = grandparent_module.module_bays.first()
3092
+ grandparent_module_bay.position = "3"
3093
+ grandparent_module_bay.save()
3094
+ parent_module = Module.objects.create(
3095
+ parent_module_bay=grandparent_module.module_bays.first(),
3096
+ module_type=self.module_type,
3097
+ status=self.status,
3098
+ )
3099
+ parent_module.clean()
3100
+ parent_module_bay = parent_module.module_bays.first()
3101
+ parent_module_bay.position = "2"
3102
+ parent_module_bay.save()
3103
+ child_module = Module.objects.create(
3104
+ parent_module_bay=parent_module.module_bays.first(),
3105
+ module_type=self.module_type,
3106
+ status=self.status,
3107
+ )
3108
+ child_module.clean()
3109
+
3110
+ self.assertEqual(
3111
+ grandparent_module.interfaces.first().name, "Interface {module.parent.parent}/{module.parent}/{module}"
3112
+ )
3113
+ self.assertEqual(parent_module.interfaces.first().name, "Interface {module.parent.parent}/{module.parent}/3")
3114
+ self.assertEqual(child_module.interfaces.first().name, "Interface {module.parent.parent}/3/2")
3115
+
3116
+ # Moving the grandparent module out of inventory populates the template variables on all descendant interfaces
3117
+ grandparent_module.parent_module_bay = self.module_bay
3118
+ grandparent_module.location = None
3119
+ grandparent_module.validated_save()
3120
+ module_bay_position = self.module_bay.position
3121
+ self.assertEqual(
3122
+ grandparent_module.interfaces.first().name,
3123
+ "Interface {module.parent.parent}/{module.parent}/" + module_bay_position,
3124
+ )
3125
+ self.assertEqual(
3126
+ parent_module.interfaces.first().name, "Interface {module.parent.parent}/" + module_bay_position + "/3"
3127
+ )
3128
+ self.assertEqual(child_module.interfaces.first().name, "Interface " + module_bay_position + "/3/2")
3129
+
3130
+
3131
+ class ModuleTypeTestCase(ModelTestCases.BaseModelTestCase):
3132
+ model = ModuleType