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

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

Potentially problematic release.


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

Files changed (704) hide show
  1. nautobot/apps/forms.py +4 -0
  2. nautobot/apps/models.py +10 -1
  3. nautobot/circuits/__init__.py +0 -1
  4. nautobot/circuits/apps.py +1 -0
  5. nautobot/circuits/factory.py +15 -3
  6. nautobot/circuits/filters.py +13 -0
  7. nautobot/circuits/forms.py +13 -0
  8. nautobot/circuits/migrations/0021_alter_circuit_status_alter_circuittermination__path.py +32 -0
  9. nautobot/circuits/migrations/0022_circuittermination_cloud_network.py +25 -0
  10. nautobot/circuits/models.py +16 -3
  11. nautobot/circuits/tables.py +16 -2
  12. nautobot/circuits/templates/circuits/circuittermination_create.html +10 -2
  13. nautobot/circuits/templates/circuits/circuittermination_retrieve.html +6 -0
  14. nautobot/circuits/templates/circuits/inc/circuit_termination.html +6 -1
  15. nautobot/circuits/tests/test_api.py +7 -5
  16. nautobot/circuits/tests/test_filters.py +12 -5
  17. nautobot/circuits/tests/test_models.py +33 -2
  18. nautobot/circuits/views.py +2 -3
  19. nautobot/cloud/__init__.py +0 -0
  20. nautobot/cloud/api/__init__.py +0 -0
  21. nautobot/cloud/api/serializers.py +54 -0
  22. nautobot/cloud/api/urls.py +16 -0
  23. nautobot/cloud/api/views.py +48 -0
  24. nautobot/cloud/apps.py +13 -0
  25. nautobot/cloud/factory.py +113 -0
  26. nautobot/cloud/filters.py +187 -0
  27. nautobot/cloud/forms.py +339 -0
  28. nautobot/cloud/homepage.py +43 -0
  29. nautobot/cloud/migrations/0001_initial.py +304 -0
  30. nautobot/cloud/migrations/__init__.py +0 -0
  31. nautobot/cloud/models.py +246 -0
  32. nautobot/cloud/navigation.py +85 -0
  33. nautobot/cloud/tables.py +157 -0
  34. nautobot/cloud/templates/cloud/cloudaccount_retrieve.html +43 -0
  35. nautobot/cloud/templates/cloud/cloudnetwork_retrieve.html +122 -0
  36. nautobot/cloud/templates/cloud/cloudnetwork_update.html +33 -0
  37. nautobot/cloud/templates/cloud/cloudresourcetype_retrieve.html +111 -0
  38. nautobot/cloud/templates/cloud/cloudservice_retrieve.html +69 -0
  39. nautobot/cloud/templates/cloud/cloudservice_update.html +25 -0
  40. nautobot/cloud/tests/__init__.py +0 -0
  41. nautobot/cloud/tests/test_api.py +248 -0
  42. nautobot/cloud/tests/test_filters.py +125 -0
  43. nautobot/cloud/tests/test_models.py +43 -0
  44. nautobot/cloud/tests/test_views.py +153 -0
  45. nautobot/cloud/urls.py +14 -0
  46. nautobot/cloud/views.py +181 -0
  47. nautobot/core/__init__.py +0 -3
  48. nautobot/core/api/metadata.py +1 -0
  49. nautobot/core/api/parsers.py +7 -1
  50. nautobot/core/api/urls.py +1 -0
  51. nautobot/core/api/utils.py +1 -0
  52. nautobot/core/api/views.py +4 -0
  53. nautobot/core/apps/__init__.py +6 -3
  54. nautobot/core/constants.py +8 -0
  55. nautobot/core/factory.py +32 -1
  56. nautobot/core/filters.py +110 -14
  57. nautobot/core/forms/fields.py +10 -4
  58. nautobot/core/forms/forms.py +11 -3
  59. nautobot/core/forms/widgets.py +18 -1
  60. nautobot/core/graphql/generators.py +2 -2
  61. nautobot/core/graphql/schema.py +28 -7
  62. nautobot/core/jobs/__init__.py +20 -3
  63. nautobot/core/jobs/cleanup.py +100 -0
  64. nautobot/core/jobs/groups.py +38 -0
  65. nautobot/core/management/commands/generate_test_data.py +116 -3
  66. nautobot/core/models/__init__.py +34 -9
  67. nautobot/core/models/generics.py +19 -3
  68. nautobot/core/models/name_color_content_types.py +7 -28
  69. nautobot/core/models/querysets.py +4 -3
  70. nautobot/core/models/tree_queries.py +1 -1
  71. nautobot/core/models/utils.py +21 -5
  72. nautobot/core/settings.py +15 -19
  73. nautobot/core/settings.yaml +48 -13
  74. nautobot/core/settings_funcs.py +103 -0
  75. nautobot/core/tables.py +130 -56
  76. nautobot/core/templates/admin/search_form.html +1 -1
  77. nautobot/core/templates/buttons/add.html +11 -3
  78. nautobot/core/templates/buttons/consolidated_bulk_action_buttons.html +13 -0
  79. nautobot/core/templates/buttons/consolidated_detail_view_action_buttons.html +13 -0
  80. nautobot/core/templates/buttons/export.html +101 -53
  81. nautobot/core/templates/buttons/job_import.html +11 -3
  82. nautobot/core/templates/generic/object_bulk_destroy.html +3 -1
  83. nautobot/core/templates/generic/object_bulk_update.html +3 -1
  84. nautobot/core/templates/generic/object_changelog.html +0 -9
  85. nautobot/core/templates/generic/object_list.html +156 -17
  86. nautobot/core/templates/generic/object_retrieve.html +80 -16
  87. nautobot/core/templates/inc/extras_features_edit_form_fields.html +8 -0
  88. nautobot/core/templates/inc/javascript.html +2 -0
  89. nautobot/core/templates/inc/media.html +2 -2
  90. nautobot/core/templates/inc/nav_menu.html +1 -0
  91. nautobot/core/templates/inc/paginator.html +7 -7
  92. nautobot/core/templates/inc/search_panel.html +2 -2
  93. nautobot/core/templates/inc/table.html +2 -2
  94. nautobot/core/templates/nautobot_config.py.j2 +28 -8
  95. nautobot/core/templates/utilities/templatetags/dynamic_group_assignment_modal.html +37 -0
  96. nautobot/core/templates/utilities/templatetags/filter_form_modal.html +2 -2
  97. nautobot/core/templates/utilities/templatetags/saved_view_modal.html +38 -0
  98. nautobot/core/templates/utilities/theme_preview.html +25 -8
  99. nautobot/core/templates/utilities/worker_status.html +152 -0
  100. nautobot/core/templatetags/buttons.py +335 -38
  101. nautobot/core/templatetags/form_helpers.py +1 -1
  102. nautobot/core/templatetags/helpers.py +181 -11
  103. nautobot/core/testing/api.py +5 -4
  104. nautobot/core/testing/filters.py +63 -14
  105. nautobot/core/testing/mixins.py +46 -0
  106. nautobot/core/testing/models.py +22 -0
  107. nautobot/core/testing/schema.py +4 -8
  108. nautobot/core/testing/views.py +31 -14
  109. nautobot/core/tests/integration/test_general_functionality.py +1 -1
  110. nautobot/core/tests/integration/test_import_objects_ui.py +1 -0
  111. nautobot/core/tests/integration/test_swagger.py +1 -1
  112. nautobot/core/tests/nautobot_config.py +0 -1
  113. nautobot/core/tests/runner.py +2 -2
  114. nautobot/core/tests/test_api.py +1 -0
  115. nautobot/core/tests/test_authentication.py +7 -2
  116. nautobot/core/tests/test_filters.py +11 -9
  117. nautobot/core/tests/test_forms.py +9 -0
  118. nautobot/core/tests/test_graphql.py +27 -16
  119. nautobot/core/tests/test_jobs.py +204 -2
  120. nautobot/core/tests/test_tables.py +3 -1
  121. nautobot/core/tests/test_templatetags_helpers.py +12 -5
  122. nautobot/core/tests/test_templatetags_netutils.py +3 -3
  123. nautobot/core/tests/test_utils.py +31 -20
  124. nautobot/core/tests/test_views.py +6 -6
  125. nautobot/core/urls.py +8 -3
  126. nautobot/core/utils/deprecation.py +29 -0
  127. nautobot/core/utils/filtering.py +12 -9
  128. nautobot/core/utils/lookup.py +37 -2
  129. nautobot/core/utils/requests.py +4 -1
  130. nautobot/core/views/__init__.py +137 -24
  131. nautobot/core/views/generic.py +119 -67
  132. nautobot/core/views/mixins.py +105 -36
  133. nautobot/core/views/paginator.py +9 -3
  134. nautobot/core/views/renderers.py +121 -56
  135. nautobot/core/views/utils.py +81 -1
  136. nautobot/dcim/__init__.py +0 -1
  137. nautobot/dcim/api/serializers.py +180 -44
  138. nautobot/dcim/api/urls.py +7 -3
  139. nautobot/dcim/api/views.py +53 -7
  140. nautobot/dcim/apps.py +3 -0
  141. nautobot/dcim/choices.py +25 -0
  142. nautobot/dcim/constants.py +7 -0
  143. nautobot/dcim/factory.py +252 -18
  144. nautobot/dcim/filters/__init__.py +373 -193
  145. nautobot/dcim/filters/mixins.py +274 -1
  146. nautobot/dcim/forms.py +834 -121
  147. nautobot/dcim/graphql/types.py +2 -2
  148. nautobot/dcim/homepage.py +1 -1
  149. nautobot/dcim/migrations/0059_add_role_field_to_interface_models.py +27 -0
  150. nautobot/dcim/migrations/0060_alter_cable_status_alter_consoleport__path_and_more.py +303 -0
  151. nautobot/dcim/migrations/0061_module_models.py +862 -0
  152. nautobot/dcim/migrations/0062_module_data_migration.py +25 -0
  153. nautobot/dcim/models/__init__.py +8 -0
  154. nautobot/dcim/models/cables.py +15 -0
  155. nautobot/dcim/models/device_component_templates.py +207 -53
  156. nautobot/dcim/models/device_components.py +282 -99
  157. nautobot/dcim/models/devices.py +472 -13
  158. nautobot/dcim/models/racks.py +0 -1
  159. nautobot/dcim/navigation.py +47 -0
  160. nautobot/dcim/signals.py +3 -3
  161. nautobot/dcim/tables/__init__.py +35 -23
  162. nautobot/dcim/tables/devices.py +248 -47
  163. nautobot/dcim/tables/devicetypes.py +65 -9
  164. nautobot/dcim/tables/racks.py +5 -1
  165. nautobot/dcim/tables/template_code.py +46 -26
  166. nautobot/dcim/templates/dcim/cable_connect.html +76 -3
  167. nautobot/dcim/templates/dcim/console_port_connection_list.html +7 -5
  168. nautobot/dcim/templates/dcim/device/base.html +14 -6
  169. nautobot/dcim/templates/dcim/device/consoleports.html +2 -3
  170. nautobot/dcim/templates/dcim/device/consoleserverports.html +2 -3
  171. nautobot/dcim/templates/dcim/device/devicebays.html +6 -7
  172. nautobot/dcim/templates/dcim/device/frontports.html +2 -3
  173. nautobot/dcim/templates/dcim/device/interfaces.html +2 -3
  174. nautobot/dcim/templates/dcim/device/inventory.html +2 -3
  175. nautobot/dcim/templates/dcim/device/modulebays.html +49 -0
  176. nautobot/dcim/templates/dcim/device/poweroutlets.html +2 -3
  177. nautobot/dcim/templates/dcim/device/powerports.html +2 -3
  178. nautobot/dcim/templates/dcim/device/rearports.html +2 -3
  179. nautobot/dcim/templates/dcim/device.html +45 -1
  180. nautobot/dcim/templates/dcim/device_component.html +13 -5
  181. nautobot/dcim/templates/dcim/device_list.html +2 -1
  182. nautobot/dcim/templates/dcim/deviceredundancygroup_retrieve.html +6 -0
  183. nautobot/dcim/templates/dcim/devicetype.html +99 -98
  184. nautobot/dcim/templates/dcim/devicetype_list.html +8 -16
  185. nautobot/dcim/templates/dcim/inc/devicetype_component_table.html +1 -1
  186. nautobot/dcim/templates/dcim/inc/moduletype_component_table.html +39 -0
  187. nautobot/dcim/templates/dcim/interface.html +17 -2
  188. nautobot/dcim/templates/dcim/interface_connection_list.html +7 -5
  189. nautobot/dcim/templates/dcim/interface_edit.html +1 -0
  190. nautobot/dcim/templates/dcim/manufacturer.html +24 -0
  191. nautobot/dcim/templates/dcim/module/base.html +97 -0
  192. nautobot/dcim/templates/dcim/module_bulk_destroy.html +5 -0
  193. nautobot/dcim/templates/dcim/module_consoleports.html +53 -0
  194. nautobot/dcim/templates/dcim/module_consoleserverports.html +53 -0
  195. nautobot/dcim/templates/dcim/module_destroy.html +5 -0
  196. nautobot/dcim/templates/dcim/module_frontports.html +53 -0
  197. nautobot/dcim/templates/dcim/module_interfaces.html +57 -0
  198. nautobot/dcim/templates/dcim/module_list.html +20 -0
  199. nautobot/dcim/templates/dcim/module_modulebays.html +49 -0
  200. nautobot/dcim/templates/dcim/module_poweroutlets.html +53 -0
  201. nautobot/dcim/templates/dcim/module_powerports.html +53 -0
  202. nautobot/dcim/templates/dcim/module_rearports.html +53 -0
  203. nautobot/dcim/templates/dcim/module_retrieve.html +63 -0
  204. nautobot/dcim/templates/dcim/module_update.html +71 -0
  205. nautobot/dcim/templates/dcim/modulebay_bulk_destroy.html +5 -0
  206. nautobot/dcim/templates/dcim/modulebay_destroy.html +8 -0
  207. nautobot/dcim/templates/dcim/modulebay_retrieve.html +101 -0
  208. nautobot/dcim/templates/dcim/moduletype_list.html +11 -0
  209. nautobot/dcim/templates/dcim/moduletype_retrieve.html +159 -0
  210. nautobot/dcim/templates/dcim/power_port_connection_list.html +7 -5
  211. nautobot/dcim/templates/dcim/softwareimagefile_retrieve.html +65 -19
  212. nautobot/dcim/tests/integration/test_cable_connect_form.py +4 -4
  213. nautobot/dcim/tests/test_api.py +693 -208
  214. nautobot/dcim/tests/test_filters.py +843 -217
  215. nautobot/dcim/tests/test_models.py +1103 -8
  216. nautobot/dcim/tests/test_views.py +1525 -343
  217. nautobot/dcim/urls.py +17 -2
  218. nautobot/dcim/utils.py +2 -3
  219. nautobot/dcim/views.py +1109 -113
  220. nautobot/extras/__init__.py +0 -1
  221. nautobot/extras/api/serializers.py +115 -3
  222. nautobot/extras/api/urls.py +12 -0
  223. nautobot/extras/api/views.py +73 -59
  224. nautobot/extras/apps.py +2 -2
  225. nautobot/extras/choices.py +43 -0
  226. nautobot/extras/context_managers.py +13 -8
  227. nautobot/extras/datasources/git.py +2 -0
  228. nautobot/extras/factory.py +460 -9
  229. nautobot/extras/filters/__init__.py +174 -3
  230. nautobot/extras/filters/mixins.py +46 -43
  231. nautobot/extras/forms/base.py +24 -5
  232. nautobot/extras/forms/forms.py +227 -8
  233. nautobot/extras/forms/mixins.py +93 -0
  234. nautobot/extras/graphql/types.py +23 -10
  235. nautobot/extras/homepage.py +26 -3
  236. nautobot/extras/jobs.py +2 -2
  237. nautobot/extras/management/__init__.py +1 -0
  238. nautobot/extras/management/commands/refresh_dynamic_group_member_caches.py +1 -16
  239. nautobot/extras/migrations/0021_customfield_changelog_data.py +1 -0
  240. nautobot/extras/migrations/0109_dynamicgroup_group_type_dynamicgroup_tags_and_more.py +108 -0
  241. nautobot/extras/migrations/0110_alter_configcontext_cluster_groups_and_more.py +111 -0
  242. nautobot/extras/migrations/0111_metadata.py +162 -0
  243. nautobot/extras/migrations/0112_dynamic_group_group_type_data_migration.py +28 -0
  244. nautobot/extras/migrations/0113_saved_views.py +77 -0
  245. nautobot/extras/models/__init__.py +15 -1
  246. nautobot/extras/models/change_logging.py +3 -3
  247. nautobot/extras/models/contacts.py +4 -0
  248. nautobot/extras/models/customfields.py +18 -3
  249. nautobot/extras/models/groups.py +389 -225
  250. nautobot/extras/models/jobs.py +87 -3
  251. nautobot/extras/models/metadata.py +441 -0
  252. nautobot/extras/models/mixins.py +72 -62
  253. nautobot/extras/models/models.py +118 -9
  254. nautobot/extras/models/relationships.py +9 -2
  255. nautobot/extras/models/tags.py +13 -2
  256. nautobot/extras/navigation.py +57 -0
  257. nautobot/extras/plugins/__init__.py +3 -1
  258. nautobot/extras/querysets.py +30 -66
  259. nautobot/extras/signals.py +109 -101
  260. nautobot/extras/tables.py +201 -17
  261. nautobot/extras/templates/extras/dynamicgroup.html +44 -15
  262. nautobot/extras/templates/extras/dynamicgroup_edit.html +2 -0
  263. nautobot/extras/templates/extras/job.html +1 -1
  264. nautobot/extras/templates/extras/job_detail.html +11 -0
  265. nautobot/extras/templates/extras/jobresult.html +61 -74
  266. nautobot/extras/templates/extras/metadatatype_create.html +89 -0
  267. nautobot/extras/templates/extras/metadatatype_retrieve.html +67 -0
  268. nautobot/extras/templates/extras/object_dynamicgroups.html +7 -0
  269. nautobot/extras/templates/extras/objectchange_list.html +0 -12
  270. nautobot/extras/templates/extras/plugins_list.html +1 -3
  271. nautobot/extras/templates/extras/role_retrieve.html +48 -0
  272. nautobot/extras/templates/extras/staticgroupassociation_retrieve.html +20 -0
  273. nautobot/extras/tests/integration/test_customfields.py +1 -0
  274. nautobot/extras/tests/test_api.py +509 -23
  275. nautobot/extras/tests/test_changelog.py +20 -9
  276. nautobot/extras/tests/test_context_managers.py +22 -15
  277. nautobot/extras/tests/test_datasources.py +13 -1
  278. nautobot/extras/tests/test_dynamicgroups.py +201 -171
  279. nautobot/extras/tests/test_filters.py +211 -12
  280. nautobot/extras/tests/test_jobs.py +6 -6
  281. nautobot/extras/tests/test_models.py +501 -4
  282. nautobot/extras/tests/test_relationships.py +1 -0
  283. nautobot/extras/tests/test_views.py +586 -8
  284. nautobot/extras/tests/test_webhooks.py +1 -1
  285. nautobot/extras/urls.py +5 -0
  286. nautobot/extras/utils.py +85 -16
  287. nautobot/extras/views.py +562 -122
  288. nautobot/ipam/__init__.py +0 -1
  289. nautobot/ipam/apps.py +1 -0
  290. nautobot/ipam/factory.py +17 -19
  291. nautobot/ipam/filters.py +13 -0
  292. nautobot/ipam/forms.py +8 -4
  293. nautobot/ipam/graphql/types.py +2 -2
  294. nautobot/ipam/migrations/0047_alter_ipaddress_role_alter_ipaddress_status_and_more.py +59 -0
  295. nautobot/ipam/models.py +20 -20
  296. nautobot/ipam/querysets.py +1 -1
  297. nautobot/ipam/signals.py +4 -2
  298. nautobot/ipam/tables.py +5 -0
  299. nautobot/ipam/templates/ipam/ipaddress_interfaces.html +1 -1
  300. nautobot/ipam/templates/ipam/ipaddress_vm_interfaces.html +1 -1
  301. nautobot/ipam/templates/ipam/prefix.html +1 -0
  302. nautobot/ipam/tests/test_api.py +37 -18
  303. nautobot/ipam/tests/test_filters.py +26 -2
  304. nautobot/ipam/tests/test_models.py +9 -2
  305. nautobot/ipam/tests/test_querysets.py +1 -1
  306. nautobot/ipam/tests/test_views.py +3 -2
  307. nautobot/ipam/urls.py +2 -2
  308. nautobot/ipam/views.py +20 -34
  309. nautobot/project-static/css/base.css +21 -0
  310. nautobot/project-static/css/dark.css +11 -0
  311. nautobot/project-static/docs/404.html +894 -90
  312. nautobot/project-static/docs/apps/index.html +894 -90
  313. nautobot/project-static/docs/apps/nautobot-apps.html +894 -90
  314. nautobot/project-static/docs/assets/_mkdocstrings.css +5 -0
  315. nautobot/project-static/docs/assets/stylesheets/main.3cba04c6.min.css +1 -0
  316. nautobot/project-static/docs/assets/stylesheets/main.3cba04c6.min.css.map +1 -0
  317. nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +921 -122
  318. nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +906 -103
  319. nautobot/project-static/docs/code-reference/nautobot/apps/api.html +1620 -905
  320. nautobot/project-static/docs/code-reference/nautobot/apps/change_logging.html +937 -146
  321. nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +979 -190
  322. nautobot/project-static/docs/code-reference/nautobot/apps/config.html +903 -101
  323. nautobot/project-static/docs/code-reference/nautobot/apps/constants.html +899 -95
  324. nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +993 -195
  325. nautobot/project-static/docs/code-reference/nautobot/apps/exceptions.html +976 -133
  326. nautobot/project-static/docs/code-reference/nautobot/apps/factory.html +1080 -274
  327. nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +1244 -336
  328. nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +1729 -877
  329. nautobot/project-static/docs/code-reference/nautobot/apps/graphql.html +1166 -383
  330. nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +2090 -1376
  331. nautobot/project-static/docs/code-reference/nautobot/apps/models.html +2248 -1424
  332. nautobot/project-static/docs/code-reference/nautobot/apps/querysets.html +914 -113
  333. nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +965 -165
  334. nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +1012 -225
  335. nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +1915 -1279
  336. nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +1848 -1104
  337. nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +906 -103
  338. nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +2335 -1701
  339. nautobot/project-static/docs/code-reference/nautobot/apps/views.html +1804 -1026
  340. nautobot/project-static/docs/development/apps/api/configuration-view.html +894 -90
  341. nautobot/project-static/docs/development/apps/api/database-backend-config.html +894 -90
  342. nautobot/project-static/docs/development/apps/api/models/django-admin.html +894 -90
  343. nautobot/project-static/docs/development/apps/api/models/global-search.html +894 -90
  344. nautobot/project-static/docs/development/apps/api/models/graphql.html +894 -90
  345. nautobot/project-static/docs/development/apps/api/models/index.html +944 -92
  346. nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +894 -90
  347. nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +894 -90
  348. nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +894 -90
  349. nautobot/project-static/docs/development/apps/api/platform-features/git-repository-content.html +894 -90
  350. nautobot/project-static/docs/development/apps/api/platform-features/index.html +894 -90
  351. nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +894 -90
  352. nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +894 -90
  353. nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +894 -90
  354. nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +894 -90
  355. nautobot/project-static/docs/development/apps/api/platform-features/uniquely-identify-objects.html +894 -90
  356. nautobot/project-static/docs/development/apps/api/prometheus.html +894 -90
  357. nautobot/project-static/docs/development/apps/api/setup.html +894 -90
  358. nautobot/project-static/docs/development/apps/api/testing.html +894 -90
  359. nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +894 -90
  360. nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +894 -90
  361. nautobot/project-static/docs/development/apps/api/ui-extensions/index.html +894 -90
  362. nautobot/project-static/docs/development/apps/api/ui-extensions/navigation.html +894 -90
  363. nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +894 -90
  364. nautobot/project-static/docs/development/apps/api/views/base-template.html +894 -90
  365. nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +894 -90
  366. nautobot/project-static/docs/development/apps/api/views/django-generic-views.html +894 -90
  367. nautobot/project-static/docs/development/apps/api/views/help-documentation.html +894 -90
  368. nautobot/project-static/docs/development/apps/api/views/index.html +894 -90
  369. nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +894 -90
  370. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +894 -90
  371. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +894 -90
  372. nautobot/project-static/docs/development/apps/api/views/notes.html +894 -90
  373. nautobot/project-static/docs/development/apps/api/views/rest-api.html +894 -90
  374. nautobot/project-static/docs/development/apps/api/views/urls.html +894 -90
  375. nautobot/project-static/docs/development/apps/index.html +894 -90
  376. nautobot/project-static/docs/development/apps/migration/code-updates.html +894 -90
  377. nautobot/project-static/docs/development/apps/migration/dependency-updates.html +894 -90
  378. nautobot/project-static/docs/development/apps/migration/from-v1.html +894 -90
  379. nautobot/project-static/docs/development/apps/migration/model-updates/dcim.html +894 -90
  380. nautobot/project-static/docs/development/apps/migration/model-updates/extras.html +894 -90
  381. nautobot/project-static/docs/development/apps/migration/model-updates/global.html +894 -90
  382. nautobot/project-static/docs/development/apps/migration/model-updates/ipam.html +894 -90
  383. nautobot/project-static/docs/development/apps/porting-from-netbox.html +894 -90
  384. nautobot/project-static/docs/development/core/application-registry.html +894 -90
  385. nautobot/project-static/docs/development/core/best-practices.html +895 -90
  386. nautobot/project-static/docs/development/core/bootstrap-ui.html +894 -90
  387. nautobot/project-static/docs/development/core/caching.html +894 -90
  388. nautobot/project-static/docs/development/core/controllers.html +894 -90
  389. nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +894 -90
  390. nautobot/project-static/docs/development/core/generic-views.html +894 -90
  391. nautobot/project-static/docs/development/core/getting-started.html +894 -90
  392. nautobot/project-static/docs/development/core/homepage.html +894 -90
  393. nautobot/project-static/docs/development/core/index.html +905 -90
  394. nautobot/project-static/docs/development/core/model-checklist.html +903 -91
  395. nautobot/project-static/docs/development/core/model-features.html +894 -90
  396. nautobot/project-static/docs/development/core/natural-keys.html +894 -90
  397. nautobot/project-static/docs/development/core/navigation-menu.html +894 -90
  398. nautobot/project-static/docs/development/core/release-checklist.html +897 -93
  399. nautobot/project-static/docs/development/core/role-internals.html +894 -90
  400. nautobot/project-static/docs/development/core/settings.html +894 -90
  401. nautobot/project-static/docs/development/core/style-guide.html +895 -91
  402. nautobot/project-static/docs/development/core/templates.html +906 -91
  403. nautobot/project-static/docs/development/core/testing.html +894 -90
  404. nautobot/project-static/docs/development/core/user-preferences.html +894 -90
  405. nautobot/project-static/docs/development/index.html +894 -90
  406. nautobot/project-static/docs/development/jobs/index.html +1271 -453
  407. nautobot/project-static/docs/development/jobs/migration/from-v1.html +894 -90
  408. nautobot/project-static/docs/index.html +9032 -13
  409. nautobot/project-static/docs/media/models/cloud_aws_direct_connect_dark.png +0 -0
  410. nautobot/project-static/docs/media/models/cloud_aws_direct_connect_light.png +0 -0
  411. nautobot/project-static/docs/models/cloud/cloudaccount.html +15 -0
  412. nautobot/project-static/docs/models/cloud/cloudnetwork.html +15 -0
  413. nautobot/project-static/docs/models/cloud/cloudnetworkprefixassignment.html +15 -0
  414. nautobot/project-static/docs/models/cloud/cloudresourcetype.html +15 -0
  415. nautobot/project-static/docs/models/cloud/cloudservice.html +15 -0
  416. nautobot/project-static/docs/models/cloud/cloudservicenetworkassignment.html +15 -0
  417. nautobot/project-static/docs/models/dcim/module.html +15 -0
  418. nautobot/project-static/docs/models/dcim/modulebay.html +15 -0
  419. nautobot/project-static/docs/models/dcim/modulebaytemplate.html +15 -0
  420. nautobot/project-static/docs/models/dcim/moduletype.html +15 -0
  421. nautobot/project-static/docs/models/extras/metadatachoice.html +15 -0
  422. nautobot/project-static/docs/models/extras/metadatatype.html +15 -0
  423. nautobot/project-static/docs/models/extras/objectmetadata.html +15 -0
  424. nautobot/project-static/docs/models/extras/role.html +15 -0
  425. nautobot/project-static/docs/models/extras/savedview.html +15 -0
  426. nautobot/project-static/docs/models/extras/staticgroupassociation.html +15 -0
  427. nautobot/project-static/docs/models/extras/status.html +15 -0
  428. nautobot/project-static/docs/objects.inv +0 -0
  429. nautobot/project-static/docs/overview/application_stack.html +902 -91
  430. nautobot/project-static/docs/overview/design_philosophy.html +896 -92
  431. nautobot/project-static/docs/overview/index.html +13 -8228
  432. nautobot/project-static/docs/release-notes/index.html +1131 -94
  433. nautobot/project-static/docs/release-notes/version-1.0.html +894 -90
  434. nautobot/project-static/docs/release-notes/version-1.1.html +894 -90
  435. nautobot/project-static/docs/release-notes/version-1.2.html +894 -90
  436. nautobot/project-static/docs/release-notes/version-1.3.html +894 -90
  437. nautobot/project-static/docs/release-notes/version-1.4.html +894 -90
  438. nautobot/project-static/docs/release-notes/version-1.5.html +895 -91
  439. nautobot/project-static/docs/release-notes/version-1.6.html +895 -91
  440. nautobot/project-static/docs/release-notes/version-2.0.html +894 -90
  441. nautobot/project-static/docs/release-notes/version-2.1.html +894 -90
  442. nautobot/project-static/docs/release-notes/version-2.2.html +1137 -196
  443. nautobot/project-static/docs/release-notes/version-2.3.html +9954 -0
  444. nautobot/project-static/docs/requirements.txt +5 -5
  445. nautobot/project-static/docs/search/search_index.json +1 -1
  446. nautobot/project-static/docs/sitemap.xml +335 -260
  447. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  448. nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +894 -90
  449. nautobot/project-static/docs/user-guide/administration/configuration/authentication/remote.html +894 -90
  450. nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +894 -90
  451. nautobot/project-static/docs/user-guide/administration/configuration/index.html +894 -90
  452. nautobot/project-static/docs/user-guide/administration/configuration/optional-settings.html +1025 -175
  453. nautobot/project-static/docs/user-guide/administration/configuration/required-settings.html +894 -90
  454. nautobot/project-static/docs/user-guide/administration/configuration/time-zones.html +894 -90
  455. nautobot/project-static/docs/user-guide/administration/guides/caching.html +894 -90
  456. nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +902 -90
  457. nautobot/project-static/docs/user-guide/administration/guides/healthcheck.html +894 -90
  458. nautobot/project-static/docs/user-guide/administration/guides/permissions.html +894 -90
  459. nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +894 -90
  460. nautobot/project-static/docs/user-guide/administration/guides/replicating-nautobot.html +894 -90
  461. nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +894 -90
  462. nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +894 -90
  463. nautobot/project-static/docs/user-guide/administration/installation/app-install.html +894 -90
  464. nautobot/project-static/docs/user-guide/administration/installation/external-authentication.html +894 -90
  465. nautobot/project-static/docs/user-guide/administration/installation/http-server.html +946 -155
  466. nautobot/project-static/docs/user-guide/administration/installation/index.html +903 -95
  467. nautobot/project-static/docs/user-guide/administration/installation/install_system.html +936 -124
  468. nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +956 -159
  469. nautobot/project-static/docs/user-guide/administration/installation/services.html +915 -114
  470. nautobot/project-static/docs/user-guide/administration/installation-extras/docker.html +910 -101
  471. nautobot/project-static/docs/user-guide/administration/installation-extras/health-checks.html +894 -90
  472. nautobot/project-static/docs/user-guide/administration/installation-extras/selinux-troubleshooting.html +894 -90
  473. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-netbox.html +894 -90
  474. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +894 -90
  475. nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +977 -121
  476. nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +894 -90
  477. nautobot/project-static/docs/user-guide/administration/upgrading/database-backup.html +894 -90
  478. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/after-you-upgrade.html +894 -90
  479. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/before-you-upgrade.html +894 -90
  480. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/for-developers.html +894 -90
  481. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/index.html +894 -90
  482. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/whats-changed.html +894 -90
  483. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/region-and-site-data-migration-guide.html +894 -90
  484. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +894 -90
  485. nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +894 -90
  486. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuit.html +894 -90
  487. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittermination.html +895 -91
  488. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittype.html +894 -90
  489. nautobot/project-static/docs/user-guide/core-data-model/circuits/provider.html +898 -90
  490. nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +897 -93
  491. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloud.html +8984 -0
  492. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudaccount.html +8828 -0
  493. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetwork.html +8829 -0
  494. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetworkprefixassignment.html +8828 -0
  495. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudresourcetype.html +8829 -0
  496. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservice.html +8833 -0
  497. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservicenetworkassignment.html +8828 -0
  498. nautobot/project-static/docs/user-guide/core-data-model/dcim/cable.html +908 -104
  499. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +925 -107
  500. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +925 -107
  501. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +920 -102
  502. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +925 -107
  503. nautobot/project-static/docs/user-guide/core-data-model/dcim/controller.html +908 -104
  504. nautobot/project-static/docs/user-guide/core-data-model/dcim/controllermanageddevicegroup.html +908 -104
  505. nautobot/project-static/docs/user-guide/core-data-model/dcim/device.html +915 -107
  506. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +922 -118
  507. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +923 -119
  508. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicefamily.html +920 -116
  509. nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +908 -104
  510. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +916 -107
  511. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +928 -110
  512. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +938 -120
  513. nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +930 -108
  514. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +908 -104
  515. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +939 -121
  516. nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +930 -112
  517. nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +920 -116
  518. nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +923 -119
  519. nautobot/project-static/docs/user-guide/core-data-model/dcim/manufacturer.html +925 -117
  520. nautobot/project-static/docs/user-guide/core-data-model/dcim/module.html +8828 -0
  521. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebay.html +8846 -0
  522. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebaytemplate.html +8843 -0
  523. nautobot/project-static/docs/user-guide/core-data-model/dcim/moduletype.html +8823 -0
  524. nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +918 -114
  525. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerfeed.html +908 -104
  526. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +942 -85
  527. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +926 -108
  528. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerpanel.html +908 -104
  529. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +945 -88
  530. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +923 -105
  531. nautobot/project-static/docs/user-guide/core-data-model/dcim/rack.html +931 -127
  532. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackgroup.html +920 -116
  533. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackreservation.html +908 -104
  534. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +924 -106
  535. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +926 -108
  536. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +908 -104
  537. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareversion.html +908 -104
  538. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualchassis.html +908 -104
  539. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +938 -90
  540. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +894 -90
  541. nautobot/project-static/docs/user-guide/core-data-model/extras/contact.html +899 -91
  542. nautobot/project-static/docs/user-guide/core-data-model/extras/team.html +899 -91
  543. nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +894 -90
  544. nautobot/project-static/docs/user-guide/core-data-model/ipam/namespace.html +894 -90
  545. nautobot/project-static/docs/user-guide/core-data-model/ipam/prefix.html +894 -90
  546. nautobot/project-static/docs/user-guide/core-data-model/ipam/rir.html +894 -90
  547. nautobot/project-static/docs/user-guide/core-data-model/ipam/routetarget.html +894 -90
  548. nautobot/project-static/docs/user-guide/core-data-model/ipam/service.html +894 -90
  549. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +894 -90
  550. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlangroup.html +894 -90
  551. nautobot/project-static/docs/user-guide/core-data-model/ipam/vrf.html +894 -90
  552. nautobot/project-static/docs/user-guide/core-data-model/overview/introduction.html +894 -90
  553. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenant.html +903 -98
  554. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenantgroup.html +894 -90
  555. nautobot/project-static/docs/user-guide/core-data-model/virtualization/cluster.html +894 -90
  556. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustergroup.html +894 -90
  557. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustertype.html +894 -90
  558. nautobot/project-static/docs/user-guide/core-data-model/virtualization/virtualmachine.html +894 -90
  559. nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +899 -91
  560. nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +894 -90
  561. nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +894 -90
  562. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +894 -90
  563. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +894 -90
  564. nautobot/project-static/docs/user-guide/feature-guides/getting-started/index.html +894 -90
  565. nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +894 -90
  566. nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +894 -90
  567. nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +894 -90
  568. nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +894 -90
  569. nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +894 -90
  570. nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +894 -90
  571. nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +894 -90
  572. nautobot/project-static/docs/user-guide/feature-guides/graphql.html +894 -90
  573. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/clear-view-button.png +0 -0
  574. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/cleared-view.png +0 -0
  575. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/config-table-columns-to-locations.png +0 -0
  576. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/configure-button.png +0 -0
  577. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/create-saved-view-success.png +0 -0
  578. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/current-saved-view-drop-down-menu.png +0 -0
  579. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/default-location-list-view.png +0 -0
  580. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/dropdown-button-after-new-saved-view.png +0 -0
  581. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/filter-application-to-locations.png +0 -0
  582. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/filter-button.png +0 -0
  583. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/global-default-location-list-view.png +0 -0
  584. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/location-list-view-with-saved-views.png +0 -0
  585. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/navigation-menu.png +0 -0
  586. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/save-as-new-view-drop-down.png +0 -0
  587. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/save-view-modal.png +0 -0
  588. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/saved-view-admin-edit-buttons.png +0 -0
  589. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/saved-view-admin-edit-success.png +0 -0
  590. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/saved-view-admin-edit-view-unchecked.png +0 -0
  591. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/saved-view-admin-edit-view.png +0 -0
  592. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/saved-view-different-user.png +0 -0
  593. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/saved-view-modal-unchecked.png +0 -0
  594. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/set-as-my-default-button.png +0 -0
  595. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/set-as-my-default-success.png +0 -0
  596. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/unsaved-saved-view.png +0 -0
  597. nautobot/project-static/docs/user-guide/feature-guides/images/saved-views/updated-saved-view.png +0 -0
  598. nautobot/project-static/docs/user-guide/feature-guides/ip-address-merge-tool.html +894 -90
  599. nautobot/project-static/docs/user-guide/feature-guides/relationships.html +894 -90
  600. nautobot/project-static/docs/user-guide/feature-guides/software-image-files-and-versions.html +894 -90
  601. nautobot/project-static/docs/user-guide/index.html +894 -90
  602. nautobot/project-static/docs/user-guide/platform-functionality/change-logging.html +894 -90
  603. nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +894 -90
  604. nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +894 -90
  605. nautobot/project-static/docs/user-guide/platform-functionality/customlink.html +894 -90
  606. nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +1260 -787
  607. nautobot/project-static/docs/user-guide/platform-functionality/exporttemplate.html +897 -93
  608. nautobot/project-static/docs/user-guide/platform-functionality/externalintegration.html +894 -90
  609. nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +894 -90
  610. nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +894 -90
  611. nautobot/project-static/docs/user-guide/platform-functionality/graphqlquery.html +894 -90
  612. nautobot/project-static/docs/user-guide/platform-functionality/imageattachment.html +894 -90
  613. nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +894 -90
  614. nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +894 -90
  615. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +894 -90
  616. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +894 -90
  617. nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +898 -90
  618. nautobot/project-static/docs/user-guide/platform-functionality/napalm.html +894 -90
  619. nautobot/project-static/docs/user-guide/platform-functionality/note.html +897 -93
  620. nautobot/project-static/docs/user-guide/platform-functionality/objectmetadata.html +9061 -0
  621. nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +897 -93
  622. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +894 -90
  623. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +894 -90
  624. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +894 -90
  625. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/ui-related-endpoints.html +894 -90
  626. nautobot/project-static/docs/user-guide/platform-functionality/role.html +897 -93
  627. nautobot/project-static/docs/user-guide/platform-functionality/savedview.html +9137 -0
  628. nautobot/project-static/docs/user-guide/platform-functionality/secret.html +897 -93
  629. nautobot/project-static/docs/user-guide/platform-functionality/staticgroupassociation.html +8933 -0
  630. nautobot/project-static/docs/user-guide/platform-functionality/status.html +894 -90
  631. nautobot/project-static/docs/user-guide/platform-functionality/tag.html +894 -90
  632. nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +952 -123
  633. nautobot/project-static/docs/user-guide/platform-functionality/users/objectpermission.html +894 -90
  634. nautobot/project-static/docs/user-guide/platform-functionality/users/token.html +894 -90
  635. nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +894 -90
  636. nautobot/project-static/js/forms.js +71 -0
  637. nautobot/project-static/js/table_sorting_indicator.js +46 -0
  638. nautobot/project-static/js/tableconfig.js +6 -1
  639. nautobot/project-static/materialdesignicons-7.4.47/css/materialdesignicons.min.css +3 -0
  640. nautobot/project-static/{materialdesignicons-6.5.95 → materialdesignicons-7.4.47}/fonts/materialdesignicons-webfont.eot +0 -0
  641. nautobot/project-static/{materialdesignicons-6.5.95 → materialdesignicons-7.4.47}/fonts/materialdesignicons-webfont.ttf +0 -0
  642. nautobot/project-static/materialdesignicons-7.4.47/fonts/materialdesignicons-webfont.woff +0 -0
  643. nautobot/project-static/materialdesignicons-7.4.47/fonts/materialdesignicons-webfont.woff2 +0 -0
  644. nautobot/tenancy/__init__.py +0 -1
  645. nautobot/tenancy/apps.py +1 -0
  646. nautobot/tenancy/factory.py +3 -2
  647. nautobot/tenancy/filters/__init__.py +1 -0
  648. nautobot/tenancy/forms.py +1 -1
  649. nautobot/tenancy/templates/tenancy/tenant.html +24 -20
  650. nautobot/tenancy/views.py +11 -10
  651. nautobot/users/__init__.py +0 -1
  652. nautobot/users/api/serializers.py +1 -1
  653. nautobot/users/api/views.py +4 -2
  654. nautobot/users/apps.py +3 -2
  655. nautobot/users/factory.py +3 -3
  656. nautobot/users/migrations/0010_user_default_saved_views.py +20 -0
  657. nautobot/users/models.py +12 -0
  658. nautobot/users/tests/test_filters.py +6 -3
  659. nautobot/users/urls.py +8 -0
  660. nautobot/virtualization/__init__.py +0 -1
  661. nautobot/virtualization/apps.py +1 -0
  662. nautobot/virtualization/filters.py +6 -1
  663. nautobot/virtualization/forms.py +11 -3
  664. nautobot/virtualization/graphql/types.py +2 -2
  665. nautobot/virtualization/migrations/0029_add_role_field_to_interface_models.py +27 -0
  666. nautobot/virtualization/migrations/0030_alter_virtualmachine_local_config_context_data_owner_content_type_and_more.py +67 -0
  667. nautobot/virtualization/models.py +0 -2
  668. nautobot/virtualization/tables.py +12 -8
  669. nautobot/virtualization/templates/virtualization/virtualmachine.html +1 -1
  670. nautobot/virtualization/templates/virtualization/vminterface.html +7 -1
  671. nautobot/virtualization/templates/virtualization/vminterface_edit.html +1 -0
  672. nautobot/virtualization/tests/test_api.py +9 -4
  673. nautobot/virtualization/tests/test_filters.py +22 -0
  674. nautobot/virtualization/tests/test_models.py +7 -3
  675. nautobot/virtualization/tests/test_views.py +19 -3
  676. nautobot/virtualization/urls.py +2 -2
  677. nautobot/virtualization/views.py +10 -32
  678. {nautobot-2.2.8.dist-info → nautobot-2.3.0.dist-info}/METADATA +21 -19
  679. {nautobot-2.2.8.dist-info → nautobot-2.3.0.dist-info}/RECORD +684 -564
  680. nautobot/project-static/docs/assets/stylesheets/main.76a95c52.min.css +0 -1
  681. nautobot/project-static/docs/assets/stylesheets/main.76a95c52.min.css.map +0 -1
  682. nautobot/project-static/materialdesignicons-6.5.95/.github/ISSUE_TEMPLATE.md +0 -3
  683. nautobot/project-static/materialdesignicons-6.5.95/README.md +0 -25
  684. nautobot/project-static/materialdesignicons-6.5.95/css/materialdesignicons.css +0 -26654
  685. nautobot/project-static/materialdesignicons-6.5.95/css/materialdesignicons.css.map +0 -16
  686. nautobot/project-static/materialdesignicons-6.5.95/css/materialdesignicons.min.css +0 -3
  687. nautobot/project-static/materialdesignicons-6.5.95/css/materialdesignicons.min.css.map +0 -16
  688. nautobot/project-static/materialdesignicons-6.5.95/fonts/materialdesignicons-webfont.woff +0 -0
  689. nautobot/project-static/materialdesignicons-6.5.95/fonts/materialdesignicons-webfont.woff2 +0 -0
  690. nautobot/project-static/materialdesignicons-6.5.95/package.json +0 -28
  691. nautobot/project-static/materialdesignicons-6.5.95/preview.html +0 -717
  692. nautobot/project-static/materialdesignicons-6.5.95/scss/_animated.scss +0 -27
  693. nautobot/project-static/materialdesignicons-6.5.95/scss/_core.scss +0 -10
  694. nautobot/project-static/materialdesignicons-6.5.95/scss/_extras.scss +0 -65
  695. nautobot/project-static/materialdesignicons-6.5.95/scss/_functions.scss +0 -20
  696. nautobot/project-static/materialdesignicons-6.5.95/scss/_icons.scss +0 -10
  697. nautobot/project-static/materialdesignicons-6.5.95/scss/_path.scss +0 -10
  698. nautobot/project-static/materialdesignicons-6.5.95/scss/_variables.scss +0 -6606
  699. nautobot/project-static/materialdesignicons-6.5.95/scss/materialdesignicons.scss +0 -8
  700. /nautobot/project-static/{materialdesignicons-6.5.95 → materialdesignicons-7.4.47}/LICENSE +0 -0
  701. {nautobot-2.2.8.dist-info → nautobot-2.3.0.dist-info}/LICENSE.txt +0 -0
  702. {nautobot-2.2.8.dist-info → nautobot-2.3.0.dist-info}/NOTICE +0 -0
  703. {nautobot-2.2.8.dist-info → nautobot-2.3.0.dist-info}/WHEEL +0 -0
  704. {nautobot-2.2.8.dist-info → nautobot-2.3.0.dist-info}/entry_points.txt +0 -0
@@ -1,3 +1,5 @@
1
+ import uuid
2
+
1
3
  from django.contrib.auth import get_user_model
2
4
  from django.contrib.contenttypes.models import ContentType
3
5
  from django.db.models import Q
@@ -20,6 +22,7 @@ from nautobot.dcim.choices import (
20
22
  RackWidthChoices,
21
23
  SubdeviceRoleChoices,
22
24
  )
25
+ from nautobot.dcim.constants import NONCONNECTABLE_IFACE_TYPES, VIRTUAL_IFACE_TYPES
23
26
  from nautobot.dcim.filters import (
24
27
  CableFilterSet,
25
28
  ConsolePortFilterSet,
@@ -45,6 +48,10 @@ from nautobot.dcim.filters import (
45
48
  LocationFilterSet,
46
49
  LocationTypeFilterSet,
47
50
  ManufacturerFilterSet,
51
+ ModuleBayFilterSet,
52
+ ModuleBayTemplateFilterSet,
53
+ ModuleFilterSet,
54
+ ModuleTypeFilterSet,
48
55
  PlatformFilterSet,
49
56
  PowerFeedFilterSet,
50
57
  PowerOutletFilterSet,
@@ -86,6 +93,10 @@ from nautobot.dcim.models import (
86
93
  Location,
87
94
  LocationType,
88
95
  Manufacturer,
96
+ Module,
97
+ ModuleBay,
98
+ ModuleBayTemplate,
99
+ ModuleType,
89
100
  Platform,
90
101
  PowerFeed,
91
102
  PowerOutlet,
@@ -457,7 +468,7 @@ def common_test_data(cls):
457
468
  )
458
469
 
459
470
  InterfaceTemplate.objects.create(
460
- name="Interface 1",
471
+ name="Test Interface 1",
461
472
  description="Interface Description 1",
462
473
  device_type=device_types[0],
463
474
  label="interface1",
@@ -465,7 +476,7 @@ def common_test_data(cls):
465
476
  type=InterfaceTypeChoices.TYPE_1GE_SFP,
466
477
  )
467
478
  InterfaceTemplate.objects.create(
468
- name="Interface 2",
479
+ name="Test Interface 2",
469
480
  description="Interface Description 2",
470
481
  device_type=device_types[1],
471
482
  label="interface2",
@@ -473,7 +484,7 @@ def common_test_data(cls):
473
484
  type=InterfaceTypeChoices.TYPE_1GE_GBIC,
474
485
  )
475
486
  InterfaceTemplate.objects.create(
476
- name="Interface 3",
487
+ name="Test Interface 3",
477
488
  description="Interface Description 3",
478
489
  device_type=device_types[2],
479
490
  label="interface3",
@@ -554,7 +565,27 @@ def common_test_data(cls):
554
565
  label="devicebay3",
555
566
  description="Device Bay Description 3",
556
567
  )
557
-
568
+ ModuleBayTemplate.objects.create(
569
+ device_type=device_types[0],
570
+ name="device test module bay 1",
571
+ position=1,
572
+ label="devicemodulebay1",
573
+ description="device test module bay 1 description",
574
+ )
575
+ ModuleBayTemplate.objects.create(
576
+ device_type=device_types[1],
577
+ name="device test module bay 2",
578
+ position=2,
579
+ label="devicemodulebay2",
580
+ description="device test module bay 2 description",
581
+ )
582
+ ModuleBayTemplate.objects.create(
583
+ device_type=device_types[2],
584
+ name="device test module bay 3",
585
+ position=3,
586
+ label="devicemodulebay3",
587
+ description="device test module bay 3 description",
588
+ )
558
589
  secrets_groups = (
559
590
  SecretsGroup.objects.create(name="Secrets group 1"),
560
591
  SecretsGroup.objects.create(name="Secrets group 2"),
@@ -624,6 +655,121 @@ def common_test_data(cls):
624
655
  external_integrations = iter(ExternalIntegration.objects.all())
625
656
  device_redundancy_groups = iter(DeviceRedundancyGroup.objects.all())
626
657
 
658
+ module_types = (
659
+ ModuleType.objects.create(
660
+ manufacturer=cls.manufacturers[0], model="Filter Test Module Type 1", comments="Module Type 1"
661
+ ),
662
+ ModuleType.objects.create(
663
+ manufacturer=cls.manufacturers[1], model="Filter Test Module Type 2", comments="Module Type 2"
664
+ ),
665
+ ModuleType.objects.create(
666
+ manufacturer=cls.manufacturers[2], model="Filter Test Module Type 3", comments="Module Type 3"
667
+ ),
668
+ )
669
+
670
+ # Create 3 of each component template on the first two module types
671
+ for i in range(6):
672
+ ConsolePortTemplate.objects.create(
673
+ name=f"Test Filters Module Console Port {i+1}",
674
+ module_type=module_types[i % 2],
675
+ )
676
+ ConsoleServerPortTemplate.objects.create(
677
+ name=f"Test Filters Module Console Server Port {i+1}",
678
+ module_type=module_types[i % 2],
679
+ )
680
+ ppt = PowerPortTemplate.objects.create(
681
+ name=f"Test Filters Module Power Port {i+1}",
682
+ module_type=module_types[i % 2],
683
+ )
684
+ PowerOutletTemplate.objects.create(
685
+ name=f"Test Filters Module Power Outlet {i+1}",
686
+ power_port_template=ppt,
687
+ module_type=module_types[i % 2],
688
+ )
689
+ InterfaceTemplate.objects.create(
690
+ name=f"Test Filters Module Interface {i+1}",
691
+ type=InterfaceTypeChoices.TYPE_1GE_FIXED,
692
+ module_type=module_types[i % 2],
693
+ )
694
+ rpt = RearPortTemplate.objects.create(
695
+ name=f"Test Filters Module Rear Port {i+1}",
696
+ module_type=module_types[i % 2],
697
+ type=PortTypeChoices.TYPE_8P8C,
698
+ positions=10,
699
+ )
700
+ FrontPortTemplate.objects.create(
701
+ name=f"Test Filters Module Front Port {i+1}",
702
+ module_type=module_types[i % 2],
703
+ rear_port_template=rpt,
704
+ rear_port_position=i + 1,
705
+ type=PortTypeChoices.TYPE_8P8C,
706
+ )
707
+ ModuleBayTemplate.objects.create(
708
+ name=f"Test Filters Module Module Bay {i+1}",
709
+ position=i + 1,
710
+ module_type=module_types[i % 2],
711
+ )
712
+
713
+ module_roles = Role.objects.get_for_model(Module)
714
+ cls.module_statuses = Status.objects.get_for_model(Module)
715
+ cls.modules = (
716
+ Module.objects.create(
717
+ module_type=module_types[0],
718
+ status=cls.module_statuses[0],
719
+ asset_tag="Test Filter Asset Tag Module1",
720
+ serial="Test Filter Serial Module1",
721
+ role=module_roles[0],
722
+ tenant=tenants[0],
723
+ parent_module_bay=cls.devices[0].module_bays.first(),
724
+ ),
725
+ Module.objects.create(
726
+ module_type=module_types[1],
727
+ status=cls.module_statuses[0],
728
+ asset_tag="Test Filter Asset Tag Module2",
729
+ serial="Test Filter Serial Module2",
730
+ role=module_roles[0],
731
+ tenant=tenants[1],
732
+ parent_module_bay=cls.devices[1].module_bays.first(),
733
+ ),
734
+ Module.objects.create(
735
+ module_type=module_types[2],
736
+ status=cls.module_statuses[0],
737
+ asset_tag="Test Filter Asset Tag Module3",
738
+ serial="Test Filter Serial Module3",
739
+ role=module_roles[1],
740
+ tenant=tenants[2],
741
+ parent_module_bay=cls.devices[2].module_bays.first(),
742
+ ),
743
+ )
744
+ cls.modules[0].tags.set(Tag.objects.get_for_model(Module))
745
+ cls.modules[1].tags.set(Tag.objects.get_for_model(Module)[:3])
746
+
747
+ Module.objects.create(
748
+ module_type=module_types[0],
749
+ status=cls.module_statuses[1],
750
+ asset_tag="Test Filter Asset Tag Module4",
751
+ serial="Test Filter Serial Module4",
752
+ role=module_roles[1],
753
+ tenant=tenants[0],
754
+ parent_module_bay=cls.modules[0].module_bays.first(),
755
+ )
756
+ Module.objects.create(
757
+ module_type=module_types[1],
758
+ status=cls.module_statuses[1],
759
+ asset_tag="Test Filter Asset Tag Module5",
760
+ serial="Test Filter Serial Module5",
761
+ tenant=tenants[1],
762
+ parent_module_bay=cls.modules[1].module_bays.first(),
763
+ )
764
+ Module.objects.create(
765
+ module_type=module_types[2],
766
+ status=cls.module_statuses[1],
767
+ asset_tag="Test Filter Asset Tag Module6",
768
+ serial="Test Filter Serial Module6",
769
+ tenant=tenants[2],
770
+ parent_module_bay=cls.modules[1].module_bays.last(),
771
+ )
772
+
627
773
  cls.controllers = (
628
774
  Controller.objects.create(
629
775
  name="Controller 1",
@@ -697,6 +843,157 @@ def common_test_data(cls):
697
843
  cls.controller_managed_device_groups[1].tags.set(Tag.objects.get_for_model(ControllerManagedDeviceGroup)[:3])
698
844
 
699
845
 
846
+ class ComponentTemplateTestMixin:
847
+ generic_filter_tests = [
848
+ ("description",),
849
+ ("device_type", "device_type__id"),
850
+ ("device_type", "device_type__model"),
851
+ ("label",),
852
+ ("name",),
853
+ ]
854
+
855
+ @classmethod
856
+ def setUpTestData(cls):
857
+ common_test_data(cls)
858
+
859
+
860
+ class ModularComponentTemplateTestMixin(ComponentTemplateTestMixin):
861
+ generic_filter_tests = [
862
+ *ComponentTemplateTestMixin.generic_filter_tests,
863
+ ("module_type", "module_type__id"),
864
+ ("module_type", "module_type__model"),
865
+ ]
866
+
867
+
868
+ class DeviceComponentTestMixin:
869
+ generic_filter_tests = [
870
+ ("description",),
871
+ ("device", "device__id"),
872
+ ("device", "device__name"),
873
+ ("label",),
874
+ ("name",),
875
+ ]
876
+
877
+ @classmethod
878
+ def setUpTestData(cls):
879
+ common_test_data(cls)
880
+
881
+
882
+ class ModularDeviceComponentTestMixin(DeviceComponentTestMixin):
883
+ generic_filter_tests = [
884
+ ("description",),
885
+ ("label",),
886
+ ("name",),
887
+ ("module", "module__id"),
888
+ ("module", "module__module_type__model"),
889
+ ]
890
+
891
+ def test_device(self):
892
+ """Test that the device filter returns all components for a device and its modules."""
893
+ model = self.queryset.model._meta.model_name
894
+ manufacturer = Manufacturer.objects.first()
895
+ device_type = DeviceType.objects.create(
896
+ manufacturer=manufacturer, model=f"Test Device Filter for {model} Device Type"
897
+ )
898
+ device = Device.objects.create(
899
+ device_type=device_type,
900
+ name=f"Test Device Filter for {model} Device",
901
+ location=self.loc0,
902
+ role=self.device_roles[0],
903
+ status=Status.objects.get_for_model(Device).first(),
904
+ )
905
+ parent_module_bay = ModuleBay.objects.create(name="Parent module bay", position="1", parent_device=device)
906
+ module_type = ModuleType.objects.create(
907
+ manufacturer=manufacturer, model=f"Test Device Filter for {model} Module Type", comments="Module Type test"
908
+ )
909
+ module = Module.objects.create(
910
+ module_type=module_type, parent_module_bay=parent_module_bay, status=self.module_statuses[0]
911
+ )
912
+ child_module_bay = ModuleBay.objects.create(name="Child module bay", position="1", parent_module=module)
913
+ child_module = Module.objects.create(
914
+ module_type=module_type, parent_module_bay=child_module_bay, status=self.module_statuses[0]
915
+ )
916
+ top_level_component = self.queryset.create(device=device, name=f"Top level {model}")
917
+ second_level_component = self.queryset.create(module=module, name=f"Second level {model}")
918
+ third_level_component = self.queryset.create(module=child_module, name=f"Third level {model}")
919
+ with self.subTest("device filter (pk)"):
920
+ self.assertQuerySetEqual(
921
+ self.filterset({"device": [device.pk]}, self.queryset).qs,
922
+ [top_level_component, second_level_component, third_level_component],
923
+ ordered=False,
924
+ )
925
+ with self.subTest("device filter (name)"):
926
+ self.assertQuerySetEqual(
927
+ self.filterset({"device": [device.name]}, self.queryset).qs,
928
+ [top_level_component, second_level_component, third_level_component],
929
+ ordered=False,
930
+ )
931
+
932
+ with self.subTest("device filter (pk) with an invalid uuid"):
933
+ self.assertFalse(self.filterset({"device": [uuid.uuid4()]}, self.queryset).is_valid())
934
+
935
+
936
+ class ModuleDeviceCommonTestsMixin:
937
+ def test_has_empty_module_bays(self):
938
+ test_instances = self.queryset.all()[:2]
939
+ ModuleBay.objects.create(
940
+ **{
941
+ f"parent_{self.queryset.model._meta.model_name}": test_instances[0],
942
+ "name": "test filters position 1",
943
+ "position": 1,
944
+ }
945
+ )
946
+ ModuleBay.objects.create(
947
+ **{
948
+ f"parent_{self.queryset.model._meta.model_name}": test_instances[1],
949
+ "name": "test filters position 1",
950
+ "position": 1,
951
+ }
952
+ )
953
+ with self.subTest():
954
+ params = {"has_empty_module_bays": True}
955
+ qs = self.filterset(params, self.queryset).qs
956
+ self.assertGreater(qs.count(), 0)
957
+ for instance in qs:
958
+ self.assertTrue(instance.module_bays.filter(installed_module__isnull=True).exists())
959
+ with self.subTest():
960
+ params = {"has_empty_module_bays": False}
961
+ qs = self.filterset(params, self.queryset).qs
962
+ self.assertGreater(qs.count(), 0)
963
+ for instance in qs:
964
+ self.assertFalse(instance.module_bays.filter(installed_module__isnull=True).exists())
965
+
966
+ def test_has_modules(self):
967
+ with self.subTest():
968
+ params = {"has_modules": True}
969
+ qs = self.filterset(params, self.queryset).qs
970
+ self.assertGreater(qs.count(), 0)
971
+ for instance in qs:
972
+ self.assertTrue(instance.module_bays.filter(installed_module__isnull=False).exists())
973
+ with self.subTest():
974
+ params = {"has_modules": False}
975
+ qs = self.filterset(params, self.queryset).qs
976
+ self.assertGreater(qs.count(), 0)
977
+ for instance in qs:
978
+ self.assertFalse(instance.module_bays.filter(installed_module__isnull=False).exists())
979
+
980
+
981
+ class PathEndpointModelTestMixin:
982
+ def test_connected(self):
983
+ with self.subTest():
984
+ params = {"connected": True}
985
+ self.assertQuerysetEqualAndNotEmpty(
986
+ self.filterset(params, self.queryset).qs,
987
+ self.queryset.filter(_path__is_active=True),
988
+ )
989
+ with self.subTest():
990
+ params = {"connected": False}
991
+ self.assertQuerysetEqualAndNotEmpty(
992
+ self.filterset(params, self.queryset).qs,
993
+ self.queryset.filter(Q(_path__isnull=True) | Q(_path__is_active=False)),
994
+ )
995
+
996
+
700
997
  class LocationTypeFilterSetTestCase(FilterTestCases.NameOnlyFilterTestCase):
701
998
  queryset = LocationType.objects.all()
702
999
  filterset = LocationTypeFilterSet
@@ -1008,16 +1305,17 @@ class DeviceTypeTestCase(FilterTestCases.FilterTestCase):
1008
1305
  ("console_server_port_templates", "console_server_port_templates__name"),
1009
1306
  ("device_bay_templates", "device_bay_templates__id"),
1010
1307
  ("device_bay_templates", "device_bay_templates__name"),
1308
+ ("device_family", "device_family__id"),
1309
+ ("device_family", "device_family__name"),
1011
1310
  ("devices", "devices__id"),
1012
1311
  ("front_port_templates", "front_port_templates__id"),
1013
1312
  ("front_port_templates", "front_port_templates__name"),
1014
- ("device_family", "device_family__id"),
1015
- ("device_family", "device_family__name"),
1016
1313
  ("interface_templates", "interface_templates__id"),
1017
1314
  ("interface_templates", "interface_templates__name"),
1018
1315
  ("manufacturer", "manufacturer__id"),
1019
1316
  ("manufacturer", "manufacturer__name"),
1020
1317
  ("model",),
1318
+ ("module_bay_templates", "module_bay_templates__id"),
1021
1319
  ("part_number",),
1022
1320
  ("power_outlet_templates", "power_outlet_templates__id"),
1023
1321
  ("power_outlet_templates", "power_outlet_templates__name"),
@@ -1158,13 +1456,13 @@ class DeviceTypeTestCase(FilterTestCases.FilterTestCase):
1158
1456
  params = {"pass_through_ports": True}
1159
1457
  self.assertQuerysetEqual(
1160
1458
  self.filterset(params, self.queryset).qs,
1161
- self.queryset.filter(query),
1459
+ self.queryset.filter(query).distinct(),
1162
1460
  )
1163
1461
  with self.subTest():
1164
1462
  params = {"pass_through_ports": False}
1165
1463
  self.assertQuerysetEqual(
1166
1464
  self.filterset(params, self.queryset).qs,
1167
- self.queryset.filter(~query),
1465
+ self.queryset.filter(~query).distinct(),
1168
1466
  )
1169
1467
 
1170
1468
  def test_device_bays(self):
@@ -1188,36 +1486,21 @@ class DeviceTypeTestCase(FilterTestCases.FilterTestCase):
1188
1486
  self.assertEqual(self.filterset(params, self.queryset).qs.values_list("pk", flat=True)[0], value)
1189
1487
 
1190
1488
 
1191
- class Mixins:
1192
- class ComponentTemplateMixin(FilterTestCases.FilterTestCase):
1193
- generic_filter_tests = [
1194
- ("description",),
1195
- ("device_type", "device_type__id"),
1196
- ("device_type", "device_type__model"),
1197
- ("label",),
1198
- ("name",),
1199
- ]
1200
-
1201
- @classmethod
1202
- def setUpTestData(cls):
1203
- common_test_data(cls)
1204
-
1205
-
1206
- class ConsolePortTemplateTestCase(Mixins.ComponentTemplateMixin):
1489
+ class ConsolePortTemplateTestCase(ModularComponentTemplateTestMixin, FilterTestCases.FilterTestCase):
1207
1490
  queryset = ConsolePortTemplate.objects.all()
1208
1491
  filterset = ConsolePortTemplateFilterSet
1209
1492
 
1210
1493
 
1211
- class ConsoleServerPortTemplateTestCase(Mixins.ComponentTemplateMixin):
1494
+ class ConsoleServerPortTemplateTestCase(ModularComponentTemplateTestMixin, FilterTestCases.FilterTestCase):
1212
1495
  queryset = ConsoleServerPortTemplate.objects.all()
1213
1496
  filterset = ConsoleServerPortTemplateFilterSet
1214
1497
 
1215
1498
 
1216
- class PowerPortTemplateTestCase(Mixins.ComponentTemplateMixin):
1499
+ class PowerPortTemplateTestCase(ModularComponentTemplateTestMixin, FilterTestCases.FilterTestCase):
1217
1500
  queryset = PowerPortTemplate.objects.all()
1218
1501
  filterset = PowerPortTemplateFilterSet
1219
1502
  generic_filter_tests = [
1220
- *Mixins.ComponentTemplateMixin.generic_filter_tests,
1503
+ *ModularComponentTemplateTestMixin.generic_filter_tests,
1221
1504
  ("allocated_draw",),
1222
1505
  ("maximum_draw",),
1223
1506
  ("power_outlet_templates", "power_outlet_templates__id"),
@@ -1239,11 +1522,12 @@ class PowerPortTemplateTestCase(Mixins.ComponentTemplateMixin):
1239
1522
  )
1240
1523
 
1241
1524
 
1242
- class PowerOutletTemplateTestCase(Mixins.ComponentTemplateMixin):
1525
+ class PowerOutletTemplateTestCase(ModularComponentTemplateTestMixin, FilterTestCases.FilterTestCase):
1243
1526
  queryset = PowerOutletTemplate.objects.all()
1244
1527
  filterset = PowerOutletTemplateFilterSet
1245
1528
  generic_filter_tests = [
1246
- *Mixins.ComponentTemplateMixin.generic_filter_tests,
1529
+ *ModularComponentTemplateTestMixin.generic_filter_tests,
1530
+ ("feed_leg",),
1247
1531
  ("power_port_template", "power_port_template__id"),
1248
1532
  ("power_port_template", "power_port_template__name"),
1249
1533
  ]
@@ -1261,26 +1545,14 @@ class PowerOutletTemplateTestCase(Mixins.ComponentTemplateMixin):
1261
1545
  description="Power Outlet Description 4",
1262
1546
  )
1263
1547
 
1264
- def test_feed_leg(self):
1265
- # TODO: Not a generic_filter_test because this is a single-value filter
1266
- params = {"feed_leg": [PowerOutletFeedLegChoices.FEED_LEG_A]}
1267
- self.assertQuerysetEqual(
1268
- self.filterset(params, self.queryset).qs,
1269
- self.queryset.filter(feed_leg=PowerOutletFeedLegChoices.FEED_LEG_A),
1270
- )
1271
-
1272
1548
 
1273
- class InterfaceTemplateTestCase(Mixins.ComponentTemplateMixin):
1549
+ class InterfaceTemplateTestCase(ModularComponentTemplateTestMixin, FilterTestCases.FilterTestCase):
1274
1550
  queryset = InterfaceTemplate.objects.all()
1275
1551
  filterset = InterfaceTemplateFilterSet
1276
-
1277
- def test_type(self):
1278
- # TODO: Not a generic_filter_test because this is a single-value filter
1279
- params = {"type": [InterfaceTypeChoices.TYPE_1GE_FIXED]}
1280
- self.assertQuerysetEqual(
1281
- self.filterset(params, self.queryset).qs,
1282
- self.queryset.filter(type=InterfaceTypeChoices.TYPE_1GE_FIXED),
1283
- )
1552
+ generic_filter_tests = [
1553
+ *ModularComponentTemplateTestMixin.generic_filter_tests,
1554
+ ("type",),
1555
+ ]
1284
1556
 
1285
1557
  def test_mgmt_only(self):
1286
1558
  # TODO: Not a generic_filter_test because this is a boolean filter but not a RelatedMembershipBooleanFilter
@@ -1298,11 +1570,11 @@ class InterfaceTemplateTestCase(Mixins.ComponentTemplateMixin):
1298
1570
  )
1299
1571
 
1300
1572
 
1301
- class FrontPortTemplateTestCase(Mixins.ComponentTemplateMixin):
1573
+ class FrontPortTemplateTestCase(ModularComponentTemplateTestMixin, FilterTestCases.FilterTestCase):
1302
1574
  queryset = FrontPortTemplate.objects.all()
1303
1575
  filterset = FrontPortTemplateFilterSet
1304
1576
  generic_filter_tests = [
1305
- *Mixins.ComponentTemplateMixin.generic_filter_tests,
1577
+ *ModularComponentTemplateTestMixin.generic_filter_tests,
1306
1578
  ("rear_port_position",),
1307
1579
  ("rear_port_template", "rear_port_template__id"),
1308
1580
  ]
@@ -1316,11 +1588,11 @@ class FrontPortTemplateTestCase(Mixins.ComponentTemplateMixin):
1316
1588
  )
1317
1589
 
1318
1590
 
1319
- class RearPortTemplateTestCase(Mixins.ComponentTemplateMixin):
1591
+ class RearPortTemplateTestCase(ModularComponentTemplateTestMixin, FilterTestCases.FilterTestCase):
1320
1592
  queryset = RearPortTemplate.objects.all()
1321
1593
  filterset = RearPortTemplateFilterSet
1322
1594
  generic_filter_tests = [
1323
- *Mixins.ComponentTemplateMixin.generic_filter_tests,
1595
+ *ModularComponentTemplateTestMixin.generic_filter_tests,
1324
1596
  ("front_port_templates", "front_port_templates__id"),
1325
1597
  ]
1326
1598
 
@@ -1355,7 +1627,7 @@ class RearPortTemplateTestCase(Mixins.ComponentTemplateMixin):
1355
1627
  )
1356
1628
 
1357
1629
 
1358
- class DeviceBayTemplateTestCase(Mixins.ComponentTemplateMixin):
1630
+ class DeviceBayTemplateTestCase(ComponentTemplateTestMixin, FilterTestCases.FilterTestCase):
1359
1631
  queryset = DeviceBayTemplate.objects.all()
1360
1632
  filterset = DeviceBayTemplateFilterSet
1361
1633
 
@@ -1402,7 +1674,11 @@ class PlatformTestCase(FilterTestCases.NameOnlyFilterTestCase):
1402
1674
  self.assertEqual(self.filterset(params, self.queryset).qs.count(), len(virtual_machines))
1403
1675
 
1404
1676
 
1405
- class DeviceTestCase(FilterTestCases.FilterTestCase, FilterTestCases.TenancyFilterTestCaseMixin):
1677
+ class DeviceTestCase(
1678
+ ModuleDeviceCommonTestsMixin,
1679
+ FilterTestCases.FilterTestCase,
1680
+ FilterTestCases.TenancyFilterTestCaseMixin,
1681
+ ):
1406
1682
  queryset = Device.objects.all()
1407
1683
  filterset = DeviceFilterSet
1408
1684
  tenancy_related_name = "devices"
@@ -1424,9 +1700,11 @@ class DeviceTestCase(FilterTestCases.FilterTestCase, FilterTestCases.TenancyFilt
1424
1700
  ("device_type", "device_type__model"),
1425
1701
  ("front_ports", "front_ports__id"),
1426
1702
  ("interfaces", "interfaces__id"),
1703
+ ("interfaces", "interfaces__name"),
1427
1704
  ("mac_address", "interfaces__mac_address"),
1428
1705
  ("manufacturer", "device_type__manufacturer__id"),
1429
1706
  ("manufacturer", "device_type__manufacturer__name"),
1707
+ ("module_bays", "module_bays__id"),
1430
1708
  ("name",),
1431
1709
  ("platform", "platform__id"),
1432
1710
  ("platform", "platform__name"),
@@ -1510,7 +1788,7 @@ class DeviceTestCase(FilterTestCases.FilterTestCase, FilterTestCases.TenancyFilt
1510
1788
  )
1511
1789
 
1512
1790
  # Assign primary IPs for filtering
1513
- interfaces = Interface.objects.all()
1791
+ interfaces = Interface.objects.filter(device__isnull=False)
1514
1792
  ipaddr_status = Status.objects.get_for_model(IPAddress).first()
1515
1793
  prefix_status = Status.objects.get_for_model(Prefix).first()
1516
1794
  namespace = Namespace.objects.first()
@@ -1681,21 +1959,17 @@ class DeviceTestCase(FilterTestCases.FilterTestCase, FilterTestCases.TenancyFilt
1681
1959
  )
1682
1960
 
1683
1961
 
1684
- class ConsolePortTestCase(FilterTestCases.FilterTestCase):
1962
+ class ConsolePortTestCase(PathEndpointModelTestMixin, ModularDeviceComponentTestMixin, FilterTestCases.FilterTestCase):
1685
1963
  queryset = ConsolePort.objects.all()
1686
1964
  filterset = ConsolePortFilterSet
1687
1965
  generic_filter_tests = [
1966
+ *ModularDeviceComponentTestMixin.generic_filter_tests,
1688
1967
  ("cable", "cable__id"),
1689
- ("description",),
1690
- ("device", "device__id"),
1691
- ("device", "device__name"),
1692
- ("label",),
1693
- ("name",),
1694
1968
  ]
1695
1969
 
1696
1970
  @classmethod
1697
1971
  def setUpTestData(cls):
1698
- common_test_data(cls)
1972
+ super().setUpTestData()
1699
1973
 
1700
1974
  devices = (
1701
1975
  Device.objects.get(name="Device 1"),
@@ -1730,31 +2004,20 @@ class ConsolePortTestCase(FilterTestCases.FilterTestCase):
1730
2004
  )
1731
2005
  # Third port is not connected
1732
2006
 
1733
- def test_connected(self):
1734
- # TODO: Not a generic_filter_test because this is a boolean filter but not a RelatedMembershipBooleanFilter
1735
- with self.subTest():
1736
- params = {"connected": True}
1737
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
1738
- with self.subTest():
1739
- params = {"connected": False}
1740
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
1741
-
1742
2007
 
1743
- class ConsoleServerPortTestCase(FilterTestCases.FilterTestCase):
2008
+ class ConsoleServerPortTestCase(
2009
+ PathEndpointModelTestMixin, ModularDeviceComponentTestMixin, FilterTestCases.FilterTestCase
2010
+ ):
1744
2011
  queryset = ConsoleServerPort.objects.all()
1745
2012
  filterset = ConsoleServerPortFilterSet
1746
2013
  generic_filter_tests = [
2014
+ *ModularDeviceComponentTestMixin.generic_filter_tests,
1747
2015
  ("cable", "cable__id"),
1748
- ("description",),
1749
- ("device", "device__id"),
1750
- ("device", "device__name"),
1751
- ("label",),
1752
- ("name",),
1753
2016
  ]
1754
2017
 
1755
2018
  @classmethod
1756
2019
  def setUpTestData(cls):
1757
- common_test_data(cls)
2020
+ super().setUpTestData()
1758
2021
 
1759
2022
  devices = (
1760
2023
  Device.objects.get(name="Device 1"),
@@ -1789,28 +2052,15 @@ class ConsoleServerPortTestCase(FilterTestCases.FilterTestCase):
1789
2052
  )
1790
2053
  # Third port is not connected
1791
2054
 
1792
- def test_connected(self):
1793
- # TODO: Not a generic_filter_test because this is a boolean filter but not a RelatedMembershipBooleanFilter
1794
- with self.subTest():
1795
- params = {"connected": True}
1796
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
1797
- with self.subTest():
1798
- params = {"connected": False}
1799
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
1800
-
1801
2055
 
1802
- class PowerPortTestCase(FilterTestCases.FilterTestCase):
2056
+ class PowerPortTestCase(PathEndpointModelTestMixin, ModularDeviceComponentTestMixin, FilterTestCases.FilterTestCase):
1803
2057
  queryset = PowerPort.objects.all()
1804
2058
  filterset = PowerPortFilterSet
1805
2059
  generic_filter_tests = [
2060
+ *ModularDeviceComponentTestMixin.generic_filter_tests,
1806
2061
  ("allocated_draw",),
1807
2062
  ("cable", "cable__id"),
1808
- ("description",),
1809
- ("device", "device__id"),
1810
- ("device", "device__name"),
1811
- ("label",),
1812
2063
  ("maximum_draw",),
1813
- ("name",),
1814
2064
  ("power_outlets", "power_outlets__id"),
1815
2065
  ("power_outlets", "power_outlets__name"),
1816
2066
  ]
@@ -1854,32 +2104,20 @@ class PowerPortTestCase(FilterTestCases.FilterTestCase):
1854
2104
  )
1855
2105
  # Third port is not connected
1856
2106
 
1857
- def test_connected(self):
1858
- # TODO: Not a generic_filter_test because this is a boolean filter but not a RelatedMembershipBooleanFilter
1859
- with self.subTest():
1860
- params = {"connected": True}
1861
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
1862
- with self.subTest():
1863
- params = {"connected": False}
1864
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
1865
-
1866
2107
 
1867
- class PowerOutletTestCase(FilterTestCases.FilterTestCase):
2108
+ class PowerOutletTestCase(PathEndpointModelTestMixin, ModularDeviceComponentTestMixin, FilterTestCases.FilterTestCase):
1868
2109
  queryset = PowerOutlet.objects.all()
1869
2110
  filterset = PowerOutletFilterSet
1870
2111
  generic_filter_tests = [
2112
+ *ModularDeviceComponentTestMixin.generic_filter_tests,
1871
2113
  ("cable", "cable__id"),
1872
- ("description",),
1873
- ("device", "device__id"),
1874
- ("device", "device__name"),
1875
- ("label",),
1876
- ("name",),
2114
+ ("feed_leg",),
1877
2115
  ("power_port", "power_port__id"),
1878
2116
  ]
1879
2117
 
1880
2118
  @classmethod
1881
2119
  def setUpTestData(cls):
1882
- common_test_data(cls)
2120
+ super().setUpTestData()
1883
2121
 
1884
2122
  devices = (
1885
2123
  Device.objects.get(name="Device 1"),
@@ -1915,26 +2153,12 @@ class PowerOutletTestCase(FilterTestCases.FilterTestCase):
1915
2153
  )
1916
2154
  # Third port is not connected
1917
2155
 
1918
- def test_feed_leg(self):
1919
- # TODO: Not a generic_filter_test because this is a single-value filter
1920
- # 2.0 TODO: Support filtering for multiple values
1921
- params = {"feed_leg": [PowerOutletFeedLegChoices.FEED_LEG_A]}
1922
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
1923
-
1924
- def test_connected(self):
1925
- # TODO: Not a generic_filter_test because this is a boolean filter but not a RelatedMembershipBooleanFilter
1926
- with self.subTest():
1927
- params = {"connected": True}
1928
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
1929
- with self.subTest():
1930
- params = {"connected": False}
1931
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
1932
-
1933
2156
 
1934
- class InterfaceTestCase(FilterTestCases.FilterTestCase):
2157
+ class InterfaceTestCase(PathEndpointModelTestMixin, ModularDeviceComponentTestMixin, FilterTestCases.FilterTestCase):
1935
2158
  queryset = Interface.objects.all()
1936
2159
  filterset = InterfaceFilterSet
1937
2160
  generic_filter_tests = [
2161
+ # parent class generic_filter_tests intentionally excluded
1938
2162
  ("bridge", "bridge__id"),
1939
2163
  ("bridge", "bridge__name"),
1940
2164
  ("bridged_interfaces", "bridged_interfaces__id"),
@@ -1944,17 +2168,20 @@ class InterfaceTestCase(FilterTestCases.FilterTestCase):
1944
2168
  ("child_interfaces", "child_interfaces__name"),
1945
2169
  ("description",),
1946
2170
  # ("device", "device__id"), # TODO - InterfaceFilterSet overrides device as a MultiValueCharFilter on name only
1947
- ("device", "device__name"),
1948
2171
  ("label",),
1949
2172
  ("lag", "lag__id"),
1950
2173
  ("lag", "lag__name"),
1951
2174
  ("mac_address",),
1952
2175
  ("member_interfaces", "member_interfaces__id"),
1953
2176
  ("member_interfaces", "member_interfaces__name"),
2177
+ ("module", "module__id"),
2178
+ ("module", "module__module_type__model"),
1954
2179
  ("mtu",),
1955
2180
  ("name",),
1956
2181
  ("parent_interface", "parent_interface__id"),
1957
2182
  ("parent_interface", "parent_interface__name"),
2183
+ ("role", "role__id"),
2184
+ ("role", "role__name"),
1958
2185
  ("status", "status__id"),
1959
2186
  ("status", "status__name"),
1960
2187
  ("type",),
@@ -1966,7 +2193,7 @@ class InterfaceTestCase(FilterTestCases.FilterTestCase):
1966
2193
 
1967
2194
  @classmethod
1968
2195
  def setUpTestData(cls):
1969
- common_test_data(cls)
2196
+ super().setUpTestData()
1970
2197
 
1971
2198
  devices = (
1972
2199
  Device.objects.get(name="Device 1"),
@@ -1976,15 +2203,17 @@ class InterfaceTestCase(FilterTestCases.FilterTestCase):
1976
2203
  vlans = VLAN.objects.all()[:3]
1977
2204
 
1978
2205
  interface_statuses = Status.objects.get_for_model(Interface)
2206
+ interface_roles = Role.objects.get_for_model(Interface)
1979
2207
 
1980
2208
  # Cabled interfaces
1981
2209
  cabled_interfaces = (
1982
- Interface.objects.get(name="Interface 1"),
1983
- Interface.objects.get(name="Interface 2"),
1984
- Interface.objects.get(name="Interface 3"),
2210
+ Interface.objects.get(name="Test Interface 1"),
2211
+ Interface.objects.get(name="Test Interface 2"),
2212
+ Interface.objects.get(name="Test Interface 3"),
1985
2213
  Interface.objects.create(
1986
2214
  device=devices[2],
1987
2215
  name="Parent Interface 1",
2216
+ role=interface_roles[0],
1988
2217
  type=InterfaceTypeChoices.TYPE_OTHER,
1989
2218
  mode=InterfaceModeChoices.MODE_TAGGED,
1990
2219
  enabled=True,
@@ -2004,6 +2233,7 @@ class InterfaceTestCase(FilterTestCases.FilterTestCase):
2004
2233
  Interface.objects.create(
2005
2234
  device=devices[2],
2006
2235
  name="Parent Interface 3",
2236
+ role=interface_roles[1],
2007
2237
  type=InterfaceTypeChoices.TYPE_OTHER,
2008
2238
  mode=InterfaceModeChoices.MODE_TAGGED,
2009
2239
  enabled=False,
@@ -2069,6 +2299,7 @@ class InterfaceTestCase(FilterTestCases.FilterTestCase):
2069
2299
  Interface.objects.create(
2070
2300
  device=cabled_interfaces[3].device,
2071
2301
  name="Child 1",
2302
+ role=interface_roles[2],
2072
2303
  parent_interface=cabled_interfaces[3],
2073
2304
  status=interface_statuses[3],
2074
2305
  type=InterfaceTypeChoices.TYPE_VIRTUAL,
@@ -2083,6 +2314,7 @@ class InterfaceTestCase(FilterTestCases.FilterTestCase):
2083
2314
  Interface.objects.create(
2084
2315
  device=cabled_interfaces[5].device,
2085
2316
  name="Child 3",
2317
+ role=interface_roles[0],
2086
2318
  parent_interface=cabled_interfaces[5],
2087
2319
  status=interface_statuses[3],
2088
2320
  type=InterfaceTypeChoices.TYPE_VIRTUAL,
@@ -2099,12 +2331,14 @@ class InterfaceTestCase(FilterTestCases.FilterTestCase):
2099
2331
  Interface.objects.create(
2100
2332
  device=devices[2],
2101
2333
  name="Bridge 2",
2334
+ role=interface_roles[1],
2102
2335
  status=interface_statuses[3],
2103
2336
  type=InterfaceTypeChoices.TYPE_BRIDGE,
2104
2337
  ),
2105
2338
  Interface.objects.create(
2106
2339
  device=devices[2],
2107
2340
  name="Bridge 3",
2341
+ role=interface_roles[2],
2108
2342
  status=interface_statuses[3],
2109
2343
  type=InterfaceTypeChoices.TYPE_BRIDGE,
2110
2344
  ),
@@ -2112,6 +2346,7 @@ class InterfaceTestCase(FilterTestCases.FilterTestCase):
2112
2346
  Interface.objects.create(
2113
2347
  device=bridge_interfaces[0].device,
2114
2348
  name="Bridged 1",
2349
+ role=interface_roles[0],
2115
2350
  bridge=bridge_interfaces[0],
2116
2351
  status=interface_statuses[3],
2117
2352
  type=InterfaceTypeChoices.TYPE_1GE_SFP,
@@ -2126,6 +2361,7 @@ class InterfaceTestCase(FilterTestCases.FilterTestCase):
2126
2361
  Interface.objects.create(
2127
2362
  device=bridge_interfaces[2].device,
2128
2363
  name="Bridged 3",
2364
+ role=interface_roles[1],
2129
2365
  bridge=bridge_interfaces[2],
2130
2366
  status=interface_statuses[3],
2131
2367
  type=InterfaceTypeChoices.TYPE_1GE_SFP,
@@ -2136,6 +2372,7 @@ class InterfaceTestCase(FilterTestCases.FilterTestCase):
2136
2372
  Interface.objects.create(
2137
2373
  device=devices[2],
2138
2374
  name="LAG 1",
2375
+ role=interface_roles[0],
2139
2376
  type=InterfaceTypeChoices.TYPE_LAG,
2140
2377
  status=interface_statuses[3],
2141
2378
  ),
@@ -2148,6 +2385,7 @@ class InterfaceTestCase(FilterTestCases.FilterTestCase):
2148
2385
  Interface.objects.create(
2149
2386
  device=devices[2],
2150
2387
  name="LAG 3",
2388
+ role=interface_roles[1],
2151
2389
  type=InterfaceTypeChoices.TYPE_LAG,
2152
2390
  status=interface_statuses[3],
2153
2391
  ),
@@ -2163,6 +2401,7 @@ class InterfaceTestCase(FilterTestCases.FilterTestCase):
2163
2401
  device=devices[2],
2164
2402
  name="Member 2",
2165
2403
  lag=lag_interfaces[1],
2404
+ role=interface_roles[2],
2166
2405
  type=InterfaceTypeChoices.TYPE_1GE_SFP,
2167
2406
  status=interface_statuses[3],
2168
2407
  )
@@ -2174,32 +2413,35 @@ class InterfaceTestCase(FilterTestCases.FilterTestCase):
2174
2413
  status=interface_statuses[3],
2175
2414
  )
2176
2415
 
2177
- def test_connected(self):
2178
- # TODO: Not a generic_filter_test because this is a boolean filter but not a RelatedMembershipBooleanFilter
2179
- with self.subTest():
2180
- params = {"connected": True}
2181
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
2182
- with self.subTest():
2183
- params = {"connected": False}
2184
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 17)
2185
-
2186
2416
  def test_enabled(self):
2187
2417
  # TODO: Not a generic_filter_test because this is a boolean filter but not a RelatedMembershipBooleanFilter
2188
2418
  with self.subTest():
2189
2419
  params = {"enabled": True}
2190
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 19)
2420
+ self.assertQuerysetEqualAndNotEmpty(
2421
+ self.filterset(params, self.queryset).qs,
2422
+ self.queryset.filter(**params),
2423
+ )
2191
2424
  with self.subTest():
2192
2425
  params = {"enabled": False}
2193
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
2426
+ self.assertQuerysetEqualAndNotEmpty(
2427
+ self.filterset(params, self.queryset).qs,
2428
+ self.queryset.filter(**params),
2429
+ )
2194
2430
 
2195
2431
  def test_mgmt_only(self):
2196
2432
  # TODO: Not a generic_filter_test because this is a boolean filter but not a RelatedMembershipBooleanFilter
2197
2433
  with self.subTest():
2198
2434
  params = {"mgmt_only": True}
2199
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
2435
+ self.assertQuerysetEqualAndNotEmpty(
2436
+ self.filterset(params, self.queryset).qs,
2437
+ self.queryset.filter(**params),
2438
+ )
2200
2439
  with self.subTest():
2201
2440
  params = {"mgmt_only": False}
2202
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 17)
2441
+ self.assertQuerysetEqualAndNotEmpty(
2442
+ self.filterset(params, self.queryset).qs,
2443
+ self.queryset.filter(**params),
2444
+ )
2203
2445
 
2204
2446
  def test_mode(self):
2205
2447
  # TODO: Not a generic_filter_test because this is a single-value filter
@@ -2264,15 +2506,161 @@ class InterfaceTestCase(FilterTestCases.FilterTestCase):
2264
2506
  with self.subTest():
2265
2507
  self.assertFalse(queryset.filter(name="int4").exists())
2266
2508
 
2509
+ def test_device(self):
2510
+ """
2511
+ Test that the device filter returns all components for a device and its
2512
+ modules, including virtual chassis member devices and their modules.
2513
+ """
2514
+ status = Status.objects.get_for_model(Interface).first()
2515
+ manufacturer = Manufacturer.objects.first()
2516
+ device_type = DeviceType.objects.create(
2517
+ manufacturer=manufacturer, model="Test Device Filter for Interface Device Type"
2518
+ )
2519
+ device_vc_master = Device.objects.create(
2520
+ device_type=device_type,
2521
+ name="Test Device Filter for Interface Device VC Master",
2522
+ location=self.loc0,
2523
+ role=self.device_roles[0],
2524
+ status=Status.objects.get_for_model(Device).first(),
2525
+ )
2526
+ vc = VirtualChassis.objects.create(
2527
+ name="Test Device Filter for Interface Virtual Chassis", master=device_vc_master
2528
+ )
2529
+ device_vc_master.virtual_chassis = vc
2530
+ device_vc_master.save()
2531
+ parent_module_bay = ModuleBay.objects.create(
2532
+ name="Parent module bay", position="1", parent_device=device_vc_master
2533
+ )
2534
+ module_type = ModuleType.objects.create(
2535
+ manufacturer=manufacturer, model="Test Device Filter for Interface Module Type", comments="Module Type test"
2536
+ )
2537
+ module = Module.objects.create(
2538
+ module_type=module_type, parent_module_bay=parent_module_bay, status=self.module_statuses[0]
2539
+ )
2540
+ child_module_bay = ModuleBay.objects.create(name="Child module bay", position="1", parent_module=module)
2541
+ child_module = Module.objects.create(
2542
+ module_type=module_type, parent_module_bay=child_module_bay, status=self.module_statuses[0]
2543
+ )
2544
+ top_level_interface = self.queryset.create(
2545
+ device=device_vc_master,
2546
+ name="Top level Interface VC Master",
2547
+ type=InterfaceTypeChoices.TYPE_1GE_SFP,
2548
+ status=status,
2549
+ )
2550
+ second_level_interface = self.queryset.create(
2551
+ module=module,
2552
+ name="Second level Interface VC Master",
2553
+ type=InterfaceTypeChoices.TYPE_1GE_SFP,
2554
+ status=status,
2555
+ )
2556
+ third_level_interface = self.queryset.create(
2557
+ module=child_module,
2558
+ name="Third level Interface VC Master",
2559
+ type=InterfaceTypeChoices.TYPE_1GE_SFP,
2560
+ status=status,
2561
+ )
2562
+ device_vc_member = Device.objects.create(
2563
+ device_type=device_type,
2564
+ name="Test Device Filter for Interface Device VC Member",
2565
+ location=self.loc0,
2566
+ role=self.device_roles[0],
2567
+ status=Status.objects.get_for_model(Device).first(),
2568
+ virtual_chassis=vc,
2569
+ )
2570
+ parent_module_bay_vc_member = ModuleBay.objects.create(
2571
+ name="Parent module bay", position="1", parent_device=device_vc_member
2572
+ )
2573
+ module_vc_member = Module.objects.create(
2574
+ module_type=module_type, parent_module_bay=parent_module_bay_vc_member, status=self.module_statuses[0]
2575
+ )
2576
+ child_module_bay_vc_member = ModuleBay.objects.create(
2577
+ name="Child module bay", position="1", parent_module=module_vc_member
2578
+ )
2579
+ child_module_vc_member = Module.objects.create(
2580
+ module_type=module_type, parent_module_bay=child_module_bay_vc_member, status=self.module_statuses[0]
2581
+ )
2582
+ top_level_interface_vc_member = self.queryset.create(
2583
+ device=device_vc_member,
2584
+ name="Top level Interface VC Member",
2585
+ type=InterfaceTypeChoices.TYPE_1GE_SFP,
2586
+ status=status,
2587
+ )
2588
+ second_level_interface_vc_member = self.queryset.create(
2589
+ module=module_vc_member,
2590
+ name="Second level Interface VC Member",
2591
+ type=InterfaceTypeChoices.TYPE_1GE_SFP,
2592
+ status=status,
2593
+ )
2594
+ third_level_interface_vc_member = self.queryset.create(
2595
+ module=child_module_vc_member,
2596
+ name="Third level Interface VC Member",
2597
+ type=InterfaceTypeChoices.TYPE_1GE_SFP,
2598
+ status=status,
2599
+ )
2600
+
2601
+ with self.subTest("device filter on pk"):
2602
+ self.assertQuerySetEqual(
2603
+ self.filterset({"device": [device_vc_master.pk]}, self.queryset).qs,
2604
+ [
2605
+ top_level_interface,
2606
+ second_level_interface,
2607
+ third_level_interface,
2608
+ top_level_interface_vc_member,
2609
+ second_level_interface_vc_member,
2610
+ third_level_interface_vc_member,
2611
+ ],
2612
+ ordered=False,
2613
+ )
2614
+
2615
+ with self.subTest("device filter on name"):
2616
+ self.assertQuerySetEqual(
2617
+ self.filterset({"device": [device_vc_master.name]}, self.queryset).qs,
2618
+ [
2619
+ top_level_interface,
2620
+ second_level_interface,
2621
+ third_level_interface,
2622
+ top_level_interface_vc_member,
2623
+ second_level_interface_vc_member,
2624
+ third_level_interface_vc_member,
2625
+ ],
2626
+ ordered=False,
2627
+ )
2628
+
2629
+ with self.subTest("device_id filter"):
2630
+ self.assertQuerySetEqual(
2631
+ self.filterset({"device_id": [device_vc_master.pk]}, self.queryset).qs,
2632
+ [
2633
+ top_level_interface,
2634
+ second_level_interface,
2635
+ third_level_interface,
2636
+ top_level_interface_vc_member,
2637
+ second_level_interface_vc_member,
2638
+ third_level_interface_vc_member,
2639
+ ],
2640
+ ordered=False,
2641
+ )
2642
+
2643
+ with self.subTest("device_id filter with an invalid uuid"):
2644
+ self.assertFalse(self.filterset({"device_id": [uuid.uuid4()]}, self.queryset).is_valid())
2645
+
2646
+ with self.subTest("device (pk) filter with an invalid uuid"):
2647
+ self.assertFalse(self.filterset({"device": [uuid.uuid4()]}, self.queryset).is_valid())
2648
+
2267
2649
  def test_kind(self):
2268
2650
  # TODO: Not a generic_filter_test because this is a single-value filter
2269
2651
  # 2.0 TODO: Support filtering for multiple values
2270
2652
  with self.subTest():
2271
2653
  params = {"kind": "physical"}
2272
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 12)
2654
+ self.assertQuerysetEqualAndNotEmpty(
2655
+ self.filterset(params, self.queryset).qs,
2656
+ self.queryset.exclude(type__in=NONCONNECTABLE_IFACE_TYPES),
2657
+ )
2273
2658
  with self.subTest():
2274
2659
  params = {"kind": "virtual"}
2275
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 9)
2660
+ self.assertQuerysetEqualAndNotEmpty(
2661
+ self.filterset(params, self.queryset).qs,
2662
+ self.queryset.filter(type__in=VIRTUAL_IFACE_TYPES),
2663
+ )
2276
2664
 
2277
2665
  def test_vlan(self):
2278
2666
  # TODO: Not a generic_filter_test because this is a single-value filter
@@ -2297,24 +2685,21 @@ class InterfaceTestCase(FilterTestCases.FilterTestCase):
2297
2685
  )
2298
2686
 
2299
2687
 
2300
- class FrontPortTestCase(FilterTestCases.FilterTestCase):
2688
+ class FrontPortTestCase(ModularDeviceComponentTestMixin, FilterTestCases.FilterTestCase):
2301
2689
  queryset = FrontPort.objects.all()
2302
2690
  filterset = FrontPortFilterSet
2303
2691
  generic_filter_tests = [
2304
- ("description",),
2692
+ *ModularDeviceComponentTestMixin.generic_filter_tests,
2305
2693
  ("cable", "cable__id"),
2306
- ("device", "device__id"),
2307
- ("device", "device__name"),
2308
- ("label",),
2309
- ("name",),
2310
2694
  ("rear_port", "rear_port__id"),
2311
2695
  ("rear_port", "rear_port__name"),
2312
2696
  ("rear_port_position",),
2697
+ ("type",),
2313
2698
  ]
2314
2699
 
2315
2700
  @classmethod
2316
2701
  def setUpTestData(cls):
2317
- common_test_data(cls)
2702
+ super().setUpTestData()
2318
2703
 
2319
2704
  devices = (
2320
2705
  Device.objects.get(name="Device 1"),
@@ -2378,30 +2763,93 @@ class FrontPortTestCase(FilterTestCases.FilterTestCase):
2378
2763
  )
2379
2764
  # Third port is not connected
2380
2765
 
2381
- def test_type(self):
2382
- # TODO: Not a generic_filter_test because this is a single-value filter
2383
- params = {"type": [PortTypeChoices.TYPE_8P8C]}
2384
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
2766
+ def test_device(self):
2767
+ """Test that the device filter returns all components for a device and its modules."""
2768
+ manufacturer = Manufacturer.objects.first()
2769
+ device_type = DeviceType.objects.create(
2770
+ manufacturer=manufacturer, model="Test Device Filter for FrontPort Device Type"
2771
+ )
2772
+ device = Device.objects.create(
2773
+ device_type=device_type,
2774
+ name="Test Device Filter for FrontPort Device",
2775
+ location=self.loc0,
2776
+ role=self.device_roles[0],
2777
+ status=Status.objects.get_for_model(Device).first(),
2778
+ )
2779
+ parent_module_bay = ModuleBay.objects.create(name="Parent module bay", position="1", parent_device=device)
2780
+ module_type = ModuleType.objects.create(
2781
+ manufacturer=manufacturer, model="Test Device Filter for FrontPort Module Type", comments="Module Type test"
2782
+ )
2783
+ module = Module.objects.create(
2784
+ module_type=module_type, parent_module_bay=parent_module_bay, status=self.module_statuses[0]
2785
+ )
2786
+ child_module_bay = ModuleBay.objects.create(name="Child module bay", position="1", parent_module=module)
2787
+ child_module = Module.objects.create(
2788
+ module_type=module_type, parent_module_bay=child_module_bay, status=self.module_statuses[0]
2789
+ )
2790
+ top_level_rearport = RearPort.objects.create(
2791
+ device=device,
2792
+ name="Top level Rear Port",
2793
+ type=PortTypeChoices.TYPE_8P8C,
2794
+ positions=6,
2795
+ )
2796
+ second_level_rearport = RearPort.objects.create(
2797
+ module=module,
2798
+ name="Second level Rear Port",
2799
+ type=PortTypeChoices.TYPE_8P8C,
2800
+ positions=6,
2801
+ )
2802
+ third_level_rearport = RearPort.objects.create(
2803
+ module=child_module,
2804
+ name="Third level Rear Port",
2805
+ type=PortTypeChoices.TYPE_8P8C,
2806
+ positions=6,
2807
+ )
2808
+ top_level_frontport = self.queryset.create(
2809
+ device=device,
2810
+ name="Top level Front Port",
2811
+ rear_port=top_level_rearport,
2812
+ rear_port_position=1,
2813
+ )
2814
+ second_level_frontport = self.queryset.create(
2815
+ module=module,
2816
+ name="Second level Front Port",
2817
+ rear_port=second_level_rearport,
2818
+ rear_port_position=1,
2819
+ )
2820
+ third_level_frontport = self.queryset.create(
2821
+ module=child_module,
2822
+ name="Third level Front Port",
2823
+ rear_port=third_level_rearport,
2824
+ rear_port_position=1,
2825
+ )
2826
+ self.assertQuerySetEqual(
2827
+ self.filterset({"device": [device.pk]}, self.queryset).qs,
2828
+ [top_level_frontport, second_level_frontport, third_level_frontport],
2829
+ ordered=False,
2830
+ )
2831
+ self.assertQuerySetEqual(
2832
+ self.filterset({"device": [device.name]}, self.queryset).qs,
2833
+ [top_level_frontport, second_level_frontport, third_level_frontport],
2834
+ ordered=False,
2835
+ )
2385
2836
 
2386
2837
 
2387
- class RearPortTestCase(FilterTestCases.FilterTestCase):
2838
+ class RearPortTestCase(ModularDeviceComponentTestMixin, FilterTestCases.FilterTestCase):
2388
2839
  queryset = RearPort.objects.all()
2389
2840
  filterset = RearPortFilterSet
2390
2841
  generic_filter_tests = [
2842
+ *ModularDeviceComponentTestMixin.generic_filter_tests,
2391
2843
  ("cable", "cable__id"),
2392
- ("description",),
2393
- ("device", "device__id"),
2394
- ("device", "device__name"),
2395
2844
  ("front_ports", "front_ports__id"),
2396
2845
  ("front_ports", "front_ports__name"),
2397
- ("label",),
2398
- ("name",),
2399
2846
  ("positions",),
2847
+ ("type",),
2400
2848
  ]
2401
2849
 
2402
2850
  @classmethod
2403
2851
  def setUpTestData(cls):
2404
- common_test_data(cls)
2852
+ super().setUpTestData()
2405
2853
 
2406
2854
  devices = (
2407
2855
  Device.objects.get(name="Device 1"),
@@ -2445,28 +2893,19 @@ class RearPortTestCase(FilterTestCases.FilterTestCase):
2445
2893
  )
2446
2894
  # Third port is not connected
2447
2895
 
2448
- def test_type(self):
2449
- # TODO: Not a generic_filter_test because this is a single-value filter
2450
- params = {"type": [PortTypeChoices.TYPE_8P8C]}
2451
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
2452
-
2453
2896
 
2454
- class DeviceBayTestCase(FilterTestCases.FilterTestCase):
2897
+ class DeviceBayTestCase(DeviceComponentTestMixin, FilterTestCases.FilterTestCase):
2455
2898
  queryset = DeviceBay.objects.all()
2456
2899
  filterset = DeviceBayFilterSet
2457
2900
  generic_filter_tests = [
2458
- ("description",),
2459
- ("device", "device__id"),
2460
- ("device", "device__name"),
2901
+ *DeviceComponentTestMixin.generic_filter_tests,
2461
2902
  ("installed_device", "installed_device__id"),
2462
2903
  ("installed_device", "installed_device__name"),
2463
- ("label",),
2464
- ("name",),
2465
2904
  ]
2466
2905
 
2467
2906
  @classmethod
2468
2907
  def setUpTestData(cls):
2469
- common_test_data(cls)
2908
+ super().setUpTestData()
2470
2909
 
2471
2910
  device_role = Role.objects.get_for_model(Device).first()
2472
2911
  parent_device_type = DeviceType.objects.get(model="Model 2")
@@ -2519,19 +2958,15 @@ class DeviceBayTestCase(FilterTestCases.FilterTestCase):
2519
2958
  device_bays[1].save()
2520
2959
 
2521
2960
 
2522
- class InventoryItemTestCase(FilterTestCases.FilterTestCase):
2961
+ class InventoryItemTestCase(DeviceComponentTestMixin, FilterTestCases.FilterTestCase):
2523
2962
  queryset = InventoryItem.objects.all()
2524
2963
  filterset = InventoryItemFilterSet
2525
2964
  generic_filter_tests = [
2965
+ *DeviceComponentTestMixin.generic_filter_tests,
2526
2966
  ("asset_tag",),
2527
2967
  ("children", "children__id"),
2528
- ("description",),
2529
- ("device", "device__id"),
2530
- ("device", "device__name"),
2531
- ("label",),
2532
2968
  ("manufacturer", "manufacturer__id"),
2533
2969
  ("manufacturer", "manufacturer__name"),
2534
- ("name",),
2535
2970
  ("parent", "parent__id"),
2536
2971
  ("parent", "parent__name"),
2537
2972
  ("part_id",),
@@ -2543,7 +2978,7 @@ class InventoryItemTestCase(FilterTestCases.FilterTestCase):
2543
2978
 
2544
2979
  @classmethod
2545
2980
  def setUpTestData(cls):
2546
- common_test_data(cls)
2981
+ super().setUpTestData()
2547
2982
 
2548
2983
  devices = (
2549
2984
  Device.objects.get(name="Device 1"),
@@ -2551,7 +2986,7 @@ class InventoryItemTestCase(FilterTestCases.FilterTestCase):
2551
2986
  Device.objects.get(name="Device 3"),
2552
2987
  )
2553
2988
 
2554
- software_versions = SoftwareVersion.objects.filter(software_image_files__isnull=False)[:3]
2989
+ software_versions = SoftwareVersion.objects.filter(software_image_files__isnull=False).distinct()[:3]
2555
2990
 
2556
2991
  inventory_items = (
2557
2992
  InventoryItem.objects.create(
@@ -2797,37 +3232,37 @@ class CableTestCase(FilterTestCases.FilterTestCase):
2797
3232
  Interface.objects.get(device__name="Device 6"),
2798
3233
  Interface.objects.create(
2799
3234
  device=devices[0],
2800
- name="Interface 7",
3235
+ name="Test Interface 7",
2801
3236
  type=InterfaceTypeChoices.TYPE_1GE_FIXED,
2802
3237
  status=interface_status,
2803
3238
  ),
2804
3239
  Interface.objects.create(
2805
3240
  device=devices[1],
2806
- name="Interface 8",
3241
+ name="Test Interface 8",
2807
3242
  type=InterfaceTypeChoices.TYPE_1GE_FIXED,
2808
3243
  status=interface_status,
2809
3244
  ),
2810
3245
  Interface.objects.create(
2811
3246
  device=devices[2],
2812
- name="Interface 9",
3247
+ name="Test Interface 9",
2813
3248
  type=InterfaceTypeChoices.TYPE_1GE_FIXED,
2814
3249
  status=interface_status,
2815
3250
  ),
2816
3251
  Interface.objects.create(
2817
3252
  device=devices[3],
2818
- name="Interface 10",
3253
+ name="Test Interface 10",
2819
3254
  type=InterfaceTypeChoices.TYPE_1GE_FIXED,
2820
3255
  status=interface_status,
2821
3256
  ),
2822
3257
  Interface.objects.create(
2823
3258
  device=devices[4],
2824
- name="Interface 11",
3259
+ name="Test Interface 11",
2825
3260
  type=InterfaceTypeChoices.TYPE_1GE_FIXED,
2826
3261
  status=interface_status,
2827
3262
  ),
2828
3263
  Interface.objects.create(
2829
3264
  device=devices[5],
2830
- name="Interface 12",
3265
+ name="Test Interface 12",
2831
3266
  type=InterfaceTypeChoices.TYPE_1GE_FIXED,
2832
3267
  status=interface_status,
2833
3268
  ),
@@ -2913,17 +3348,87 @@ class CableTestCase(FilterTestCases.FilterTestCase):
2913
3348
  self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
2914
3349
 
2915
3350
  def test_device(self):
2916
- # TODO: Not a generic_filter_test because this is a method filter.
2917
- devices = [
2918
- Device.objects.get(name="Device 1"),
2919
- Device.objects.get(name="Device 2"),
2920
- ]
2921
- with self.subTest():
2922
- params = {"device_id": [devices[0].pk, devices[1].pk]}
2923
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
2924
- with self.subTest():
2925
- params = {"device": [devices[0].name, devices[1].name]}
2926
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
3351
+ """Test that the device filter returns all cables for a device and its modules."""
3352
+ interfaces = Interface.objects.filter(cable__isnull=True)[:3]
3353
+ manufacturer = Manufacturer.objects.first()
3354
+ device_type = DeviceType.objects.create(
3355
+ manufacturer=manufacturer, model="Test Device Filter for Cable Device Type"
3356
+ )
3357
+ device = Device.objects.create(
3358
+ device_type=device_type,
3359
+ name="Test Device Filter for Cable Device",
3360
+ location=self.loc0,
3361
+ role=self.device_roles[0],
3362
+ status=Status.objects.get_for_model(Device).first(),
3363
+ )
3364
+ parent_module_bay = ModuleBay.objects.create(name="Parent module bay", position="1", parent_device=device)
3365
+ module_type = ModuleType.objects.create(
3366
+ manufacturer=manufacturer, model="Test Device Filter for Cable Module Type", comments="Module Type test"
3367
+ )
3368
+ module = Module.objects.create(
3369
+ module_type=module_type, parent_module_bay=parent_module_bay, status=self.module_statuses[0]
3370
+ )
3371
+ child_module_bay = ModuleBay.objects.create(name="Child module bay", position="1", parent_module=module)
3372
+ child_module = Module.objects.create(
3373
+ module_type=module_type, parent_module_bay=child_module_bay, status=self.module_statuses[0]
3374
+ )
3375
+ interface_status = Status.objects.get_for_model(Interface).first()
3376
+ top_level_interface = Interface.objects.create(
3377
+ device=device,
3378
+ name="Top level Interface",
3379
+ type=InterfaceTypeChoices.TYPE_1GE_FIXED,
3380
+ status=interface_status,
3381
+ )
3382
+ Interface.objects.create(
3383
+ module=module,
3384
+ name="Second level Interface",
3385
+ type=InterfaceTypeChoices.TYPE_1GE_FIXED,
3386
+ status=interface_status,
3387
+ )
3388
+ third_level_interface = Interface.objects.create(
3389
+ module=child_module,
3390
+ name="Third level Interface",
3391
+ type=InterfaceTypeChoices.TYPE_1GE_FIXED,
3392
+ status=interface_status,
3393
+ )
3394
+
3395
+ top_level_cable = Cable.objects.create(
3396
+ termination_a=top_level_interface,
3397
+ termination_b=interfaces[0],
3398
+ label="Test Device Filter Cable 1",
3399
+ type=CableTypeChoices.TYPE_CAT5E,
3400
+ status=self.status_connected,
3401
+ color="f44336",
3402
+ length=30,
3403
+ length_unit=CableLengthUnitChoices.UNIT_FOOT,
3404
+ )
3405
+ third_level_cable = Cable.objects.create(
3406
+ termination_a=interfaces[1],
3407
+ termination_b=third_level_interface,
3408
+ label="Test Device Filter Cable 2",
3409
+ type=CableTypeChoices.TYPE_CAT5E,
3410
+ status=self.status_connected,
3411
+ color="f44336",
3412
+ length=30,
3413
+ length_unit=CableLengthUnitChoices.UNIT_FOOT,
3414
+ )
3415
+
3416
+ with self.subTest("device_id filter"):
3417
+ self.assertQuerySetEqual(
3418
+ self.filterset({"device_id": [device.pk]}, self.queryset).qs,
3419
+ [top_level_cable, third_level_cable],
3420
+ ordered=False,
3421
+ )
3422
+
3423
+ with self.subTest("device filter"):
3424
+ self.assertQuerySetEqual(
3425
+ self.filterset({"device": [device.name]}, self.queryset).qs,
3426
+ [top_level_cable, third_level_cable],
3427
+ ordered=False,
3428
+ )
3429
+
3430
+ with self.subTest("device_id filter with an invalid uuid"):
3431
+ self.assertFalse(self.filterset({"device_id": [uuid.uuid4()]}, self.queryset).is_valid())
2927
3432
 
2928
3433
  def test_rack(self):
2929
3434
  # TODO: Not a generic_filter_test because this is a method filter.
@@ -2999,7 +3504,7 @@ class PowerPanelTestCase(FilterTestCases.FilterTestCase):
2999
3504
  PowerPanel.objects.create(name="Power Panel 4", location=cls.loc1)
3000
3505
 
3001
3506
 
3002
- class PowerFeedTestCase(FilterTestCases.FilterTestCase):
3507
+ class PowerFeedTestCase(PathEndpointModelTestMixin, FilterTestCases.FilterTestCase):
3003
3508
  queryset = PowerFeed.objects.all()
3004
3509
  filterset = PowerFeedFilterSet
3005
3510
  generic_filter_tests = [
@@ -3088,28 +3593,28 @@ class PowerFeedTestCase(FilterTestCases.FilterTestCase):
3088
3593
  )
3089
3594
 
3090
3595
  def test_type(self):
3091
- # TODO: Not a generic_filter_test because this is a single-value filter
3596
+ # TODO: Not a generic_filter_test because this field only has 2 valid choices
3092
3597
  params = {"type": [PowerFeedTypeChoices.TYPE_PRIMARY]}
3093
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
3598
+ self.assertQuerysetEqualAndNotEmpty(
3599
+ self.filterset(params, self.queryset).qs,
3600
+ self.queryset.filter(type=PowerFeedTypeChoices.TYPE_PRIMARY),
3601
+ )
3094
3602
 
3095
3603
  def test_supply(self):
3096
- # TODO: Not a generic_filter_test because this is a single-value filter
3604
+ # TODO: Not a generic_filter_test because this field only has 2 valid choices
3097
3605
  params = {"supply": [PowerFeedSupplyChoices.SUPPLY_AC]}
3098
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
3606
+ self.assertQuerysetEqualAndNotEmpty(
3607
+ self.filterset(params, self.queryset).qs,
3608
+ self.queryset.filter(supply=PowerFeedSupplyChoices.SUPPLY_AC),
3609
+ )
3099
3610
 
3100
3611
  def test_phase(self):
3101
- # TODO: Not a generic_filter_test because this is a single-value filter
3612
+ # TODO: Not a generic_filter_test because this field only has 2 valid choices
3102
3613
  params = {"phase": [PowerFeedPhaseChoices.PHASE_3PHASE]}
3103
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
3104
-
3105
- def test_connected(self):
3106
- # TODO: Not a generic_filter_test because this is a boolean filter but not a RelatedMembershipBooleanFilter
3107
- with self.subTest():
3108
- params = {"connected": True}
3109
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
3110
- with self.subTest():
3111
- params = {"connected": False}
3112
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
3614
+ self.assertQuerysetEqualAndNotEmpty(
3615
+ self.filterset(params, self.queryset).qs,
3616
+ self.queryset.filter(phase=PowerFeedPhaseChoices.PHASE_3PHASE),
3617
+ )
3113
3618
 
3114
3619
 
3115
3620
  class DeviceRedundancyGroupTestCase(FilterTestCases.FilterTestCase):
@@ -3249,7 +3754,7 @@ class InterfaceRedundancyGroupAssociationTestCase(FilterTestCases.FilterTestCase
3249
3754
 
3250
3755
  statuses = Status.objects.get_for_model(InterfaceRedundancyGroup)
3251
3756
  cls.ips = IPAddress.objects.all()
3252
- cls.interfaces = Interface.objects.all()[:4]
3757
+ cls.interfaces = Interface.objects.all()[:8]
3253
3758
 
3254
3759
  interface_redundancy_groups = (
3255
3760
  InterfaceRedundancyGroup(
@@ -3297,8 +3802,9 @@ class InterfaceRedundancyGroupAssociationTestCase(FilterTestCases.FilterTestCase
3297
3802
  interface_redundancy_groups[1].secrets_group = secrets_groups[1]
3298
3803
  interface_redundancy_groups[1].validated_save()
3299
3804
 
3300
- for i, interface in enumerate(cls.interfaces):
3301
- interface_redundancy_groups[i].add_interface(interface, 100 * i)
3805
+ for i, group in enumerate(interface_redundancy_groups):
3806
+ group.add_interface(cls.interfaces[i], 100 * i)
3807
+ group.add_interface(cls.interfaces[i + 4], 100 * (i + 4))
3302
3808
 
3303
3809
 
3304
3810
  class SoftwareImageFileFilterSetTestCase(FilterTestCases.FilterTestCase):
@@ -3458,3 +3964,123 @@ class ControllerManagedDeviceGroupFilterSetTestCase(FilterTestCases.FilterTestCa
3458
3964
  @classmethod
3459
3965
  def setUpTestData(cls):
3460
3966
  common_test_data(cls)
3967
+
3968
+
3969
+ class ModuleTestCase(
3970
+ ModuleDeviceCommonTestsMixin,
3971
+ FilterTestCases.TenancyFilterTestCaseMixin,
3972
+ FilterTestCases.FilterTestCase,
3973
+ ):
3974
+ queryset = Module.objects.all()
3975
+ filterset = ModuleFilterSet
3976
+ tenancy_related_name = "modules"
3977
+ generic_filter_tests = [
3978
+ ("asset_tag",),
3979
+ ("console_ports", "console_ports__id"),
3980
+ ("console_ports", "console_ports__name"),
3981
+ ("console_server_ports", "console_server_ports__id"),
3982
+ ("console_server_ports", "console_server_ports__name"),
3983
+ ("front_ports", "front_ports__id"),
3984
+ ("front_ports", "front_ports__name"),
3985
+ ("interfaces", "interfaces__id"),
3986
+ ("interfaces", "interfaces__name"),
3987
+ ("mac_address", "interfaces__mac_address"),
3988
+ ("manufacturer", "module_type__manufacturer__id"),
3989
+ ("manufacturer", "module_type__manufacturer__name"),
3990
+ ("module_bays", "module_bays__id"),
3991
+ ("module_type", "module_type__id"),
3992
+ ("module_type", "module_type__model"),
3993
+ ("parent_module_bay", "parent_module_bay__id"),
3994
+ ("power_outlets", "power_outlets__id"),
3995
+ ("power_outlets", "power_outlets__name"),
3996
+ ("power_ports", "power_ports__id"),
3997
+ ("power_ports", "power_ports__name"),
3998
+ ("rear_ports", "rear_ports__id"),
3999
+ ("rear_ports", "rear_ports__name"),
4000
+ ("role", "role__id"),
4001
+ ("role", "role__name"),
4002
+ ("serial",),
4003
+ ("status", "status__id"),
4004
+ ("status", "status__name"),
4005
+ ]
4006
+
4007
+ @classmethod
4008
+ def setUpTestData(cls):
4009
+ common_test_data(cls)
4010
+
4011
+ # Update existing interface objects with mac addresses for filtering
4012
+ interfaces = Interface.objects.filter(module__isnull=False)[:3]
4013
+ Interface.objects.filter(pk=interfaces[0].pk).update(mac_address="00-00-00-00-00-01")
4014
+ Interface.objects.filter(pk=interfaces[1].pk).update(mac_address="00-00-00-00-00-02")
4015
+
4016
+
4017
+ class ModuleTypeTestCase(FilterTestCases.FilterTestCase):
4018
+ queryset = ModuleType.objects.all()
4019
+ filterset = ModuleTypeFilterSet
4020
+ generic_filter_tests = [
4021
+ ("comments",),
4022
+ ("manufacturer", "manufacturer__id"),
4023
+ ("manufacturer", "manufacturer__name"),
4024
+ ("model",),
4025
+ ("part_number",),
4026
+ ("console_port_templates", "console_port_templates__id"),
4027
+ ("console_port_templates", "console_port_templates__name"),
4028
+ ("console_server_port_templates", "console_server_port_templates__id"),
4029
+ ("console_server_port_templates", "console_server_port_templates__name"),
4030
+ ("power_port_templates", "power_port_templates__id"),
4031
+ ("power_port_templates", "power_port_templates__name"),
4032
+ ("power_outlet_templates", "power_outlet_templates__id"),
4033
+ ("power_outlet_templates", "power_outlet_templates__name"),
4034
+ ("interface_templates", "interface_templates__id"),
4035
+ ("interface_templates", "interface_templates__name"),
4036
+ ("front_port_templates", "front_port_templates__id"),
4037
+ ("front_port_templates", "front_port_templates__name"),
4038
+ ("rear_port_templates", "rear_port_templates__id"),
4039
+ ("rear_port_templates", "rear_port_templates__name"),
4040
+ ("module_bay_templates", "module_bay_templates__id"),
4041
+ ]
4042
+
4043
+ @classmethod
4044
+ def setUpTestData(cls):
4045
+ common_test_data(cls)
4046
+
4047
+
4048
+ class ModuleBayTemplateTestCase(FilterTestCases.FilterTestCase):
4049
+ queryset = ModuleBayTemplate.objects.all()
4050
+ filterset = ModuleBayTemplateFilterSet
4051
+ generic_filter_tests = [
4052
+ ("description",),
4053
+ ("device_type", "device_type__id"),
4054
+ ("device_type", "device_type__model"),
4055
+ ("label",),
4056
+ ("module_type", "module_type__id"),
4057
+ ("module_type", "module_type__model"),
4058
+ ("name",),
4059
+ ("position",),
4060
+ ]
4061
+
4062
+ @classmethod
4063
+ def setUpTestData(cls):
4064
+ common_test_data(cls)
4065
+
4066
+
4067
+ class ModuleBayTestCase(FilterTestCases.FilterTestCase):
4068
+ queryset = ModuleBay.objects.all()
4069
+ filterset = ModuleBayFilterSet
4070
+ generic_filter_tests = [
4071
+ ("description",),
4072
+ ("label",),
4073
+ ("parent_device", "parent_device__id"),
4074
+ ("parent_device", "parent_device__name"),
4075
+ ("parent_module", "parent_module__id"),
4076
+ ("installed_module", "installed_module__id"),
4077
+ ("name",),
4078
+ ("position",),
4079
+ ]
4080
+
4081
+ @classmethod
4082
+ def setUpTestData(cls):
4083
+ common_test_data(cls)
4084
+ module_bays = ModuleBay.objects.all()[:2]
4085
+ module_bays[0].tags.set(Tag.objects.get_for_model(ModuleBay))
4086
+ module_bays[1].tags.set(Tag.objects.get_for_model(ModuleBay)[:3])