nautobot 2.2.7__py3-none-any.whl → 2.2.9__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 (306) hide show
  1. nautobot/core/filters.py +15 -1
  2. nautobot/core/graphql/generators.py +4 -4
  3. nautobot/core/graphql/schema.py +9 -7
  4. nautobot/core/jobs/__init__.py +4 -1
  5. nautobot/core/settings.py +13 -2
  6. nautobot/core/settings.yaml +14 -0
  7. nautobot/core/templates/nautobot_config.py.j2 +15 -0
  8. nautobot/core/tests/integration/test_general_functionality.py +36 -0
  9. nautobot/core/tests/test_graphql.py +51 -5
  10. nautobot/core/tests/test_jobs.py +82 -2
  11. nautobot/core/tests/test_templatetags_netutils.py +3 -3
  12. nautobot/dcim/models/device_components.py +7 -0
  13. nautobot/dcim/models/devices.py +14 -3
  14. nautobot/dcim/tables/devices.py +19 -4
  15. nautobot/dcim/templates/dcim/deviceredundancygroup_retrieve.html +6 -0
  16. nautobot/dcim/tests/test_models.py +106 -0
  17. nautobot/dcim/tests/test_views.py +13 -0
  18. nautobot/dcim/views.py +8 -2
  19. nautobot/extras/api/views.py +7 -59
  20. nautobot/extras/homepage.py +12 -2
  21. nautobot/extras/jobs.py +2 -2
  22. nautobot/extras/models/jobs.py +81 -0
  23. nautobot/extras/models/relationships.py +12 -0
  24. nautobot/extras/signals.py +14 -1
  25. nautobot/extras/tables.py +36 -5
  26. nautobot/extras/templates/extras/job_detail.html +11 -0
  27. nautobot/extras/tests/integration/test_dynamicgroups.py +1 -1
  28. nautobot/extras/tests/test_relationships.py +221 -1
  29. nautobot/extras/tests/test_views.py +21 -0
  30. nautobot/extras/utils.py +34 -5
  31. nautobot/extras/views.py +20 -46
  32. nautobot/ipam/models.py +9 -12
  33. nautobot/ipam/tests/test_models.py +3 -2
  34. nautobot/ipam/views.py +2 -8
  35. nautobot/project-static/css/base.css +1 -0
  36. nautobot/project-static/docs/404.html +4 -4
  37. nautobot/project-static/docs/apps/index.html +4 -4
  38. nautobot/project-static/docs/apps/nautobot-apps.html +4 -4
  39. nautobot/project-static/docs/assets/stylesheets/{main.6543a935.min.css → main.76a95c52.min.css} +1 -1
  40. nautobot/project-static/docs/assets/stylesheets/{main.6543a935.min.css.map → main.76a95c52.min.css.map} +1 -1
  41. nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +4 -4
  42. nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +4 -4
  43. nautobot/project-static/docs/code-reference/nautobot/apps/api.html +4 -4
  44. nautobot/project-static/docs/code-reference/nautobot/apps/change_logging.html +4 -4
  45. nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +4 -4
  46. nautobot/project-static/docs/code-reference/nautobot/apps/config.html +4 -4
  47. nautobot/project-static/docs/code-reference/nautobot/apps/constants.html +4 -4
  48. nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +4 -4
  49. nautobot/project-static/docs/code-reference/nautobot/apps/exceptions.html +4 -4
  50. nautobot/project-static/docs/code-reference/nautobot/apps/factory.html +4 -4
  51. nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +4 -4
  52. nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +4 -4
  53. nautobot/project-static/docs/code-reference/nautobot/apps/graphql.html +4 -4
  54. nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +4 -4
  55. nautobot/project-static/docs/code-reference/nautobot/apps/models.html +4 -4
  56. nautobot/project-static/docs/code-reference/nautobot/apps/querysets.html +4 -4
  57. nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +4 -4
  58. nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +4 -4
  59. nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +4 -4
  60. nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +4 -4
  61. nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +4 -4
  62. nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +6 -4
  63. nautobot/project-static/docs/code-reference/nautobot/apps/views.html +4 -4
  64. nautobot/project-static/docs/development/apps/api/configuration-view.html +4 -4
  65. nautobot/project-static/docs/development/apps/api/database-backend-config.html +4 -4
  66. nautobot/project-static/docs/development/apps/api/models/django-admin.html +4 -4
  67. nautobot/project-static/docs/development/apps/api/models/global-search.html +4 -4
  68. nautobot/project-static/docs/development/apps/api/models/graphql.html +4 -4
  69. nautobot/project-static/docs/development/apps/api/models/index.html +4 -4
  70. nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +4 -4
  71. nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +4 -4
  72. nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +4 -4
  73. nautobot/project-static/docs/development/apps/api/platform-features/git-repository-content.html +4 -4
  74. nautobot/project-static/docs/development/apps/api/platform-features/index.html +4 -4
  75. nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +4 -4
  76. nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +4 -4
  77. nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +4 -4
  78. nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +4 -4
  79. nautobot/project-static/docs/development/apps/api/platform-features/uniquely-identify-objects.html +4 -4
  80. nautobot/project-static/docs/development/apps/api/prometheus.html +4 -4
  81. nautobot/project-static/docs/development/apps/api/setup.html +4 -4
  82. nautobot/project-static/docs/development/apps/api/testing.html +4 -4
  83. nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +4 -4
  84. nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +4 -4
  85. nautobot/project-static/docs/development/apps/api/ui-extensions/index.html +4 -4
  86. nautobot/project-static/docs/development/apps/api/ui-extensions/navigation.html +4 -4
  87. nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +4 -4
  88. nautobot/project-static/docs/development/apps/api/views/base-template.html +4 -4
  89. nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +4 -4
  90. nautobot/project-static/docs/development/apps/api/views/django-generic-views.html +4 -4
  91. nautobot/project-static/docs/development/apps/api/views/help-documentation.html +4 -4
  92. nautobot/project-static/docs/development/apps/api/views/index.html +4 -4
  93. nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +4 -4
  94. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +4 -4
  95. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +4 -4
  96. nautobot/project-static/docs/development/apps/api/views/notes.html +4 -4
  97. nautobot/project-static/docs/development/apps/api/views/rest-api.html +4 -4
  98. nautobot/project-static/docs/development/apps/api/views/urls.html +4 -4
  99. nautobot/project-static/docs/development/apps/index.html +4 -4
  100. nautobot/project-static/docs/development/apps/migration/code-updates.html +4 -4
  101. nautobot/project-static/docs/development/apps/migration/dependency-updates.html +4 -4
  102. nautobot/project-static/docs/development/apps/migration/from-v1.html +4 -4
  103. nautobot/project-static/docs/development/apps/migration/model-updates/dcim.html +4 -4
  104. nautobot/project-static/docs/development/apps/migration/model-updates/extras.html +4 -4
  105. nautobot/project-static/docs/development/apps/migration/model-updates/global.html +4 -4
  106. nautobot/project-static/docs/development/apps/migration/model-updates/ipam.html +4 -4
  107. nautobot/project-static/docs/development/apps/porting-from-netbox.html +4 -4
  108. nautobot/project-static/docs/development/core/application-registry.html +4 -4
  109. nautobot/project-static/docs/development/core/best-practices.html +4 -4
  110. nautobot/project-static/docs/development/core/bootstrap-ui.html +4 -4
  111. nautobot/project-static/docs/development/core/caching.html +4 -4
  112. nautobot/project-static/docs/development/core/controllers.html +4 -4
  113. nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +4 -4
  114. nautobot/project-static/docs/development/core/generic-views.html +4 -4
  115. nautobot/project-static/docs/development/core/getting-started.html +4 -4
  116. nautobot/project-static/docs/development/core/homepage.html +4 -4
  117. nautobot/project-static/docs/development/core/index.html +15 -4
  118. nautobot/project-static/docs/development/core/model-checklist.html +4 -4
  119. nautobot/project-static/docs/development/core/model-features.html +4 -4
  120. nautobot/project-static/docs/development/core/natural-keys.html +4 -4
  121. nautobot/project-static/docs/development/core/navigation-menu.html +4 -4
  122. nautobot/project-static/docs/development/core/release-checklist.html +4 -4
  123. nautobot/project-static/docs/development/core/role-internals.html +4 -4
  124. nautobot/project-static/docs/development/core/settings.html +4 -4
  125. nautobot/project-static/docs/development/core/style-guide.html +4 -4
  126. nautobot/project-static/docs/development/core/templates.html +4 -4
  127. nautobot/project-static/docs/development/core/testing.html +4 -4
  128. nautobot/project-static/docs/development/core/user-preferences.html +4 -4
  129. nautobot/project-static/docs/development/index.html +4 -4
  130. nautobot/project-static/docs/development/jobs/index.html +379 -365
  131. nautobot/project-static/docs/development/jobs/migration/from-v1.html +4 -4
  132. nautobot/project-static/docs/index.html +8228 -13
  133. nautobot/project-static/docs/overview/application_stack.html +4 -4
  134. nautobot/project-static/docs/overview/design_philosophy.html +6 -6
  135. nautobot/project-static/docs/overview/index.html +13 -8228
  136. nautobot/project-static/docs/release-notes/index.html +4 -4
  137. nautobot/project-static/docs/release-notes/version-1.0.html +4 -4
  138. nautobot/project-static/docs/release-notes/version-1.1.html +4 -4
  139. nautobot/project-static/docs/release-notes/version-1.2.html +4 -4
  140. nautobot/project-static/docs/release-notes/version-1.3.html +4 -4
  141. nautobot/project-static/docs/release-notes/version-1.4.html +4 -4
  142. nautobot/project-static/docs/release-notes/version-1.5.html +4 -4
  143. nautobot/project-static/docs/release-notes/version-1.6.html +4 -4
  144. nautobot/project-static/docs/release-notes/version-2.0.html +4 -4
  145. nautobot/project-static/docs/release-notes/version-2.1.html +4 -4
  146. nautobot/project-static/docs/release-notes/version-2.2.html +419 -136
  147. nautobot/project-static/docs/requirements.txt +2 -1
  148. nautobot/project-static/docs/search/search_index.json +1 -1
  149. nautobot/project-static/docs/sitemap.xml +260 -260
  150. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  151. nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +4 -4
  152. nautobot/project-static/docs/user-guide/administration/configuration/authentication/remote.html +4 -4
  153. nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +4 -4
  154. nautobot/project-static/docs/user-guide/administration/configuration/index.html +4 -4
  155. nautobot/project-static/docs/user-guide/administration/configuration/optional-settings.html +36 -4
  156. nautobot/project-static/docs/user-guide/administration/configuration/required-settings.html +4 -4
  157. nautobot/project-static/docs/user-guide/administration/configuration/time-zones.html +4 -4
  158. nautobot/project-static/docs/user-guide/administration/guides/caching.html +4 -4
  159. nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +8 -4
  160. nautobot/project-static/docs/user-guide/administration/guides/healthcheck.html +4 -4
  161. nautobot/project-static/docs/user-guide/administration/guides/permissions.html +4 -4
  162. nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +4 -4
  163. nautobot/project-static/docs/user-guide/administration/guides/replicating-nautobot.html +4 -4
  164. nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +4 -4
  165. nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +4 -4
  166. nautobot/project-static/docs/user-guide/administration/installation/app-install.html +4 -4
  167. nautobot/project-static/docs/user-guide/administration/installation/external-authentication.html +4 -4
  168. nautobot/project-static/docs/user-guide/administration/installation/http-server.html +8 -8
  169. nautobot/project-static/docs/user-guide/administration/installation/index.html +4 -4
  170. nautobot/project-static/docs/user-guide/administration/installation/install_system.html +9 -5
  171. nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +4 -4
  172. nautobot/project-static/docs/user-guide/administration/installation/services.html +4 -4
  173. nautobot/project-static/docs/user-guide/administration/installation-extras/docker.html +4 -4
  174. nautobot/project-static/docs/user-guide/administration/installation-extras/health-checks.html +4 -4
  175. nautobot/project-static/docs/user-guide/administration/installation-extras/selinux-troubleshooting.html +4 -4
  176. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-netbox.html +4 -4
  177. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +4 -4
  178. nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +62 -10
  179. nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +4 -4
  180. nautobot/project-static/docs/user-guide/administration/upgrading/database-backup.html +4 -4
  181. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/after-you-upgrade.html +4 -4
  182. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/before-you-upgrade.html +4 -4
  183. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/for-developers.html +4 -4
  184. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/index.html +4 -4
  185. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/whats-changed.html +4 -4
  186. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/region-and-site-data-migration-guide.html +4 -4
  187. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +4 -4
  188. nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +4 -4
  189. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuit.html +4 -4
  190. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittermination.html +4 -4
  191. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittype.html +4 -4
  192. nautobot/project-static/docs/user-guide/core-data-model/circuits/provider.html +4 -4
  193. nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +4 -4
  194. nautobot/project-static/docs/user-guide/core-data-model/dcim/cable.html +4 -4
  195. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +4 -4
  196. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +4 -4
  197. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +4 -4
  198. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +4 -4
  199. nautobot/project-static/docs/user-guide/core-data-model/dcim/controller.html +4 -4
  200. nautobot/project-static/docs/user-guide/core-data-model/dcim/controllermanageddevicegroup.html +4 -4
  201. nautobot/project-static/docs/user-guide/core-data-model/dcim/device.html +4 -4
  202. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +4 -4
  203. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +4 -4
  204. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicefamily.html +4 -4
  205. nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +4 -4
  206. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +4 -4
  207. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +4 -4
  208. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +4 -4
  209. nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +4 -4
  210. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +4 -4
  211. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +4 -4
  212. nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +4 -4
  213. nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +4 -4
  214. nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +4 -4
  215. nautobot/project-static/docs/user-guide/core-data-model/dcim/manufacturer.html +4 -4
  216. nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +4 -4
  217. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerfeed.html +4 -4
  218. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +4 -4
  219. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +4 -4
  220. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerpanel.html +4 -4
  221. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +4 -4
  222. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +4 -4
  223. nautobot/project-static/docs/user-guide/core-data-model/dcim/rack.html +4 -4
  224. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackgroup.html +4 -4
  225. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackreservation.html +4 -4
  226. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +4 -4
  227. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +4 -4
  228. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +4 -4
  229. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareversion.html +4 -4
  230. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualchassis.html +4 -4
  231. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +4 -4
  232. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +4 -4
  233. nautobot/project-static/docs/user-guide/core-data-model/extras/contact.html +4 -4
  234. nautobot/project-static/docs/user-guide/core-data-model/extras/team.html +4 -4
  235. nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +4 -4
  236. nautobot/project-static/docs/user-guide/core-data-model/ipam/namespace.html +4 -4
  237. nautobot/project-static/docs/user-guide/core-data-model/ipam/prefix.html +4 -4
  238. nautobot/project-static/docs/user-guide/core-data-model/ipam/rir.html +4 -4
  239. nautobot/project-static/docs/user-guide/core-data-model/ipam/routetarget.html +4 -4
  240. nautobot/project-static/docs/user-guide/core-data-model/ipam/service.html +4 -4
  241. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +4 -4
  242. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlangroup.html +4 -4
  243. nautobot/project-static/docs/user-guide/core-data-model/ipam/vrf.html +4 -4
  244. nautobot/project-static/docs/user-guide/core-data-model/overview/introduction.html +4 -4
  245. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenant.html +4 -4
  246. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenantgroup.html +4 -4
  247. nautobot/project-static/docs/user-guide/core-data-model/virtualization/cluster.html +4 -4
  248. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustergroup.html +4 -4
  249. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustertype.html +4 -4
  250. nautobot/project-static/docs/user-guide/core-data-model/virtualization/virtualmachine.html +4 -4
  251. nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +4 -4
  252. nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +4 -4
  253. nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +4 -4
  254. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +4 -4
  255. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +4 -4
  256. nautobot/project-static/docs/user-guide/feature-guides/getting-started/index.html +4 -4
  257. nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +4 -4
  258. nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +4 -4
  259. nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +4 -4
  260. nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +4 -4
  261. nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +4 -4
  262. nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +4 -4
  263. nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +4 -4
  264. nautobot/project-static/docs/user-guide/feature-guides/graphql.html +4 -4
  265. nautobot/project-static/docs/user-guide/feature-guides/ip-address-merge-tool.html +4 -4
  266. nautobot/project-static/docs/user-guide/feature-guides/relationships.html +4 -4
  267. nautobot/project-static/docs/user-guide/feature-guides/software-image-files-and-versions.html +4 -4
  268. nautobot/project-static/docs/user-guide/index.html +4 -4
  269. nautobot/project-static/docs/user-guide/platform-functionality/change-logging.html +4 -4
  270. nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +4 -4
  271. nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +4 -4
  272. nautobot/project-static/docs/user-guide/platform-functionality/customlink.html +4 -4
  273. nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +4 -4
  274. nautobot/project-static/docs/user-guide/platform-functionality/exporttemplate.html +4 -4
  275. nautobot/project-static/docs/user-guide/platform-functionality/externalintegration.html +4 -4
  276. nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +4 -4
  277. nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +4 -4
  278. nautobot/project-static/docs/user-guide/platform-functionality/graphqlquery.html +4 -4
  279. nautobot/project-static/docs/user-guide/platform-functionality/imageattachment.html +4 -4
  280. nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +4 -4
  281. nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +4 -4
  282. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +4 -4
  283. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +4 -4
  284. nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +4 -4
  285. nautobot/project-static/docs/user-guide/platform-functionality/napalm.html +4 -4
  286. nautobot/project-static/docs/user-guide/platform-functionality/note.html +4 -4
  287. nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +4 -4
  288. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +4 -4
  289. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +4 -4
  290. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +4 -4
  291. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/ui-related-endpoints.html +4 -4
  292. nautobot/project-static/docs/user-guide/platform-functionality/role.html +4 -4
  293. nautobot/project-static/docs/user-guide/platform-functionality/secret.html +4 -4
  294. nautobot/project-static/docs/user-guide/platform-functionality/status.html +4 -4
  295. nautobot/project-static/docs/user-guide/platform-functionality/tag.html +4 -4
  296. nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +4 -4
  297. nautobot/project-static/docs/user-guide/platform-functionality/users/objectpermission.html +4 -4
  298. nautobot/project-static/docs/user-guide/platform-functionality/users/token.html +4 -4
  299. nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +4 -4
  300. nautobot/virtualization/tables.py +2 -5
  301. {nautobot-2.2.7.dist-info → nautobot-2.2.9.dist-info}/METADATA +3 -3
  302. {nautobot-2.2.7.dist-info → nautobot-2.2.9.dist-info}/RECORD +306 -305
  303. {nautobot-2.2.7.dist-info → nautobot-2.2.9.dist-info}/LICENSE.txt +0 -0
  304. {nautobot-2.2.7.dist-info → nautobot-2.2.9.dist-info}/NOTICE +0 -0
  305. {nautobot-2.2.7.dist-info → nautobot-2.2.9.dist-info}/WHEEL +0 -0
  306. {nautobot-2.2.7.dist-info → nautobot-2.2.9.dist-info}/entry_points.txt +0 -0
nautobot/core/filters.py CHANGED
@@ -7,9 +7,11 @@ from django import forms as django_forms
7
7
  from django.conf import settings
8
8
  from django.db import models
9
9
  from django.forms.utils import ErrorDict, ErrorList
10
+ from django.utils.encoding import force_str
11
+ from django.utils.text import capfirst
10
12
  import django_filters
11
13
  from django_filters.constants import EMPTY_VALUES
12
- from django_filters.utils import get_model_field, resolve_field
14
+ from django_filters.utils import get_model_field, label_for_filter, resolve_field, verbose_lookup_expr
13
15
  from drf_spectacular.types import OpenApiTypes
14
16
  from drf_spectacular.utils import extend_schema_field
15
17
 
@@ -681,6 +683,18 @@ class BaseFilterSet(django_filters.FilterSet):
681
683
  # Of course setting the negation of the existing filter's exclude attribute handles both cases
682
684
  new_filter.exclude = not filter_field.exclude
683
685
 
686
+ # If the base filter_field has a custom label, django_filters won't adjust it for the new_filter lookup,
687
+ # so we have to do it.
688
+ if filter_field.label and filter_field.label != label_for_filter(
689
+ cls.Meta.model, filter_field.field_name, filter_field.lookup_expr, filter_field.exclude
690
+ ):
691
+ # Lightly adjusted from label_for_filter() implementation:
692
+ verbose_expression = ["exclude", filter_field.label] if new_filter.exclude else [filter_field.label]
693
+ if isinstance(lookup_expr, str):
694
+ verbose_expression.append(verbose_lookup_expr(lookup_expr))
695
+ verbose_expression = [force_str(part) for part in verbose_expression if part]
696
+ new_filter.label = capfirst(" ".join(verbose_expression))
697
+
684
698
  magic_filters[new_filter_name] = new_filter
685
699
 
686
700
  return magic_filters
@@ -55,17 +55,17 @@ def generate_null_choices_resolver(name, resolver_name):
55
55
 
56
56
  def generate_filter_resolver(schema_type, resolver_name, field_name):
57
57
  """
58
- Generate function to resolve OneToMany filtering.
58
+ Generate function to resolve filtering of ManyToOne and ManyToMany related objects.
59
59
 
60
60
  Args:
61
61
  schema_type (DjangoObjectType): DjangoObjectType for a given model
62
62
  resolver_name (str): name of the resolver
63
- field_name (str): name of OneToMany field to filter
63
+ field_name (str): name of ManyToOneField, ManyToManyRel, or ManyToOneRel field to filter
64
64
  """
65
65
  filterset_class = schema_type._meta.filterset_class
66
66
 
67
- def resolve_filter(self, *args, **kwargs):
68
- if not filterset_class:
67
+ def resolve_filter(self, info, **kwargs):
68
+ if not filterset_class or not kwargs:
69
69
  return getattr(self, field_name).all()
70
70
 
71
71
  # Inverse of substitution logic from get_filtering_args_from_filterset() - transform "_type" back to "type"
@@ -6,7 +6,8 @@ import logging
6
6
  from django.conf import settings
7
7
  from django.contrib.contenttypes.models import ContentType
8
8
  from django.core.validators import ValidationError
9
- from django.db.models.fields.reverse_related import ManyToOneRel, OneToOneRel
9
+ from django.db.models import ManyToManyField
10
+ from django.db.models.fields.reverse_related import ManyToManyRel, ManyToOneRel, OneToOneRel
10
11
  import graphene
11
12
  from graphene.types import generic
12
13
 
@@ -198,9 +199,11 @@ def extend_schema_type_filter(schema_type, model):
198
199
  (DjangoObjectType): The extended schema_type object
199
200
  """
200
201
  for field in model._meta.get_fields():
201
- # Check attribute is a ManyToOne field
202
- # OneToOneRel is a subclass of ManyToOneRel, but we don't want to treat is as a list
203
- if not isinstance(field, ManyToOneRel) or isinstance(field, OneToOneRel):
202
+ # Check whether attribute is a ManyToOne or ManyToMany field
203
+ if not isinstance(field, (ManyToManyField, ManyToManyRel, ManyToOneRel)):
204
+ continue
205
+ # OneToOneRel is a subclass of ManyToOneRel, but we don't want to treat it as a list
206
+ if isinstance(field, OneToOneRel):
204
207
  continue
205
208
  child_schema_type = registry["graphql_types"].get(field.related_model._meta.label_lower)
206
209
  if child_schema_type:
@@ -367,10 +370,9 @@ def extend_schema_type_relationships(schema_type, model):
367
370
  """Extend the schema type with attributes and resolvers corresponding
368
371
  to the relationships associated with this model."""
369
372
 
370
- ct = ContentType.objects.get_for_model(model)
371
373
  relationships_by_side = {
372
- "source": Relationship.objects.filter(source_type=ct),
373
- "destination": Relationship.objects.filter(destination_type=ct),
374
+ "source": Relationship.objects.get_for_model_source(model),
375
+ "destination": Relationship.objects.get_for_model_destination(model),
374
376
  }
375
377
 
376
378
  prefix = ""
@@ -1,3 +1,4 @@
1
+ import codecs
1
2
  import contextlib
2
3
  from io import BytesIO
3
4
 
@@ -282,7 +283,9 @@ class ImportObjects(Job):
282
283
  if not csv_data and not csv_file:
283
284
  raise RunJobTaskFailed("Either csv_data or csv_file must be provided")
284
285
  if csv_file:
285
- csv_bytes = csv_file
286
+ # data_encoding is utf-8 and file_encoding is utf-8-sig
287
+ # Bytes read from the original file are decoded according to file_encoding, and the result is encoded using data_encoding.
288
+ csv_bytes = codecs.EncodedFile(csv_file, "utf-8", "utf-8-sig")
286
289
  else:
287
290
  csv_bytes = BytesIO(csv_data.encode("utf-8"))
288
291
 
nautobot/core/settings.py CHANGED
@@ -907,13 +907,24 @@ CELERY_TASK_TRACK_STARTED = True
907
907
  # If enabled, a `task-sent` event will be sent for every task so tasks can be tracked before they're consumed by a worker.
908
908
  CELERY_TASK_SEND_SENT_EVENT = True
909
909
 
910
+ # How many tasks a worker is allowed to reserve for its own consumption and execution.
911
+ # If set to zero (not recommended) a single worker can reserve all tasks even if other workers are free.
912
+ # For short running tasks (such as webhooks) you may want to set this to a larger number to increase throughput.
913
+ # Conversely, for long running tasks (such as SSoT or Golden-Config Jobs at scale) you may want to set this to 1
914
+ # so that a worker executing a long-running task will not prefetch other tasks, which would block their execution
915
+ # until the long-running task completes.
916
+ # https://docs.celeryq.dev/en/stable/userguide/optimizing.html#prefetch-limits
917
+ CELERY_WORKER_PREFETCH_MULTIPLIER = int(os.getenv("NAUTOBOT_CELERY_WORKER_PREFETCH_MULTIPLIER", "4"))
918
+
910
919
  # If enabled stdout and stderr of running jobs will be redirected to the task logger.
911
920
  CELERY_WORKER_REDIRECT_STDOUTS = is_truthy(os.getenv("NAUTOBOT_CELERY_WORKER_REDIRECT_STDOUTS", "True"))
912
921
 
913
- # The log level of log messages generated by redirected job stdout and stderr. Can be one of `DEBUG`, `INFO`, `WARNING`, `ERROR`, or `CRITICAL`.
922
+ # The log level of log messages generated by redirected job stdout and stderr.
923
+ # Can be one of `DEBUG`, `INFO`, `WARNING`, `ERROR`, or `CRITICAL`.
914
924
  CELERY_WORKER_REDIRECT_STDOUTS_LEVEL = os.getenv("NAUTOBOT_CELERY_WORKER_REDIRECT_STDOUTS_LEVEL", "WARNING")
915
925
 
916
- # Send task-related events so that tasks can be monitored using tools like flower. Sets the default value for the workers -E argument.
926
+ # Send task-related events so that tasks can be monitored using tools like flower.
927
+ # Sets the default value for the workers -E argument.
917
928
  CELERY_WORKER_SEND_TASK_EVENTS = True
918
929
 
919
930
  # Default celery queue name that will be used by workers and tasks if no queue is specified
@@ -426,6 +426,20 @@ properties:
426
426
  see_also:
427
427
  "`CELERY_TASK_SOFT_TIME_LIMIT`": "#celery_task_soft_time_limit"
428
428
  type: "integer"
429
+ CELERY_WORKER_PREFETCH_MULTIPLIER:
430
+ default: 4
431
+ description: "How many tasks a worker is allowed to reserve for its own consumption and execution."
432
+ details: >-
433
+ If set to `0` (not recommended) a single worker can reserve all tasks even if other workers are free.
434
+ For short running tasks (such as webhooks) you may want to set this to a larger number to increase throughput.
435
+ Conversely, for long-running tasks (such as SSoT or Golden-Config Jobs at scale) you may want to set this to `1`
436
+ so that a worker executing a long-running task will not prefetch other tasks, which would block their execution
437
+ until the long-running task completes.
438
+ environment_variable: "NAUTOBOT_CELERY_WORKER_PREFETCH_MULTIPLIER"
439
+ see_also:
440
+ "Celery documentation": "https://docs.celeryq.dev/en/stable/userguide/optimizing.html#prefetch-limits"
441
+ type: "integer"
442
+ version_added: "2.2.9"
429
443
  CELERY_WORKER_PROMETHEUS_PORTS:
430
444
  default: []
431
445
  description: "Ports for Prometheus metric HTTP server running on the celery worker(s)."
@@ -282,6 +282,15 @@ SECRET_KEY = os.getenv("NAUTOBOT_SECRET_KEY", "{{ secret_key }}")
282
282
  # CELERY_TASK_SOFT_TIME_LIMIT = int(os.getenv("NAUTOBOT_CELERY_TASK_SOFT_TIME_LIMIT", str(5 * 60)))
283
283
  # CELERY_TASK_TIME_LIMIT = int(os.getenv("NAUTOBOT_CELERY_TASK_TIME_LIMIT", str(10 * 60)))
284
284
 
285
+ # How many tasks a worker is allowed to reserve for its own consumption and execution.
286
+ # If set to zero (not recommended) a single worker can reserve all tasks even if other workers are free.
287
+ # For short running tasks (such as webhooks) you may want to set this to a larger number to increase throughput.
288
+ # Conversely, for long running tasks (such as SSoT or Golden-Config Jobs at scale) you may want to set this to 1
289
+ # so that a worker executing a long-running task will not prefetch other tasks, which would block their execution
290
+ # until the long-running task completes.
291
+ # https://docs.celeryq.dev/en/stable/userguide/optimizing.html#prefetch-limits
292
+ # CELERY_WORKER_PREFETCH_MULTIPLIER = int(os.getenv("NAUTOBOT_CELERY_WORKER_PREFETCH_MULTIPLIER", "4"))
293
+
285
294
  # Ports for prometheus metric HTTP server running on the celery worker.
286
295
  # Normally this should be set to a single port, unless you have multiple workers running on a single machine, i.e.
287
296
  # sharing the same available ports. In that case you need to specify a range of ports greater than or equal to the
@@ -294,6 +303,12 @@ SECRET_KEY = os.getenv("NAUTOBOT_SECRET_KEY", "{{ secret_key }}")
294
303
  # int(value) for value in os.getenv("NAUTOBOT_CELERY_WORKER_PROMETHEUS_PORTS").split(",")
295
304
  # ]
296
305
 
306
+ # If enabled stdout and stderr of running jobs will be redirected to the task logger.
307
+ # CELERY_WORKER_REDIRECT_STDOUTS = is_truthy(os.getenv("NAUTOBOT_CELERY_WORKER_REDIRECT_STDOUTS", "True"))
308
+
309
+ # The log level of log messages generated by redirected job stdout and stderr.
310
+ # Can be one of `DEBUG`, `INFO`, `WARNING`, `ERROR`, or `CRITICAL`.
311
+ # CELERY_WORKER_REDIRECT_STDOUTS_LEVEL = os.getenv("NAUTOBOT_CELERY_WORKER_REDIRECT_STDOUTS_LEVEL", "WARNING")
297
312
 
298
313
  # Number of days to retain changelog entries. Set to 0 to retain changes indefinitely. Defaults to 90 if not set here.
299
314
  #
@@ -0,0 +1,36 @@
1
+ from django.urls import reverse
2
+
3
+ from nautobot.core.testing.integration import SeleniumTestCase
4
+
5
+
6
+ class StaticMediaFailureTestCase(SeleniumTestCase):
7
+ """Integration test to make sure no static media failures are encountered."""
8
+
9
+ def setUp(self):
10
+ super().setUp()
11
+ self.user.is_superuser = True
12
+ self.user.is_staff = True
13
+ self.user.save()
14
+ self.login(self.user.username, self.password)
15
+
16
+ def tearDown(self):
17
+ self.logout()
18
+ super().tearDown()
19
+
20
+ def test_for_static_media_failure(self):
21
+ test_urls = [
22
+ reverse("home"),
23
+ reverse("api-root"),
24
+ reverse("graphql"),
25
+ reverse("api_docs"),
26
+ "/admin/",
27
+ "/static/docs/index.html",
28
+ ]
29
+ for url in test_urls:
30
+ with self.subTest(test_url=url):
31
+ self.browser.visit(self.live_server_url + url)
32
+ # Wait for body element to appear
33
+ self.assertTrue(self.browser.is_element_present_by_tag("body", wait_time=10), "Page failed to load")
34
+ # Ensure we weren't redirected to another page
35
+ self.assertEqual(self.browser.url, self.live_server_url + url)
36
+ self.assertTrue(self.browser.is_text_not_present("Static Media Failure"))
@@ -729,8 +729,8 @@ class GraphQLQueryTest(GraphQLTestCaseBase):
729
729
  cls.location_type = LocationType.objects.get(name="Campus")
730
730
  cls.location1 = Location.objects.filter(location_type=cls.location_type).first()
731
731
  cls.location2 = Location.objects.filter(location_type=cls.location_type).last()
732
- cls.location1.name = "Location-1"
733
- cls.location2.name = "Location-2"
732
+ cls.location1.name = "Campus Location-1"
733
+ cls.location2.name = "Campus Location-2"
734
734
  cls.location1.status = cls.location_statuses[0]
735
735
  cls.location2.status = cls.location_statuses[1]
736
736
  cls.location1.validated_save()
@@ -930,6 +930,7 @@ class GraphQLQueryTest(GraphQLTestCaseBase):
930
930
  cls.prefix1 = Prefix.objects.create(
931
931
  prefix="10.0.1.0/24", namespace=cls.namespace, status=cls.prefix_statuses[0]
932
932
  )
933
+ cls.prefix1.locations.add(cls.location1, cls.location2)
933
934
  cls.ipaddr1 = IPAddress.objects.create(
934
935
  address="10.0.1.1/24", namespace=cls.namespace, status=cls.ip_statuses[0]
935
936
  )
@@ -968,6 +969,7 @@ class GraphQLQueryTest(GraphQLTestCaseBase):
968
969
  cls.prefix2 = Prefix.objects.create(
969
970
  prefix="10.0.2.0/24", namespace=cls.namespace, status=cls.prefix_statuses[1]
970
971
  )
972
+ cls.prefix2.locations.add(cls.location1, cls.location2)
971
973
  cls.ipaddr2 = IPAddress.objects.create(
972
974
  address="10.0.2.1/30", namespace=cls.namespace, status=cls.ip_statuses[1]
973
975
  )
@@ -1507,9 +1509,9 @@ query {
1507
1509
  @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
1508
1510
  def test_query_locations_filter(self):
1509
1511
  filters = (
1510
- ('name: "Location-1"', 1),
1511
- ('name: ["Location-1"]', 1),
1512
- ('name: ["Location-1", "Location-2"]', 2),
1512
+ ('name: "Campus Location-1"', 1),
1513
+ ('name: ["Campus Location-1"]', 1),
1514
+ ('name: ["Campus Location-1", "Campus Location-2"]', 2),
1513
1515
  ('name__ic: "Location"', Location.objects.filter(name__icontains="Location").count()),
1514
1516
  ('name__ic: ["Location"]', Location.objects.filter(name__icontains="Location").count()),
1515
1517
  ('name__nic: "Location"', Location.objects.exclude(name__icontains="Location").count()),
@@ -1541,6 +1543,50 @@ query {
1541
1543
  self.assertIsNone(result.errors)
1542
1544
  self.assertEqual(len(result.data["locations"]), nbr_expected_results)
1543
1545
 
1546
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
1547
+ def test_query_prefixes_nested_m2m_filter(self):
1548
+ """
1549
+ Test functionality added to address https://github.com/nautobot/nautobot/issues/5906.
1550
+
1551
+ Prefix.locations is a ManyToManyField, which was not filterable in our GraphQL schema before this fix.
1552
+ """
1553
+ query = 'query { prefixes (prefix_length__gte:16) { prefix locations (location_type:["Campus"]) { name } } }'
1554
+ result = self.execute_query(query)
1555
+ self.assertIsNone(result.errors)
1556
+ found_valid_location = False
1557
+ found_invalid_location = False
1558
+ for prefix_data in result.data["prefixes"]:
1559
+ for location_data in prefix_data["locations"]:
1560
+ if location_data["name"].startswith("Campus"):
1561
+ found_valid_location = True
1562
+ else:
1563
+ print(f"Found unexpected unfiltered location {location_data['name']} under {prefix_data['prefix']}")
1564
+ found_invalid_location = True
1565
+ self.assertTrue(found_valid_location)
1566
+ self.assertFalse(found_invalid_location)
1567
+
1568
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
1569
+ def test_query_locations_nested_reverse_m2m_filter(self):
1570
+ """
1571
+ Test functionality added to address https://github.com/nautobot/nautobot/issues/5906.
1572
+
1573
+ Location.prefixes is a (reverse) ManyToManyRel, which was not filterable in our GraphQL schema before this fix.
1574
+ """
1575
+ query = "query { locations { name prefixes (prefix_length:24) { prefix } } }"
1576
+ result = self.execute_query(query)
1577
+ self.assertIsNone(result.errors)
1578
+ found_valid_prefix = False
1579
+ found_invalid_prefix = False
1580
+ for location_data in result.data["locations"]:
1581
+ for prefix_data in location_data["prefixes"]:
1582
+ if prefix_data["prefix"].endswith("/24"):
1583
+ found_valid_prefix = True
1584
+ else:
1585
+ print(f"Found unexpected unfiltered prefix {prefix_data['prefix']} under {location_data['name']}")
1586
+ found_invalid_prefix = True
1587
+ self.assertTrue(found_valid_prefix)
1588
+ self.assertFalse(found_invalid_prefix)
1589
+
1544
1590
  @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
1545
1591
  def test_query_devices_filter(self):
1546
1592
  filterset_class = DeviceFilterSet
@@ -1,12 +1,22 @@
1
1
  from pathlib import Path
2
2
 
3
3
  from django.contrib.contenttypes.models import ContentType
4
+ from django.core.files.base import ContentFile
4
5
  import yaml
5
6
 
6
7
  from nautobot.core.testing import create_job_result_and_run_job, TransactionTestCase
7
- from nautobot.dcim.models import DeviceType, Location, LocationType, Manufacturer
8
+ from nautobot.dcim.models import Device, DeviceType, Location, LocationType, Manufacturer
8
9
  from nautobot.extras.choices import JobResultStatusChoices, LogLevelChoices
9
- from nautobot.extras.models import Contact, ContactAssociation, ExportTemplate, JobLogEntry, Role, Status
10
+ from nautobot.extras.models import (
11
+ Contact,
12
+ ContactAssociation,
13
+ ExportTemplate,
14
+ FileProxy,
15
+ JobLogEntry,
16
+ Role,
17
+ Status,
18
+ )
19
+ from nautobot.ipam.models import Prefix
10
20
  from nautobot.users.models import ObjectPermission
11
21
 
12
22
 
@@ -204,6 +214,76 @@ class ImportObjectsTestCase(TransactionTestCase):
204
214
  )
205
215
  self.assertEqual(4, Status.objects.filter(name__startswith="test_status").count())
206
216
 
217
+ def test_csv_import_with_utf_8_with_bom_encoding(self):
218
+ """
219
+ A superuser running the job with a .csv file with utf_8 with bom encoding should successfully create all specified objects.
220
+ Test for bug fix https://github.com/nautobot/nautobot/issues/5812 and https://github.com/nautobot/nautobot/issues/5985
221
+ """
222
+
223
+ status = Status.objects.get(name="Active").pk
224
+ content = f"prefix,status\n192.168.1.1/32,{status}"
225
+ content = content.encode("utf-8-sig")
226
+ filename = "test.csv"
227
+ csv_file = FileProxy.objects.create(name=filename, file=ContentFile(content, name=filename))
228
+ job_result = create_job_result_and_run_job(
229
+ "nautobot.core.jobs",
230
+ "ImportObjects",
231
+ content_type=ContentType.objects.get_for_model(Prefix).pk,
232
+ csv_file=csv_file.id,
233
+ )
234
+ self.assertEqual(job_result.status, JobResultStatusChoices.STATUS_SUCCESS)
235
+ self.assertFalse(
236
+ JobLogEntry.objects.filter(job_result=job_result, log_level=LogLevelChoices.LOG_WARNING).exists()
237
+ )
238
+ self.assertFalse(
239
+ JobLogEntry.objects.filter(job_result=job_result, log_level=LogLevelChoices.LOG_ERROR).exists()
240
+ )
241
+ self.assertEqual(
242
+ 1, Prefix.objects.filter(status=Status.objects.get(name="Active"), prefix="192.168.1.1/32").count()
243
+ )
244
+ mfr = Manufacturer.objects.create(name="Test Cisco Manufacturer")
245
+ device_type = DeviceType.objects.create(
246
+ manufacturer=mfr,
247
+ model="Cisco CSR1000v",
248
+ u_height=0,
249
+ )
250
+ location_type = LocationType.objects.create(name="Test Location Type")
251
+ location_type.content_types.set([ContentType.objects.get_for_model(Device)])
252
+ location = Location.objects.create(
253
+ name="Device Location",
254
+ location_type=location_type,
255
+ status=Status.objects.get_for_model(Location).first(),
256
+ )
257
+ role = Role.objects.create(name="Device Status")
258
+ role.content_types.set([ContentType.objects.get_for_model(Device)])
259
+ content = "\n".join(
260
+ [
261
+ "serial,asset_tag,device_type,location,status,name,role",
262
+ f"1021C4,CA211,{device_type.pk},{location.pk},{status},Test-AC-01,{role}",
263
+ f"1021C5,CA212,{device_type.pk},{location.pk},{status},Test-AC-02,{role}",
264
+ ]
265
+ )
266
+ content = content.encode("utf-8-sig")
267
+ filename = "test.csv"
268
+ csv_file = FileProxy.objects.create(name=filename, file=ContentFile(content, name=filename))
269
+ job_result = create_job_result_and_run_job(
270
+ "nautobot.core.jobs",
271
+ "ImportObjects",
272
+ content_type=ContentType.objects.get_for_model(Device).pk,
273
+ csv_file=csv_file.id,
274
+ )
275
+ self.assertEqual(job_result.status, JobResultStatusChoices.STATUS_SUCCESS)
276
+ self.assertFalse(
277
+ JobLogEntry.objects.filter(job_result=job_result, log_level=LogLevelChoices.LOG_WARNING).exists()
278
+ )
279
+ self.assertFalse(
280
+ JobLogEntry.objects.filter(job_result=job_result, log_level=LogLevelChoices.LOG_ERROR).exists()
281
+ )
282
+ device_1 = Device.objects.get(name="Test-AC-01")
283
+ device_2 = Device.objects.get(name="Test-AC-02")
284
+ self.assertEqual(device_1.serial, "1021C4")
285
+ self.assertEqual(device_2.serial, "1021C5")
286
+
207
287
  def test_csv_import_bad_row(self):
208
288
  """A row of incorrect data should fail validation for that object but import all others successfully if `roll_back_if_error` is False."""
209
289
  csv_data = self.csv_data.split("\n")
@@ -20,15 +20,15 @@ class NautobotTemplateTagsNetutilsTest(TestCase):
20
20
  i = 1
21
21
  for param_name, param in signature.parameters.items():
22
22
  template_string += f" {param_name}="
23
- if param.annotation == str:
23
+ if param.annotation is str:
24
24
  template_string += f'"{i}"'
25
- elif param.annotation == bool:
25
+ elif param.annotation is bool:
26
26
  template_string += "True"
27
27
  elif param.annotation in (int, float):
28
28
  template_string += str(i)
29
29
  elif param.annotation in (list, tuple):
30
30
  template_string += "[]"
31
- elif param.annotation == dict:
31
+ elif param.annotation is dict:
32
32
  template_string += "{}"
33
33
  else:
34
34
  template_string += "None"
@@ -24,6 +24,7 @@ from nautobot.dcim.choices import (
24
24
  PowerOutletFeedLegChoices,
25
25
  PowerOutletTypeChoices,
26
26
  PowerPortTypeChoices,
27
+ SubdeviceRoleChoices,
27
28
  )
28
29
  from nautobot.dcim.constants import (
29
30
  NONCONNECTABLE_IFACE_TYPES,
@@ -1019,6 +1020,12 @@ class DeviceBay(ComponentModel):
1019
1020
  "installed_device": f"Cannot install the specified device; device is already installed in {current_bay}"
1020
1021
  }
1021
1022
  )
1023
+ if self.installed_device.device_type.subdevice_role != SubdeviceRoleChoices.ROLE_CHILD:
1024
+ raise ValidationError(
1025
+ {
1026
+ "installed_device": f'Cannot install device "{self.installed_device}"; device-type "{self.installed_device.device_type}" subdevice_role is not "child".'
1027
+ }
1028
+ )
1022
1029
 
1023
1030
  @property
1024
1031
  def parent(self):
@@ -832,9 +832,16 @@ class Device(PrimaryModel, ConfigContextModel):
832
832
  # Update Location and Rack assignment for any child Devices
833
833
  devices = Device.objects.filter(parent_bay__device=self)
834
834
  for device in devices:
835
- device.location = self.location
836
- device.rack = self.rack
837
- device.save()
835
+ save_child_device = False
836
+ if device.location != self.location:
837
+ device.location = self.location
838
+ save_child_device = True
839
+ if device.rack != self.rack:
840
+ device.rack = self.rack
841
+ save_child_device = True
842
+
843
+ if save_child_device:
844
+ device.save()
838
845
 
839
846
  def create_components(self):
840
847
  """Create device components from the device type definition."""
@@ -1058,6 +1065,10 @@ class DeviceRedundancyGroup(PrimaryModel):
1058
1065
  def devices_sorted(self):
1059
1066
  return self.devices.order_by("device_redundancy_group_priority")
1060
1067
 
1068
+ @property
1069
+ def controllers_sorted(self):
1070
+ return self.controllers.order_by("name")
1071
+
1061
1072
  def __str__(self):
1062
1073
  return self.name
1063
1074
 
@@ -938,17 +938,32 @@ class VirtualChassisTable(BaseTable):
938
938
  class DeviceRedundancyGroupTable(BaseTable):
939
939
  pk = ToggleColumn()
940
940
  name = tables.Column(linkify=True)
941
- device_count = tables.TemplateColumn(
942
- template_code=LINKED_RECORD_COUNT,
941
+ device_count = LinkedCountColumn(
942
+ viewname="dcim:device_list",
943
+ url_params={"device_redundancy_group": "pk"},
943
944
  verbose_name="Devices",
944
945
  )
946
+ controller_count = LinkedCountColumn(
947
+ viewname="dcim:controller_list",
948
+ url_params={"controller_device_redundancy_group": "pk"},
949
+ verbose_name="Controllers",
950
+ )
945
951
  secrets_group = tables.Column(linkify=True)
946
952
  tags = TagColumn(url_name="dcim:deviceredundancygroup_list")
947
953
 
948
954
  class Meta(BaseTable.Meta):
949
955
  model = DeviceRedundancyGroup
950
- fields = ("pk", "name", "status", "failover_strategy", "device_count", "secrets_group", "tags")
951
- default_columns = ("pk", "name", "status", "failover_strategy", "device_count")
956
+ fields = (
957
+ "pk",
958
+ "name",
959
+ "status",
960
+ "failover_strategy",
961
+ "controller_count",
962
+ "device_count",
963
+ "secrets_group",
964
+ "tags",
965
+ )
966
+ default_columns = ("pk", "name", "status", "failover_strategy", "controller_count", "device_count")
952
967
 
953
968
 
954
969
  #
@@ -45,6 +45,12 @@
45
45
  {% endblock content_right_page %}
46
46
 
47
47
  {% block content_full_width_page %}
48
+ <div class="panel panel-default">
49
+ <div class="panel-heading">
50
+ <strong>Controllers</strong>
51
+ </div>
52
+ {% include 'responsive_table.html' with table=controllers_table %}
53
+ </div>
48
54
  <div class="panel panel-default">
49
55
  <div class="panel-heading">
50
56
  <strong>Devices</strong>