nautobot 2.4.10__py3-none-any.whl → 2.4.11__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 (416) hide show
  1. nautobot/cloud/tests/test_views.py +13 -1
  2. nautobot/cloud/views.py +39 -9
  3. nautobot/core/celery/__init__.py +21 -0
  4. nautobot/core/celery/encoders.py +3 -0
  5. nautobot/core/forms/forms.py +4 -1
  6. nautobot/core/jobs/bulk_actions.py +8 -8
  7. nautobot/core/jobs/cleanup.py +11 -0
  8. nautobot/core/management/commands/generate_test_data.py +2 -1
  9. nautobot/core/templates/generic/object_retrieve.html +1 -1
  10. nautobot/core/testing/mixins.py +19 -1
  11. nautobot/core/testing/views.py +104 -8
  12. nautobot/core/tests/test_jobs.py +20 -4
  13. nautobot/core/tests/test_utils.py +193 -0
  14. nautobot/core/tests/test_views_utils.py +53 -2
  15. nautobot/core/ui/object_detail.py +4 -0
  16. nautobot/core/utils/lookup.py +4 -2
  17. nautobot/core/utils/module_loading.py +86 -58
  18. nautobot/core/views/generic.py +2 -12
  19. nautobot/core/views/mixins.py +19 -1
  20. nautobot/core/views/renderers.py +4 -13
  21. nautobot/core/views/utils.py +16 -0
  22. nautobot/dcim/api/serializers.py +13 -0
  23. nautobot/dcim/api/urls.py +1 -0
  24. nautobot/dcim/api/views.py +20 -0
  25. nautobot/dcim/apps.py +1 -0
  26. nautobot/dcim/factory.py +11 -0
  27. nautobot/dcim/filters/__init__.py +110 -0
  28. nautobot/dcim/forms.py +205 -19
  29. nautobot/dcim/migrations/0070_modulefamily_models.py +92 -0
  30. nautobot/dcim/models/__init__.py +2 -0
  31. nautobot/dcim/models/device_component_templates.py +14 -0
  32. nautobot/dcim/models/device_components.py +13 -1
  33. nautobot/dcim/models/devices.py +62 -0
  34. nautobot/dcim/navigation.py +16 -0
  35. nautobot/dcim/tables/__init__.py +2 -0
  36. nautobot/dcim/tables/devices.py +48 -0
  37. nautobot/dcim/tables/devicetypes.py +35 -1
  38. nautobot/dcim/tables/template_code.py +2 -0
  39. nautobot/dcim/templates/dcim/controllermanageddevicegroup_retrieve.html +1 -90
  40. nautobot/dcim/templates/dcim/inc/cable_toggle_buttons.html +1 -1
  41. nautobot/dcim/templates/dcim/interfaceredundancygroup_retrieve.html +1 -63
  42. nautobot/dcim/templates/dcim/location.html +2 -249
  43. nautobot/dcim/templates/dcim/location_edit.html +2 -38
  44. nautobot/dcim/templates/dcim/location_retrieve.html +249 -0
  45. nautobot/dcim/templates/dcim/location_update.html +38 -0
  46. nautobot/dcim/templates/dcim/module_update.html +1 -0
  47. nautobot/dcim/templates/dcim/modulebay_retrieve.html +93 -1
  48. nautobot/dcim/templates/dcim/modulefamily_retrieve.html +31 -0
  49. nautobot/dcim/templates/dcim/moduletype_retrieve.html +6 -0
  50. nautobot/dcim/templates/dcim/powerfeed_retrieve.html +1 -160
  51. nautobot/dcim/tests/test_api.py +35 -0
  52. nautobot/dcim/tests/test_filters.py +102 -3
  53. nautobot/dcim/tests/test_models.py +146 -0
  54. nautobot/dcim/tests/test_views.py +70 -97
  55. nautobot/dcim/urls.py +4 -22
  56. nautobot/dcim/views.py +439 -153
  57. nautobot/extras/api/views.py +9 -2
  58. nautobot/extras/datasources/git.py +11 -3
  59. nautobot/extras/forms/forms.py +9 -5
  60. nautobot/extras/jobs.py +4 -2
  61. nautobot/extras/models/datasources.py +5 -8
  62. nautobot/extras/models/jobs.py +5 -0
  63. nautobot/extras/plugins/__init__.py +3 -0
  64. nautobot/extras/tables.py +40 -3
  65. nautobot/extras/templates/extras/configcontext.html +2 -220
  66. nautobot/extras/templates/extras/configcontext_edit.html +2 -50
  67. nautobot/extras/templates/extras/configcontext_retrieve.html +2 -0
  68. nautobot/extras/templates/extras/configcontext_update.html +50 -0
  69. nautobot/extras/templates/extras/configcontextschema.html +2 -48
  70. nautobot/extras/templates/extras/configcontextschema_edit.html +2 -19
  71. nautobot/extras/templates/extras/configcontextschema_retrieve.html +48 -0
  72. nautobot/extras/templates/extras/configcontextschema_update.html +19 -0
  73. nautobot/extras/templates/extras/inc/configcontext_data.html +1 -0
  74. nautobot/extras/templates/extras/inc/json_data.html +1 -1
  75. nautobot/extras/templates/extras/inc/json_format.html +2 -2
  76. nautobot/extras/templates/extras/job_edit.html +12 -6
  77. nautobot/extras/templates/extras/tag.html +2 -52
  78. nautobot/extras/templates/extras/tag_edit.html +2 -15
  79. nautobot/extras/templates/extras/tag_retrieve.html +52 -0
  80. nautobot/extras/templates/extras/tag_update.html +15 -0
  81. nautobot/extras/templates/extras/team_retrieve.html +2 -2
  82. nautobot/extras/tests/test_api.py +15 -15
  83. nautobot/extras/tests/test_filters.py +4 -4
  84. nautobot/extras/tests/test_jobs.py +23 -10
  85. nautobot/extras/tests/test_models.py +19 -8
  86. nautobot/extras/tests/test_plugins.py +6 -3
  87. nautobot/extras/tests/test_views.py +66 -11
  88. nautobot/extras/urls.py +4 -134
  89. nautobot/extras/views.py +113 -158
  90. nautobot/ipam/models.py +19 -4
  91. nautobot/ipam/tables.py +19 -0
  92. nautobot/ipam/templates/ipam/vlan.html +2 -84
  93. nautobot/ipam/templates/ipam/vlan_edit.html +2 -24
  94. nautobot/ipam/templates/ipam/vlan_retrieve.html +84 -0
  95. nautobot/ipam/templates/ipam/vlan_update.html +24 -0
  96. nautobot/ipam/tests/test_views.py +5 -0
  97. nautobot/ipam/urls.py +1 -21
  98. nautobot/ipam/views.py +45 -70
  99. nautobot/project-static/docs/404.html +31 -8
  100. nautobot/project-static/docs/apps/index.html +31 -8
  101. nautobot/project-static/docs/apps/nautobot-apps.html +31 -8
  102. nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +31 -8
  103. nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +31 -8
  104. nautobot/project-static/docs/code-reference/nautobot/apps/api.html +31 -8
  105. nautobot/project-static/docs/code-reference/nautobot/apps/change_logging.html +31 -8
  106. nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +31 -8
  107. nautobot/project-static/docs/code-reference/nautobot/apps/config.html +31 -8
  108. nautobot/project-static/docs/code-reference/nautobot/apps/constants.html +31 -8
  109. nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +31 -8
  110. nautobot/project-static/docs/code-reference/nautobot/apps/events.html +31 -8
  111. nautobot/project-static/docs/code-reference/nautobot/apps/exceptions.html +31 -8
  112. nautobot/project-static/docs/code-reference/nautobot/apps/factory.html +31 -8
  113. nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +31 -8
  114. nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +31 -8
  115. nautobot/project-static/docs/code-reference/nautobot/apps/graphql.html +31 -8
  116. nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +31 -8
  117. nautobot/project-static/docs/code-reference/nautobot/apps/models.html +31 -8
  118. nautobot/project-static/docs/code-reference/nautobot/apps/querysets.html +31 -8
  119. nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +31 -8
  120. nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +31 -8
  121. nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +120 -8
  122. nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +31 -8
  123. nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +31 -8
  124. nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +31 -8
  125. nautobot/project-static/docs/code-reference/nautobot/apps/views.html +31 -8
  126. nautobot/project-static/docs/development/apps/api/configuration-view.html +31 -8
  127. nautobot/project-static/docs/development/apps/api/database-backend-config.html +31 -8
  128. nautobot/project-static/docs/development/apps/api/models/django-admin.html +31 -8
  129. nautobot/project-static/docs/development/apps/api/models/global-search.html +31 -8
  130. nautobot/project-static/docs/development/apps/api/models/graphql.html +31 -8
  131. nautobot/project-static/docs/development/apps/api/models/index.html +31 -8
  132. nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +40 -8
  133. nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +31 -8
  134. nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +31 -8
  135. nautobot/project-static/docs/development/apps/api/platform-features/git-repository-content.html +31 -8
  136. nautobot/project-static/docs/development/apps/api/platform-features/index.html +31 -8
  137. nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +31 -8
  138. nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +31 -8
  139. nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +31 -8
  140. nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +32 -9
  141. nautobot/project-static/docs/development/apps/api/platform-features/table-extensions.html +31 -8
  142. nautobot/project-static/docs/development/apps/api/platform-features/uniquely-identify-objects.html +31 -8
  143. nautobot/project-static/docs/development/apps/api/prometheus.html +31 -8
  144. nautobot/project-static/docs/development/apps/api/setup.html +31 -8
  145. nautobot/project-static/docs/development/apps/api/testing.html +31 -8
  146. nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +31 -8
  147. nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +31 -8
  148. nautobot/project-static/docs/development/apps/api/ui-extensions/index.html +31 -8
  149. nautobot/project-static/docs/development/apps/api/ui-extensions/navigation.html +31 -8
  150. nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +31 -8
  151. nautobot/project-static/docs/development/apps/api/views/base-template.html +31 -8
  152. nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +31 -8
  153. nautobot/project-static/docs/development/apps/api/views/django-generic-views.html +31 -8
  154. nautobot/project-static/docs/development/apps/api/views/help-documentation.html +31 -8
  155. nautobot/project-static/docs/development/apps/api/views/index.html +31 -8
  156. nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +31 -8
  157. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +31 -8
  158. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +31 -8
  159. nautobot/project-static/docs/development/apps/api/views/notes.html +31 -8
  160. nautobot/project-static/docs/development/apps/api/views/rest-api.html +31 -8
  161. nautobot/project-static/docs/development/apps/api/views/urls.html +31 -8
  162. nautobot/project-static/docs/development/apps/index.html +31 -8
  163. nautobot/project-static/docs/development/apps/migration/code-updates.html +31 -8
  164. nautobot/project-static/docs/development/apps/migration/dependency-updates.html +31 -8
  165. nautobot/project-static/docs/development/apps/migration/from-v1.html +31 -8
  166. nautobot/project-static/docs/development/apps/migration/model-updates/dcim.html +31 -8
  167. nautobot/project-static/docs/development/apps/migration/model-updates/extras.html +31 -8
  168. nautobot/project-static/docs/development/apps/migration/model-updates/global.html +31 -8
  169. nautobot/project-static/docs/development/apps/migration/model-updates/ipam.html +31 -8
  170. nautobot/project-static/docs/development/apps/migration/ui-component-framework/best-practices.html +31 -8
  171. nautobot/project-static/docs/development/apps/migration/ui-component-framework/custom-content.html +31 -8
  172. nautobot/project-static/docs/development/apps/migration/ui-component-framework/index.html +31 -8
  173. nautobot/project-static/docs/development/apps/migration/ui-component-framework/migration-steps.html +31 -8
  174. nautobot/project-static/docs/development/apps/porting-from-netbox.html +31 -8
  175. nautobot/project-static/docs/development/core/application-registry.html +31 -8
  176. nautobot/project-static/docs/development/core/best-practices.html +31 -8
  177. nautobot/project-static/docs/development/core/bootstrap-ui.html +31 -8
  178. nautobot/project-static/docs/development/core/caching.html +31 -8
  179. nautobot/project-static/docs/development/core/controllers.html +31 -8
  180. nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +31 -8
  181. nautobot/project-static/docs/development/core/generic-views.html +31 -8
  182. nautobot/project-static/docs/development/core/getting-started.html +31 -8
  183. nautobot/project-static/docs/development/core/homepage.html +31 -8
  184. nautobot/project-static/docs/development/core/index.html +31 -8
  185. nautobot/project-static/docs/development/core/minikube-dev-environment-for-k8s-jobs.html +31 -8
  186. nautobot/project-static/docs/development/core/model-checklist.html +31 -8
  187. nautobot/project-static/docs/development/core/model-features.html +31 -8
  188. nautobot/project-static/docs/development/core/natural-keys.html +31 -8
  189. nautobot/project-static/docs/development/core/navigation-menu.html +31 -8
  190. nautobot/project-static/docs/development/core/release-checklist.html +31 -8
  191. nautobot/project-static/docs/development/core/role-internals.html +31 -8
  192. nautobot/project-static/docs/development/core/settings.html +31 -8
  193. nautobot/project-static/docs/development/core/style-guide.html +31 -8
  194. nautobot/project-static/docs/development/core/templates.html +31 -8
  195. nautobot/project-static/docs/development/core/testing.html +31 -8
  196. nautobot/project-static/docs/development/core/ui-component-framework.html +31 -8
  197. nautobot/project-static/docs/development/core/user-preferences.html +31 -8
  198. nautobot/project-static/docs/development/index.html +31 -8
  199. nautobot/project-static/docs/development/jobs/getting-started.html +35 -8
  200. nautobot/project-static/docs/development/jobs/index.html +31 -8
  201. nautobot/project-static/docs/development/jobs/installation.html +31 -8
  202. nautobot/project-static/docs/development/jobs/job-extensions.html +31 -8
  203. nautobot/project-static/docs/development/jobs/job-logging.html +31 -8
  204. nautobot/project-static/docs/development/jobs/job-patterns.html +31 -8
  205. nautobot/project-static/docs/development/jobs/job-structure.html +31 -8
  206. nautobot/project-static/docs/development/jobs/migration/from-v1.html +31 -8
  207. nautobot/project-static/docs/development/jobs/testing.html +31 -8
  208. nautobot/project-static/docs/index.html +31 -8
  209. nautobot/project-static/docs/insert-analytics.sh +36 -0
  210. nautobot/project-static/docs/objects.inv +0 -0
  211. nautobot/project-static/docs/overview/application_stack.html +31 -8
  212. nautobot/project-static/docs/overview/design_philosophy.html +31 -8
  213. nautobot/project-static/docs/release-notes/index.html +31 -8
  214. nautobot/project-static/docs/release-notes/version-1.0.html +31 -8
  215. nautobot/project-static/docs/release-notes/version-1.1.html +31 -8
  216. nautobot/project-static/docs/release-notes/version-1.2.html +31 -8
  217. nautobot/project-static/docs/release-notes/version-1.3.html +31 -8
  218. nautobot/project-static/docs/release-notes/version-1.4.html +31 -8
  219. nautobot/project-static/docs/release-notes/version-1.5.html +31 -8
  220. nautobot/project-static/docs/release-notes/version-1.6.html +31 -8
  221. nautobot/project-static/docs/release-notes/version-2.0.html +31 -8
  222. nautobot/project-static/docs/release-notes/version-2.1.html +31 -8
  223. nautobot/project-static/docs/release-notes/version-2.2.html +31 -8
  224. nautobot/project-static/docs/release-notes/version-2.3.html +31 -8
  225. nautobot/project-static/docs/release-notes/version-2.4.html +252 -8
  226. nautobot/project-static/docs/search/search_index.json +1 -1
  227. nautobot/project-static/docs/sitemap.xml +302 -298
  228. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  229. nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +31 -8
  230. nautobot/project-static/docs/user-guide/administration/configuration/authentication/remote.html +31 -8
  231. nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +31 -8
  232. nautobot/project-static/docs/user-guide/administration/configuration/index.html +31 -8
  233. nautobot/project-static/docs/user-guide/administration/configuration/redis.html +31 -8
  234. nautobot/project-static/docs/user-guide/administration/configuration/settings.html +31 -8
  235. nautobot/project-static/docs/user-guide/administration/configuration/time-zones.html +31 -8
  236. nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +31 -8
  237. nautobot/project-static/docs/user-guide/administration/guides/docker.html +31 -8
  238. nautobot/project-static/docs/user-guide/administration/guides/health-checks.html +31 -8
  239. nautobot/project-static/docs/user-guide/administration/guides/permissions.html +31 -8
  240. nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +31 -8
  241. nautobot/project-static/docs/user-guide/administration/guides/replicating-nautobot.html +31 -8
  242. nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +31 -8
  243. nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +31 -8
  244. nautobot/project-static/docs/user-guide/administration/guides/selinux-troubleshooting.html +31 -8
  245. nautobot/project-static/docs/user-guide/administration/installation/app-install.html +31 -8
  246. nautobot/project-static/docs/user-guide/administration/installation/external-authentication.html +31 -8
  247. nautobot/project-static/docs/user-guide/administration/installation/http-server.html +31 -8
  248. nautobot/project-static/docs/user-guide/administration/installation/index.html +31 -8
  249. nautobot/project-static/docs/user-guide/administration/installation/install_system.html +31 -8
  250. nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +31 -8
  251. nautobot/project-static/docs/user-guide/administration/installation/services.html +31 -8
  252. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-netbox.html +31 -8
  253. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +31 -8
  254. nautobot/project-static/docs/user-guide/administration/security/index.html +31 -8
  255. nautobot/project-static/docs/user-guide/administration/security/notices.html +31 -8
  256. nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +31 -8
  257. nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +31 -8
  258. nautobot/project-static/docs/user-guide/administration/upgrading/database-backup.html +31 -8
  259. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/after-you-upgrade.html +31 -8
  260. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/before-you-upgrade.html +31 -8
  261. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/for-developers.html +31 -8
  262. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/index.html +31 -8
  263. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/whats-changed.html +31 -8
  264. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/region-and-site-data-migration-guide.html +31 -8
  265. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +31 -8
  266. nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +31 -8
  267. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuit.html +31 -8
  268. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittermination.html +31 -8
  269. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittype.html +31 -8
  270. nautobot/project-static/docs/user-guide/core-data-model/circuits/provider.html +31 -8
  271. nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +31 -8
  272. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloud.html +31 -8
  273. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudaccount.html +31 -8
  274. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetwork.html +31 -8
  275. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetworkprefixassignment.html +31 -8
  276. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudresourcetype.html +31 -8
  277. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservice.html +31 -8
  278. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservicenetworkassignment.html +31 -8
  279. nautobot/project-static/docs/user-guide/core-data-model/dcim/cable.html +31 -8
  280. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +31 -8
  281. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +31 -8
  282. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +31 -8
  283. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +31 -8
  284. nautobot/project-static/docs/user-guide/core-data-model/dcim/controller.html +31 -8
  285. nautobot/project-static/docs/user-guide/core-data-model/dcim/controllermanageddevicegroup.html +31 -8
  286. nautobot/project-static/docs/user-guide/core-data-model/dcim/device.html +31 -8
  287. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +31 -8
  288. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +31 -8
  289. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicefamily.html +31 -8
  290. nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +43 -20
  291. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +31 -8
  292. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +31 -8
  293. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +31 -8
  294. nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +31 -8
  295. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +31 -8
  296. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +31 -8
  297. nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +31 -8
  298. nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +31 -8
  299. nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +31 -8
  300. nautobot/project-static/docs/user-guide/core-data-model/dcim/manufacturer.html +31 -8
  301. nautobot/project-static/docs/user-guide/core-data-model/dcim/module.html +35 -8
  302. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebay.html +35 -8
  303. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebaytemplate.html +35 -8
  304. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulefamily.html +10261 -0
  305. nautobot/project-static/docs/user-guide/core-data-model/dcim/moduletype.html +34 -11
  306. nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +31 -8
  307. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerfeed.html +31 -8
  308. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +31 -8
  309. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +31 -8
  310. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerpanel.html +31 -8
  311. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +31 -8
  312. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +31 -8
  313. nautobot/project-static/docs/user-guide/core-data-model/dcim/rack.html +31 -8
  314. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackgroup.html +31 -8
  315. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackreservation.html +31 -8
  316. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +31 -8
  317. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +31 -8
  318. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +31 -8
  319. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareversion.html +31 -8
  320. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualchassis.html +31 -8
  321. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualdevicecontext.html +31 -8
  322. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +31 -8
  323. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +31 -8
  324. nautobot/project-static/docs/user-guide/core-data-model/extras/contact.html +31 -8
  325. nautobot/project-static/docs/user-guide/core-data-model/extras/team.html +31 -8
  326. nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +31 -8
  327. nautobot/project-static/docs/user-guide/core-data-model/ipam/namespace.html +31 -8
  328. nautobot/project-static/docs/user-guide/core-data-model/ipam/prefix.html +31 -8
  329. nautobot/project-static/docs/user-guide/core-data-model/ipam/rir.html +31 -8
  330. nautobot/project-static/docs/user-guide/core-data-model/ipam/routetarget.html +31 -8
  331. nautobot/project-static/docs/user-guide/core-data-model/ipam/service.html +31 -8
  332. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +31 -8
  333. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlangroup.html +31 -8
  334. nautobot/project-static/docs/user-guide/core-data-model/ipam/vrf.html +31 -8
  335. nautobot/project-static/docs/user-guide/core-data-model/overview/introduction.html +31 -8
  336. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenant.html +31 -8
  337. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenantgroup.html +31 -8
  338. nautobot/project-static/docs/user-guide/core-data-model/virtualization/cluster.html +31 -8
  339. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustergroup.html +31 -8
  340. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustertype.html +31 -8
  341. nautobot/project-static/docs/user-guide/core-data-model/virtualization/virtualmachine.html +31 -8
  342. nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +31 -8
  343. nautobot/project-static/docs/user-guide/core-data-model/wireless/index.html +31 -8
  344. nautobot/project-static/docs/user-guide/core-data-model/wireless/radioprofile.html +31 -8
  345. nautobot/project-static/docs/user-guide/core-data-model/wireless/supporteddatarate.html +31 -8
  346. nautobot/project-static/docs/user-guide/core-data-model/wireless/wirelessnetwork.html +31 -8
  347. nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +31 -8
  348. nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +31 -8
  349. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +31 -8
  350. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +31 -8
  351. nautobot/project-static/docs/user-guide/feature-guides/getting-started/index.html +31 -8
  352. nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +31 -8
  353. nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +31 -8
  354. nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +31 -8
  355. nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +31 -8
  356. nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +31 -8
  357. nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +31 -8
  358. nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +41 -15
  359. nautobot/project-static/docs/user-guide/feature-guides/graphql.html +31 -8
  360. nautobot/project-static/docs/user-guide/feature-guides/ip-address-merge-tool.html +31 -8
  361. nautobot/project-static/docs/user-guide/feature-guides/relationships.html +31 -8
  362. nautobot/project-static/docs/user-guide/feature-guides/software-image-files-and-versions.html +31 -8
  363. nautobot/project-static/docs/user-guide/feature-guides/wireless-networks-and-controllers.html +31 -8
  364. nautobot/project-static/docs/user-guide/index.html +31 -8
  365. nautobot/project-static/docs/user-guide/platform-functionality/change-logging.html +31 -8
  366. nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +31 -8
  367. nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +31 -8
  368. nautobot/project-static/docs/user-guide/platform-functionality/customlink.html +31 -8
  369. nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +31 -8
  370. nautobot/project-static/docs/user-guide/platform-functionality/events.html +31 -8
  371. nautobot/project-static/docs/user-guide/platform-functionality/exporttemplate.html +31 -8
  372. nautobot/project-static/docs/user-guide/platform-functionality/externalintegration.html +31 -8
  373. nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +37 -9
  374. nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +31 -8
  375. nautobot/project-static/docs/user-guide/platform-functionality/graphqlquery.html +31 -8
  376. nautobot/project-static/docs/user-guide/platform-functionality/imageattachment.html +31 -8
  377. nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +31 -8
  378. nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +31 -8
  379. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +31 -8
  380. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +31 -8
  381. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobqueue.html +31 -8
  382. nautobot/project-static/docs/user-guide/platform-functionality/jobs/kubernetes-job-support.html +31 -8
  383. nautobot/project-static/docs/user-guide/platform-functionality/jobs/managing-jobs.html +31 -8
  384. nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +31 -8
  385. nautobot/project-static/docs/user-guide/platform-functionality/napalm.html +31 -8
  386. nautobot/project-static/docs/user-guide/platform-functionality/note.html +31 -8
  387. nautobot/project-static/docs/user-guide/platform-functionality/objectmetadata.html +31 -8
  388. nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +31 -8
  389. nautobot/project-static/docs/user-guide/platform-functionality/rendering-jinja-templates.html +31 -8
  390. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +31 -8
  391. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +31 -8
  392. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +31 -8
  393. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/ui-related-endpoints.html +31 -8
  394. nautobot/project-static/docs/user-guide/platform-functionality/role.html +31 -8
  395. nautobot/project-static/docs/user-guide/platform-functionality/savedview.html +31 -8
  396. nautobot/project-static/docs/user-guide/platform-functionality/secret.html +31 -8
  397. nautobot/project-static/docs/user-guide/platform-functionality/staticgroupassociation.html +31 -8
  398. nautobot/project-static/docs/user-guide/platform-functionality/status.html +31 -8
  399. nautobot/project-static/docs/user-guide/platform-functionality/tag.html +31 -8
  400. nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +31 -8
  401. nautobot/project-static/docs/user-guide/platform-functionality/users/objectpermission.html +31 -8
  402. nautobot/project-static/docs/user-guide/platform-functionality/users/token.html +31 -8
  403. nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +31 -8
  404. nautobot/tenancy/tables.py +2 -0
  405. nautobot/virtualization/tests/test_views.py +1 -1
  406. nautobot/wireless/forms.py +0 -1
  407. nautobot/wireless/models.py +1 -1
  408. nautobot/wireless/tables.py +7 -0
  409. {nautobot-2.4.10.dist-info → nautobot-2.4.11.dist-info}/METADATA +4 -4
  410. {nautobot-2.4.10.dist-info → nautobot-2.4.11.dist-info}/RECORD +416 -401
  411. /nautobot/dcim/templates/dcim/{platform_edit.html → platform_create.html} +0 -0
  412. /nautobot/extras/test_jobs/{pass.py → pass_job.py} +0 -0
  413. {nautobot-2.4.10.dist-info → nautobot-2.4.11.dist-info}/LICENSE.txt +0 -0
  414. {nautobot-2.4.10.dist-info → nautobot-2.4.11.dist-info}/NOTICE +0 -0
  415. {nautobot-2.4.10.dist-info → nautobot-2.4.11.dist-info}/WHEEL +0 -0
  416. {nautobot-2.4.10.dist-info → nautobot-2.4.11.dist-info}/entry_points.txt +0 -0
@@ -1,3 +1,7 @@
1
+ import os
2
+ import os.path
3
+ import sys
4
+ import tempfile
1
5
  from unittest import mock
2
6
  import uuid
3
7
 
@@ -17,6 +21,11 @@ from nautobot.core.models import fields as core_fields, utils as models_utils, v
17
21
  from nautobot.core.testing import TestCase
18
22
  from nautobot.core.utils import data as data_utils, filtering, lookup, querysets, requests
19
23
  from nautobot.core.utils.migrations import update_object_change_ct_for_replaced_models
24
+ from nautobot.core.utils.module_loading import (
25
+ check_name_safe_to_import_privately,
26
+ clear_module_from_sys_modules,
27
+ import_modules_privately,
28
+ )
20
29
  from nautobot.dcim import filters as dcim_filters, forms as dcim_forms, models as dcim_models, tables
21
30
  from nautobot.extras import models as extras_models, utils as extras_utils
22
31
  from nautobot.extras.choices import ObjectChangeActionChoices, RelationshipTypeChoices
@@ -959,6 +968,190 @@ class TestMigrationUtils(TestCase):
959
968
  self.assertEqual(ObjectChange.objects.get(request_id=request_id).related_object_type, location_ct)
960
969
 
961
970
 
971
+ class TestModuleLoadingUtils(TestCase):
972
+ def test_check_name_safe_to_import_privately(self):
973
+ for invalid in (
974
+ "foo.bar", # not a valid identifier
975
+ "😂", # not a valid identifier
976
+ "from", # reserved keyword
977
+ "sys", # Python builtin
978
+ "nautobot", # installed package
979
+ "tkinter", # system library
980
+ ):
981
+ with self.subTest(f"Invalid name: {invalid}"):
982
+ permitted, reason = check_name_safe_to_import_privately(invalid)
983
+ self.assertFalse(permitted)
984
+ self.assertIsInstance(reason, str)
985
+
986
+ def _create_test_files(self, root_directory: str, contents: dict):
987
+ """Helper function to create arbitrary text files in a given directory."""
988
+ for relative_path, file_contents in contents.items():
989
+ os.makedirs(os.path.dirname(os.path.join(root_directory, relative_path)), exist_ok=True)
990
+ with open(os.path.join(root_directory, relative_path), "wt") as fd:
991
+ fd.write(file_contents)
992
+
993
+ def test_import_modules_privately_jobs_root_case(self):
994
+ with tempfile.TemporaryDirectory() as tempdir:
995
+ try:
996
+ contents = {
997
+ # Job file treated as a standalone module
998
+ "some_jobs.py": 'name = "some_jobs"',
999
+ # Job subdirectory treated as a package
1000
+ "my_jobs/__init__.py": '''\
1001
+ import my_jobs.some_submodule
1002
+ from . import relative_submodule
1003
+ name = "my_jobs"''',
1004
+ "my_jobs/some_submodule/__init__.py": 'name = "my_jobs.some_submodule"',
1005
+ "my_jobs/relative_submodule/__init__.py": 'name = "my_jobs.relative_submodule"',
1006
+ # Job file that shouldn't be loaded as it conflicts
1007
+ "tkinter.py": 'name = "tkinter"',
1008
+ # Job submodule that shouldn't be loaded as it conflicts
1009
+ "turtle/__init__.py": 'name = "turtle"',
1010
+ }
1011
+ self._create_test_files(tempdir, contents)
1012
+
1013
+ modules = import_modules_privately(tempdir, ignore_import_errors=False)
1014
+ self.assertEqual(["my_jobs", "some_jobs"], sorted([module.__name__ for module in modules]))
1015
+ # assertIn/assertNotIn are super noisy when dealing with the huge sys.modules dict, so instead:
1016
+ if "some_jobs" not in sys.modules:
1017
+ self.fail("Valid module wasn't loaded from JOBS_ROOT")
1018
+ if "my_jobs" not in sys.modules:
1019
+ self.fail("Valid package wasn't loaded from JOBS_ROOT")
1020
+ with self.assertRaises(KeyError, msg="conflicting module name was loaded unsafely from JOBS_ROOT"):
1021
+ sys.modules["tkinter"]
1022
+ with self.assertRaises(KeyError, msg="conflicting package name was loaded unsafely from JOBS_ROOT"):
1023
+ sys.modules["turtle"]
1024
+
1025
+ self.assertEqual(sys.modules["some_jobs"].name, "some_jobs")
1026
+ self.assertEqual(sys.modules["my_jobs"].name, "my_jobs")
1027
+ self.assertEqual(sys.modules["my_jobs"].some_submodule.name, "my_jobs.some_submodule")
1028
+ # self.assertEqual(sys.modules["my_jobs"].relative_submodule.name, "my_jobs.relative_submodule")
1029
+
1030
+ finally:
1031
+ clear_module_from_sys_modules("some_jobs")
1032
+ clear_module_from_sys_modules("my_jobs")
1033
+
1034
+ self.assertNotIn("some_jobs", sys.modules.keys())
1035
+ self.assertNotIn("my_jobs", sys.modules.keys())
1036
+ self.assertNotIn("my_jobs.some_submodule", sys.modules.keys())
1037
+
1038
+ # Test reloading of modules after code changes
1039
+ try:
1040
+ contents["some_jobs.py"] = 'name = "some_jobs_new"'
1041
+ contents["my_jobs/__init__.py"] = '''\
1042
+ import my_jobs.some_submodule
1043
+ from . import relative_submodule
1044
+ name = "my_jobs_new"'''
1045
+ contents["my_jobs/some_submodule/__init__.py"] = 'name = "my_jobs.some_submodule_new"'
1046
+ self._create_test_files(tempdir, contents)
1047
+
1048
+ modules = import_modules_privately(tempdir, ignore_import_errors=False)
1049
+ self.assertEqual(["my_jobs", "some_jobs"], sorted([module.__name__ for module in modules]))
1050
+ # assertIn/assertNotIn are super noisy when dealing with the huge sys.modules dict, so instead:
1051
+ if "some_jobs" not in sys.modules:
1052
+ self.fail("Valid module wasn't loaded from JOBS_ROOT")
1053
+ if "my_jobs" not in sys.modules:
1054
+ self.fail("Valid package wasn't loaded from JOBS_ROOT")
1055
+ with self.assertRaises(KeyError, msg="conflicting module name was loaded unsafely from JOBS_ROOT"):
1056
+ sys.modules["tkinter"]
1057
+ with self.assertRaises(KeyError, msg="conflicting package name was loaded unsafely from JOBS_ROOT"):
1058
+ sys.modules["turtle"]
1059
+
1060
+ self.assertEqual(sys.modules["some_jobs"].name, "some_jobs_new")
1061
+ self.assertEqual(sys.modules["my_jobs"].name, "my_jobs_new")
1062
+ self.assertEqual(sys.modules["my_jobs"].some_submodule.name, "my_jobs.some_submodule_new")
1063
+
1064
+ finally:
1065
+ clear_module_from_sys_modules("some_jobs")
1066
+ clear_module_from_sys_modules("my_jobs")
1067
+
1068
+ self.assertNotIn("some_jobs", sys.modules.keys())
1069
+ self.assertNotIn("my_jobs", sys.modules.keys())
1070
+ self.assertNotIn("my_jobs.some_submodule", sys.modules.keys())
1071
+
1072
+ def test_import_modules_privately_git_repo_jobs_case(self):
1073
+ with tempfile.TemporaryDirectory() as tempdir:
1074
+ try:
1075
+ contents = {
1076
+ # Repo that we intend to load
1077
+ "my_repo/__init__.py": 'name = "my_repo"',
1078
+ "my_repo/jobs/__init__.py": '''\
1079
+ import my_repo.jobs.some_jobs
1080
+ from . import some_other_jobs
1081
+ name = "my_repo.jobs"''',
1082
+ "my_repo/jobs/some_jobs.py": 'name = "my_repo.jobs.some_jobs"',
1083
+ "my_repo/jobs/some_other_jobs.py": 'name = "my_repo.jobs.some_other_jobs"',
1084
+ # A separate repo, not intended to be loaded
1085
+ "other_repo/__init__.py": "",
1086
+ # File that shouldn't be loaded as it conflicts
1087
+ "tkinter.py": "",
1088
+ # Package that shouldn't be loaded as it conflicts
1089
+ "turtle/__init__.py": "",
1090
+ }
1091
+ self._create_test_files(tempdir, contents)
1092
+
1093
+ modules = import_modules_privately(tempdir, module_path=["my_repo", "jobs"], ignore_import_errors=False)
1094
+ self.assertEqual(["my_repo", "my_repo.jobs"], sorted([module.__name__ for module in modules]))
1095
+ # assertIn/assertNotIn are super noisy when dealing with the huge sys.modules dict, so instead:
1096
+ if "my_repo" not in sys.modules:
1097
+ self.fail("Valid repo wasn't loaded from GIT_ROOT")
1098
+ if "my_repo.jobs" not in sys.modules:
1099
+ self.fail("Valid repo subdirectory wasn't loaded from GIT_ROOT")
1100
+ with self.assertRaises(KeyError, msg="unexpected repo was loaded from GIT_ROOT"):
1101
+ sys.modules["other_repo"]
1102
+ with self.assertRaises(KeyError, msg="conflicting module name was loaded unsafely from GIT_ROOT"):
1103
+ sys.modules["tkinter"]
1104
+ with self.assertRaises(KeyError, msg="conflicting package name was loaded unsafely from GIT_ROOT"):
1105
+ sys.modules["turtle"]
1106
+
1107
+ self.assertEqual(sys.modules["my_repo"].name, "my_repo")
1108
+ self.assertEqual(sys.modules["my_repo.jobs"].name, "my_repo.jobs")
1109
+ self.assertEqual(sys.modules["my_repo.jobs"].some_jobs.name, "my_repo.jobs.some_jobs")
1110
+ self.assertEqual(sys.modules["my_repo.jobs"].some_other_jobs.name, "my_repo.jobs.some_other_jobs")
1111
+
1112
+ finally:
1113
+ clear_module_from_sys_modules("my_repo")
1114
+
1115
+ self.assertNotIn("my_repo", sys.modules.keys())
1116
+ self.assertNotIn("my_repo.jobs", sys.modules.keys())
1117
+
1118
+ # Test reloading of modules after code changes
1119
+ try:
1120
+ contents["my_repo/__init__.py"] = 'name = "my_repo_new"'
1121
+ contents["my_repo/jobs/__init__.py"] = '''\
1122
+ import my_repo.jobs.some_jobs
1123
+ from . import some_other_jobs
1124
+ name = "my_repo.jobs_new"'''
1125
+ contents["my_repo/jobs/some_jobs.py"] = 'name = "my_repo.jobs.some_jobs_new"'
1126
+ contents["my_repo/jobs/some_other_jobs.py"] = 'name = "my_repo.jobs.some_other_jobs_new"'
1127
+ self._create_test_files(tempdir, contents)
1128
+
1129
+ modules = import_modules_privately(tempdir, module_path=["my_repo", "jobs"], ignore_import_errors=False)
1130
+ self.assertEqual(["my_repo", "my_repo.jobs"], sorted([module.__name__ for module in modules]))
1131
+ # assertIn/assertNotIn are super noisy when dealing with the huge sys.modules dict, so instead:
1132
+ if "my_repo" not in sys.modules:
1133
+ self.fail("Valid repo wasn't loaded from GIT_ROOT")
1134
+ if "my_repo.jobs" not in sys.modules:
1135
+ self.fail("Valid repo subdirectory wasn't loaded from GIT_ROOT")
1136
+ with self.assertRaises(KeyError, msg="unexpected repo was loaded from GIT_ROOT"):
1137
+ sys.modules["other_repo"]
1138
+ with self.assertRaises(KeyError, msg="conflicting module name was loaded unsafely from GIT_ROOT"):
1139
+ sys.modules["tkinter"]
1140
+ with self.assertRaises(KeyError, msg="conflicting package name was loaded unsafely from GIT_ROOT"):
1141
+ sys.modules["turtle"]
1142
+
1143
+ self.assertEqual(sys.modules["my_repo"].name, "my_repo_new")
1144
+ self.assertEqual(sys.modules["my_repo.jobs"].name, "my_repo.jobs_new")
1145
+ self.assertEqual(sys.modules["my_repo.jobs"].some_jobs.name, "my_repo.jobs.some_jobs_new")
1146
+ self.assertEqual(sys.modules["my_repo.jobs"].some_other_jobs.name, "my_repo.jobs.some_other_jobs_new")
1147
+
1148
+ finally:
1149
+ clear_module_from_sys_modules("my_repo")
1150
+
1151
+ self.assertNotIn("my_repo", sys.modules.keys())
1152
+ self.assertNotIn("my_repo.jobs", sys.modules.keys())
1153
+
1154
+
962
1155
  class TestQuerySetUtils(TestCase):
963
1156
  def test_maybe_select_related(self):
964
1157
  # If possible, select_related should be called
@@ -1,13 +1,16 @@
1
1
  import urllib.parse
2
2
 
3
+ from django.contrib.auth.models import AnonymousUser
3
4
  from django.db import ProgrammingError
4
5
  from django.test import TestCase
5
6
 
6
7
  from nautobot.core.models.querysets import count_related
7
- from nautobot.core.views.utils import check_filter_for_display, prepare_cloned_fields
8
+ from nautobot.core.testing import TransactionTestCase
9
+ from nautobot.core.views.utils import check_filter_for_display, get_saved_views_for_user, prepare_cloned_fields
8
10
  from nautobot.dcim.filters import DeviceFilterSet
9
11
  from nautobot.dcim.models import Device, DeviceRedundancyGroup, DeviceType, InventoryItem, Location, Manufacturer
10
- from nautobot.extras.models import Role, Status
12
+ from nautobot.extras.models import Role, SavedView, Status
13
+ from nautobot.users.models import User
11
14
 
12
15
 
13
16
  class CheckFilterForDisplayTest(TestCase):
@@ -168,3 +171,51 @@ class CheckPrepareClonedFields(TestCase):
168
171
  self.assertTrue(isinstance(query_params["description"], list))
169
172
  self.assertTrue(len(query_params["description"]) == 1)
170
173
  self.assertTrue(query_params["description"][0] == description)
174
+
175
+
176
+ class GetSavedViewsForUserTestCase(TransactionTestCase):
177
+ """
178
+ Class to test `get_saved_views_for_user`.
179
+ """
180
+
181
+ def create_saved_view(self, name, owner=None, is_shared=False):
182
+ """Helper to create a SavedView."""
183
+ return SavedView.objects.create(
184
+ name=name, owner=owner or self.user, view="dcim:device_list", is_shared=is_shared
185
+ )
186
+
187
+ def setUp(self):
188
+ super().setUp()
189
+ self.user2 = User.objects.create_user(username="second_user")
190
+ self.create_saved_view(name="saved_view")
191
+ self.create_saved_view(name="saved_view_shared", is_shared=True)
192
+ self.create_saved_view(name="saved_view_different_owner", owner=self.user2)
193
+ self.create_saved_view(name="saved_view_shared_different_owner", is_shared=True, owner=self.user2)
194
+
195
+ def test_user_with_permissions_get_all_saved_views(self):
196
+ """Test if for user with permissions method will return all saved views."""
197
+ self.add_permissions("extras.view_savedview")
198
+ saved_views = get_saved_views_for_user(self.user, "dcim:device_list")
199
+ self.assertEqual(saved_views.count(), 4)
200
+ expected_names = [
201
+ "saved_view",
202
+ "saved_view_different_owner",
203
+ "saved_view_shared",
204
+ "saved_view_shared_different_owner",
205
+ ]
206
+ self.assertEqual(list(saved_views.values_list("name", flat=True)), expected_names)
207
+
208
+ def test_user_without_permissions_get_shared_views_and_own_views_only(self):
209
+ """Test if user without permissions can see shared views and own views."""
210
+ saved_views = get_saved_views_for_user(self.user, "dcim:device_list")
211
+ self.assertEqual(saved_views.count(), 3)
212
+ expected_names = ["saved_view", "saved_view_shared", "saved_view_shared_different_owner"]
213
+ self.assertEqual(list(saved_views.values_list("name", flat=True)), expected_names)
214
+
215
+ def test_anonymous_user_get_shared_views_only(self):
216
+ """Test if method is working with anonymous users and return only shared views."""
217
+ user = AnonymousUser()
218
+ saved_views = get_saved_views_for_user(user, "dcim:device_list")
219
+ self.assertEqual(saved_views.count(), 2)
220
+ expected_names = ["saved_view_shared", "saved_view_shared_different_owner"]
221
+ self.assertEqual(list(saved_views.values_list("name", flat=True)), expected_names)
@@ -1266,6 +1266,10 @@ class ObjectFieldsPanel(KeyValueTablePanel):
1266
1266
 
1267
1267
  data[field_name] = field_value
1268
1268
 
1269
+ # Ensuring the `name` field is displayed first, if present.
1270
+ if "name" in data:
1271
+ data = {"name": data["name"], **{k: v for k, v in data.items() if k != "name"}}
1272
+
1269
1273
  return data
1270
1274
 
1271
1275
  def render_key(self, key, value, context: Context):
@@ -314,13 +314,15 @@ def get_created_and_last_updated_usernames_for_model(instance):
314
314
  created_by = None
315
315
  last_updated_by = None
316
316
  try:
317
- created_by_record = object_change_records.filter(action=ObjectChangeActionChoices.ACTION_CREATE).first()
317
+ created_by_record = (
318
+ object_change_records.filter(action=ObjectChangeActionChoices.ACTION_CREATE).only("user_name").first()
319
+ )
318
320
  if created_by_record is not None:
319
321
  created_by = created_by_record.user_name
320
322
  except ObjectChange.DoesNotExist:
321
323
  pass
322
324
 
323
- last_updated_by_record = object_change_records.first()
325
+ last_updated_by_record = object_change_records.only("user_name").first()
324
326
  if last_updated_by_record:
325
327
  last_updated_by = last_updated_by_record.user_name
326
328
 
@@ -1,29 +1,13 @@
1
- from contextlib import contextmanager
2
- import importlib
3
- from importlib.util import find_spec, module_from_spec
1
+ from importlib.machinery import FileFinder, SOURCE_SUFFIXES, SourceFileLoader
2
+ from importlib.util import module_from_spec
3
+ from keyword import iskeyword
4
4
  import logging
5
- import os
6
5
  import pkgutil
7
6
  import sys
8
7
 
9
8
  logger = logging.getLogger(__name__)
10
9
 
11
10
 
12
- @contextmanager
13
- def _temporarily_add_to_sys_path(path):
14
- """
15
- Allow loading of modules and packages from within the provided directory by temporarily modifying `sys.path`.
16
-
17
- On exit, it restores the original `sys.path` value.
18
- """
19
- old_sys_path = sys.path.copy()
20
- sys.path.insert(0, path)
21
- try:
22
- yield
23
- finally:
24
- sys.path = old_sys_path
25
-
26
-
27
11
  def clear_module_from_sys_modules(module_name):
28
12
  """
29
13
  Remove the module and all its submodules from sys.modules.
@@ -33,7 +17,29 @@ def clear_module_from_sys_modules(module_name):
33
17
  del sys.modules[name]
34
18
 
35
19
 
36
- def import_modules_privately(path, module_path=None, ignore_import_errors=True):
20
+ def check_name_safe_to_import_privately(name: str) -> tuple[bool, str]:
21
+ """
22
+ Make sure the given package/module name is "safe" to import from the filesystem.
23
+
24
+ In other words, make sure it's:
25
+ - a valid Python identifier and not a reserved keyword
26
+ - not the name of an existing "real" Python package or builtin
27
+
28
+ Returns:
29
+ (bool, str): Whether safe to load, and an explanatory string fragment for logging/exception messages.
30
+ """
31
+ if not name.isidentifier():
32
+ return False, "not a valid identifier"
33
+ if iskeyword(name):
34
+ return False, "a reserved keyword"
35
+ if name in sys.builtin_module_names:
36
+ return False, "a Python builtin"
37
+ if any(module_info.name == name for module_info in pkgutil.iter_modules()):
38
+ return False, "the name of an installed Python package"
39
+ return True, "a valid and non-conflicting module name"
40
+
41
+
42
+ def import_modules_privately(path, module_path=None, module_prefix="", ignore_import_errors=True):
37
43
  """
38
44
  Import modules from the filesystem without adding the path permanently to `sys.path`.
39
45
 
@@ -49,49 +55,71 @@ def import_modules_privately(path, module_path=None, ignore_import_errors=True):
49
55
  ignore_import_errors (bool): Exceptions raised while importing modules will be caught and logged.
50
56
  If this is set as False, they will then be re-raised to be handled by the caller of this function.
51
57
  """
52
- if module_path is None:
53
- module_path = []
54
- module_prefix = None
58
+ loaded_modules = []
59
+ # We formerly used pkgutil.walk_packages() here to handle submodule loading with a multi-entry module_path,
60
+ # but that has the downside (and risk!) of automatically importing all packages that it finds in the given path,
61
+ # whether or not we actually want to do so. So instead, for the case where a module_path is provided, we need to
62
+ # iteratively import each submodule ourselves.
63
+ if module_path:
64
+ # Git repository case - e.g. import_modules_privately(settings.GIT_ROOT, module_path=[repository_slug, "jobs"])
65
+ # Here we want to ONLY auto-load the module sequence identified by module_path.
66
+ permitted, reason = check_name_safe_to_import_privately(module_path[0])
67
+ if not permitted:
68
+ logger.error("Unable to load module %r from %s as it is %s", module_path[0], path, reason)
69
+ else:
70
+ module = None
71
+ module_name = module_path.pop(0)
72
+ submodule_name = module_name
73
+ try:
74
+ while True:
75
+ finder = FileFinder(path, (SourceFileLoader, SOURCE_SUFFIXES))
76
+ finder.invalidate_caches()
77
+ spec = finder.find_spec(module_name)
78
+ if spec is None or spec.loader is None:
79
+ logger.error("Unable to find module spec and/or loader for %r", submodule_name)
80
+ break
81
+ spec.name = submodule_name
82
+ spec.loader.name = submodule_name
83
+ submodule = module_from_spec(spec)
84
+ sys.modules[submodule_name] = submodule
85
+ spec.loader.exec_module(submodule)
86
+ if module is not None:
87
+ setattr(module, module_name, submodule)
88
+ module = submodule
89
+ loaded_modules.append(module)
90
+ if module_path:
91
+ submodule_name = f"{module_name}.{module_path[0]}"
92
+ module_name = module_path.pop(0)
93
+ path = module.__path__[0]
94
+ else:
95
+ break
96
+ except Exception as exc:
97
+ logger.error("Unable to load module %s from %s: %s", module_name, path, exc)
98
+ if not ignore_import_errors:
99
+ raise
55
100
  else:
56
- module_prefix = ".".join(module_path)
57
- with _temporarily_add_to_sys_path(path):
58
- for finder, discovered_module_name, is_package in pkgutil.walk_packages([path], onerror=logger.error):
59
- if module_prefix and not (
60
- module_prefix.startswith(f"{discovered_module_name}.") # my_repo/__init__.py
61
- or discovered_module_name == module_prefix # my_repo/jobs.py
62
- or discovered_module_name.startswith(f"{module_prefix}.") # my_repo/jobs/foobar.py
63
- ):
101
+ # JOBS_ROOT case - import ALL top-level modules/packages that we can find in the given path;
102
+ # they can implement and import submodules as desired by themselves, but we only autoimport top-level ones.
103
+ for finder, discovered_module_name, _ in pkgutil.iter_modules([path]):
104
+ permitted, reason = check_name_safe_to_import_privately(discovered_module_name)
105
+ if not permitted:
106
+ logger.error("Unable to load module %r from %s as it is %s", discovered_module_name, path, reason)
64
107
  continue
65
- try:
66
- existing_module = find_spec(discovered_module_name)
67
- except (ModuleNotFoundError, ValueError):
68
- existing_module = None
69
- if existing_module is not None:
70
- existing_module_path = os.path.realpath(existing_module.origin)
71
- if not existing_module_path.startswith(path):
72
- logger.error(
73
- "Unable to load module %s from %s as it conflicts with existing module %s",
74
- discovered_module_name,
75
- path,
76
- existing_module_path,
77
- )
78
- continue
79
-
80
- if discovered_module_name in sys.modules:
81
- clear_module_from_sys_modules(discovered_module_name)
108
+ module_name = discovered_module_name
109
+ if module_name in sys.modules:
110
+ clear_module_from_sys_modules(module_name)
82
111
 
83
112
  try:
84
- if not is_package:
85
- spec = finder.find_spec(discovered_module_name)
86
- if spec is None:
87
- raise ValueError("Unable to find module spec")
88
- module = module_from_spec(spec)
89
- sys.modules[discovered_module_name] = module
90
- spec.loader.exec_module(module)
91
- else:
92
- module = importlib.import_module(discovered_module_name)
93
- importlib.reload(module)
113
+ spec = finder.find_spec(discovered_module_name)
114
+ if spec is None or spec.loader is None:
115
+ logger.error("Unable to find module spec and/or loader for %r", discovered_module_name)
116
+ continue
117
+ module = module_from_spec(spec)
118
+ sys.modules[module_name] = module
119
+ spec.loader.exec_module(module)
120
+ loaded_modules.append(module)
94
121
  except Exception as exc:
95
122
  logger.error("Unable to load module %s from %s: %s", discovered_module_name, path, exc)
96
123
  if not ignore_import_errors:
97
124
  raise
125
+ return loaded_modules
@@ -54,6 +54,7 @@ from nautobot.core.views.utils import (
54
54
  check_filter_for_display,
55
55
  common_detail_view_context,
56
56
  get_csv_form_fields_from_serializer_class,
57
+ get_saved_views_for_user,
57
58
  handle_protectederror,
58
59
  import_csv_helper,
59
60
  prepare_cloned_fields,
@@ -315,18 +316,7 @@ class ObjectListView(ObjectPermissionRequiredMixin, View):
315
316
  table_config_form = None
316
317
  current_saved_view = None
317
318
  current_saved_view_pk = self.request.GET.get("saved_view", None)
318
- # We are not using .restrict(request.user, "view") here
319
- # User should be able to see any saved view that he has the list view access to.
320
- if user.has_perms(["extras.view_savedview"]):
321
- saved_views = SavedView.objects.filter(view=list_url).order_by("name").only("pk", "name")
322
- else:
323
- shared_saved_views = (
324
- SavedView.objects.filter(view=list_url, is_shared=True).order_by("name").only("pk", "name")
325
- )
326
- user_owned_saved_views = (
327
- SavedView.objects.filter(view=list_url, owner=user).order_by("name").only("pk", "name")
328
- )
329
- saved_views = shared_saved_views | user_owned_saved_views
319
+ saved_views = get_saved_views_for_user(user, list_url)
330
320
 
331
321
  if current_saved_view_pk:
332
322
  try:
@@ -239,6 +239,9 @@ class NautobotViewSetMixin(GenericViewSet, AccessMixin, GetReturnURLMixin, FormV
239
239
  table_class = None
240
240
  notes_form_class = NoteForm
241
241
  permission_classes = []
242
+ # custom view attributes used for permission checks and handling
243
+ custom_view_base_action = None
244
+ custom_view_additional_permissions = None
242
245
 
243
246
  def get_permissions_for_model(self, model, actions):
244
247
  """
@@ -249,6 +252,10 @@ class NautobotViewSetMixin(GenericViewSet, AccessMixin, GetReturnURLMixin, FormV
249
252
  """
250
253
  model_permissions = []
251
254
  for action in actions:
255
+ # Append additional object permissions if specified.
256
+ if self.custom_view_additional_permissions:
257
+ model_permissions.append(*self.custom_view_additional_permissions)
258
+ # Append the model-level permissions for the action.
252
259
  model_permissions.append(f"{model._meta.app_label}.{action}_{model._meta.model_name}")
253
260
  return model_permissions
254
261
 
@@ -501,7 +508,13 @@ class NautobotViewSetMixin(GenericViewSet, AccessMixin, GetReturnURLMixin, FormV
501
508
 
502
509
  def get_action(self):
503
510
  """Helper method for retrieving action and if action not set defaulting to action name."""
504
- return PERMISSIONS_ACTION_MAP.get(self.action, self.action)
511
+ if self.custom_view_base_action:
512
+ return self.custom_view_base_action
513
+ if self.action in PERMISSIONS_ACTION_MAP:
514
+ # If the action is in the action_map, return the mapped permission
515
+ return PERMISSIONS_ACTION_MAP[self.action]
516
+
517
+ return self.action
505
518
 
506
519
  def get_extra_context(self, request, instance=None):
507
520
  """
@@ -1032,6 +1045,11 @@ class BulkEditAndBulkDeleteModelMixin:
1032
1045
 
1033
1046
  filter_query_params = new_filter_query_params
1034
1047
 
1048
+ if nullified_fields := request.POST.getlist("_nullify"):
1049
+ form_data["_nullify"] = nullified_fields
1050
+ else:
1051
+ form_data["_nullify"] = []
1052
+
1035
1053
  job_form = BulkEditObjects.as_form(
1036
1054
  data={
1037
1055
  "form_data": form_data,
@@ -28,6 +28,7 @@ from nautobot.core.views.utils import (
28
28
  check_filter_for_display,
29
29
  common_detail_view_context,
30
30
  get_csv_form_fields_from_serializer_class,
31
+ get_saved_views_for_user,
31
32
  view_changes_not_saved,
32
33
  )
33
34
  from nautobot.extras.models import SavedView
@@ -235,7 +236,8 @@ class NautobotHTMLRenderer(renderers.BrowsableAPIRenderer):
235
236
  if view.filterset_form_class is not None:
236
237
  filter_form = view.filterset_form_class(view.filter_params, label_suffix="")
237
238
  table = self.construct_table(view, request=request, permissions=permissions)
238
- search_form = SearchForm(data=view.filter_params)
239
+ q_placeholder = "Search " + bettertitle(model._meta.verbose_name_plural)
240
+ search_form = SearchForm(data=view.filter_params, q_placeholder=q_placeholder)
239
241
  elif view.action == "destroy":
240
242
  form = form_class(initial=request.GET)
241
243
  elif view.action in ["create", "update"]:
@@ -309,18 +311,7 @@ class NautobotHTMLRenderer(renderers.BrowsableAPIRenderer):
309
311
  list_url = f"{resolved_path.app_name}:{resolved_path.url_name}"
310
312
  saved_views = None
311
313
  if model.is_saved_view_model:
312
- # We are not using .restrict(request.user, "view") here
313
- # User should be able to see any saved view that he has the list view access to.
314
- if request.user.has_perms(["extras.view_savedview"]):
315
- saved_views = SavedView.objects.filter(view=list_url).order_by("name").only("pk", "name")
316
- else:
317
- shared_saved_views = (
318
- SavedView.objects.filter(view=list_url, is_shared=True).order_by("name").only("pk", "name")
319
- )
320
- user_owned_saved_views = (
321
- SavedView.objects.filter(view=list_url, owner=request.user).order_by("name").only("pk", "name")
322
- )
323
- saved_views = shared_saved_views | user_owned_saved_views
314
+ saved_views = get_saved_views_for_user(request.user, list_url)
324
315
 
325
316
  new_changes_not_applied = view_changes_not_saved(request, view, self.saved_view)
326
317
  context.update(
@@ -17,6 +17,7 @@ from nautobot.core.utils.data import is_uuid
17
17
  from nautobot.core.utils.filtering import get_filter_field_label
18
18
  from nautobot.core.utils.lookup import get_created_and_last_updated_usernames_for_model, get_form_for_model
19
19
  from nautobot.core.views.paginator import EnhancedPaginator, get_paginate_count
20
+ from nautobot.extras.models import SavedView
20
21
  from nautobot.extras.tables import AssociatedContactsTable, DynamicGroupTable, ObjectMetadataTable
21
22
 
22
23
 
@@ -382,3 +383,18 @@ def common_detail_view_context(request, instance):
382
383
  context["associated_object_metadata_table"] = None
383
384
 
384
385
  return context
386
+
387
+
388
+ def get_saved_views_for_user(user, list_url):
389
+ # We are not using .restrict(request.user, "view") here
390
+ # User should be able to see any saved view that he has the list view access to.
391
+ saved_views = SavedView.objects.filter(view=list_url).order_by("name").only("pk", "name")
392
+ if user.has_perms(["extras.view_savedview"]):
393
+ return saved_views
394
+
395
+ shared_saved_views = saved_views.filter(is_shared=True)
396
+ if user.is_authenticated:
397
+ user_owned_saved_views = SavedView.objects.filter(view=list_url, owner=user).order_by("name").only("pk", "name")
398
+ return shared_saved_views | user_owned_saved_views
399
+
400
+ return shared_saved_views
@@ -79,6 +79,7 @@ from nautobot.dcim.models import (
79
79
  Module,
80
80
  ModuleBay,
81
81
  ModuleBayTemplate,
82
+ ModuleFamily,
82
83
  ModuleType,
83
84
  PathEndpoint,
84
85
  Platform,
@@ -1124,3 +1125,15 @@ class InterfaceVDCAssignmentSerializer(ValidatedModelSerializer):
1124
1125
  class Meta:
1125
1126
  model = InterfaceVDCAssignment
1126
1127
  fields = "__all__"
1128
+
1129
+
1130
+ class ModuleFamilySerializer(NautobotModelSerializer):
1131
+ """API serializer for ModuleFamily objects."""
1132
+
1133
+ url = serializers.HyperlinkedIdentityField(view_name="dcim-api:modulefamily-detail")
1134
+ module_type_count = serializers.IntegerField(read_only=True)
1135
+ module_bay_count = serializers.IntegerField(read_only=True)
1136
+
1137
+ class Meta:
1138
+ model = ModuleFamily
1139
+ fields = "__all__"