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

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

Potentially problematic release.


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

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