nautobot 2.3.1__py3-none-any.whl → 2.3.3__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.
Files changed (339) hide show
  1. nautobot/core/celery/schedulers.py +18 -0
  2. nautobot/core/settings.yaml +3 -3
  3. nautobot/core/tables.py +1 -1
  4. nautobot/core/templates/home.html +4 -3
  5. nautobot/core/templatetags/buttons.py +1 -1
  6. nautobot/core/tests/runner.py +27 -9
  7. nautobot/core/tests/test_utils.py +13 -0
  8. nautobot/core/utils/lookup.py +7 -1
  9. nautobot/core/views/utils.py +3 -3
  10. nautobot/dcim/factory.py +3 -3
  11. nautobot/dcim/tables/devices.py +7 -7
  12. nautobot/dcim/templates/dcim/device.html +12 -0
  13. nautobot/dcim/templates/dcim/softwareimagefile_retrieve.html +12 -0
  14. nautobot/dcim/utils.py +9 -6
  15. nautobot/extras/api/serializers.py +2 -0
  16. nautobot/extras/context_managers.py +11 -4
  17. nautobot/extras/filters/__init__.py +14 -2
  18. nautobot/extras/forms/forms.py +6 -0
  19. nautobot/extras/forms/mixins.py +2 -2
  20. nautobot/extras/jobs.py +0 -1
  21. nautobot/extras/management/__init__.py +3 -0
  22. nautobot/extras/migrations/0115_scheduledjob_time_zone.py +23 -0
  23. nautobot/extras/models/groups.py +4 -1
  24. nautobot/extras/models/jobs.py +24 -11
  25. nautobot/extras/tables.py +34 -4
  26. nautobot/extras/templates/extras/scheduledjob.html +13 -2
  27. nautobot/extras/tests/test_api.py +17 -18
  28. nautobot/extras/tests/test_context_managers.py +33 -14
  29. nautobot/extras/tests/test_dynamicgroups.py +11 -0
  30. nautobot/extras/tests/test_filters.py +57 -1
  31. nautobot/extras/tests/test_models.py +304 -1
  32. nautobot/extras/tests/test_views.py +4 -2
  33. nautobot/extras/views.py +7 -0
  34. nautobot/ipam/api/views.py +9 -2
  35. nautobot/ipam/choices.py +17 -0
  36. nautobot/ipam/factory.py +6 -0
  37. nautobot/ipam/filters.py +1 -1
  38. nautobot/ipam/forms.py +5 -3
  39. nautobot/ipam/migrations/0048_vrf_status.py +23 -0
  40. nautobot/ipam/migrations/0049_vrf_data_migration.py +25 -0
  41. nautobot/ipam/models.py +6 -0
  42. nautobot/ipam/tables.py +3 -2
  43. nautobot/ipam/templates/ipam/vrf.html +4 -0
  44. nautobot/ipam/templates/ipam/vrf_edit.html +1 -0
  45. nautobot/ipam/tests/test_api.py +44 -3
  46. nautobot/ipam/tests/test_views.py +3 -0
  47. nautobot/project-static/css/base.css +6 -0
  48. nautobot/project-static/docs/404.html +23 -23
  49. nautobot/project-static/docs/apps/index.html +25 -25
  50. nautobot/project-static/docs/apps/nautobot-apps.html +24 -24
  51. nautobot/project-static/docs/assets/javascripts/bundle.56dfad97.min.js +16 -0
  52. nautobot/project-static/docs/assets/javascripts/bundle.56dfad97.min.js.map +7 -0
  53. nautobot/project-static/docs/assets/javascripts/workers/{search.b8dbb3d2.min.js → search.07f07601.min.js} +1 -1
  54. nautobot/project-static/docs/assets/javascripts/workers/{search.b8dbb3d2.min.js.map → search.07f07601.min.js.map} +1 -1
  55. nautobot/project-static/docs/assets/stylesheets/main.35f28582.min.css +1 -0
  56. nautobot/project-static/docs/assets/stylesheets/main.35f28582.min.css.map +1 -0
  57. nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +26 -26
  58. nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +26 -26
  59. nautobot/project-static/docs/code-reference/nautobot/apps/api.html +58 -58
  60. nautobot/project-static/docs/code-reference/nautobot/apps/change_logging.html +32 -31
  61. nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +31 -31
  62. nautobot/project-static/docs/code-reference/nautobot/apps/config.html +25 -25
  63. nautobot/project-static/docs/code-reference/nautobot/apps/constants.html +25 -25
  64. nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +30 -30
  65. nautobot/project-static/docs/code-reference/nautobot/apps/exceptions.html +34 -34
  66. nautobot/project-static/docs/code-reference/nautobot/apps/factory.html +41 -41
  67. nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +48 -48
  68. nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +92 -92
  69. nautobot/project-static/docs/code-reference/nautobot/apps/graphql.html +41 -41
  70. nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +85 -85
  71. nautobot/project-static/docs/code-reference/nautobot/apps/models.html +84 -84
  72. nautobot/project-static/docs/code-reference/nautobot/apps/querysets.html +26 -26
  73. nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +28 -28
  74. nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +40 -40
  75. nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +78 -78
  76. nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +77 -77
  77. nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +26 -26
  78. nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +80 -80
  79. nautobot/project-static/docs/code-reference/nautobot/apps/views.html +67 -67
  80. nautobot/project-static/docs/development/apps/api/configuration-view.html +25 -25
  81. nautobot/project-static/docs/development/apps/api/database-backend-config.html +25 -25
  82. nautobot/project-static/docs/development/apps/api/models/django-admin.html +25 -25
  83. nautobot/project-static/docs/development/apps/api/models/global-search.html +25 -25
  84. nautobot/project-static/docs/development/apps/api/models/graphql.html +25 -25
  85. nautobot/project-static/docs/development/apps/api/models/index.html +25 -25
  86. nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +25 -25
  87. nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +25 -25
  88. nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +25 -25
  89. nautobot/project-static/docs/development/apps/api/platform-features/git-repository-content.html +25 -25
  90. nautobot/project-static/docs/development/apps/api/platform-features/index.html +25 -25
  91. nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +25 -25
  92. nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +25 -25
  93. nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +25 -25
  94. nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +25 -25
  95. nautobot/project-static/docs/development/apps/api/platform-features/uniquely-identify-objects.html +25 -25
  96. nautobot/project-static/docs/development/apps/api/prometheus.html +25 -25
  97. nautobot/project-static/docs/development/apps/api/setup.html +25 -25
  98. nautobot/project-static/docs/development/apps/api/testing.html +25 -25
  99. nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +25 -25
  100. nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +25 -25
  101. nautobot/project-static/docs/development/apps/api/ui-extensions/index.html +25 -25
  102. nautobot/project-static/docs/development/apps/api/ui-extensions/navigation.html +25 -25
  103. nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +25 -25
  104. nautobot/project-static/docs/development/apps/api/views/base-template.html +25 -25
  105. nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +25 -25
  106. nautobot/project-static/docs/development/apps/api/views/django-generic-views.html +25 -25
  107. nautobot/project-static/docs/development/apps/api/views/help-documentation.html +25 -25
  108. nautobot/project-static/docs/development/apps/api/views/index.html +25 -25
  109. nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +25 -25
  110. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +25 -25
  111. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +25 -25
  112. nautobot/project-static/docs/development/apps/api/views/notes.html +25 -25
  113. nautobot/project-static/docs/development/apps/api/views/rest-api.html +25 -25
  114. nautobot/project-static/docs/development/apps/api/views/urls.html +25 -25
  115. nautobot/project-static/docs/development/apps/index.html +25 -25
  116. nautobot/project-static/docs/development/apps/migration/code-updates.html +25 -25
  117. nautobot/project-static/docs/development/apps/migration/dependency-updates.html +25 -25
  118. nautobot/project-static/docs/development/apps/migration/from-v1.html +25 -25
  119. nautobot/project-static/docs/development/apps/migration/model-updates/dcim.html +25 -25
  120. nautobot/project-static/docs/development/apps/migration/model-updates/extras.html +25 -25
  121. nautobot/project-static/docs/development/apps/migration/model-updates/global.html +25 -25
  122. nautobot/project-static/docs/development/apps/migration/model-updates/ipam.html +25 -25
  123. nautobot/project-static/docs/development/apps/porting-from-netbox.html +25 -25
  124. nautobot/project-static/docs/development/core/application-registry.html +25 -25
  125. nautobot/project-static/docs/development/core/best-practices.html +25 -25
  126. nautobot/project-static/docs/development/core/bootstrap-ui.html +25 -25
  127. nautobot/project-static/docs/development/core/caching.html +25 -25
  128. nautobot/project-static/docs/development/core/controllers.html +25 -25
  129. nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +25 -25
  130. nautobot/project-static/docs/development/core/generic-views.html +25 -25
  131. nautobot/project-static/docs/development/core/getting-started.html +25 -25
  132. nautobot/project-static/docs/development/core/homepage.html +25 -25
  133. nautobot/project-static/docs/development/core/index.html +25 -25
  134. nautobot/project-static/docs/development/core/model-checklist.html +25 -25
  135. nautobot/project-static/docs/development/core/model-features.html +25 -25
  136. nautobot/project-static/docs/development/core/natural-keys.html +25 -25
  137. nautobot/project-static/docs/development/core/navigation-menu.html +25 -25
  138. nautobot/project-static/docs/development/core/release-checklist.html +25 -25
  139. nautobot/project-static/docs/development/core/role-internals.html +25 -25
  140. nautobot/project-static/docs/development/core/settings.html +25 -25
  141. nautobot/project-static/docs/development/core/style-guide.html +25 -25
  142. nautobot/project-static/docs/development/core/templates.html +25 -25
  143. nautobot/project-static/docs/development/core/testing.html +25 -25
  144. nautobot/project-static/docs/development/core/user-preferences.html +25 -25
  145. nautobot/project-static/docs/development/index.html +25 -25
  146. nautobot/project-static/docs/development/jobs/index.html +25 -25
  147. nautobot/project-static/docs/development/jobs/migration/from-v1.html +25 -25
  148. nautobot/project-static/docs/index.html +30 -30
  149. nautobot/project-static/docs/overview/application_stack.html +31 -31
  150. nautobot/project-static/docs/overview/design_philosophy.html +25 -25
  151. nautobot/project-static/docs/release-notes/index.html +25 -25
  152. nautobot/project-static/docs/release-notes/version-1.0.html +25 -25
  153. nautobot/project-static/docs/release-notes/version-1.1.html +25 -25
  154. nautobot/project-static/docs/release-notes/version-1.2.html +25 -25
  155. nautobot/project-static/docs/release-notes/version-1.3.html +25 -25
  156. nautobot/project-static/docs/release-notes/version-1.4.html +25 -25
  157. nautobot/project-static/docs/release-notes/version-1.5.html +25 -25
  158. nautobot/project-static/docs/release-notes/version-1.6.html +25 -25
  159. nautobot/project-static/docs/release-notes/version-2.0.html +25 -25
  160. nautobot/project-static/docs/release-notes/version-2.1.html +25 -25
  161. nautobot/project-static/docs/release-notes/version-2.2.html +25 -25
  162. nautobot/project-static/docs/release-notes/version-2.3.html +318 -61
  163. nautobot/project-static/docs/search/search_index.json +1 -1
  164. nautobot/project-static/docs/sitemap.xml +271 -542
  165. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  166. nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +25 -25
  167. nautobot/project-static/docs/user-guide/administration/configuration/authentication/remote.html +25 -25
  168. nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +25 -25
  169. nautobot/project-static/docs/user-guide/administration/configuration/index.html +25 -25
  170. nautobot/project-static/docs/user-guide/administration/configuration/optional-settings.html +28 -28
  171. nautobot/project-static/docs/user-guide/administration/configuration/required-settings.html +25 -25
  172. nautobot/project-static/docs/user-guide/administration/configuration/time-zones.html +25 -25
  173. nautobot/project-static/docs/user-guide/administration/guides/caching.html +25 -25
  174. nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +25 -25
  175. nautobot/project-static/docs/user-guide/administration/guides/healthcheck.html +25 -25
  176. nautobot/project-static/docs/user-guide/administration/guides/permissions.html +25 -25
  177. nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +25 -25
  178. nautobot/project-static/docs/user-guide/administration/guides/replicating-nautobot.html +25 -25
  179. nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +25 -25
  180. nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +25 -25
  181. nautobot/project-static/docs/user-guide/administration/installation/app-install.html +25 -25
  182. nautobot/project-static/docs/user-guide/administration/installation/external-authentication.html +25 -25
  183. nautobot/project-static/docs/user-guide/administration/installation/http-server.html +25 -25
  184. nautobot/project-static/docs/user-guide/administration/installation/index.html +25 -25
  185. nautobot/project-static/docs/user-guide/administration/installation/install_system.html +25 -25
  186. nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +25 -25
  187. nautobot/project-static/docs/user-guide/administration/installation/services.html +25 -25
  188. nautobot/project-static/docs/user-guide/administration/installation-extras/docker.html +25 -25
  189. nautobot/project-static/docs/user-guide/administration/installation-extras/health-checks.html +25 -25
  190. nautobot/project-static/docs/user-guide/administration/installation-extras/selinux-troubleshooting.html +25 -25
  191. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-netbox.html +25 -25
  192. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +25 -25
  193. nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +25 -25
  194. nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +25 -25
  195. nautobot/project-static/docs/user-guide/administration/upgrading/database-backup.html +25 -25
  196. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/after-you-upgrade.html +25 -25
  197. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/before-you-upgrade.html +25 -25
  198. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/for-developers.html +25 -25
  199. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/index.html +25 -25
  200. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/whats-changed.html +25 -25
  201. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/region-and-site-data-migration-guide.html +25 -25
  202. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +25 -25
  203. nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +25 -25
  204. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuit.html +25 -25
  205. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittermination.html +25 -25
  206. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittype.html +25 -25
  207. nautobot/project-static/docs/user-guide/core-data-model/circuits/provider.html +25 -25
  208. nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +25 -25
  209. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloud.html +25 -25
  210. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudaccount.html +25 -25
  211. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetwork.html +25 -25
  212. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetworkprefixassignment.html +25 -25
  213. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudresourcetype.html +25 -25
  214. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservice.html +25 -25
  215. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservicenetworkassignment.html +25 -25
  216. nautobot/project-static/docs/user-guide/core-data-model/dcim/cable.html +25 -25
  217. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +25 -25
  218. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +25 -25
  219. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +25 -25
  220. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +25 -25
  221. nautobot/project-static/docs/user-guide/core-data-model/dcim/controller.html +25 -25
  222. nautobot/project-static/docs/user-guide/core-data-model/dcim/controllermanageddevicegroup.html +25 -25
  223. nautobot/project-static/docs/user-guide/core-data-model/dcim/device.html +25 -25
  224. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +25 -25
  225. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +25 -25
  226. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicefamily.html +25 -25
  227. nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +25 -25
  228. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +25 -25
  229. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +25 -25
  230. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +25 -25
  231. nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +25 -25
  232. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +25 -25
  233. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +25 -25
  234. nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +25 -25
  235. nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +25 -25
  236. nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +25 -25
  237. nautobot/project-static/docs/user-guide/core-data-model/dcim/manufacturer.html +25 -25
  238. nautobot/project-static/docs/user-guide/core-data-model/dcim/module.html +25 -25
  239. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebay.html +25 -25
  240. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebaytemplate.html +25 -25
  241. nautobot/project-static/docs/user-guide/core-data-model/dcim/moduletype.html +25 -25
  242. nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +25 -25
  243. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerfeed.html +25 -25
  244. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +25 -25
  245. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +25 -25
  246. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerpanel.html +25 -25
  247. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +25 -25
  248. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +25 -25
  249. nautobot/project-static/docs/user-guide/core-data-model/dcim/rack.html +25 -25
  250. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackgroup.html +25 -25
  251. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackreservation.html +25 -25
  252. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +25 -25
  253. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +25 -25
  254. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +25 -25
  255. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareversion.html +25 -25
  256. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualchassis.html +25 -25
  257. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +25 -25
  258. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +25 -25
  259. nautobot/project-static/docs/user-guide/core-data-model/extras/contact.html +25 -25
  260. nautobot/project-static/docs/user-guide/core-data-model/extras/team.html +25 -25
  261. nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +25 -25
  262. nautobot/project-static/docs/user-guide/core-data-model/ipam/namespace.html +25 -25
  263. nautobot/project-static/docs/user-guide/core-data-model/ipam/prefix.html +25 -25
  264. nautobot/project-static/docs/user-guide/core-data-model/ipam/rir.html +25 -25
  265. nautobot/project-static/docs/user-guide/core-data-model/ipam/routetarget.html +25 -25
  266. nautobot/project-static/docs/user-guide/core-data-model/ipam/service.html +25 -25
  267. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +25 -25
  268. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlangroup.html +25 -25
  269. nautobot/project-static/docs/user-guide/core-data-model/ipam/vrf.html +25 -25
  270. nautobot/project-static/docs/user-guide/core-data-model/overview/introduction.html +25 -25
  271. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenant.html +25 -25
  272. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenantgroup.html +25 -25
  273. nautobot/project-static/docs/user-guide/core-data-model/virtualization/cluster.html +25 -25
  274. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustergroup.html +25 -25
  275. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustertype.html +25 -25
  276. nautobot/project-static/docs/user-guide/core-data-model/virtualization/virtualmachine.html +25 -25
  277. nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +25 -25
  278. nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +25 -25
  279. nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +25 -25
  280. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +25 -25
  281. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +25 -25
  282. nautobot/project-static/docs/user-guide/feature-guides/getting-started/index.html +25 -25
  283. nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +25 -25
  284. nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +25 -25
  285. nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +25 -25
  286. nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +25 -25
  287. nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +25 -25
  288. nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +25 -25
  289. nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +25 -25
  290. nautobot/project-static/docs/user-guide/feature-guides/graphql.html +25 -25
  291. nautobot/project-static/docs/user-guide/feature-guides/ip-address-merge-tool.html +25 -25
  292. nautobot/project-static/docs/user-guide/feature-guides/relationships.html +25 -25
  293. nautobot/project-static/docs/user-guide/feature-guides/software-image-files-and-versions.html +25 -25
  294. nautobot/project-static/docs/user-guide/index.html +25 -25
  295. nautobot/project-static/docs/user-guide/platform-functionality/change-logging.html +25 -25
  296. nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +25 -25
  297. nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +25 -25
  298. nautobot/project-static/docs/user-guide/platform-functionality/customlink.html +25 -25
  299. nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +25 -25
  300. nautobot/project-static/docs/user-guide/platform-functionality/exporttemplate.html +25 -25
  301. nautobot/project-static/docs/user-guide/platform-functionality/externalintegration.html +25 -25
  302. nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +25 -25
  303. nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +25 -25
  304. nautobot/project-static/docs/user-guide/platform-functionality/graphqlquery.html +25 -25
  305. nautobot/project-static/docs/user-guide/platform-functionality/imageattachment.html +25 -25
  306. nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +25 -25
  307. nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +25 -25
  308. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +25 -25
  309. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +25 -25
  310. nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +25 -25
  311. nautobot/project-static/docs/user-guide/platform-functionality/napalm.html +25 -25
  312. nautobot/project-static/docs/user-guide/platform-functionality/note.html +25 -25
  313. nautobot/project-static/docs/user-guide/platform-functionality/objectmetadata.html +25 -25
  314. nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +25 -25
  315. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +25 -25
  316. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +25 -25
  317. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +25 -25
  318. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/ui-related-endpoints.html +25 -25
  319. nautobot/project-static/docs/user-guide/platform-functionality/role.html +25 -25
  320. nautobot/project-static/docs/user-guide/platform-functionality/savedview.html +25 -25
  321. nautobot/project-static/docs/user-guide/platform-functionality/secret.html +25 -25
  322. nautobot/project-static/docs/user-guide/platform-functionality/staticgroupassociation.html +25 -25
  323. nautobot/project-static/docs/user-guide/platform-functionality/status.html +25 -25
  324. nautobot/project-static/docs/user-guide/platform-functionality/tag.html +25 -25
  325. nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +25 -25
  326. nautobot/project-static/docs/user-guide/platform-functionality/users/objectpermission.html +25 -25
  327. nautobot/project-static/docs/user-guide/platform-functionality/users/token.html +25 -25
  328. nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +25 -25
  329. nautobot/project-static/js/homepage_layout.js +3 -0
  330. {nautobot-2.3.1.dist-info → nautobot-2.3.3.dist-info}/METADATA +4 -4
  331. {nautobot-2.3.1.dist-info → nautobot-2.3.3.dist-info}/RECORD +335 -332
  332. nautobot/project-static/docs/assets/javascripts/bundle.fe8b6f2b.min.js +0 -29
  333. nautobot/project-static/docs/assets/javascripts/bundle.fe8b6f2b.min.js.map +0 -7
  334. nautobot/project-static/docs/assets/stylesheets/main.3cba04c6.min.css +0 -1
  335. nautobot/project-static/docs/assets/stylesheets/main.3cba04c6.min.css.map +0 -1
  336. {nautobot-2.3.1.dist-info → nautobot-2.3.3.dist-info}/LICENSE.txt +0 -0
  337. {nautobot-2.3.1.dist-info → nautobot-2.3.3.dist-info}/NOTICE +0 -0
  338. {nautobot-2.3.1.dist-info → nautobot-2.3.3.dist-info}/WHEEL +0 -0
  339. {nautobot-2.3.1.dist-info → nautobot-2.3.3.dist-info}/entry_points.txt +0 -0
@@ -14,8 +14,15 @@ from django.db.models import ProtectedError
14
14
  from django.db.utils import IntegrityError
15
15
  from django.test import override_settings
16
16
  from django.test.utils import isolate_apps
17
- from django.utils.timezone import now
17
+ from django.utils.timezone import get_default_timezone, now
18
+ from django_celery_beat.tzcrontab import TzAwareCrontab
18
19
  from jinja2.exceptions import TemplateAssertionError, TemplateSyntaxError
20
+ import time_machine
21
+
22
+ try:
23
+ from zoneinfo import ZoneInfo
24
+ except ImportError: # python 3.8
25
+ from backports.zoneinfo import ZoneInfo
19
26
 
20
27
  from nautobot.circuits.models import CircuitType
21
28
  from nautobot.core.choices import ColorChoices
@@ -30,6 +37,7 @@ from nautobot.dcim.models import (
30
37
  Platform,
31
38
  )
32
39
  from nautobot.extras.choices import (
40
+ JobExecutionType,
33
41
  JobResultStatusChoices,
34
42
  LogLevelChoices,
35
43
  MetadataTypeDataTypeChoices,
@@ -65,6 +73,7 @@ from nautobot.extras.models import (
65
73
  ObjectMetadata,
66
74
  Role,
67
75
  SavedView,
76
+ ScheduledJob,
68
77
  Secret,
69
78
  SecretsGroup,
70
79
  SecretsGroupAssociation,
@@ -1082,12 +1091,15 @@ class JobModelTest(ModelTestCases.BaseModelTestCase):
1082
1091
  cls.app_job = JobModel.objects.get(job_class_name="ExampleJob")
1083
1092
 
1084
1093
  def test_job_class(self):
1094
+ self.assertIsNotNone(self.local_job.job_class)
1085
1095
  self.assertEqual(self.local_job.job_class.description, "Validate job import")
1086
1096
 
1097
+ self.assertIsNotNone(self.app_job.job_class)
1087
1098
  self.assertEqual(self.app_job.job_class, ExampleJob)
1088
1099
 
1089
1100
  def test_class_path(self):
1090
1101
  self.assertEqual(self.local_job.class_path, "pass.TestPass")
1102
+ self.assertIsNotNone(self.local_job.job_class)
1091
1103
  self.assertEqual(self.local_job.class_path, self.local_job.job_class.class_path)
1092
1104
 
1093
1105
  self.assertEqual(self.app_job.class_path, "example_app.jobs.ExampleJob")
@@ -1109,6 +1121,7 @@ class JobModelTest(ModelTestCases.BaseModelTestCase):
1109
1121
  self.assertTrue(job_model.enabled)
1110
1122
  else:
1111
1123
  self.assertFalse(job_model.enabled)
1124
+ self.assertIsNotNone(job_model.job_class)
1112
1125
  for field_name in JOB_OVERRIDABLE_FIELDS:
1113
1126
  if field_name == "name" and "duplicate_name" in job_model.job_class.__module__:
1114
1127
  pass # name field for test_duplicate_name jobs tested in test_duplicate_job_name below
@@ -1164,6 +1177,7 @@ class JobModelTest(ModelTestCases.BaseModelTestCase):
1164
1177
  setattr(self.job_containing_sensitive_variables, f"{field_name}_override", False)
1165
1178
  self.job_containing_sensitive_variables.validated_save()
1166
1179
  self.job_containing_sensitive_variables.refresh_from_db()
1180
+ self.assertIsNotNone(self.job_containing_sensitive_variables.job_class)
1167
1181
  for field_name in overridden_attrs:
1168
1182
  self.assertEqual(
1169
1183
  getattr(self.job_containing_sensitive_variables, field_name),
@@ -1790,6 +1804,295 @@ class SavedViewTest(ModelTestCases.BaseModelTestCase):
1790
1804
  self.assertEqual(self.ipaddress_global_sv.is_shared, True)
1791
1805
 
1792
1806
 
1807
+ @override_settings(TIME_ZONE="UTC")
1808
+ class ScheduledJobTest(ModelTestCases.BaseModelTestCase):
1809
+ """Tests for the `ScheduledJob` model class."""
1810
+
1811
+ model = ScheduledJob
1812
+
1813
+ def setUp(self):
1814
+ self.user = User.objects.create_user(username="scheduledjobuser")
1815
+ self.job_model = JobModel.objects.get(name="TestPass")
1816
+
1817
+ self.daily_utc_job = ScheduledJob.objects.create(
1818
+ name="Daily UTC Job",
1819
+ task="pass.TestPass",
1820
+ job_model=self.job_model,
1821
+ interval=JobExecutionType.TYPE_DAILY,
1822
+ start_time=datetime(year=2050, month=1, day=22, hour=17, minute=0, tzinfo=get_default_timezone()),
1823
+ time_zone=get_default_timezone(),
1824
+ )
1825
+ self.daily_est_job = ScheduledJob.objects.create(
1826
+ name="Daily EST Job",
1827
+ task="pass.TestPass",
1828
+ job_model=self.job_model,
1829
+ interval=JobExecutionType.TYPE_DAILY,
1830
+ start_time=datetime(year=2050, month=1, day=22, hour=17, minute=0, tzinfo=ZoneInfo("America/New_York")),
1831
+ time_zone=ZoneInfo("America/New_York"),
1832
+ )
1833
+ self.crontab_utc_job = ScheduledJob.create_schedule(
1834
+ job_model=self.job_model,
1835
+ user=self.user,
1836
+ name="Crontab UTC Job",
1837
+ interval=JobExecutionType.TYPE_CUSTOM,
1838
+ crontab="0 17 * * *",
1839
+ )
1840
+ self.crontab_est_job = ScheduledJob.objects.create(
1841
+ name="Crontab EST Job",
1842
+ task="pass.TestPass",
1843
+ job_model=self.job_model,
1844
+ interval=JobExecutionType.TYPE_CUSTOM,
1845
+ start_time=datetime(year=2050, month=1, day=22, hour=17, minute=0, tzinfo=ZoneInfo("America/New_York")),
1846
+ time_zone=ZoneInfo("America/New_York"),
1847
+ crontab="0 17 * * *",
1848
+ )
1849
+ self.one_off_utc_job = ScheduledJob.objects.create(
1850
+ name="One-off UTC Job",
1851
+ task="pass.TestPass",
1852
+ job_model=self.job_model,
1853
+ interval=JobExecutionType.TYPE_FUTURE,
1854
+ start_time=datetime(year=2050, month=1, day=22, hour=0, minute=0, tzinfo=ZoneInfo("UTC")),
1855
+ time_zone=ZoneInfo("UTC"),
1856
+ )
1857
+ self.one_off_est_job = ScheduledJob.create_schedule(
1858
+ job_model=self.job_model,
1859
+ user=self.user,
1860
+ name="One-off EST Job",
1861
+ interval=JobExecutionType.TYPE_FUTURE,
1862
+ start_time=datetime(year=2050, month=1, day=22, hour=0, minute=0, tzinfo=ZoneInfo("America/New_York")),
1863
+ )
1864
+
1865
+ def test_schedule(self):
1866
+ """Test the schedule property."""
1867
+ with self.subTest("Test TYPE_DAILY schedules"):
1868
+ daily_utc_schedule = self.daily_utc_job.schedule
1869
+ daily_est_schedule = self.daily_est_job.schedule
1870
+ self.assertIsInstance(daily_utc_schedule, TzAwareCrontab)
1871
+ self.assertIsInstance(daily_est_schedule, TzAwareCrontab)
1872
+ self.assertNotEqual(daily_utc_schedule, daily_est_schedule)
1873
+ # Crontabs are validated in test_to_cron()
1874
+
1875
+ with self.subTest("Test TYPE_CUSTOM schedules"):
1876
+ crontab_utc_schedule = self.crontab_utc_job.schedule
1877
+ crontab_est_schedule = self.crontab_est_job.schedule
1878
+ self.assertIsInstance(crontab_utc_schedule, TzAwareCrontab)
1879
+ self.assertIsInstance(crontab_est_schedule, TzAwareCrontab)
1880
+ self.assertNotEqual(crontab_utc_schedule, crontab_est_schedule)
1881
+ # Crontabs are validated in test_to_cron()
1882
+
1883
+ with self.subTest("Test TYPE_FUTURE schedules"):
1884
+ # TYPE_FUTURE schedules are one off, not cron tabs:
1885
+ self.assertEqual(self.one_off_utc_job.schedule.clocked_time, self.one_off_utc_job.start_time)
1886
+ self.assertEqual(self.one_off_est_job.schedule.clocked_time, self.one_off_est_job.start_time)
1887
+ self.assertEqual(
1888
+ self.one_off_est_job.schedule.clocked_time - self.one_off_utc_job.schedule.clocked_time,
1889
+ timedelta(hours=5),
1890
+ )
1891
+
1892
+ def test_to_cron(self):
1893
+ """Test the to_cron() method and its interaction with time zone variants."""
1894
+
1895
+ with self.subTest("Test TYPE_DAILY schedule with UTC time zone and UTC schedule time zone"):
1896
+ self.daily_utc_job.refresh_from_db()
1897
+ daily_utc_schedule = self.daily_utc_job.to_cron()
1898
+ self.assertEqual(daily_utc_schedule.tz, ZoneInfo("UTC"))
1899
+ self.assertEqual(daily_utc_schedule.hour, {17})
1900
+ self.assertEqual(daily_utc_schedule.minute, {0})
1901
+ last_run = datetime(2050, 1, 21, 17, 0, tzinfo=ZoneInfo("UTC"))
1902
+ with time_machine.travel("2050-01-22 16:59 +0000"):
1903
+ is_due, _ = daily_utc_schedule.is_due(last_run_at=last_run)
1904
+ self.assertFalse(is_due)
1905
+ with time_machine.travel("2050-01-22 17:00 +0000"):
1906
+ is_due, _ = daily_utc_schedule.is_due(last_run_at=last_run)
1907
+ self.assertTrue(is_due)
1908
+
1909
+ with self.subTest("Test TYPE_DAILY schedule with UTC time zone and EST schedule time zone"):
1910
+ self.daily_est_job.refresh_from_db()
1911
+ daily_est_schedule = self.daily_est_job.to_cron()
1912
+ self.assertEqual(daily_est_schedule.tz, ZoneInfo("America/New_York"))
1913
+ self.assertEqual(daily_est_schedule.hour, {17})
1914
+ self.assertEqual(daily_est_schedule.minute, {0})
1915
+ last_run = datetime(2050, 1, 21, 22, 0, tzinfo=ZoneInfo("UTC"))
1916
+ with time_machine.travel("2050-01-22 21:59 +0000"):
1917
+ is_due, _ = daily_est_schedule.is_due(last_run_at=last_run)
1918
+ self.assertFalse(is_due)
1919
+ with time_machine.travel("2050-01-22 22:00 +0000"):
1920
+ is_due, _ = daily_est_schedule.is_due(last_run_at=last_run)
1921
+ self.assertTrue(is_due)
1922
+
1923
+ with self.subTest("Test TYPE_CUSTOM schedule with UTC time zone and UTC schedule time zone"):
1924
+ self.crontab_utc_job.refresh_from_db()
1925
+ crontab_utc_schedule = self.crontab_utc_job.to_cron()
1926
+ self.assertEqual(crontab_utc_schedule.tz, ZoneInfo("UTC"))
1927
+ self.assertEqual(crontab_utc_schedule.hour, {17})
1928
+ self.assertEqual(crontab_utc_schedule.minute, {0})
1929
+
1930
+ with self.subTest("Test TYPE_CUSTOM schedule with UTC time zone and EST schedule time zone"):
1931
+ self.crontab_est_job.refresh_from_db()
1932
+ crontab_est_schedule = self.crontab_est_job.to_cron()
1933
+ self.assertEqual(crontab_est_schedule.tz, ZoneInfo("America/New_York"))
1934
+ self.assertEqual(crontab_est_schedule.hour, {17})
1935
+ self.assertEqual(crontab_est_schedule.minute, {0})
1936
+
1937
+ with self.subTest("Test TYPE_FUTURE schedules do not map to cron"):
1938
+ with self.assertRaises(ValueError):
1939
+ self.one_off_utc_job.to_cron()
1940
+ with self.assertRaises(ValueError):
1941
+ self.one_off_est_job.to_cron()
1942
+
1943
+ with override_settings(TIME_ZONE="America/New_York"):
1944
+ with self.subTest("Test TYPE_DAILY schedule with EST time zone and UTC schedule time zone"):
1945
+ self.daily_utc_job.refresh_from_db()
1946
+ daily_utc_schedule = self.daily_utc_job.to_cron()
1947
+ self.assertEqual(daily_utc_schedule.tz, ZoneInfo("UTC"))
1948
+ self.assertEqual(daily_utc_schedule.hour, {17})
1949
+ self.assertEqual(daily_utc_schedule.minute, {0})
1950
+ last_run = datetime(2050, 1, 21, 12, 0, tzinfo=ZoneInfo("America/New_York"))
1951
+ with time_machine.travel("2050-01-22 11:59 -0500"):
1952
+ is_due, _ = daily_utc_schedule.is_due(last_run_at=last_run)
1953
+ self.assertFalse(is_due)
1954
+ with time_machine.travel("2050-01-22 12:00 -0500"):
1955
+ is_due, _ = daily_utc_schedule.is_due(last_run_at=last_run)
1956
+ self.assertTrue(is_due)
1957
+
1958
+ with self.subTest("Test TYPE_DAILY schedule with EST time zone and EST schedule time zone"):
1959
+ self.daily_est_job.refresh_from_db()
1960
+ daily_est_schedule = self.daily_est_job.to_cron()
1961
+ self.assertEqual(daily_est_schedule.tz, ZoneInfo("America/New_York"))
1962
+ self.assertEqual(daily_est_schedule.hour, {17})
1963
+ self.assertEqual(daily_est_schedule.minute, {0})
1964
+ last_run = datetime(2050, 1, 21, 22, 0, tzinfo=ZoneInfo("America/New_York"))
1965
+ with time_machine.travel("2050-01-22 16:59 -0500"):
1966
+ is_due, _ = daily_est_schedule.is_due(last_run_at=last_run)
1967
+ self.assertFalse(is_due)
1968
+ with time_machine.travel("2050-01-22 17:00 -0500"):
1969
+ is_due, _ = daily_est_schedule.is_due(last_run_at=last_run)
1970
+ self.assertTrue(is_due)
1971
+
1972
+ with self.subTest("Test TYPE_CUSTOM schedule with EST time zone and UTC schedule time zone"):
1973
+ self.crontab_utc_job.refresh_from_db()
1974
+ crontab_utc_schedule = self.crontab_utc_job.to_cron()
1975
+ self.assertEqual(crontab_utc_schedule.tz, ZoneInfo("UTC"))
1976
+ self.assertEqual(crontab_utc_schedule.hour, {17})
1977
+ self.assertEqual(crontab_utc_schedule.minute, {0})
1978
+
1979
+ with self.subTest("Test TYPE_CUSTOM schedule with EST time zone and EST schedule time zone"):
1980
+ self.crontab_est_job.refresh_from_db()
1981
+ crontab_est_schedule = self.crontab_est_job.to_cron()
1982
+ self.assertEqual(crontab_est_schedule.tz, ZoneInfo("America/New_York"))
1983
+ self.assertEqual(crontab_est_schedule.hour, {17})
1984
+ self.assertEqual(crontab_est_schedule.minute, {0})
1985
+
1986
+ def test_crontab_dst(self):
1987
+ """Test that TYPE_CUSTOM behavior around DST is as expected."""
1988
+ cronjob = ScheduledJob.objects.create(
1989
+ name="DST Aware Cronjob",
1990
+ task="pass.TestPass",
1991
+ job_model=self.job_model,
1992
+ enabled=False,
1993
+ interval=JobExecutionType.TYPE_CUSTOM,
1994
+ start_time=datetime(year=2024, month=1, day=1, hour=17, minute=0, tzinfo=ZoneInfo("America/New_York")),
1995
+ crontab="0 17 * * *", # 5 PM local time
1996
+ time_zone=ZoneInfo("America/New_York"),
1997
+ )
1998
+
1999
+ # Before DST takes effect
2000
+ with self.subTest("Test UTC time zone with EST job"):
2001
+ cronjob.refresh_from_db()
2002
+ crontab = cronjob.to_cron()
2003
+ with time_machine.travel("2024-03-09 21:59 +0000"):
2004
+ is_due, _ = crontab.is_due(last_run_at=datetime(2024, 3, 8, 17, 0, tzinfo=ZoneInfo("America/New_York")))
2005
+ self.assertFalse(is_due)
2006
+ with time_machine.travel("2024-03-09 22:00 +0000"):
2007
+ is_due, _ = crontab.is_due(last_run_at=datetime(2024, 3, 8, 17, 0, tzinfo=ZoneInfo("America/New_York")))
2008
+ self.assertTrue(is_due)
2009
+
2010
+ with self.subTest("Test EST time zone with EST job"), override_settings(TIME_ZONE="America/New_York"):
2011
+ cronjob.refresh_from_db()
2012
+ crontab = cronjob.to_cron()
2013
+ with time_machine.travel("2024-03-09 16:59 -0500"):
2014
+ is_due, _ = crontab.is_due(last_run_at=datetime(2024, 3, 8, 17, 0, tzinfo=ZoneInfo("America/New_York")))
2015
+ self.assertFalse(is_due)
2016
+ with time_machine.travel("2024-03-09 17:00 -0500"):
2017
+ is_due, _ = crontab.is_due(last_run_at=datetime(2024, 3, 8, 17, 0, tzinfo=ZoneInfo("America/New_York")))
2018
+ self.assertTrue(is_due)
2019
+
2020
+ # Day that DST takes effect
2021
+ with self.subTest("Test UTC time zone with EDT job"):
2022
+ cronjob.refresh_from_db()
2023
+ crontab = cronjob.to_cron()
2024
+ with time_machine.travel("2024-03-10 20:59 +0000"):
2025
+ is_due, _ = crontab.is_due(last_run_at=datetime(2024, 3, 9, 17, 0, tzinfo=ZoneInfo("America/New_York")))
2026
+ self.assertFalse(is_due)
2027
+ with time_machine.travel("2024-03-10 21:00 +0000"):
2028
+ is_due, _ = crontab.is_due(last_run_at=datetime(2024, 3, 9, 17, 0, tzinfo=ZoneInfo("America/New_York")))
2029
+ self.assertTrue(is_due)
2030
+
2031
+ with self.subTest("Test EDT time zone with EDT job"), override_settings(TIME_ZONE="America/New_York"):
2032
+ cronjob.refresh_from_db()
2033
+ crontab = cronjob.to_cron()
2034
+ with time_machine.travel("2024-03-10 16:59 -0400"):
2035
+ is_due, _ = crontab.is_due(last_run_at=datetime(2024, 3, 9, 17, 0, tzinfo=ZoneInfo("America/New_York")))
2036
+ self.assertFalse(is_due)
2037
+ with time_machine.travel("2024-03-10 17:00 -0400"):
2038
+ is_due, _ = crontab.is_due(last_run_at=datetime(2024, 3, 9, 17, 0, tzinfo=ZoneInfo("America/New_York")))
2039
+ self.assertTrue(is_due)
2040
+
2041
+ def test_daily_dst(self):
2042
+ """Test the interaction of TYPE_DAILY around DST."""
2043
+ daily = ScheduledJob.objects.create(
2044
+ name="Daily Job",
2045
+ task="pass.TestPass",
2046
+ job_model=self.job_model,
2047
+ enabled=False,
2048
+ interval=JobExecutionType.TYPE_DAILY,
2049
+ start_time=datetime(year=2024, month=1, day=1, hour=17, minute=0, tzinfo=ZoneInfo("America/New_York")),
2050
+ time_zone=ZoneInfo("America/New_York"),
2051
+ )
2052
+
2053
+ # Before DST takes effect
2054
+ with self.subTest("Test UTC time zone with EST job"):
2055
+ daily.refresh_from_db()
2056
+ crontab = daily.to_cron()
2057
+ with time_machine.travel("2024-03-09 21:59 +0000"):
2058
+ is_due, _ = crontab.is_due(last_run_at=datetime(2024, 3, 8, 17, 0, tzinfo=ZoneInfo("America/New_York")))
2059
+ self.assertFalse(is_due)
2060
+ with time_machine.travel("2024-03-09 22:00 +0000"):
2061
+ is_due, _ = crontab.is_due(last_run_at=datetime(2024, 3, 8, 17, 0, tzinfo=ZoneInfo("America/New_York")))
2062
+ self.assertTrue(is_due)
2063
+
2064
+ with self.subTest("Test EST time zone with EST job"), override_settings(TIME_ZONE="America/New_York"):
2065
+ daily.refresh_from_db()
2066
+ crontab = daily.to_cron()
2067
+ with time_machine.travel("2024-03-09 16:59 -0500"):
2068
+ is_due, _ = crontab.is_due(last_run_at=datetime(2024, 3, 8, 17, 0, tzinfo=ZoneInfo("America/New_York")))
2069
+ self.assertFalse(is_due)
2070
+ with time_machine.travel("2024-03-09 17:00 -0500"):
2071
+ is_due, _ = crontab.is_due(last_run_at=datetime(2024, 3, 8, 17, 0, tzinfo=ZoneInfo("America/New_York")))
2072
+ self.assertTrue(is_due)
2073
+
2074
+ # Day that DST takes effect
2075
+ with self.subTest("Test UTC time zone with EDT job"):
2076
+ daily.refresh_from_db()
2077
+ crontab = daily.to_cron()
2078
+ with time_machine.travel("2024-03-10 20:59 +0000"):
2079
+ is_due, _ = crontab.is_due(last_run_at=datetime(2024, 3, 9, 17, 0, tzinfo=ZoneInfo("America/New_York")))
2080
+ self.assertFalse(is_due)
2081
+ with time_machine.travel("2024-03-10 21:00 +0000"):
2082
+ is_due, _ = crontab.is_due(last_run_at=datetime(2024, 3, 9, 17, 0, tzinfo=ZoneInfo("America/New_York")))
2083
+ self.assertTrue(is_due)
2084
+
2085
+ with self.subTest("Test EDT time zone with EDT job"), override_settings(TIME_ZONE="America/New_York"):
2086
+ daily.refresh_from_db()
2087
+ crontab = daily.to_cron()
2088
+ with time_machine.travel("2024-03-10 16:59 -0400"):
2089
+ is_due, _ = crontab.is_due(last_run_at=datetime(2024, 3, 9, 17, 0, tzinfo=ZoneInfo("America/New_York")))
2090
+ self.assertFalse(is_due)
2091
+ with time_machine.travel("2024-03-10 17:00 -0400"):
2092
+ is_due, _ = crontab.is_due(last_run_at=datetime(2024, 3, 9, 17, 0, tzinfo=ZoneInfo("America/New_York")))
2093
+ self.assertTrue(is_due)
2094
+
2095
+
1793
2096
  class SecretTest(ModelTestCases.BaseModelTestCase):
1794
2097
  """
1795
2098
  Tests for the `Secret` model class.
@@ -1744,16 +1744,17 @@ class ScheduledJobTestCase(
1744
1744
  ScheduledJob.objects.create(
1745
1745
  name="test2",
1746
1746
  task="pass.TestPass",
1747
- interval=JobExecutionType.TYPE_IMMEDIATELY,
1747
+ interval=JobExecutionType.TYPE_DAILY,
1748
1748
  user=user,
1749
1749
  start_time=timezone.now(),
1750
1750
  )
1751
1751
  ScheduledJob.objects.create(
1752
1752
  name="test3",
1753
1753
  task="pass.TestPass",
1754
- interval=JobExecutionType.TYPE_IMMEDIATELY,
1754
+ interval=JobExecutionType.TYPE_CUSTOM,
1755
1755
  user=user,
1756
1756
  start_time=timezone.now(),
1757
+ crontab="15 10 * * *",
1757
1758
  )
1758
1759
 
1759
1760
  def test_only_enabled_is_listed(self):
@@ -3007,6 +3008,7 @@ class JobCustomTemplateTestCase(TestCase):
3007
3008
  cls.run_url = reverse("extras:job_run", kwargs={"pk": cls.example_job.pk})
3008
3009
 
3009
3010
  def test_rendering_custom_template(self):
3011
+ self.assertIsNotNone(self.example_job.job_class)
3010
3012
  obj_perm = ObjectPermission(name="Test permission", actions=["view", "run"])
3011
3013
  obj_perm.save()
3012
3014
  obj_perm.users.add(self.user)
nautobot/extras/views.py CHANGED
@@ -2,6 +2,7 @@ import logging
2
2
  from urllib.parse import parse_qs
3
3
 
4
4
  from celery import chain
5
+ from django.conf import settings
5
6
  from django.contrib import messages
6
7
  from django.contrib.auth.models import AnonymousUser
7
8
  from django.contrib.contenttypes.models import ContentType
@@ -23,6 +24,11 @@ from django_tables2 import RequestConfig
23
24
  from jsonschema.validators import Draft7Validator
24
25
  from rest_framework.decorators import action
25
26
 
27
+ try:
28
+ from zoneinfo import ZoneInfo
29
+ except ImportError: # python 3.8
30
+ from backports.zoneinfo import ZoneInfo
31
+
26
32
  from nautobot.core.forms import restrict_form_fields
27
33
  from nautobot.core.models.querysets import count_related
28
34
  from nautobot.core.models.utils import pretty_print_query
@@ -1916,6 +1922,7 @@ class ScheduledJobView(generic.ObjectView):
1916
1922
  return {
1917
1923
  "labels": labels,
1918
1924
  "job_class_found": (job_class is not None),
1925
+ "default_time_zone": ZoneInfo(settings.TIME_ZONE),
1919
1926
  **super().get_extra_context(request, instance),
1920
1927
  }
1921
1928
 
@@ -202,7 +202,7 @@ class PrefixViewSet(NautobotModelViewSet):
202
202
  if requested_prefix["prefix_length"] >= available_prefix.prefixlen:
203
203
  allocated_prefix = f"{available_prefix.network}/{requested_prefix['prefix_length']}"
204
204
  requested_prefix["prefix"] = allocated_prefix
205
- requested_prefix["namespace"] = prefix.namespace.pk
205
+ requested_prefix["namespace"] = prefix.namespace
206
206
  break
207
207
  else:
208
208
  return Response(
@@ -210,6 +210,10 @@ class PrefixViewSet(NautobotModelViewSet):
210
210
  status=status.HTTP_204_NO_CONTENT,
211
211
  )
212
212
 
213
+ # The serializer usage above has mapped "custom_fields" dict to "_custom_field_data".
214
+ # We need to convert it back to "custom_fields" as we're going to deserialize it a second time below
215
+ requested_prefix["custom_fields"] = requested_prefix.pop("_custom_field_data", {})
216
+
213
217
  # Remove the allocated prefix from the list of available prefixes
214
218
  available_prefixes.remove(allocated_prefix)
215
219
 
@@ -299,7 +303,10 @@ class PrefixViewSet(NautobotModelViewSet):
299
303
  prefix_length = prefix.prefix.prefixlen
300
304
  for requested_ip in requested_ips:
301
305
  requested_ip["address"] = f"{next(available_ips)}/{prefix_length}"
302
- requested_ip["namespace"] = prefix.namespace.pk
306
+ requested_ip["namespace"] = prefix.namespace
307
+ # The serializer usage above has mapped "custom_fields" dict to "_custom_field_data".
308
+ # We need to convert it back to "custom_fields" as we're going to deserialize it a second time below
309
+ requested_ip["custom_fields"] = requested_ip.pop("_custom_field_data", {})
303
310
 
304
311
  # Initialize the serializer with a list or a single object depending on what was requested
305
312
  context = {"request": request, "depth": 0}
nautobot/ipam/choices.py CHANGED
@@ -102,6 +102,23 @@ class IPAddressTypeChoices(ChoiceSet):
102
102
  )
103
103
 
104
104
 
105
+ #
106
+ # VRFs
107
+ #
108
+
109
+
110
+ class VRFStatusChoices(ChoiceSet):
111
+ STATUS_ACTIVE = "active"
112
+ STATUS_DOWN = "down"
113
+ STATUS_DEPRECATED = "deprecated"
114
+
115
+ CHOICES = (
116
+ (STATUS_ACTIVE, "Active"),
117
+ (STATUS_DOWN, "Down"),
118
+ (STATUS_DEPRECATED, "Deprecated"),
119
+ )
120
+
121
+
105
122
  #
106
123
  # VLANs
107
124
  #
nautobot/ipam/factory.py CHANGED
@@ -84,6 +84,7 @@ class VRFFactory(PrimaryModelFactory):
84
84
  model = VRF
85
85
  exclude = (
86
86
  "has_description",
87
+ "has_status",
87
88
  "has_tenant",
88
89
  )
89
90
 
@@ -103,6 +104,11 @@ class VRFFactory(PrimaryModelFactory):
103
104
  has_description = NautobotBoolIterator()
104
105
  description = factory.Maybe("has_description", factory.Faker("text", max_nb_chars=CHARFIELD_MAX_LENGTH), "")
105
106
 
107
+ has_status = NautobotBoolIterator()
108
+ status = factory.Maybe(
109
+ "has_status", random_instance(lambda: Status.objects.get_for_model(VRF), allow_null=False), None
110
+ )
111
+
106
112
  namespace = random_instance(Namespace, allow_null=False)
107
113
 
108
114
  @factory.post_generation
nautobot/ipam/filters.py CHANGED
@@ -66,7 +66,7 @@ class NamespaceFilterSet(NautobotFilterSet):
66
66
  fields = "__all__"
67
67
 
68
68
 
69
- class VRFFilterSet(NautobotFilterSet, TenancyModelFilterSetMixin):
69
+ class VRFFilterSet(NautobotFilterSet, StatusModelFilterSetMixin, TenancyModelFilterSetMixin):
70
70
  q = SearchFilter(
71
71
  filter_predicates={
72
72
  "name": "icontains",
nautobot/ipam/forms.py CHANGED
@@ -125,6 +125,7 @@ class VRFForm(NautobotModelForm, TenancyForm):
125
125
  "name",
126
126
  "rd",
127
127
  "namespace",
128
+ "status",
128
129
  "description",
129
130
  "import_targets",
130
131
  "export_targets",
@@ -140,10 +141,11 @@ class VRFForm(NautobotModelForm, TenancyForm):
140
141
  }
141
142
  help_texts = {
142
143
  "rd": "Route distinguisher unique to this Namespace (as defined in RFC 4364)",
144
+ "status": "Operational status of this VRF",
143
145
  }
144
146
 
145
147
 
146
- class VRFBulkEditForm(TagsBulkEditFormMixin, NautobotBulkEditForm):
148
+ class VRFBulkEditForm(TagsBulkEditFormMixin, StatusModelBulkEditFormMixin, NautobotBulkEditForm):
147
149
  pk = forms.ModelMultipleChoiceField(queryset=VRF.objects.all(), widget=forms.MultipleHiddenInput())
148
150
  namespace = DynamicModelChoiceField(queryset=Namespace.objects.all(), required=False)
149
151
  tenant = DynamicModelChoiceField(queryset=Tenant.objects.all(), required=False)
@@ -162,9 +164,9 @@ class VRFBulkEditForm(TagsBulkEditFormMixin, NautobotBulkEditForm):
162
164
  ]
163
165
 
164
166
 
165
- class VRFFilterForm(NautobotFilterForm, TenancyFilterForm):
167
+ class VRFFilterForm(NautobotFilterForm, StatusModelFilterFormMixin, TenancyFilterForm):
166
168
  model = VRF
167
- field_order = ["q", "import_targets", "export_targets", "tenant_group", "tenant"]
169
+ field_order = ["q", "import_targets", "export_targets", "status", "tenant_group", "tenant"]
168
170
  q = forms.CharField(required=False, label="Search")
169
171
  import_targets = DynamicModelMultipleChoiceField(
170
172
  queryset=RouteTarget.objects.all(), to_field_name="name", required=False
@@ -0,0 +1,23 @@
1
+ # Generated by Django 4.2.15 on 2024-08-26 17:20
2
+
3
+ from django.db import migrations
4
+ import django.db.models.deletion
5
+
6
+ import nautobot.extras.models.statuses
7
+
8
+
9
+ class Migration(migrations.Migration):
10
+ dependencies = [
11
+ ("extras", "0114_computedfield_grouping"),
12
+ ("ipam", "0047_alter_ipaddress_role_alter_ipaddress_status_and_more"),
13
+ ]
14
+
15
+ operations = [
16
+ migrations.AddField(
17
+ model_name="vrf",
18
+ name="status",
19
+ field=nautobot.extras.models.statuses.StatusField(
20
+ blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to="extras.status"
21
+ ),
22
+ ),
23
+ ]
@@ -0,0 +1,25 @@
1
+ # Generated by Django 4.2.15 on 2024-08-26 18:05
2
+
3
+ from django.db import migrations
4
+
5
+ from nautobot.extras.management import clear_status_choices, populate_status_choices
6
+
7
+
8
+ def populate_vrf_status_choices(apps, schema_editor):
9
+ """Create default Status records for the VRF model."""
10
+ populate_status_choices(apps, schema_editor, models=["ipam.VRF"])
11
+
12
+
13
+ def clear_vrf_status_choices(apps, schema_editor):
14
+ """Remove default Status records for the VRF model."""
15
+ clear_status_choices(apps, schema_editor, models=["ipam.VRF"])
16
+
17
+
18
+ class Migration(migrations.Migration):
19
+ dependencies = [
20
+ ("ipam", "0048_vrf_status"),
21
+ ]
22
+
23
+ operations = [
24
+ migrations.RunPython(populate_vrf_status_choices, clear_vrf_status_choices),
25
+ ]
nautobot/ipam/models.py CHANGED
@@ -97,6 +97,7 @@ def get_default_namespace_pk():
97
97
  "custom_validators",
98
98
  "export_templates",
99
99
  "graphql",
100
+ "statuses",
100
101
  "webhooks",
101
102
  )
102
103
  class VRF(PrimaryModel):
@@ -114,6 +115,7 @@ class VRF(PrimaryModel):
114
115
  verbose_name="Route distinguisher",
115
116
  help_text="Unique route distinguisher (as defined in RFC 4364)",
116
117
  )
118
+ status = StatusField(blank=True, null=True)
117
119
  namespace = models.ForeignKey(
118
120
  "ipam.Namespace",
119
121
  on_delete=models.PROTECT,
@@ -533,6 +535,10 @@ class Prefix(PrimaryModel):
533
535
  def __str__(self):
534
536
  return str(self.prefix)
535
537
 
538
+ @property
539
+ def display(self):
540
+ return f"{self.prefix}: {self.namespace}"
541
+
536
542
  def _deconstruct_prefix(self, prefix):
537
543
  if prefix:
538
544
  if isinstance(prefix, str):
nautobot/ipam/tables.py CHANGED
@@ -217,7 +217,7 @@ class NamespaceTable(BaseTable):
217
217
  #
218
218
 
219
219
 
220
- class VRFTable(BaseTable):
220
+ class VRFTable(StatusTableMixin, BaseTable):
221
221
  pk = ToggleColumn()
222
222
  name = tables.LinkColumn()
223
223
  # rd = tables.Column(verbose_name="RD")
@@ -232,6 +232,7 @@ class VRFTable(BaseTable):
232
232
  "pk",
233
233
  "name",
234
234
  # "rd",
235
+ "status",
235
236
  "namespace",
236
237
  "tenant",
237
238
  "description",
@@ -240,7 +241,7 @@ class VRFTable(BaseTable):
240
241
  "tags",
241
242
  )
242
243
  # default_columns = ("pk", "name", "rd", "namespace", "tenant", "description")
243
- default_columns = ("pk", "name", "namespace", "tenant", "description")
244
+ default_columns = ("pk", "name", "status", "namespace", "tenant", "description")
244
245
 
245
246
 
246
247
  class VRFDeviceAssignmentTable(BaseTable):
@@ -19,6 +19,10 @@
19
19
  <td>Tenant</td>
20
20
  <td>{{ object.tenant|hyperlinked_object }}</td>
21
21
  </tr>
22
+ <tr>
23
+ <td>Status</td>
24
+ <td>{{ object.status|hyperlinked_object_with_color }}</td>
25
+ </tr>
22
26
  <tr>
23
27
  <td>Description</td>
24
28
  <td>{{ object.description|placeholder }}</td>
@@ -8,6 +8,7 @@
8
8
  {% render_field form.name %}
9
9
  {% render_field form.namespace %}
10
10
  {% render_field form.rd %}
11
+ {% render_field form.status %}
11
12
  {% render_field form.description %}
12
13
  </div>
13
14
  </div>