nautobot 2.3.8__py3-none-any.whl → 2.3.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 (318) hide show
  1. nautobot/apps/tables.py +2 -0
  2. nautobot/core/forms/__init__.py +4 -0
  3. nautobot/core/forms/fields.py +32 -0
  4. nautobot/core/jobs/__init__.py +24 -8
  5. nautobot/core/models/tree_queries.py +8 -0
  6. nautobot/core/settings.py +7 -0
  7. nautobot/core/settings.yaml +10 -0
  8. nautobot/core/signals.py +5 -4
  9. nautobot/core/templates/nautobot_config.py.j2 +4 -0
  10. nautobot/dcim/forms.py +30 -27
  11. nautobot/dcim/models/device_components.py +5 -0
  12. nautobot/dcim/tables/devices.py +4 -2
  13. nautobot/dcim/tests/test_models.py +16 -0
  14. nautobot/extras/context_managers.py +7 -8
  15. nautobot/extras/datasources/__init__.py +2 -0
  16. nautobot/extras/datasources/git.py +30 -49
  17. nautobot/extras/datasources/registry.py +2 -2
  18. nautobot/extras/jobs.py +17 -5
  19. nautobot/extras/models/datasources.py +6 -0
  20. nautobot/extras/models/groups.py +47 -33
  21. nautobot/extras/models/jobs.py +1 -1
  22. nautobot/extras/plugins/__init__.py +165 -0
  23. nautobot/extras/templates/extras/plugin_detail.html +33 -0
  24. nautobot/extras/tests/test_context_managers.py +16 -7
  25. nautobot/extras/tests/test_datasources.py +88 -1
  26. nautobot/extras/tests/test_dynamicgroups.py +12 -0
  27. nautobot/extras/tests/test_plugins.py +94 -0
  28. nautobot/extras/views.py +3 -1
  29. nautobot/project-static/docs/404.html +24 -3
  30. nautobot/project-static/docs/apps/index.html +24 -3
  31. nautobot/project-static/docs/apps/nautobot-apps.html +24 -3
  32. nautobot/project-static/docs/assets/javascripts/{bundle.525ec568.min.js → bundle.83f73b43.min.js} +2 -2
  33. nautobot/project-static/docs/assets/javascripts/{bundle.525ec568.min.js.map → bundle.83f73b43.min.js.map} +3 -3
  34. nautobot/project-static/docs/assets/stylesheets/main.0253249f.min.css +1 -0
  35. nautobot/project-static/docs/assets/stylesheets/main.0253249f.min.css.map +1 -0
  36. nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +24 -3
  37. nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +24 -3
  38. nautobot/project-static/docs/code-reference/nautobot/apps/api.html +24 -3
  39. nautobot/project-static/docs/code-reference/nautobot/apps/change_logging.html +24 -3
  40. nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +24 -3
  41. nautobot/project-static/docs/code-reference/nautobot/apps/config.html +24 -3
  42. nautobot/project-static/docs/code-reference/nautobot/apps/constants.html +24 -3
  43. nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +24 -3
  44. nautobot/project-static/docs/code-reference/nautobot/apps/exceptions.html +24 -3
  45. nautobot/project-static/docs/code-reference/nautobot/apps/factory.html +24 -3
  46. nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +24 -3
  47. nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +24 -3
  48. nautobot/project-static/docs/code-reference/nautobot/apps/graphql.html +24 -3
  49. nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +49 -6
  50. nautobot/project-static/docs/code-reference/nautobot/apps/models.html +24 -3
  51. nautobot/project-static/docs/code-reference/nautobot/apps/querysets.html +24 -3
  52. nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +24 -3
  53. nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +138 -3
  54. nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +24 -3
  55. nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +24 -3
  56. nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +24 -3
  57. nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +24 -3
  58. nautobot/project-static/docs/code-reference/nautobot/apps/views.html +24 -3
  59. nautobot/project-static/docs/development/apps/api/configuration-view.html +24 -3
  60. nautobot/project-static/docs/development/apps/api/database-backend-config.html +24 -3
  61. nautobot/project-static/docs/development/apps/api/models/django-admin.html +24 -3
  62. nautobot/project-static/docs/development/apps/api/models/global-search.html +24 -3
  63. nautobot/project-static/docs/development/apps/api/models/graphql.html +24 -3
  64. nautobot/project-static/docs/development/apps/api/models/index.html +24 -3
  65. nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +24 -3
  66. nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +24 -3
  67. nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +24 -3
  68. nautobot/project-static/docs/development/apps/api/platform-features/git-repository-content.html +24 -3
  69. nautobot/project-static/docs/development/apps/api/platform-features/index.html +24 -3
  70. nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +24 -3
  71. nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +24 -3
  72. nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +24 -3
  73. nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +27 -6
  74. nautobot/project-static/docs/development/apps/api/platform-features/table-extensions.html +8823 -0
  75. nautobot/project-static/docs/development/apps/api/platform-features/uniquely-identify-objects.html +27 -6
  76. nautobot/project-static/docs/development/apps/api/prometheus.html +24 -3
  77. nautobot/project-static/docs/development/apps/api/setup.html +33 -11
  78. nautobot/project-static/docs/development/apps/api/testing.html +24 -3
  79. nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +24 -3
  80. nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +24 -3
  81. nautobot/project-static/docs/development/apps/api/ui-extensions/index.html +24 -3
  82. nautobot/project-static/docs/development/apps/api/ui-extensions/navigation.html +24 -3
  83. nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +24 -3
  84. nautobot/project-static/docs/development/apps/api/views/base-template.html +24 -3
  85. nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +24 -3
  86. nautobot/project-static/docs/development/apps/api/views/django-generic-views.html +24 -3
  87. nautobot/project-static/docs/development/apps/api/views/help-documentation.html +24 -3
  88. nautobot/project-static/docs/development/apps/api/views/index.html +24 -3
  89. nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +24 -3
  90. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +24 -3
  91. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +24 -3
  92. nautobot/project-static/docs/development/apps/api/views/notes.html +24 -3
  93. nautobot/project-static/docs/development/apps/api/views/rest-api.html +24 -3
  94. nautobot/project-static/docs/development/apps/api/views/urls.html +24 -3
  95. nautobot/project-static/docs/development/apps/index.html +24 -3
  96. nautobot/project-static/docs/development/apps/migration/code-updates.html +24 -3
  97. nautobot/project-static/docs/development/apps/migration/dependency-updates.html +24 -3
  98. nautobot/project-static/docs/development/apps/migration/from-v1.html +24 -3
  99. nautobot/project-static/docs/development/apps/migration/model-updates/dcim.html +24 -3
  100. nautobot/project-static/docs/development/apps/migration/model-updates/extras.html +24 -3
  101. nautobot/project-static/docs/development/apps/migration/model-updates/global.html +24 -3
  102. nautobot/project-static/docs/development/apps/migration/model-updates/ipam.html +24 -3
  103. nautobot/project-static/docs/development/apps/porting-from-netbox.html +24 -3
  104. nautobot/project-static/docs/development/core/application-registry.html +24 -3
  105. nautobot/project-static/docs/development/core/best-practices.html +24 -3
  106. nautobot/project-static/docs/development/core/bootstrap-ui.html +24 -3
  107. nautobot/project-static/docs/development/core/caching.html +24 -3
  108. nautobot/project-static/docs/development/core/controllers.html +24 -3
  109. nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +24 -3
  110. nautobot/project-static/docs/development/core/generic-views.html +24 -3
  111. nautobot/project-static/docs/development/core/getting-started.html +24 -3
  112. nautobot/project-static/docs/development/core/homepage.html +24 -3
  113. nautobot/project-static/docs/development/core/index.html +24 -3
  114. nautobot/project-static/docs/development/core/model-checklist.html +24 -3
  115. nautobot/project-static/docs/development/core/model-features.html +24 -3
  116. nautobot/project-static/docs/development/core/natural-keys.html +24 -3
  117. nautobot/project-static/docs/development/core/navigation-menu.html +24 -3
  118. nautobot/project-static/docs/development/core/release-checklist.html +24 -3
  119. nautobot/project-static/docs/development/core/role-internals.html +24 -3
  120. nautobot/project-static/docs/development/core/settings.html +24 -3
  121. nautobot/project-static/docs/development/core/style-guide.html +24 -3
  122. nautobot/project-static/docs/development/core/templates.html +24 -3
  123. nautobot/project-static/docs/development/core/testing.html +24 -3
  124. nautobot/project-static/docs/development/core/user-preferences.html +24 -3
  125. nautobot/project-static/docs/development/index.html +24 -3
  126. nautobot/project-static/docs/development/jobs/index.html +24 -3
  127. nautobot/project-static/docs/development/jobs/migration/from-v1.html +24 -3
  128. nautobot/project-static/docs/index.html +24 -3
  129. nautobot/project-static/docs/objects.inv +0 -0
  130. nautobot/project-static/docs/overview/application_stack.html +24 -3
  131. nautobot/project-static/docs/overview/design_philosophy.html +24 -3
  132. nautobot/project-static/docs/release-notes/index.html +24 -3
  133. nautobot/project-static/docs/release-notes/version-1.0.html +24 -3
  134. nautobot/project-static/docs/release-notes/version-1.1.html +24 -3
  135. nautobot/project-static/docs/release-notes/version-1.2.html +24 -3
  136. nautobot/project-static/docs/release-notes/version-1.3.html +24 -3
  137. nautobot/project-static/docs/release-notes/version-1.4.html +24 -3
  138. nautobot/project-static/docs/release-notes/version-1.5.html +24 -3
  139. nautobot/project-static/docs/release-notes/version-1.6.html +24 -3
  140. nautobot/project-static/docs/release-notes/version-2.0.html +24 -3
  141. nautobot/project-static/docs/release-notes/version-2.1.html +24 -3
  142. nautobot/project-static/docs/release-notes/version-2.2.html +24 -3
  143. nautobot/project-static/docs/release-notes/version-2.3.html +285 -114
  144. nautobot/project-static/docs/requirements.txt +1 -1
  145. nautobot/project-static/docs/search/search_index.json +1 -1
  146. nautobot/project-static/docs/sitemap.xml +273 -269
  147. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  148. nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +24 -3
  149. nautobot/project-static/docs/user-guide/administration/configuration/authentication/remote.html +24 -3
  150. nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +24 -3
  151. nautobot/project-static/docs/user-guide/administration/configuration/index.html +24 -3
  152. nautobot/project-static/docs/user-guide/administration/configuration/redis.html +24 -3
  153. nautobot/project-static/docs/user-guide/administration/configuration/settings.html +29 -4
  154. nautobot/project-static/docs/user-guide/administration/configuration/time-zones.html +24 -3
  155. nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +24 -3
  156. nautobot/project-static/docs/user-guide/administration/guides/docker.html +24 -3
  157. nautobot/project-static/docs/user-guide/administration/guides/health-checks.html +24 -3
  158. nautobot/project-static/docs/user-guide/administration/guides/permissions.html +24 -3
  159. nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +24 -3
  160. nautobot/project-static/docs/user-guide/administration/guides/replicating-nautobot.html +24 -3
  161. nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +24 -3
  162. nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +24 -3
  163. nautobot/project-static/docs/user-guide/administration/guides/selinux-troubleshooting.html +24 -3
  164. nautobot/project-static/docs/user-guide/administration/installation/app-install.html +24 -3
  165. nautobot/project-static/docs/user-guide/administration/installation/external-authentication.html +24 -3
  166. nautobot/project-static/docs/user-guide/administration/installation/http-server.html +24 -3
  167. nautobot/project-static/docs/user-guide/administration/installation/index.html +24 -3
  168. nautobot/project-static/docs/user-guide/administration/installation/install_system.html +24 -3
  169. nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +24 -3
  170. nautobot/project-static/docs/user-guide/administration/installation/services.html +24 -3
  171. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-netbox.html +24 -3
  172. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +24 -3
  173. nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +24 -3
  174. nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +24 -3
  175. nautobot/project-static/docs/user-guide/administration/upgrading/database-backup.html +24 -3
  176. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/after-you-upgrade.html +24 -3
  177. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/before-you-upgrade.html +24 -3
  178. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/for-developers.html +24 -3
  179. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/index.html +24 -3
  180. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/whats-changed.html +24 -3
  181. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/region-and-site-data-migration-guide.html +24 -3
  182. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +24 -3
  183. nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +24 -3
  184. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuit.html +24 -3
  185. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittermination.html +24 -3
  186. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittype.html +24 -3
  187. nautobot/project-static/docs/user-guide/core-data-model/circuits/provider.html +24 -3
  188. nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +24 -3
  189. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloud.html +24 -3
  190. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudaccount.html +24 -3
  191. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetwork.html +24 -3
  192. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetworkprefixassignment.html +24 -3
  193. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudresourcetype.html +24 -3
  194. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservice.html +24 -3
  195. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservicenetworkassignment.html +24 -3
  196. nautobot/project-static/docs/user-guide/core-data-model/dcim/cable.html +24 -3
  197. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +24 -3
  198. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +24 -3
  199. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +24 -3
  200. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +24 -3
  201. nautobot/project-static/docs/user-guide/core-data-model/dcim/controller.html +24 -3
  202. nautobot/project-static/docs/user-guide/core-data-model/dcim/controllermanageddevicegroup.html +24 -3
  203. nautobot/project-static/docs/user-guide/core-data-model/dcim/device.html +24 -3
  204. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +24 -3
  205. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +24 -3
  206. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicefamily.html +24 -3
  207. nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +24 -3
  208. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +24 -3
  209. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +24 -3
  210. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +24 -3
  211. nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +24 -3
  212. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +24 -3
  213. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +24 -3
  214. nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +24 -3
  215. nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +24 -3
  216. nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +24 -3
  217. nautobot/project-static/docs/user-guide/core-data-model/dcim/manufacturer.html +24 -3
  218. nautobot/project-static/docs/user-guide/core-data-model/dcim/module.html +24 -3
  219. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebay.html +24 -3
  220. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebaytemplate.html +24 -3
  221. nautobot/project-static/docs/user-guide/core-data-model/dcim/moduletype.html +24 -3
  222. nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +24 -3
  223. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerfeed.html +24 -3
  224. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +24 -3
  225. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +24 -3
  226. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerpanel.html +24 -3
  227. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +24 -3
  228. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +24 -3
  229. nautobot/project-static/docs/user-guide/core-data-model/dcim/rack.html +24 -3
  230. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackgroup.html +24 -3
  231. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackreservation.html +24 -3
  232. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +24 -3
  233. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +24 -3
  234. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +24 -3
  235. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareversion.html +24 -3
  236. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualchassis.html +24 -3
  237. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +24 -3
  238. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +24 -3
  239. nautobot/project-static/docs/user-guide/core-data-model/extras/contact.html +24 -3
  240. nautobot/project-static/docs/user-guide/core-data-model/extras/team.html +24 -3
  241. nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +24 -3
  242. nautobot/project-static/docs/user-guide/core-data-model/ipam/namespace.html +24 -3
  243. nautobot/project-static/docs/user-guide/core-data-model/ipam/prefix.html +24 -3
  244. nautobot/project-static/docs/user-guide/core-data-model/ipam/rir.html +24 -3
  245. nautobot/project-static/docs/user-guide/core-data-model/ipam/routetarget.html +24 -3
  246. nautobot/project-static/docs/user-guide/core-data-model/ipam/service.html +24 -3
  247. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +24 -3
  248. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlangroup.html +24 -3
  249. nautobot/project-static/docs/user-guide/core-data-model/ipam/vrf.html +24 -3
  250. nautobot/project-static/docs/user-guide/core-data-model/overview/introduction.html +24 -3
  251. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenant.html +24 -3
  252. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenantgroup.html +24 -3
  253. nautobot/project-static/docs/user-guide/core-data-model/virtualization/cluster.html +24 -3
  254. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustergroup.html +24 -3
  255. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustertype.html +24 -3
  256. nautobot/project-static/docs/user-guide/core-data-model/virtualization/virtualmachine.html +24 -3
  257. nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +24 -3
  258. nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +24 -3
  259. nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +24 -3
  260. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +24 -3
  261. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +24 -3
  262. nautobot/project-static/docs/user-guide/feature-guides/getting-started/index.html +24 -3
  263. nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +24 -3
  264. nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +24 -3
  265. nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +24 -3
  266. nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +24 -3
  267. nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +24 -3
  268. nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +24 -3
  269. nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +24 -3
  270. nautobot/project-static/docs/user-guide/feature-guides/graphql.html +24 -3
  271. nautobot/project-static/docs/user-guide/feature-guides/ip-address-merge-tool.html +24 -3
  272. nautobot/project-static/docs/user-guide/feature-guides/relationships.html +24 -3
  273. nautobot/project-static/docs/user-guide/feature-guides/software-image-files-and-versions.html +24 -3
  274. nautobot/project-static/docs/user-guide/index.html +24 -3
  275. nautobot/project-static/docs/user-guide/platform-functionality/change-logging.html +24 -3
  276. nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +24 -3
  277. nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +24 -3
  278. nautobot/project-static/docs/user-guide/platform-functionality/customlink.html +24 -3
  279. nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +24 -3
  280. nautobot/project-static/docs/user-guide/platform-functionality/exporttemplate.html +24 -3
  281. nautobot/project-static/docs/user-guide/platform-functionality/externalintegration.html +24 -3
  282. nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +24 -3
  283. nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +24 -3
  284. nautobot/project-static/docs/user-guide/platform-functionality/graphqlquery.html +24 -3
  285. nautobot/project-static/docs/user-guide/platform-functionality/imageattachment.html +24 -3
  286. nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +24 -3
  287. nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +24 -3
  288. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +24 -3
  289. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +24 -3
  290. nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +24 -3
  291. nautobot/project-static/docs/user-guide/platform-functionality/napalm.html +24 -3
  292. nautobot/project-static/docs/user-guide/platform-functionality/note.html +24 -3
  293. nautobot/project-static/docs/user-guide/platform-functionality/objectmetadata.html +24 -3
  294. nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +24 -3
  295. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +24 -3
  296. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +24 -3
  297. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +24 -3
  298. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/ui-related-endpoints.html +24 -3
  299. nautobot/project-static/docs/user-guide/platform-functionality/role.html +24 -3
  300. nautobot/project-static/docs/user-guide/platform-functionality/savedview.html +24 -3
  301. nautobot/project-static/docs/user-guide/platform-functionality/secret.html +24 -3
  302. nautobot/project-static/docs/user-guide/platform-functionality/staticgroupassociation.html +24 -3
  303. nautobot/project-static/docs/user-guide/platform-functionality/status.html +24 -3
  304. nautobot/project-static/docs/user-guide/platform-functionality/tag.html +24 -3
  305. nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +24 -3
  306. nautobot/project-static/docs/user-guide/platform-functionality/users/objectpermission.html +24 -3
  307. nautobot/project-static/docs/user-guide/platform-functionality/users/token.html +24 -3
  308. nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +24 -3
  309. nautobot/project-static/js/forms.js +41 -5
  310. nautobot/virtualization/tables.py +1 -1
  311. {nautobot-2.3.8.dist-info → nautobot-2.3.9.dist-info}/METADATA +2 -2
  312. {nautobot-2.3.8.dist-info → nautobot-2.3.9.dist-info}/RECORD +316 -315
  313. nautobot/project-static/docs/assets/stylesheets/main.8c3ca2c6.min.css +0 -1
  314. nautobot/project-static/docs/assets/stylesheets/main.8c3ca2c6.min.css.map +0 -1
  315. {nautobot-2.3.8.dist-info → nautobot-2.3.9.dist-info}/LICENSE.txt +0 -0
  316. {nautobot-2.3.8.dist-info → nautobot-2.3.9.dist-info}/NOTICE +0 -0
  317. {nautobot-2.3.8.dist-info → nautobot-2.3.9.dist-info}/WHEEL +0 -0
  318. {nautobot-2.3.8.dist-info → nautobot-2.3.9.dist-info}/entry_points.txt +0 -0
@@ -8,6 +8,7 @@ from django.contrib.contenttypes.models import ContentType
8
8
  from django.core.exceptions import ValidationError
9
9
  from django.core.serializers.json import DjangoJSONEncoder
10
10
  from django.db import models
11
+ from django.db.models.signals import pre_delete
11
12
  from django.utils.functional import cached_property
12
13
  import django_filters
13
14
 
@@ -319,24 +320,18 @@ class DynamicGroup(PrimaryModel):
319
320
  if isinstance(value, models.QuerySet):
320
321
  if value.model != self.model:
321
322
  raise TypeError(f"QuerySet does not contain {self.model._meta.label_lower} objects")
322
- to_remove = self.members.exclude(pk__in=value.values_list("pk", flat=True))
323
+ to_remove = self.members.only("id").difference(value.only("id"))
323
324
  self._remove_members(to_remove)
324
- to_add = value.exclude(pk__in=self.members.values_list("pk", flat=True))
325
+ to_add = value.only("id").difference(self.members.only("id"))
325
326
  self._add_members(to_add)
326
327
  else:
327
328
  for obj in value:
328
329
  if not isinstance(obj, self.model):
329
330
  raise TypeError(f"{obj} is not a {self.model._meta.label_lower}")
330
- to_remove = []
331
- for member in self.members:
332
- if member not in value:
333
- to_remove.append(member)
331
+ existing_members = self.members
332
+ to_remove = [obj for obj in existing_members if obj not in value]
334
333
  self._remove_members(to_remove)
335
- to_add = []
336
- members = self.members
337
- for candidate in value:
338
- if candidate not in members:
339
- to_add.append(candidate)
334
+ to_add = [obj for obj in value if obj not in existing_members]
340
335
  self._add_members(to_add)
341
336
 
342
337
  return self.members
@@ -345,63 +340,81 @@ class DynamicGroup(PrimaryModel):
345
340
  """Add the given list or QuerySet of objects to this staticly defined group."""
346
341
  if self.group_type != DynamicGroupTypeChoices.TYPE_STATIC:
347
342
  raise ValidationError(f"Group {self} is not staticly defined, adding members directly is not permitted.")
348
- return self._add_members(objects_to_add)
349
-
350
- def _add_members(self, objects_to_add):
351
- """Internal API for adding the given list or QuerySet of objects to the cached/static members of this group."""
352
343
  if isinstance(objects_to_add, models.QuerySet):
353
344
  if objects_to_add.model != self.model:
354
345
  raise TypeError(f"QuerySet does not contain {self.model._meta.label_lower} objects")
346
+ objects_to_add = objects_to_add.only("id").difference(self.members.only("id"))
355
347
  else:
356
348
  for obj in objects_to_add:
357
349
  if not isinstance(obj, self.model):
358
350
  raise TypeError(f"{obj} is not a {self.model._meta.label_lower}")
351
+ existing_members = self.members
352
+ objects_to_add = [obj for obj in objects_to_add if obj not in existing_members]
353
+ return self._add_members(objects_to_add)
359
354
 
355
+ def _add_members(self, objects_to_add):
356
+ """
357
+ Internal API for adding the given list or QuerySet of objects to the cached/static members of this group.
358
+
359
+ Assumes that objects_to_add has already been filtered to exclude any existing member objects.
360
+ """
360
361
  if self.group_type == DynamicGroupTypeChoices.TYPE_STATIC:
361
362
  for obj in objects_to_add:
362
363
  # We don't use `.bulk_create()` currently because we want change logging for these creates.
363
364
  # Might be a good future performance improvement though.
364
- StaticGroupAssociation.all_objects.get_or_create(
365
+ StaticGroupAssociation.all_objects.create(
365
366
  dynamic_group=self, associated_object_type=self.content_type, associated_object_id=obj.pk
366
367
  )
367
368
  else:
368
369
  # Cached/hidden static group associations, so we can use bulk-create to bypass change logging.
369
- existing_members = self.members
370
370
  sgas = [
371
371
  StaticGroupAssociation(
372
372
  dynamic_group=self, associated_object_type=self.content_type, associated_object_id=obj.pk
373
373
  )
374
374
  for obj in objects_to_add
375
- if obj not in existing_members
376
375
  ]
377
- StaticGroupAssociation.all_objects.bulk_create(sgas)
376
+ StaticGroupAssociation.all_objects.bulk_create(sgas, batch_size=1000)
378
377
 
379
378
  def remove_members(self, objects_to_remove):
380
379
  """Remove the given list or QuerySet of objects from this staticly defined group."""
381
380
  if self.group_type != DynamicGroupTypeChoices.TYPE_STATIC:
382
381
  raise ValidationError(f"Group {self} is not staticly defined, removing members directly is not permitted.")
383
- return self._remove_members(objects_to_remove)
384
-
385
- def _remove_members(self, objects_to_remove):
386
- """Internal API for removing the given list or QuerySet from the cached/static members of this Group."""
387
382
  if isinstance(objects_to_remove, models.QuerySet):
388
383
  if objects_to_remove.model != self.model:
389
384
  raise TypeError(f"QuerySet does not contain {self.model._meta.label_lower} objects")
390
- StaticGroupAssociation.all_objects.filter(
391
- dynamic_group=self,
392
- associated_object_type=self.content_type,
393
- associated_object_id__in=objects_to_remove.values_list("pk", flat=True),
394
- ).delete()
395
385
  else:
396
- pks_to_remove = set()
397
386
  for obj in objects_to_remove:
398
387
  if not isinstance(obj, self.model):
399
388
  raise TypeError(f"{obj} is not a {self.model._meta.label_lower}")
400
- pks_to_remove.add(obj.pk)
389
+ return self._remove_members(objects_to_remove)
401
390
 
402
- StaticGroupAssociation.all_objects.filter(
403
- dynamic_group=self, associated_object_type=self.content_type, associated_object_id__in=pks_to_remove
404
- ).delete()
391
+ def _remove_members(self, objects_to_remove):
392
+ """Internal API for removing the given list or QuerySet from the cached/static members of this Group."""
393
+ from nautobot.extras.signals import _handle_deleted_object # avoid circular import
394
+
395
+ # For non-static groups, we aren't going to change log the StaticGroupAssociation deletes anyway,
396
+ # so save some performance on signals -- important especially when we're dealing with thousands of records
397
+ if self.group_type != DynamicGroupTypeChoices.TYPE_STATIC:
398
+ logger.debug("Temporarily disconnecting the _handle_deleted_object signal for performance")
399
+ pre_delete.disconnect(_handle_deleted_object)
400
+ try:
401
+ if isinstance(objects_to_remove, models.QuerySet):
402
+ StaticGroupAssociation.all_objects.filter(
403
+ dynamic_group=self,
404
+ associated_object_type=self.content_type,
405
+ associated_object_id__in=objects_to_remove.values_list("id", flat=True),
406
+ ).delete()
407
+ else:
408
+ pks_to_remove = [obj.id for obj in objects_to_remove]
409
+ StaticGroupAssociation.all_objects.filter(
410
+ dynamic_group=self,
411
+ associated_object_type=self.content_type,
412
+ associated_object_id__in=pks_to_remove,
413
+ ).delete()
414
+ finally:
415
+ if self.group_type != DynamicGroupTypeChoices.TYPE_STATIC:
416
+ logger.debug("Re-connecting the _handle_deleted_object signal")
417
+ pre_delete.connect(_handle_deleted_object)
405
418
 
406
419
  @property
407
420
  @method_deprecated("Members are now cached in the database via StaticGroupAssociations rather than in Redis.")
@@ -430,6 +443,7 @@ class DynamicGroup(PrimaryModel):
430
443
  else:
431
444
  raise RuntimeError(f"Unknown/invalid group_type {self.group_type}")
432
445
 
446
+ logger.debug("Refreshing members cache for %s", self)
433
447
  self._set_members(members)
434
448
  logger.debug("Refreshed cache for %s, now with %d members", self, self.count)
435
449
 
@@ -754,7 +754,7 @@ class JobResult(BaseModel, CustomFieldModel):
754
754
  grouping="main",
755
755
  ):
756
756
  """
757
- General-purpose API for storing log messages in a JobResult's 'data' field.
757
+ General-purpose API for creating JobLogEntry records associated with a JobResult.
758
758
 
759
759
  message (str): Message to log (an attempt will be made to sanitize sensitive information from this message)
760
760
  obj (object): Object associated with this message, if any
@@ -94,6 +94,7 @@ class NautobotAppConfig(NautobotConfig):
94
94
  metrics = "metrics.metrics"
95
95
  menu_items = "navigation.menu_items"
96
96
  secrets_providers = "secrets.secrets_providers"
97
+ table_extensions = "table_extensions.table_extensions"
97
98
  template_extensions = "template_content.template_extensions"
98
99
  override_views = "views.override_views"
99
100
 
@@ -211,6 +212,9 @@ class NautobotAppConfig(NautobotConfig):
211
212
  )
212
213
  register_override_views(override_views, self.name)
213
214
 
215
+ # Register tables extensions (if any).
216
+ self._register_table_extensions()
217
+
214
218
  @classmethod
215
219
  def validate(cls, user_config, nautobot_version):
216
220
  """Validate the user_config for baseline correctness."""
@@ -262,6 +266,13 @@ class NautobotAppConfig(NautobotConfig):
262
266
  if setting not in user_config and setting not in cls.constance_config:
263
267
  user_config[setting] = value
264
268
 
269
+ def _register_table_extensions(self):
270
+ """Register tables extensions (if any)."""
271
+ table_extensions = import_object(f"{self.__module__}.{self.table_extensions}")
272
+ if table_extensions is not None:
273
+ register_table_extensions(table_extensions, self.name)
274
+ self.features["table_extensions"] = get_table_extension_features(table_extensions)
275
+
265
276
 
266
277
  @class_deprecated_in_favor_of(NautobotAppConfig)
267
278
  class PluginConfig(NautobotAppConfig):
@@ -491,6 +502,160 @@ def register_filter_extensions(filter_extensions, plugin_name):
491
502
  )
492
503
 
493
504
 
505
+ #
506
+ # Table Extensions
507
+ #
508
+
509
+
510
+ class TableExtension:
511
+ """Template class for extending Tables.
512
+
513
+ An app can override the default columns for a table by either:
514
+ - Extending the original default columns to include custom columns.
515
+ - add_to_default_columns = ("my_app_name_new_column",)
516
+ - Removing native columns from the default columns.
517
+ - remove_from_default_columns = ("tenant",)
518
+ """
519
+
520
+ model = None
521
+ table_columns = {}
522
+ add_to_default_columns = ()
523
+ remove_from_default_columns = ()
524
+
525
+ @classmethod
526
+ def alter_queryset(cls, queryset):
527
+ """Alter the View class QuerySet.
528
+
529
+ This is a good place to add `prefetch_related` to the view queryset.
530
+ example:
531
+ return queryset.prefetch_related("my_model_set")
532
+ """
533
+ return queryset
534
+
535
+ @classmethod
536
+ def _get_table_columns_registrations(cls):
537
+ """Return a list of register labels fro each column."""
538
+ if not cls.table_columns:
539
+ return []
540
+ return [f"{cls.model} -> {column_name}" for column_name in cls.table_columns]
541
+
542
+ @classmethod
543
+ def _get_add_to_default_columns_registrations(cls):
544
+ """Return a list of register labels for each column added to defaults."""
545
+ if not cls.add_to_default_columns:
546
+ return []
547
+ return [f"{cls.model} -> {cls.add_to_default_columns}"]
548
+
549
+ @classmethod
550
+ def _get_remove_from_default_columns_registrations(cls):
551
+ """Return a list of register labels for each column removed from defaults."""
552
+ if not cls.remove_from_default_columns:
553
+ return []
554
+ return [f"{cls.model} -> {cls.remove_from_default_columns}"]
555
+
556
+
557
+ def get_table_extension_features(table_extensions):
558
+ """Return a dictionary of TableExtension features for the App detail view."""
559
+ return {
560
+ "columns": [
561
+ label
562
+ for table_extension in table_extensions
563
+ for label in table_extension._get_table_columns_registrations()
564
+ ],
565
+ "add_to_default_columns": [
566
+ label
567
+ for table_extension in table_extensions
568
+ for label in table_extension._get_add_to_default_columns_registrations()
569
+ ],
570
+ "remove_from_default_columns": [
571
+ label
572
+ for table_extension in table_extensions
573
+ for label in table_extension._get_remove_from_default_columns_registrations()
574
+ ],
575
+ }
576
+
577
+
578
+ def register_table_extensions(table_extensions, app_name):
579
+ """Register a list of TableExtension classes."""
580
+ for table_extension in table_extensions:
581
+ _validate_is_subclass_of_table_extension(table_extension)
582
+ _add_columns_into_model_table(table_extension, app_name)
583
+ _modify_default_table_columns(table_extension, app_name)
584
+ _alter_table_view_queryset(table_extension, app_name)
585
+
586
+
587
+ def _add_columns_into_model_table(table_extension, app_name):
588
+ """Inject each new column into the Model Table."""
589
+ from nautobot.core.utils.lookup import get_table_for_model
590
+
591
+ if not isinstance(table_extension.table_columns, dict):
592
+ error = f"{app_name} TableExtension: 'table_columns' attribute must be of type 'dict'."
593
+ logger.error(error)
594
+ return
595
+
596
+ table = get_table_for_model(table_extension.model)
597
+ for name, column in table_extension.table_columns.items():
598
+ _validate_table_column_name_is_prefixed_with_app_name(name, app_name)
599
+ _add_column_to_table_base_columns(table, name, column, app_name)
600
+
601
+
602
+ def _add_column_to_table_base_columns(table, column_name, column, app_name):
603
+ """Attach a column to an existing table."""
604
+ import django_tables2
605
+
606
+ if not isinstance(column, django_tables2.Column):
607
+ raise TypeError(f"Custom column `{column_name}` is not an instance of django_tables2.Column.")
608
+
609
+ if column_name in table.base_columns:
610
+ logger.error(
611
+ f"{app_name}: There was a name conflict with existing table column `{column_name}`, the custom column was ignored."
612
+ )
613
+ else:
614
+ table.base_columns[column_name] = column
615
+
616
+
617
+ def _alter_table_view_queryset(table_extension, app_name):
618
+ """Replace the model view queryset with an optimized queryset from the app."""
619
+ from nautobot.core.utils.lookup import get_view_for_model
620
+
621
+ # TODO: Investigate if there is a more targeted way to patch only the list view queryset
622
+ # when targeting a subclass of `NautobotUIViewSet`.
623
+ view = get_view_for_model(table_extension.model, view_type="List")
624
+ view.queryset = table_extension.alter_queryset(view.queryset)
625
+
626
+
627
+ def _modify_default_table_columns(table_extension, app_name):
628
+ """Add or remove columns from the table default columns."""
629
+ from nautobot.core.utils.lookup import get_table_for_model
630
+
631
+ table = get_table_for_model(table_extension.model)
632
+ message = (
633
+ f"{app_name}: Cannot {{action}} column `{{column_name}}` {{preposition}} the default columns for `{table}`."
634
+ )
635
+
636
+ for column_name in table_extension.add_to_default_columns:
637
+ if column_name in table.base_columns:
638
+ table.Meta.default_columns = (*table.Meta.default_columns, column_name)
639
+ else:
640
+ logger.debug(message.format(action="add", column_name=column_name, preposition="to"))
641
+
642
+ for column_name in table_extension.remove_from_default_columns:
643
+ if column_name in table.Meta.default_columns:
644
+ table.Meta.default_columns = tuple(name for name in table.Meta.default_columns if name != column_name)
645
+ else:
646
+ logger.debug(message.format(action="remove", column_name=column_name, preposition="from"))
647
+
648
+
649
+ def _validate_is_subclass_of_table_extension(table_extension):
650
+ if not issubclass(table_extension, TableExtension):
651
+ raise TypeError(f"{table_extension} is not a subclass of nautobot.apps.filters.TableExtension!")
652
+
653
+
654
+ def _validate_table_column_name_is_prefixed_with_app_name(name, app_name):
655
+ if not name.startswith(f"{app_name}_"):
656
+ raise ValueError(f"Attempted to create a custom table column `{name}` that did not start with `{app_name}`")
657
+
658
+
494
659
  #
495
660
  # Navigation menu links
496
661
  #
@@ -277,6 +277,39 @@
277
277
  {% endif %}
278
278
  </td>
279
279
  </tr>
280
+ <tr>
281
+ <td>Table Extensions</td>
282
+ <td>
283
+ {% if features.table_extensions %}
284
+ {% if features.table_extensions.columns %}
285
+ <b>Custom Columns</b>
286
+ <ul class="list-unstyled">
287
+ {% for column in features.table_extensions.columns %}
288
+ <li><code>{{ column }}</code></li>
289
+ {% endfor %}
290
+ </ul>
291
+ {% endif %}
292
+ {% if features.table_extensions.add_to_default_columns %}
293
+ <b>Additional Default Columns</b>
294
+ <ul class="list-unstyled">
295
+ {% for column in features.table_extensions.add_to_default_columns %}
296
+ <li><code>{{ column }}</code></li>
297
+ {% endfor %}
298
+ </ul>
299
+ {% endif %}
300
+ {% if features.table_extensions.remove_from_default_columns %}
301
+ <b>Remove from Default Columns</b>
302
+ <ul class="list-unstyled">
303
+ {% for column in features.table_extensions.remove_from_default_columns %}
304
+ <li><code>{{ column }}</code></li>
305
+ {% endfor %}
306
+ </ul>
307
+ {% endif %}
308
+ {% else %}
309
+ {% include 'utilities/render_boolean.html' with value=features.table_extensions %}
310
+ {% endif %}
311
+ </td>
312
+ </tr>
280
313
  <tr>
281
314
  <td>Views/URLs</td>
282
315
  <td>
@@ -5,7 +5,7 @@ from django.contrib.contenttypes.models import ContentType
5
5
  from django.test import TestCase
6
6
 
7
7
  from nautobot.core.celery import app
8
- from nautobot.core.testing import TransactionTestCase
8
+ from nautobot.core.testing import get_job_class_and_model, TransactionTestCase
9
9
  from nautobot.core.utils.lookup import get_changes_for_model
10
10
  from nautobot.dcim.models import (
11
11
  DeviceType,
@@ -22,7 +22,7 @@ from nautobot.extras.context_managers import (
22
22
  deferred_change_logging_for_bulk_operation,
23
23
  web_request_context,
24
24
  )
25
- from nautobot.extras.models import Status, Webhook
25
+ from nautobot.extras.models import JobHook, Status, Webhook
26
26
  from nautobot.extras.utils import bulk_delete_with_bulk_change_logging
27
27
 
28
28
  # Use the proper swappable User model
@@ -74,7 +74,7 @@ class WebRequestContextTestCase(TestCase):
74
74
  self.assertEqual(oc_list[0].changed_object, location)
75
75
  self.assertEqual(oc_list[0].action, ObjectChangeActionChoices.ACTION_CREATE)
76
76
 
77
- @mock.patch("nautobot.extras.jobs.enqueue_job_hooks")
77
+ @mock.patch("nautobot.extras.jobs.enqueue_job_hooks", return_value=True)
78
78
  @mock.patch("nautobot.extras.context_managers.enqueue_webhooks")
79
79
  def test_create_then_delete(self, mock_enqueue_webhooks, mock_enqueue_job_hooks):
80
80
  """Test that a create followed by a delete is logged as two changes"""
@@ -88,11 +88,13 @@ class WebRequestContextTestCase(TestCase):
88
88
 
89
89
  location = Location.objects.filter(pk=location_pk)
90
90
  self.assertFalse(location.exists())
91
- oc_list = get_changes_for_model(Location).filter(changed_object_id=location_pk)
91
+ oc_list = get_changes_for_model(Location).filter(changed_object_id=location_pk).order_by("time")
92
92
  self.assertEqual(len(oc_list), 2)
93
- self.assertEqual(oc_list[0].action, ObjectChangeActionChoices.ACTION_DELETE)
94
- self.assertEqual(oc_list[1].action, ObjectChangeActionChoices.ACTION_CREATE)
95
- mock_enqueue_job_hooks.assert_has_calls([mock.call(oc_list[0]), mock.call(oc_list[1])])
93
+ self.assertEqual(oc_list[0].action, ObjectChangeActionChoices.ACTION_CREATE)
94
+ self.assertEqual(oc_list[1].action, ObjectChangeActionChoices.ACTION_DELETE)
95
+ mock_enqueue_job_hooks.assert_has_calls(
96
+ [mock.call(oc_list[0], may_reload_jobs=True), mock.call(oc_list[1], may_reload_jobs=False)],
97
+ )
96
98
  mock_enqueue_webhooks.assert_has_calls([mock.call(oc_list[0]), mock.call(oc_list[1])])
97
99
 
98
100
  def test_update_then_delete(self):
@@ -307,6 +309,13 @@ class BulkEditDeleteChangeLogging(TestCase):
307
309
  for i in range(1, 4)
308
310
  ]
309
311
  Location.objects.bulk_create(locations)
312
+ # Create a JobHook that applies to Locations
313
+ _, job_model = get_job_class_and_model("job_hook_receiver", "TestJobHookReceiverLog")
314
+ mock_import_jobs.assert_called_once()
315
+ mock_import_jobs.reset_mock()
316
+ job_hook = JobHook.objects.create(name="JobHookTest", type_update=True, job=job_model)
317
+ job_hook.content_types.set([ContentType.objects.get_for_model(Location)])
318
+
310
319
  pk_list = []
311
320
  with web_request_context(self.user):
312
321
  with deferred_change_logging_for_bulk_operation():
@@ -412,7 +412,7 @@ class GitTest(TransactionTestCase):
412
412
 
413
413
  def test_pull_git_repository_and_refresh_data_with_bad_data(self):
414
414
  """
415
- The test_pull_git_repository_and_refresh_data job should gracefully handle bad data in the Git repository
415
+ The test_pull_git_repository_and_refresh_data job should gracefully handle bad data in the Git repository.
416
416
  """
417
417
  with tempfile.TemporaryDirectory() as tempdir:
418
418
  with self.settings(GIT_ROOT=tempdir):
@@ -433,6 +433,18 @@ class GitTest(TransactionTestCase):
433
433
  job_result.result,
434
434
  )
435
435
 
436
+ # Due to transaction rollback on failure, the database should still/again match the pre-sync state, of
437
+ # no records owned by the repository.
438
+ self.assertFalse(ConfigContextSchema.objects.filter(owner_object_id=self.repo.id).exists())
439
+ self.assertFalse(ConfigContext.objects.filter(owner_object_id=self.repo.id).exists())
440
+ self.assertFalse(ExportTemplate.objects.filter(owner_object_id=self.repo.id).exists())
441
+ self.assertFalse(Job.objects.filter(module_name__startswith=f"{self.repo.slug}.").exists())
442
+ device = Device.objects.get(name=self.device.name)
443
+ self.assertIsNone(device.local_config_context_data)
444
+ self.assertIsNone(device.local_config_context_data_owner)
445
+ self.repo.refresh_from_db()
446
+ self.assertEqual(self.repo.current_head, "")
447
+
436
448
  # Check for specific log messages
437
449
  log_entries = JobLogEntry.objects.filter(job_result=job_result)
438
450
  warning_logs = log_entries.filter(log_level=LogLevelChoices.LOG_WARNING)
@@ -592,6 +604,81 @@ class GitTest(TransactionTestCase):
592
604
 
593
605
  self.assert_job_exists(installed=False)
594
606
 
607
+ def test_git_repository_sync_rollback(self):
608
+ """
609
+ Once a "known-good" sync state is achieved, resync to a new "bad" head commit should fail and be rolled back.
610
+ """
611
+ with tempfile.TemporaryDirectory() as tempdir:
612
+ with self.settings(GIT_ROOT=tempdir):
613
+ # Initially have a successful sync to a good commit that provides data
614
+ self.repo.branch = "valid-files" # actually a tag
615
+ self.repo.save()
616
+ job_model = GitRepositorySync().job_model
617
+ job_result = run_job_for_testing(job=job_model, repository=self.repo.pk)
618
+ job_result.refresh_from_db()
619
+ self.assertEqual(
620
+ job_result.status,
621
+ JobResultStatusChoices.STATUS_SUCCESS,
622
+ (job_result.traceback, list(job_result.job_log_entries.values_list("message", flat=True))),
623
+ )
624
+
625
+ self.assert_explicit_config_context_exists("Frobozz 1000 NTP servers")
626
+ self.assert_implicit_config_context_exists("Location context")
627
+ self.assert_config_context_schema_record_exists("Config Context Schema 1")
628
+ self.assert_device_exists(self.device.name)
629
+ self.assert_export_template_device("template.j2")
630
+ self.assert_export_template_html_exist("template2.html")
631
+ self.assert_export_template_vlan_exists("template.j2")
632
+ self.assert_job_exists(name="MyJob")
633
+ self.assert_job_exists(name="MyJobButtonReceiver")
634
+ self.assert_job_exists(name="MyJobHookReceiver")
635
+
636
+ # Create JobButton and JobHook
637
+ JobButton.objects.create(
638
+ name="MyJobButton", enabled=True, text="Click me", job=Job.objects.get(name="MyJobButtonReceiver")
639
+ )
640
+ JobHook.objects.create(name="MyJobHook", enabled=True, job=Job.objects.get(name="MyJobHookReceiver"))
641
+
642
+ self.repo.refresh_from_db()
643
+ self.assertNotEqual(self.repo.current_head, "")
644
+ good_current_head = self.repo.current_head
645
+
646
+ # Now change to the `main` branch (which includes the current commit, followed by a "bad" commit)
647
+ self.repo.branch = "main"
648
+ self.repo.save()
649
+
650
+ # Resync, attempting and failing to update to the new commit
651
+ job_result = run_job_for_testing(job=job_model, repository=self.repo.pk)
652
+ job_result.refresh_from_db()
653
+ self.assertEqual(
654
+ job_result.status,
655
+ JobResultStatusChoices.STATUS_FAILURE,
656
+ job_result.result,
657
+ )
658
+ log_entries = JobLogEntry.objects.filter(job_result=job_result)
659
+
660
+ # Assert database changes were rolled back
661
+ self.repo.refresh_from_db()
662
+ try:
663
+ self.assertEqual(self.repo.current_head, good_current_head)
664
+ self.assert_explicit_config_context_exists("Frobozz 1000 NTP servers")
665
+ self.assert_implicit_config_context_exists("Location context")
666
+ self.assert_config_context_schema_record_exists("Config Context Schema 1")
667
+ self.assert_device_exists(self.device.name)
668
+ self.assert_export_template_device("template.j2")
669
+ self.assert_export_template_html_exist("template2.html")
670
+ self.assert_export_template_vlan_exists("template.j2")
671
+ self.assert_job_exists(name="MyJob")
672
+ self.assert_job_exists(name="MyJobButtonReceiver")
673
+ self.assert_job_exists(name="MyJobHookReceiver")
674
+ self.assertTrue(JobButton.objects.get(name="MyJobButton").enabled)
675
+ self.assertTrue(JobHook.objects.get(name="MyJobHook").enabled)
676
+ except Exception:
677
+ for log in log_entries:
678
+ print(log.message)
679
+ print(job_result.traceback)
680
+ raise
681
+
595
682
  def test_git_dry_run(self):
596
683
  with tempfile.TemporaryDirectory() as tempdir:
597
684
  with self.settings(GIT_ROOT=tempdir):
@@ -304,6 +304,10 @@ class DynamicGroupModelTest(DynamicGroupTestBase): # TODO: BaseModelTestCase mi
304
304
  sg.add_members(Prefix.objects.filter(ip_version=4))
305
305
  self.assertIsInstance(sg.members, PrefixQuerySet)
306
306
  self.assertQuerysetEqualAndNotEmpty(sg.members, Prefix.objects.filter(ip_version=4))
307
+ # test cumulative construction and alternate code path
308
+ sg.add_members(list(Prefix.objects.filter(ip_version=6)))
309
+ self.assertQuerysetEqualAndNotEmpty(sg.members, Prefix.objects.all())
310
+ self.assertEqual(sg.static_group_associations.count(), Prefix.objects.all().count())
307
311
  # test duplicate objects aren't re-added
308
312
  sg.add_members(Prefix.objects.all())
309
313
  self.assertQuerysetEqualAndNotEmpty(sg.members, Prefix.objects.all())
@@ -321,10 +325,18 @@ class DynamicGroupModelTest(DynamicGroupTestBase): # TODO: BaseModelTestCase mi
321
325
  sg.remove_members(list(Prefix.objects.filter(ip_version=4)))
322
326
  self.assertQuerysetEqualAndNotEmpty(sg.members, Prefix.objects.filter(ip_version=6))
323
327
  self.assertEqual(sg.static_group_associations.count(), Prefix.objects.filter(ip_version=6).count())
328
+ # test cumulative removal and alternate code path
329
+ sg.remove_members(list(Prefix.objects.filter(ip_version=6)))
330
+ self.assertQuerysetEqual(sg.members, Prefix.objects.none())
331
+ self.assertEqual(sg.static_group_associations.count(), 0)
324
332
 
325
333
  # test property setter
326
334
  sg.members = Prefix.objects.filter(ip_version=4)
327
335
  self.assertQuerysetEqualAndNotEmpty(sg.members, Prefix.objects.filter(ip_version=4))
336
+ sg.members = Prefix.objects.filter(ip_version=6)
337
+ self.assertQuerysetEqualAndNotEmpty(sg.members, Prefix.objects.filter(ip_version=6))
338
+ sg.members = list(Prefix.objects.filter(ip_version=4))
339
+ self.assertQuerysetEqualAndNotEmpty(sg.members, Prefix.objects.filter(ip_version=4))
328
340
  sg.members = list(Prefix.objects.filter(ip_version=6))
329
341
  self.assertQuerysetEqualAndNotEmpty(sg.members, Prefix.objects.filter(ip_version=6))
330
342