nautobot 2.3.0b1__py3-none-any.whl → 2.3.1__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 (380) hide show
  1. nautobot/cloud/factory.py +2 -0
  2. nautobot/cloud/filters.py +3 -0
  3. nautobot/cloud/forms.py +8 -2
  4. nautobot/cloud/migrations/0001_initial.py +1 -1
  5. nautobot/cloud/models.py +1 -2
  6. nautobot/cloud/tables.py +1 -17
  7. nautobot/cloud/templates/cloud/cloudnetwork_retrieve.html +1 -7
  8. nautobot/cloud/templates/cloud/cloudresourcetype_retrieve.html +11 -0
  9. nautobot/cloud/templates/cloud/cloudservice_retrieve.html +4 -0
  10. nautobot/cloud/tests/test_filters.py +12 -0
  11. nautobot/cloud/tests/test_views.py +17 -0
  12. nautobot/cloud/views.py +1 -1
  13. nautobot/core/celery/__init__.py +5 -2
  14. nautobot/core/filters.py +15 -1
  15. nautobot/core/forms/forms.py +10 -2
  16. nautobot/core/graphql/generators.py +2 -2
  17. nautobot/core/graphql/schema.py +6 -14
  18. nautobot/core/jobs/__init__.py +4 -1
  19. nautobot/core/management/commands/generate_test_data.py +2 -2
  20. nautobot/core/models/__init__.py +2 -2
  21. nautobot/core/settings.py +13 -2
  22. nautobot/core/settings.yaml +16 -2
  23. nautobot/core/tables.py +3 -0
  24. nautobot/core/templates/generic/object_retrieve.html +6 -6
  25. nautobot/core/templates/inc/computed_fields/panel_data.html +36 -24
  26. nautobot/core/templates/inc/object_details_advanced_panel.html +1 -1
  27. nautobot/core/templates/nautobot_config.py.j2 +15 -0
  28. nautobot/core/testing/filters.py +12 -1
  29. nautobot/core/tests/integration/test_general_functionality.py +1 -1
  30. nautobot/core/tests/test_jobs.py +74 -1
  31. nautobot/core/views/__init__.py +1 -1
  32. nautobot/core/views/generic.py +1 -1
  33. nautobot/core/views/mixins.py +1 -1
  34. nautobot/core/views/utils.py +8 -6
  35. nautobot/dcim/factory.py +4 -1
  36. nautobot/dcim/filters/__init__.py +4 -0
  37. nautobot/dcim/forms.py +24 -0
  38. nautobot/dcim/migrations/0061_module_models.py +1 -0
  39. nautobot/dcim/models/device_components.py +7 -0
  40. nautobot/dcim/models/devices.py +18 -19
  41. nautobot/dcim/models/racks.py +0 -1
  42. nautobot/dcim/tables/devices.py +17 -3
  43. nautobot/dcim/tables/devicetypes.py +1 -1
  44. nautobot/dcim/templates/dcim/device/base.html +1 -1
  45. nautobot/dcim/templates/dcim/device.html +3 -3
  46. nautobot/dcim/templates/dcim/deviceredundancygroup_retrieve.html +6 -0
  47. nautobot/dcim/templates/dcim/moduletype_retrieve.html +17 -0
  48. nautobot/dcim/templates/dcim/softwareimagefile_retrieve.html +3 -3
  49. nautobot/dcim/tests/test_api.py +2 -0
  50. nautobot/dcim/tests/test_filters.py +14 -7
  51. nautobot/dcim/tests/test_forms.py +54 -0
  52. nautobot/dcim/tests/test_models.py +40 -1
  53. nautobot/dcim/tests/test_views.py +45 -2
  54. nautobot/dcim/views.py +4 -1
  55. nautobot/extras/api/serializers.py +0 -1
  56. nautobot/extras/api/views.py +7 -59
  57. nautobot/extras/factory.py +50 -12
  58. nautobot/extras/filters/__init__.py +4 -1
  59. nautobot/extras/forms/base.py +10 -4
  60. nautobot/extras/forms/forms.py +1 -0
  61. nautobot/extras/homepage.py +12 -2
  62. nautobot/extras/jobs.py +2 -2
  63. nautobot/extras/migrations/0111_metadata.py +4 -4
  64. nautobot/extras/migrations/0114_computedfield_grouping.py +17 -0
  65. nautobot/extras/models/customfields.py +54 -0
  66. nautobot/extras/models/jobs.py +83 -0
  67. nautobot/extras/models/metadata.py +18 -18
  68. nautobot/extras/models/models.py +2 -0
  69. nautobot/extras/signals.py +14 -1
  70. nautobot/extras/tables.py +43 -14
  71. nautobot/extras/templates/extras/computedfield.html +4 -0
  72. nautobot/extras/templates/extras/job_detail.html +11 -0
  73. nautobot/extras/tests/test_api.py +16 -9
  74. nautobot/extras/tests/test_jobs.py +2 -2
  75. nautobot/extras/tests/test_models.py +20 -18
  76. nautobot/extras/tests/test_views.py +23 -3
  77. nautobot/extras/utils.py +35 -6
  78. nautobot/extras/views.py +28 -51
  79. nautobot/ipam/filters.py +1 -1
  80. nautobot/ipam/forms.py +1 -1
  81. nautobot/ipam/models.py +9 -20
  82. nautobot/ipam/querysets.py +26 -0
  83. nautobot/ipam/tables.py +4 -0
  84. nautobot/ipam/tests/test_models.py +89 -2
  85. nautobot/ipam/views.py +10 -15
  86. nautobot/project-static/css/base.css +1 -0
  87. nautobot/project-static/docs/404.html +18 -18
  88. nautobot/project-static/docs/apps/index.html +18 -18
  89. nautobot/project-static/docs/apps/nautobot-apps.html +18 -18
  90. nautobot/project-static/docs/assets/stylesheets/main.3cba04c6.min.css +1 -0
  91. nautobot/project-static/docs/assets/stylesheets/main.3cba04c6.min.css.map +1 -0
  92. nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +18 -18
  93. nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +18 -18
  94. nautobot/project-static/docs/code-reference/nautobot/apps/api.html +66 -18
  95. nautobot/project-static/docs/code-reference/nautobot/apps/change_logging.html +18 -18
  96. nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +18 -18
  97. nautobot/project-static/docs/code-reference/nautobot/apps/config.html +18 -18
  98. nautobot/project-static/docs/code-reference/nautobot/apps/constants.html +18 -18
  99. nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +18 -18
  100. nautobot/project-static/docs/code-reference/nautobot/apps/exceptions.html +66 -18
  101. nautobot/project-static/docs/code-reference/nautobot/apps/factory.html +34 -18
  102. nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +82 -63
  103. nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +75 -111
  104. nautobot/project-static/docs/code-reference/nautobot/apps/graphql.html +18 -18
  105. nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +34 -18
  106. nautobot/project-static/docs/code-reference/nautobot/apps/models.html +161 -18
  107. nautobot/project-static/docs/code-reference/nautobot/apps/querysets.html +18 -18
  108. nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +18 -18
  109. nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +18 -18
  110. nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +18 -18
  111. nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +18 -18
  112. nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +18 -18
  113. nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +21 -19
  114. nautobot/project-static/docs/code-reference/nautobot/apps/views.html +34 -18
  115. nautobot/project-static/docs/development/apps/api/configuration-view.html +18 -18
  116. nautobot/project-static/docs/development/apps/api/database-backend-config.html +18 -18
  117. nautobot/project-static/docs/development/apps/api/models/django-admin.html +18 -18
  118. nautobot/project-static/docs/development/apps/api/models/global-search.html +18 -18
  119. nautobot/project-static/docs/development/apps/api/models/graphql.html +18 -18
  120. nautobot/project-static/docs/development/apps/api/models/index.html +33 -22
  121. nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +18 -18
  122. nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +18 -18
  123. nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +18 -18
  124. nautobot/project-static/docs/development/apps/api/platform-features/git-repository-content.html +18 -18
  125. nautobot/project-static/docs/development/apps/api/platform-features/index.html +18 -18
  126. nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +18 -18
  127. nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +18 -18
  128. nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +18 -18
  129. nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +18 -18
  130. nautobot/project-static/docs/development/apps/api/platform-features/uniquely-identify-objects.html +18 -18
  131. nautobot/project-static/docs/development/apps/api/prometheus.html +18 -18
  132. nautobot/project-static/docs/development/apps/api/setup.html +18 -18
  133. nautobot/project-static/docs/development/apps/api/testing.html +18 -18
  134. nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +18 -18
  135. nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +18 -18
  136. nautobot/project-static/docs/development/apps/api/ui-extensions/index.html +18 -18
  137. nautobot/project-static/docs/development/apps/api/ui-extensions/navigation.html +18 -18
  138. nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +18 -18
  139. nautobot/project-static/docs/development/apps/api/views/base-template.html +18 -18
  140. nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +18 -18
  141. nautobot/project-static/docs/development/apps/api/views/django-generic-views.html +18 -18
  142. nautobot/project-static/docs/development/apps/api/views/help-documentation.html +18 -18
  143. nautobot/project-static/docs/development/apps/api/views/index.html +18 -18
  144. nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +18 -18
  145. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +18 -18
  146. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +18 -18
  147. nautobot/project-static/docs/development/apps/api/views/notes.html +18 -18
  148. nautobot/project-static/docs/development/apps/api/views/rest-api.html +18 -18
  149. nautobot/project-static/docs/development/apps/api/views/urls.html +18 -18
  150. nautobot/project-static/docs/development/apps/index.html +18 -18
  151. nautobot/project-static/docs/development/apps/migration/code-updates.html +18 -18
  152. nautobot/project-static/docs/development/apps/migration/dependency-updates.html +18 -18
  153. nautobot/project-static/docs/development/apps/migration/from-v1.html +18 -18
  154. nautobot/project-static/docs/development/apps/migration/model-updates/dcim.html +18 -18
  155. nautobot/project-static/docs/development/apps/migration/model-updates/extras.html +18 -18
  156. nautobot/project-static/docs/development/apps/migration/model-updates/global.html +18 -18
  157. nautobot/project-static/docs/development/apps/migration/model-updates/ipam.html +18 -18
  158. nautobot/project-static/docs/development/apps/porting-from-netbox.html +18 -18
  159. nautobot/project-static/docs/development/core/application-registry.html +18 -18
  160. nautobot/project-static/docs/development/core/best-practices.html +18 -18
  161. nautobot/project-static/docs/development/core/bootstrap-ui.html +18 -18
  162. nautobot/project-static/docs/development/core/caching.html +18 -18
  163. nautobot/project-static/docs/development/core/controllers.html +18 -18
  164. nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +18 -18
  165. nautobot/project-static/docs/development/core/generic-views.html +18 -18
  166. nautobot/project-static/docs/development/core/getting-started.html +18 -18
  167. nautobot/project-static/docs/development/core/homepage.html +18 -18
  168. nautobot/project-static/docs/development/core/index.html +29 -18
  169. nautobot/project-static/docs/development/core/model-checklist.html +26 -20
  170. nautobot/project-static/docs/development/core/model-features.html +18 -18
  171. nautobot/project-static/docs/development/core/natural-keys.html +18 -18
  172. nautobot/project-static/docs/development/core/navigation-menu.html +18 -18
  173. nautobot/project-static/docs/development/core/release-checklist.html +18 -18
  174. nautobot/project-static/docs/development/core/role-internals.html +18 -18
  175. nautobot/project-static/docs/development/core/settings.html +18 -18
  176. nautobot/project-static/docs/development/core/style-guide.html +19 -19
  177. nautobot/project-static/docs/development/core/templates.html +18 -18
  178. nautobot/project-static/docs/development/core/testing.html +18 -18
  179. nautobot/project-static/docs/development/core/user-preferences.html +18 -18
  180. nautobot/project-static/docs/development/index.html +18 -18
  181. nautobot/project-static/docs/development/jobs/index.html +393 -379
  182. nautobot/project-static/docs/development/jobs/migration/from-v1.html +18 -18
  183. nautobot/project-static/docs/index.html +9032 -13
  184. nautobot/project-static/docs/models/extras/metadatachoice.html +3 -3
  185. nautobot/project-static/docs/models/extras/metadatatype.html +3 -3
  186. nautobot/project-static/docs/models/extras/objectmetadata.html +3 -3
  187. nautobot/project-static/docs/objects.inv +0 -0
  188. nautobot/project-static/docs/overview/application_stack.html +18 -18
  189. nautobot/project-static/docs/overview/design_philosophy.html +20 -20
  190. nautobot/project-static/docs/overview/index.html +13 -9032
  191. nautobot/project-static/docs/release-notes/index.html +252 -19
  192. nautobot/project-static/docs/release-notes/version-1.0.html +18 -18
  193. nautobot/project-static/docs/release-notes/version-1.1.html +18 -18
  194. nautobot/project-static/docs/release-notes/version-1.2.html +18 -18
  195. nautobot/project-static/docs/release-notes/version-1.3.html +18 -18
  196. nautobot/project-static/docs/release-notes/version-1.4.html +18 -18
  197. nautobot/project-static/docs/release-notes/version-1.5.html +18 -18
  198. nautobot/project-static/docs/release-notes/version-1.6.html +18 -18
  199. nautobot/project-static/docs/release-notes/version-2.0.html +18 -18
  200. nautobot/project-static/docs/release-notes/version-2.1.html +18 -18
  201. nautobot/project-static/docs/release-notes/version-2.2.html +248 -111
  202. nautobot/project-static/docs/release-notes/version-2.3.html +644 -90
  203. nautobot/project-static/docs/requirements.txt +3 -3
  204. nautobot/project-static/docs/search/search_index.json +1 -1
  205. nautobot/project-static/docs/sitemap.xml +278 -278
  206. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  207. nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +18 -18
  208. nautobot/project-static/docs/user-guide/administration/configuration/authentication/remote.html +18 -18
  209. nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +18 -18
  210. nautobot/project-static/docs/user-guide/administration/configuration/index.html +18 -18
  211. nautobot/project-static/docs/user-guide/administration/configuration/optional-settings.html +52 -20
  212. nautobot/project-static/docs/user-guide/administration/configuration/required-settings.html +18 -18
  213. nautobot/project-static/docs/user-guide/administration/configuration/time-zones.html +18 -18
  214. nautobot/project-static/docs/user-guide/administration/guides/caching.html +18 -18
  215. nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +22 -18
  216. nautobot/project-static/docs/user-guide/administration/guides/healthcheck.html +18 -18
  217. nautobot/project-static/docs/user-guide/administration/guides/permissions.html +18 -18
  218. nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +18 -18
  219. nautobot/project-static/docs/user-guide/administration/guides/replicating-nautobot.html +18 -18
  220. nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +18 -18
  221. nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +18 -18
  222. nautobot/project-static/docs/user-guide/administration/installation/app-install.html +18 -18
  223. nautobot/project-static/docs/user-guide/administration/installation/external-authentication.html +18 -18
  224. nautobot/project-static/docs/user-guide/administration/installation/http-server.html +69 -82
  225. nautobot/project-static/docs/user-guide/administration/installation/index.html +24 -24
  226. nautobot/project-static/docs/user-guide/administration/installation/install_system.html +60 -52
  227. nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +80 -87
  228. nautobot/project-static/docs/user-guide/administration/installation/services.html +37 -44
  229. nautobot/project-static/docs/user-guide/administration/installation-extras/docker.html +18 -18
  230. nautobot/project-static/docs/user-guide/administration/installation-extras/health-checks.html +18 -18
  231. nautobot/project-static/docs/user-guide/administration/installation-extras/selinux-troubleshooting.html +18 -18
  232. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-netbox.html +18 -18
  233. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +18 -18
  234. nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +76 -24
  235. nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +18 -18
  236. nautobot/project-static/docs/user-guide/administration/upgrading/database-backup.html +18 -18
  237. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/after-you-upgrade.html +18 -18
  238. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/before-you-upgrade.html +18 -18
  239. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/for-developers.html +18 -18
  240. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/index.html +18 -18
  241. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/whats-changed.html +18 -18
  242. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/region-and-site-data-migration-guide.html +18 -18
  243. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +18 -18
  244. nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +18 -18
  245. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuit.html +18 -18
  246. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittermination.html +18 -18
  247. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittype.html +18 -18
  248. nautobot/project-static/docs/user-guide/core-data-model/circuits/provider.html +18 -18
  249. nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +18 -18
  250. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloud.html +18 -18
  251. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudaccount.html +18 -18
  252. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetwork.html +18 -18
  253. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetworkprefixassignment.html +18 -18
  254. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudresourcetype.html +18 -18
  255. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservice.html +18 -18
  256. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservicenetworkassignment.html +18 -18
  257. nautobot/project-static/docs/user-guide/core-data-model/dcim/cable.html +18 -18
  258. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +18 -18
  259. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +18 -18
  260. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +18 -18
  261. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +18 -18
  262. nautobot/project-static/docs/user-guide/core-data-model/dcim/controller.html +18 -18
  263. nautobot/project-static/docs/user-guide/core-data-model/dcim/controllermanageddevicegroup.html +18 -18
  264. nautobot/project-static/docs/user-guide/core-data-model/dcim/device.html +19 -19
  265. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +18 -18
  266. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +18 -18
  267. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicefamily.html +18 -18
  268. nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +18 -18
  269. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +18 -18
  270. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +18 -18
  271. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +18 -18
  272. nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +18 -18
  273. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +18 -18
  274. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +18 -18
  275. nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +18 -18
  276. nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +18 -18
  277. nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +18 -18
  278. nautobot/project-static/docs/user-guide/core-data-model/dcim/manufacturer.html +19 -19
  279. nautobot/project-static/docs/user-guide/core-data-model/dcim/module.html +18 -18
  280. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebay.html +18 -18
  281. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebaytemplate.html +18 -18
  282. nautobot/project-static/docs/user-guide/core-data-model/dcim/moduletype.html +18 -18
  283. nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +18 -18
  284. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerfeed.html +18 -18
  285. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +18 -18
  286. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +18 -18
  287. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerpanel.html +18 -18
  288. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +18 -18
  289. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +18 -18
  290. nautobot/project-static/docs/user-guide/core-data-model/dcim/rack.html +18 -18
  291. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackgroup.html +18 -18
  292. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackreservation.html +18 -18
  293. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +18 -18
  294. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +18 -18
  295. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +18 -18
  296. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareversion.html +18 -18
  297. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualchassis.html +18 -18
  298. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +62 -18
  299. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +18 -18
  300. nautobot/project-static/docs/user-guide/core-data-model/extras/contact.html +18 -18
  301. nautobot/project-static/docs/user-guide/core-data-model/extras/team.html +18 -18
  302. nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +18 -18
  303. nautobot/project-static/docs/user-guide/core-data-model/ipam/namespace.html +18 -18
  304. nautobot/project-static/docs/user-guide/core-data-model/ipam/prefix.html +18 -18
  305. nautobot/project-static/docs/user-guide/core-data-model/ipam/rir.html +18 -18
  306. nautobot/project-static/docs/user-guide/core-data-model/ipam/routetarget.html +18 -18
  307. nautobot/project-static/docs/user-guide/core-data-model/ipam/service.html +18 -18
  308. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +18 -18
  309. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlangroup.html +18 -18
  310. nautobot/project-static/docs/user-guide/core-data-model/ipam/vrf.html +18 -18
  311. nautobot/project-static/docs/user-guide/core-data-model/overview/introduction.html +18 -18
  312. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenant.html +18 -18
  313. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenantgroup.html +18 -18
  314. nautobot/project-static/docs/user-guide/core-data-model/virtualization/cluster.html +18 -18
  315. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustergroup.html +18 -18
  316. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustertype.html +18 -18
  317. nautobot/project-static/docs/user-guide/core-data-model/virtualization/virtualmachine.html +18 -18
  318. nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +18 -18
  319. nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +18 -18
  320. nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +18 -18
  321. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +18 -18
  322. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +18 -18
  323. nautobot/project-static/docs/user-guide/feature-guides/getting-started/index.html +18 -18
  324. nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +18 -18
  325. nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +18 -18
  326. nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +18 -18
  327. nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +18 -18
  328. nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +18 -18
  329. nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +18 -18
  330. nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +18 -18
  331. nautobot/project-static/docs/user-guide/feature-guides/graphql.html +18 -18
  332. nautobot/project-static/docs/user-guide/feature-guides/ip-address-merge-tool.html +18 -18
  333. nautobot/project-static/docs/user-guide/feature-guides/relationships.html +18 -18
  334. nautobot/project-static/docs/user-guide/feature-guides/software-image-files-and-versions.html +18 -18
  335. nautobot/project-static/docs/user-guide/index.html +18 -18
  336. nautobot/project-static/docs/user-guide/platform-functionality/change-logging.html +18 -18
  337. nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +18 -18
  338. nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +18 -18
  339. nautobot/project-static/docs/user-guide/platform-functionality/customlink.html +18 -18
  340. nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +18 -18
  341. nautobot/project-static/docs/user-guide/platform-functionality/exporttemplate.html +18 -18
  342. nautobot/project-static/docs/user-guide/platform-functionality/externalintegration.html +18 -18
  343. nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +18 -18
  344. nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +18 -18
  345. nautobot/project-static/docs/user-guide/platform-functionality/graphqlquery.html +18 -18
  346. nautobot/project-static/docs/user-guide/platform-functionality/imageattachment.html +18 -18
  347. nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +18 -18
  348. nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +21 -21
  349. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +18 -18
  350. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +18 -18
  351. nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +18 -18
  352. nautobot/project-static/docs/user-guide/platform-functionality/napalm.html +36 -36
  353. nautobot/project-static/docs/user-guide/platform-functionality/note.html +33 -33
  354. nautobot/project-static/docs/user-guide/platform-functionality/{metadata.html → objectmetadata.html} +197 -84
  355. nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +21 -21
  356. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +18 -18
  357. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +18 -18
  358. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +18 -18
  359. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/ui-related-endpoints.html +18 -18
  360. nautobot/project-static/docs/user-guide/platform-functionality/role.html +18 -18
  361. nautobot/project-static/docs/user-guide/platform-functionality/savedview.html +18 -18
  362. nautobot/project-static/docs/user-guide/platform-functionality/secret.html +18 -18
  363. nautobot/project-static/docs/user-guide/platform-functionality/staticgroupassociation.html +18 -18
  364. nautobot/project-static/docs/user-guide/platform-functionality/status.html +18 -18
  365. nautobot/project-static/docs/user-guide/platform-functionality/tag.html +18 -18
  366. nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +18 -18
  367. nautobot/project-static/docs/user-guide/platform-functionality/users/objectpermission.html +18 -18
  368. nautobot/project-static/docs/user-guide/platform-functionality/users/token.html +18 -18
  369. nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +18 -18
  370. nautobot/tenancy/templates/tenancy/tenant.html +4 -4
  371. nautobot/virtualization/models.py +0 -2
  372. nautobot/virtualization/tables.py +2 -5
  373. {nautobot-2.3.0b1.dist-info → nautobot-2.3.1.dist-info}/METADATA +3 -3
  374. {nautobot-2.3.0b1.dist-info → nautobot-2.3.1.dist-info}/RECORD +378 -377
  375. nautobot/project-static/docs/assets/stylesheets/main.76a95c52.min.css +0 -1
  376. nautobot/project-static/docs/assets/stylesheets/main.76a95c52.min.css.map +0 -1
  377. {nautobot-2.3.0b1.dist-info → nautobot-2.3.1.dist-info}/LICENSE.txt +0 -0
  378. {nautobot-2.3.0b1.dist-info → nautobot-2.3.1.dist-info}/NOTICE +0 -0
  379. {nautobot-2.3.0b1.dist-info → nautobot-2.3.1.dist-info}/WHEEL +0 -0
  380. {nautobot-2.3.0b1.dist-info → nautobot-2.3.1.dist-info}/entry_points.txt +0 -0
@@ -656,9 +656,15 @@ def common_test_data(cls):
656
656
  device_redundancy_groups = iter(DeviceRedundancyGroup.objects.all())
657
657
 
658
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"),
659
+ ModuleType.objects.create(
660
+ manufacturer=cls.manufacturers[0], model="Filter Test Module Type 1", comments="Module Type 1"
661
+ ),
662
+ ModuleType.objects.create(
663
+ manufacturer=cls.manufacturers[1], model="Filter Test Module Type 2", comments="Module Type 2"
664
+ ),
665
+ ModuleType.objects.create(
666
+ manufacturer=cls.manufacturers[2], model="Filter Test Module Type 3", comments="Module Type 3"
667
+ ),
662
668
  )
663
669
 
664
670
  # Create 3 of each component template on the first two module types
@@ -898,7 +904,7 @@ class ModularDeviceComponentTestMixin(DeviceComponentTestMixin):
898
904
  )
899
905
  parent_module_bay = ModuleBay.objects.create(name="Parent module bay", position="1", parent_device=device)
900
906
  module_type = ModuleType.objects.create(
901
- manufacturer=manufacturer, model=f"Test Device Filter for {model} Module Type"
907
+ manufacturer=manufacturer, model=f"Test Device Filter for {model} Module Type", comments="Module Type test"
902
908
  )
903
909
  module = Module.objects.create(
904
910
  module_type=module_type, parent_module_bay=parent_module_bay, status=self.module_statuses[0]
@@ -2526,7 +2532,7 @@ class InterfaceTestCase(PathEndpointModelTestMixin, ModularDeviceComponentTestMi
2526
2532
  name="Parent module bay", position="1", parent_device=device_vc_master
2527
2533
  )
2528
2534
  module_type = ModuleType.objects.create(
2529
- manufacturer=manufacturer, model="Test Device Filter for Interface Module Type"
2535
+ manufacturer=manufacturer, model="Test Device Filter for Interface Module Type", comments="Module Type test"
2530
2536
  )
2531
2537
  module = Module.objects.create(
2532
2538
  module_type=module_type, parent_module_bay=parent_module_bay, status=self.module_statuses[0]
@@ -2772,7 +2778,7 @@ class FrontPortTestCase(ModularDeviceComponentTestMixin, FilterTestCases.FilterT
2772
2778
  )
2773
2779
  parent_module_bay = ModuleBay.objects.create(name="Parent module bay", position="1", parent_device=device)
2774
2780
  module_type = ModuleType.objects.create(
2775
- manufacturer=manufacturer, model="Test Device Filter for FrontPort Module Type"
2781
+ manufacturer=manufacturer, model="Test Device Filter for FrontPort Module Type", comments="Module Type test"
2776
2782
  )
2777
2783
  module = Module.objects.create(
2778
2784
  module_type=module_type, parent_module_bay=parent_module_bay, status=self.module_statuses[0]
@@ -3357,7 +3363,7 @@ class CableTestCase(FilterTestCases.FilterTestCase):
3357
3363
  )
3358
3364
  parent_module_bay = ModuleBay.objects.create(name="Parent module bay", position="1", parent_device=device)
3359
3365
  module_type = ModuleType.objects.create(
3360
- manufacturer=manufacturer, model="Test Device Filter for Cable Module Type"
3366
+ manufacturer=manufacturer, model="Test Device Filter for Cable Module Type", comments="Module Type test"
3361
3367
  )
3362
3368
  module = Module.objects.create(
3363
3369
  module_type=module_type, parent_module_bay=parent_module_bay, status=self.module_statuses[0]
@@ -4012,6 +4018,7 @@ class ModuleTypeTestCase(FilterTestCases.FilterTestCase):
4012
4018
  queryset = ModuleType.objects.all()
4013
4019
  filterset = ModuleTypeFilterSet
4014
4020
  generic_filter_tests = [
4021
+ ("comments",),
4015
4022
  ("manufacturer", "manufacturer__id"),
4016
4023
  ("manufacturer", "manufacturer__name"),
4017
4024
  ("model",),
@@ -12,6 +12,8 @@ from nautobot.dcim.models import (
12
12
  Manufacturer,
13
13
  Platform,
14
14
  Rack,
15
+ SoftwareImageFile,
16
+ SoftwareVersion,
15
17
  )
16
18
  from nautobot.extras.models import Role, SecretsGroup, Status
17
19
  from nautobot.ipam.models import VLAN
@@ -43,6 +45,15 @@ class DeviceTestCase(FormTestCases.BaseFormTestCase):
43
45
  cls.manufacturer = cls.device_type.manufacturer
44
46
  cls.platform = Platform.objects.filter(manufacturer=cls.device_type.manufacturer).first()
45
47
  cls.device_role = Role.objects.get_for_model(Device).first()
48
+ cls.software_version_contains_no_valid_image_for_device_type = SoftwareVersion.objects.create(
49
+ platform=cls.platform,
50
+ version="New version 1.0.0",
51
+ status=Status.objects.get_for_model(SoftwareVersion).first(),
52
+ )
53
+ cls.software_version = SoftwareVersion.objects.first()
54
+ cls.software_image_files = SoftwareImageFile.objects.exclude(software_version=cls.software_version).exclude(
55
+ default_image=True
56
+ )
46
57
 
47
58
  Device.objects.create(
48
59
  name="Device 1",
@@ -134,6 +145,49 @@ class DeviceTestCase(FormTestCases.BaseFormTestCase):
134
145
  self.assertFalse(form.is_valid())
135
146
  self.assertIn("face", form.errors)
136
147
 
148
+ def test_no_software_image_file_specified_is_valid(self):
149
+ form = DeviceForm(
150
+ data={
151
+ "name": "New Device",
152
+ "role": self.device_role.pk,
153
+ "tenant": None,
154
+ "manufacturer": self.manufacturer.pk,
155
+ "device_type": self.device_type.pk,
156
+ "location": self.location.pk,
157
+ "rack": None,
158
+ "face": None,
159
+ "position": None,
160
+ "platform": self.platform.pk,
161
+ "status": self.device_status.pk,
162
+ "secrets_group": SecretsGroup.objects.first().pk,
163
+ "software_version": self.software_version_contains_no_valid_image_for_device_type.pk,
164
+ "software_image_files": [],
165
+ }
166
+ )
167
+ self.assertTrue(form.is_valid())
168
+
169
+ def test_invalid_software_image_file_specified(self):
170
+ form = DeviceForm(
171
+ data={
172
+ "name": "New Device",
173
+ "role": self.device_role.pk,
174
+ "tenant": None,
175
+ "manufacturer": self.manufacturer.pk,
176
+ "device_type": self.device_type.pk,
177
+ "location": self.location.pk,
178
+ "rack": None,
179
+ "face": None,
180
+ "position": None,
181
+ "platform": self.platform.pk,
182
+ "status": self.device_status.pk,
183
+ "secrets_group": SecretsGroup.objects.first().pk,
184
+ "software_version": self.software_version.pk,
185
+ "software_image_files": list(self.software_image_files.values_list("pk", flat=True)),
186
+ }
187
+ )
188
+ self.assertFalse(form.is_valid())
189
+ self.assertIn("software_image_files", form.errors)
190
+
137
191
  def test_non_racked_device_with_position(self):
138
192
  form = DeviceForm(
139
193
  data={
@@ -1723,11 +1723,19 @@ class DeviceTestCase(ModelTestCases.BaseModelTestCase):
1723
1723
  software_version.software_image_files.all().update(default_image=False)
1724
1724
  self.device_type.software_image_files.set([])
1725
1725
  self.device.software_version = software_version
1726
+ invalid_software_image_file = SoftwareImageFile.objects.filter(default_image=False).first()
1727
+ invalid_software_image_file.device_types.set([])
1728
+ self.device.software_image_files.set([invalid_software_image_file])
1726
1729
 
1727
- # No device type or default image match
1730
+ # There is an invalid non-default software_image_file specified for the software version
1731
+ # It is not a default image and it does not match any device type
1728
1732
  with self.assertRaises(ValidationError):
1729
1733
  self.device.validated_save()
1730
1734
 
1735
+ # user should be able to specify any software version without specifying software_image_files
1736
+ self.device.software_image_files.set([])
1737
+ self.device.validated_save()
1738
+
1731
1739
  # Default image matches
1732
1740
  software_image_file = software_version.software_image_files.first()
1733
1741
  software_image_file.default_image = True
@@ -1903,6 +1911,37 @@ class DeviceTestCase(ModelTestCases.BaseModelTestCase):
1903
1911
  self.assertNotEqual(child_mtime_after_parent_rack_update_save, child_mtime_after_parent_site_update_save)
1904
1912
 
1905
1913
 
1914
+ class DeviceBayTestCase(ModelTestCases.BaseModelTestCase):
1915
+ model = DeviceBay
1916
+
1917
+ def setUp(self):
1918
+ self.devices = Device.objects.filter(device_type__subdevice_role=SubdeviceRoleChoices.ROLE_PARENT)
1919
+ devicetype = DeviceType.objects.create(
1920
+ manufacturer=self.devices[0].device_type.manufacturer,
1921
+ model="TestDeviceType1",
1922
+ u_height=0,
1923
+ subdevice_role=SubdeviceRoleChoices.ROLE_CHILD,
1924
+ )
1925
+ child_device = Device.objects.create(
1926
+ device_type=devicetype,
1927
+ role=self.devices[0].role,
1928
+ name="TestDevice1",
1929
+ status=self.devices[0].status,
1930
+ location=self.devices[0].location,
1931
+ )
1932
+ DeviceBay.objects.create(device=self.devices[0], name="Device Bay 1", installed_device=child_device)
1933
+
1934
+ def test_assigning_installed_device(self):
1935
+ server = Device.objects.exclude(device_type__subdevice_role=SubdeviceRoleChoices.ROLE_CHILD).last()
1936
+ bay = DeviceBay(device=self.devices[1], name="Device Bay Err", installed_device=server)
1937
+ with self.assertRaises(ValidationError) as err:
1938
+ bay.validated_save()
1939
+ self.assertIn(
1940
+ f'Cannot install device "{server}"; device-type "{server.device_type}" subdevice_role is not "child".',
1941
+ str(err.exception),
1942
+ )
1943
+
1944
+
1906
1945
  class DeviceTypeToSoftwareImageFileTestCase(ModelTestCases.BaseModelTestCase):
1907
1946
  model = DeviceTypeToSoftwareImageFile
1908
1947
 
@@ -817,6 +817,7 @@ class DeviceTypeTestCase(
817
817
  cls.bulk_edit_data = {
818
818
  "u_height": 0,
819
819
  "is_full_depth": False,
820
+ "comments": "changed comment",
820
821
  }
821
822
 
822
823
  def test_list_has_correct_links(self):
@@ -1196,6 +1197,7 @@ class ModuleTypeTestCase(
1196
1197
  ModuleType.objects.create(
1197
1198
  model="Test Module Type 1",
1198
1199
  manufacturer=manufacturers[0],
1200
+ comments="test comment",
1199
1201
  )
1200
1202
  ModuleType.objects.create(
1201
1203
  model="Test Module Type 2",
@@ -1215,10 +1217,12 @@ class ModuleTypeTestCase(
1215
1217
  "model": "Test Module Type X",
1216
1218
  "part_number": "123ABC",
1217
1219
  "tags": [t.pk for t in Tag.objects.get_for_model(ModuleType)],
1220
+ "comments": "test comment",
1218
1221
  }
1219
1222
 
1220
1223
  cls.bulk_edit_data = {
1221
1224
  "manufacturer": manufacturers[1].pk,
1225
+ "comments": "changed comment",
1222
1226
  }
1223
1227
 
1224
1228
  def test_list_has_correct_links(self):
@@ -2080,7 +2084,7 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
2080
2084
  status_active = statuses[0]
2081
2085
 
2082
2086
  # We want unique sets of software image files for each device type
2083
- software_image_files = list(SoftwareImageFile.objects.all()[:4])
2087
+ software_image_files = list(SoftwareImageFile.objects.filter(default_image=False)[:4])
2084
2088
  software_versions = list(SoftwareVersion.objects.filter(software_image_files__isnull=False)[:2])
2085
2089
  software_image_files[0].software_version = software_versions[0]
2086
2090
  software_image_files[1].software_version = software_versions[0]
@@ -2090,6 +2094,10 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
2090
2094
  software_image_file.save()
2091
2095
  devicetypes[0].software_image_files.set(software_image_files[:2])
2092
2096
  devicetypes[1].software_image_files.set(software_image_files[2:])
2097
+ # Only valid software image files are those that belong to the device type or default images
2098
+ valid_software_image_files = software_image_files[2:] + [
2099
+ SoftwareImageFile.objects.filter(default_image=True).first()
2100
+ ]
2093
2101
 
2094
2102
  cls.custom_fields = (
2095
2103
  CustomField.objects.create(
@@ -2201,7 +2209,7 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
2201
2209
  "cf_crash_counter": -1,
2202
2210
  "cr_router-id": None,
2203
2211
  "software_version": software_versions[1].pk,
2204
- "software_image_files": [f.pk for f in software_versions[0].software_image_files.all()],
2212
+ "software_image_files": [f.pk for f in valid_software_image_files],
2205
2213
  }
2206
2214
 
2207
2215
  cls.bulk_edit_data = {
@@ -2359,6 +2367,19 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
2359
2367
  sorted(interface_ips),
2360
2368
  )
2361
2369
 
2370
+ with self.subTest("Assert Assigning IPAddress Without Selecting Any IPAddress Raises Exception"):
2371
+ assign_ip_form_data["pk"] = []
2372
+ assign_ip_request = {
2373
+ "path": reverse("ipam:ipaddress_assign")
2374
+ + f"?interface={self.interfaces[1].id}&return_url={device_list_url}",
2375
+ "data": post_data(assign_ip_form_data),
2376
+ }
2377
+ response = self.client.post(**assign_ip_request, follow=True)
2378
+ self.assertHttpStatus(response, 200)
2379
+ self.assertIn(
2380
+ "Please select at least one IP Address from the table.", response.content.decode(response.charset)
2381
+ )
2382
+
2362
2383
  @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
2363
2384
  def test_device_rearports(self):
2364
2385
  device = Device.objects.first()
@@ -4172,6 +4193,28 @@ class VirtualChassisTestCase(ViewTestCases.PrimaryObjectViewTestCase):
4172
4193
  "domain": "domain-x",
4173
4194
  }
4174
4195
 
4196
+ def test_device_interfaces_count_correct(self):
4197
+ """
4198
+ This checks whether the other memebers' interfaces are included in the
4199
+ interfaces tab of the master device and whether the interface count on the tab header is
4200
+ rendered correctly.
4201
+ """
4202
+ self.user.is_superuser = True
4203
+ self.user.save()
4204
+ interface_status = Status.objects.get_for_model(Interface).first()
4205
+ Interface.objects.create(device=self.devices[0], name="eth0", status=interface_status)
4206
+ Interface.objects.create(device=self.devices[0], name="eth1", status=interface_status)
4207
+ Interface.objects.create(device=self.devices[1], name="device 1 interface 1", status=interface_status)
4208
+ Interface.objects.create(device=self.devices[1], name="device 1 interface 2", status=interface_status)
4209
+ Interface.objects.create(device=self.devices[2], name="device 2 interface 1", status=interface_status)
4210
+ Interface.objects.create(device=self.devices[2], name="device 2 interface 2", status=interface_status)
4211
+ response = self.client.get(reverse("dcim:device_interfaces", kwargs={"pk": self.devices[0].pk}))
4212
+ self.assertIn('Interfaces <span class="badge">6</span>', str(response.content))
4213
+ self.assertIn("device 1 interface 1", str(response.content))
4214
+ self.assertIn("device 1 interface 2", str(response.content))
4215
+ self.assertIn("device 2 interface 1", str(response.content))
4216
+ self.assertIn("device 2 interface 2", str(response.content))
4217
+
4175
4218
  def test_device_column_visible(self):
4176
4219
  """
4177
4220
  This checks whether the device column on a device's interfaces
nautobot/dcim/views.py CHANGED
@@ -1923,7 +1923,7 @@ class DeviceInterfacesView(DeviceComponentTabView):
1923
1923
 
1924
1924
  def get_extra_context(self, request, instance):
1925
1925
  interfaces = (
1926
- instance.all_interfaces.restrict(request.user, "view")
1926
+ instance.vc_interfaces.restrict(request.user, "view")
1927
1927
  .prefetch_related(
1928
1928
  Prefetch("ip_addresses", queryset=IPAddress.objects.restrict(request.user)),
1929
1929
  Prefetch("member_interfaces", queryset=Interface.objects.restrict(request.user)),
@@ -4049,6 +4049,9 @@ class DeviceRedundancyGroupUIViewSet(NautobotUIViewSet):
4049
4049
  devices_table = tables.DeviceTable(devices)
4050
4050
  devices_table.columns.show("device_redundancy_group_priority")
4051
4051
  context["devices_table"] = devices_table
4052
+ controllers = instance.controllers_sorted.restrict(request.user)
4053
+ controllers_table = tables.ControllerTable(controllers)
4054
+ context["controllers_table"] = controllers_table
4052
4055
  return context
4053
4056
 
4054
4057
 
@@ -228,7 +228,6 @@ class ContactAssociationSerializer(NautobotModelSerializer):
228
228
  fields = "__all__"
229
229
  validators = []
230
230
  extra_kwargs = {
231
- "role": {"required": False},
232
231
  "contact": {"required": False},
233
232
  "team": {"required": False},
234
233
  }
@@ -1,5 +1,3 @@
1
- from datetime import timedelta
2
-
3
1
  from django.conf import settings
4
2
  from django.contrib.contenttypes.models import ContentType
5
3
  from django.forms import ValidationError as FormsValidationError
@@ -492,59 +490,6 @@ class ImageAttachmentViewSet(ModelViewSet):
492
490
  #
493
491
 
494
492
 
495
- def _create_schedule(serializer, data, job_model, user, approval_required, task_queue=None):
496
- """
497
- This is an internal function to create a scheduled job from API data.
498
- It has to handle both once-offs (i.e. of type TYPE_FUTURE) and interval
499
- jobs.
500
- """
501
- type_ = serializer["interval"]
502
- if type_ == JobExecutionType.TYPE_IMMEDIATELY:
503
- time = timezone.now()
504
- name = serializer.get("name") or f"{job_model.name} - {time}"
505
- elif type_ == JobExecutionType.TYPE_CUSTOM:
506
- time = serializer.get("start_time") # doing .get("key", "default") returns None instead of "default"
507
- if time is None:
508
- # "start_time" is checked against models.ScheduledJob.earliest_possible_time()
509
- # which returns timezone.now() + timedelta(seconds=15)
510
- time = timezone.now() + timedelta(seconds=20)
511
- name = serializer["name"]
512
- else:
513
- time = serializer["start_time"]
514
- name = serializer["name"]
515
- crontab = serializer.get("crontab", "")
516
-
517
- celery_kwargs = {
518
- "nautobot_job_profile": False,
519
- "queue": task_queue,
520
- }
521
-
522
- # 2.0 TODO: To revisit this as part of a larger Jobs cleanup in 2.0.
523
- #
524
- # We pass in task and job_model here partly for forward/backward compatibility logic, and
525
- # part fallback safety. It's mildly useful to store both the task module/class name and the JobModel
526
- # FK on the ScheduledJob, as in the case where the JobModel gets deleted (and the FK becomes
527
- # null) you still have a bit of context on the ScheduledJob as to what it was originally
528
- # scheduled for.
529
- scheduled_job = ScheduledJob(
530
- name=name,
531
- task=job_model.class_path,
532
- job_model=job_model,
533
- start_time=time,
534
- description=f"Nautobot job {name} scheduled by {user} for {time}",
535
- kwargs=data,
536
- celery_kwargs=celery_kwargs,
537
- interval=type_,
538
- one_off=(type_ == JobExecutionType.TYPE_FUTURE),
539
- user=user,
540
- approval_required=approval_required,
541
- crontab=crontab,
542
- queue=task_queue,
543
- )
544
- scheduled_job.validated_save()
545
- return scheduled_job
546
-
547
-
548
493
  class JobViewSetBase(
549
494
  NautobotAPIVersionMixin,
550
495
  # note no CreateModelMixin
@@ -744,13 +689,16 @@ class JobViewSetBase(
744
689
 
745
690
  # Try to create a ScheduledJob, or...
746
691
  if schedule_data:
747
- schedule = _create_schedule(
748
- schedule_data,
749
- job_class.serialize_data(cleaned_data),
692
+ schedule = ScheduledJob.create_schedule(
750
693
  job_model,
751
694
  request.user,
752
- approval_required,
695
+ name=schedule_data.get("name"),
696
+ start_time=schedule_data.get("start_time"),
697
+ interval=schedule_data.get("interval"),
698
+ crontab=schedule_data.get("crontab", ""),
699
+ approval_required=approval_required,
753
700
  task_queue=input_serializer.validated_data.get("task_queue", None),
701
+ **job_class.serialize_data(cleaned_data),
754
702
  )
755
703
  else:
756
704
  schedule = None
@@ -251,6 +251,24 @@ class MetadataTypeFactory(PrimaryModelFactory):
251
251
  )
252
252
 
253
253
 
254
+ def _available_field_names(metadata_type, assigned_object):
255
+ field_names = [field.name for field in assigned_object._meta.get_fields()]
256
+ # Avoid collisions, see ObjectMetadata.clean()
257
+ existing_metadata_scoped_fields = ObjectMetadata.objects.filter(
258
+ metadata_type=metadata_type,
259
+ assigned_object_type=ContentType.objects.get_for_model(assigned_object),
260
+ assigned_object_id=assigned_object.pk,
261
+ ).values_list("scoped_fields", flat=True)
262
+ for existing_scoped_fields in existing_metadata_scoped_fields:
263
+ if existing_scoped_fields:
264
+ field_names = sorted(set(field_names).difference(existing_scoped_fields))
265
+ else:
266
+ field_names = []
267
+ break
268
+
269
+ return field_names
270
+
271
+
254
272
  class ObjectMetadataFactory(BaseModelFactory):
255
273
  """ObjectMetadata model factory"""
256
274
 
@@ -263,7 +281,6 @@ class ObjectMetadataFactory(BaseModelFactory):
263
281
  MetadataType.objects.all(),
264
282
  allow_null=False,
265
283
  )
266
- scoped_fields = factory.Faker("pylist", allowed_types=[str])
267
284
 
268
285
  @factory.lazy_attribute
269
286
  def contact(self):
@@ -308,22 +325,43 @@ class ObjectMetadataFactory(BaseModelFactory):
308
325
  raise RuntimeError(f"Unsupported metadatatype datatype {metadata_type_data_type}")
309
326
 
310
327
  @factory.lazy_attribute
311
- def assigned_object_type(self):
312
- while True:
313
- allowed_content_types = list(self.metadata_type.content_types.values_list("pk", flat=True))
314
- content_type = factory.random.randgen.choice(
315
- ContentType.objects.filter(FeatureQuery("metadata").get_query(), pk__in=allowed_content_types)
316
- )
328
+ def assigned_object(self):
329
+ allowed_content_types = list(self.metadata_type.content_types.all())
330
+ for content_type in factory.random.randgen.sample(allowed_content_types, len(allowed_content_types)):
317
331
  # It does not have a get_absolute_url attribute and is causing failure in API unittests
318
332
  if content_type.app_label == "extras" and content_type.model == "taggeditem":
319
333
  continue
320
- if content_type.model_class().objects.exists():
321
- return content_type
334
+
335
+ assigned_model = content_type.model_class()
336
+ queryset = assigned_model.objects.all()
337
+
338
+ if not queryset.exists():
339
+ continue
340
+
341
+ for _ in range(10):
342
+ assigned_object = factory.random.randgen.choice(queryset)
343
+ if _available_field_names(self.metadata_type, assigned_object):
344
+ return assigned_object
345
+
346
+ raise RuntimeError(f"Couldn't find any suitable instances not already covered by {self.metadata_type}")
322
347
 
323
348
  @factory.lazy_attribute
324
- def assigned_object_id(self):
325
- queryset = self.assigned_object_type.model_class().objects.all()
326
- return factory.random.randgen.choice(queryset).pk
349
+ def scoped_fields(self):
350
+ all_field_names = [field.name for field in self.assigned_object._meta.get_fields()]
351
+ field_names = _available_field_names(self.metadata_type, self.assigned_object)
352
+ if not field_names:
353
+ raise RuntimeError(
354
+ f"All existing scoped_fields for {self.metadata_type} are covered by existing ObjectMetadata for {self.assigned_object}"
355
+ )
356
+
357
+ if len(field_names) < len(all_field_names):
358
+ minimum_fields = 1 # don't allow an empty list since that would cover all fields
359
+ else:
360
+ minimum_fields = 0
361
+
362
+ return factory.random.randgen.sample(
363
+ field_names, k=factory.random.randgen.randint(minimum_fields, len(field_names))
364
+ )
327
365
 
328
366
 
329
367
  class ObjectChangeFactory(BaseModelFactory):
@@ -181,6 +181,7 @@ class ComputedFieldFilterSet(BaseFilterSet):
181
181
  "description": "icontains",
182
182
  "content_type__app_label": "icontains",
183
183
  "content_type__model": "icontains",
184
+ "grouping": "icontains",
184
185
  "template": "icontains",
185
186
  "fallback_value": "icontains",
186
187
  },
@@ -192,6 +193,7 @@ class ComputedFieldFilterSet(BaseFilterSet):
192
193
  fields = (
193
194
  "content_type",
194
195
  "key",
196
+ "grouping",
195
197
  "template",
196
198
  "fallback_value",
197
199
  "weight",
@@ -422,6 +424,7 @@ class CustomFieldFilterSet(BaseFilterSet):
422
424
  filter_predicates={
423
425
  "label": "icontains",
424
426
  "description": "icontains",
427
+ "grouping": "icontains",
425
428
  },
426
429
  )
427
430
  content_types = ContentTypeMultipleChoiceFilter(
@@ -430,7 +433,7 @@ class CustomFieldFilterSet(BaseFilterSet):
430
433
 
431
434
  class Meta:
432
435
  model = CustomField
433
- fields = ["id", "content_types", "label", "required", "filter_logic", "weight"]
436
+ fields = ["id", "content_types", "label", "grouping", "required", "filter_logic", "weight"]
434
437
 
435
438
 
436
439
  class CustomFieldChoiceFilterSet(BaseFilterSet):
@@ -26,11 +26,12 @@ __all__ = (
26
26
 
27
27
 
28
28
  class NautobotModelForm(
29
+ BootstrapMixin,
30
+ # The below must be listed *after* BootstrapMixin so that BootstrapMixin applies to their dynamic form fields
29
31
  CustomFieldModelFormMixin,
30
32
  DynamicGroupModelFormMixin,
31
33
  NoteModelFormMixin,
32
34
  RelationshipModelFormMixin,
33
- BootstrapMixin,
34
35
  ):
35
36
  """
36
37
  This class exists to combine common functionality and is used to inherit from throughout the
@@ -40,9 +41,10 @@ class NautobotModelForm(
40
41
 
41
42
 
42
43
  class NautobotFilterForm(
43
- ContactTeamModelFilterFormMixin,
44
44
  BootstrapMixin,
45
- CustomFieldModelFilterFormMixin, # currently must come *after* BootstrapMixin to get proper CSS classes applied
45
+ # The below must be listed *after* BootstrapMixin so that BootstrapMixin applies to their dynamic form fields
46
+ ContactTeamModelFilterFormMixin,
47
+ CustomFieldModelFilterFormMixin,
46
48
  RelationshipModelFilterFormMixin,
47
49
  ):
48
50
  """
@@ -53,6 +55,10 @@ class NautobotFilterForm(
53
55
 
54
56
 
55
57
  class NautobotBulkEditForm(
56
- BootstrapMixin, CustomFieldModelBulkEditFormMixin, RelationshipModelBulkEditFormMixin, NoteModelBulkEditFormMixin
58
+ BootstrapMixin,
59
+ # The below must be listed *after* BootstrapMixin so that BootstrapMixin applies to their dynamic form fields
60
+ CustomFieldModelBulkEditFormMixin,
61
+ NoteModelBulkEditFormMixin,
62
+ RelationshipModelBulkEditFormMixin,
57
63
  ):
58
64
  """Base class for bulk-edit forms for models that support relationships, custom fields and notes."""
@@ -219,6 +219,7 @@ class ComputedFieldForm(BootstrapMixin, forms.ModelForm):
219
219
  fields = (
220
220
  "content_type",
221
221
  "label",
222
+ "grouping",
222
223
  "key",
223
224
  "description",
224
225
  "template",
@@ -7,14 +7,24 @@ def get_job_results(request):
7
7
  """Callback function to collect job history for panel."""
8
8
  return (
9
9
  JobResult.objects.filter(status__in=JobResultStatusChoices.READY_STATES)
10
- .defer("result")
10
+ .restrict(request.user, "view")
11
+ .only("id", "name", "status", "date_done", "user")
11
12
  .order_by("-date_done")[:10]
12
13
  )
13
14
 
14
15
 
15
16
  def get_changelog(request):
16
17
  """Callback function to collect changelog for panel."""
17
- return ObjectChange.objects.restrict(request.user, "view")[:15]
18
+ return ObjectChange.objects.restrict(request.user, "view").only(
19
+ "id",
20
+ "action",
21
+ "changed_object",
22
+ "changed_object_id",
23
+ "changed_object_type",
24
+ "object_repr",
25
+ "user_name",
26
+ "time",
27
+ )[:15]
18
28
 
19
29
 
20
30
  layout = (
nautobot/extras/jobs.py CHANGED
@@ -20,7 +20,7 @@ from django.conf import settings
20
20
  from django.contrib.auth import get_user_model
21
21
  from django.core.exceptions import ObjectDoesNotExist
22
22
  from django.core.files.base import ContentFile
23
- from django.core.files.uploadedfile import InMemoryUploadedFile
23
+ from django.core.files.uploadedfile import UploadedFile
24
24
  from django.core.validators import RegexValidator
25
25
  from django.db.models import Model
26
26
  from django.db.models.query import QuerySet
@@ -539,7 +539,7 @@ class BaseJob:
539
539
  elif isinstance(value, Model):
540
540
  return_data[field_name] = value.pk
541
541
  # FileVar (Save each FileVar as a FileProxy)
542
- elif isinstance(value, InMemoryUploadedFile):
542
+ elif isinstance(value, UploadedFile):
543
543
  return_data[field_name] = BaseJob._save_file_to_proxy(value)
544
544
  # IPAddressVar, IPAddressWithMaskVar, IPNetworkVar
545
545
  elif isinstance(value, netaddr.ip.BaseIP):