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

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

Potentially problematic release.


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

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