nautobot 2.0.0a3__py3-none-any.whl → 2.0.0b1__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 (780) hide show
  1. nautobot/apps/api.py +6 -8
  2. nautobot/apps/forms.py +0 -2
  3. nautobot/apps/ui.py +0 -8
  4. nautobot/circuits/api/serializers.py +9 -117
  5. nautobot/circuits/api/urls.py +1 -1
  6. nautobot/circuits/api/views.py +0 -1
  7. nautobot/circuits/forms.py +0 -65
  8. nautobot/circuits/migrations/0014_related_name_changes.py +1 -1
  9. nautobot/circuits/migrations/0016_tagsfield.py +34 -0
  10. nautobot/circuits/migrations/0017_fixup_null_statuses.py +22 -0
  11. nautobot/circuits/migrations/0018_status_nonnullable.py +22 -0
  12. nautobot/circuits/models.py +3 -87
  13. nautobot/circuits/navigation.py +14 -69
  14. nautobot/circuits/signals.py +0 -2
  15. nautobot/circuits/tables.py +39 -1
  16. nautobot/circuits/tests/integration/test_relationships.py +9 -9
  17. nautobot/circuits/tests/test_api.py +4 -8
  18. nautobot/circuits/tests/test_filters.py +10 -4
  19. nautobot/circuits/tests/test_models.py +5 -1
  20. nautobot/circuits/tests/test_views.py +27 -5
  21. nautobot/circuits/views.py +18 -10
  22. nautobot/core/api/__init__.py +8 -2
  23. nautobot/core/api/fields.py +15 -6
  24. nautobot/core/api/filter_backends.py +3 -2
  25. nautobot/core/api/metadata.py +237 -30
  26. nautobot/core/api/mixins.py +94 -0
  27. nautobot/core/api/pagination.py +4 -0
  28. nautobot/core/api/parsers.py +154 -0
  29. nautobot/core/api/renderers.py +153 -2
  30. nautobot/core/api/schema.py +46 -2
  31. nautobot/core/api/serializers.py +377 -35
  32. nautobot/core/api/urls.py +11 -3
  33. nautobot/core/api/utils.py +174 -2
  34. nautobot/core/api/versioning.py +32 -10
  35. nautobot/core/api/views.py +266 -72
  36. nautobot/core/apps/__init__.py +138 -220
  37. nautobot/core/celery/__init__.py +112 -41
  38. nautobot/core/celery/backends.py +19 -12
  39. nautobot/core/celery/control.py +46 -0
  40. nautobot/core/celery/encoders.py +53 -0
  41. nautobot/core/celery/log.py +38 -0
  42. nautobot/core/celery/schedulers.py +23 -4
  43. nautobot/core/celery/task.py +1 -16
  44. nautobot/core/checks.py +0 -27
  45. nautobot/core/choices.py +0 -113
  46. nautobot/core/{cli.py → cli/__init__.py} +1 -1
  47. nautobot/core/cli/__main__.py +3 -0
  48. nautobot/core/constants.py +0 -24
  49. nautobot/core/context_processors.py +12 -0
  50. nautobot/core/filters.py +2 -2
  51. nautobot/core/forms/__init__.py +0 -4
  52. nautobot/core/forms/fields.py +38 -65
  53. nautobot/core/forms/forms.py +4 -1
  54. nautobot/core/forms/utils.py +0 -52
  55. nautobot/core/graphql/schema.py +4 -27
  56. nautobot/core/jobs/__init__.py +75 -0
  57. nautobot/core/management/commands/build_ui.py +255 -0
  58. nautobot/core/management/commands/generate_test_data.py +3 -2
  59. nautobot/core/management/commands/post_upgrade.py +24 -24
  60. nautobot/core/models/__init__.py +26 -1
  61. nautobot/core/models/fields.py +24 -5
  62. nautobot/core/models/generics.py +2 -42
  63. nautobot/core/models/managers.py +5 -0
  64. nautobot/core/models/name_color_content_types.py +0 -14
  65. nautobot/core/models/tree_queries.py +14 -4
  66. nautobot/core/models/utils.py +5 -6
  67. nautobot/core/models/validators.py +17 -8
  68. nautobot/core/releases.py +8 -10
  69. nautobot/core/settings.py +80 -42
  70. nautobot/core/tables.py +5 -5
  71. nautobot/core/tasks.py +4 -7
  72. nautobot/core/templates/base.html +1 -49
  73. nautobot/core/templates/base_django.html +49 -0
  74. nautobot/core/templates/base_react.html +55 -0
  75. nautobot/core/templates/buttons/export.html +6 -4
  76. nautobot/core/templates/generic/object_bulk_create.html +10 -21
  77. nautobot/core/templates/generic/object_list.html +3 -1
  78. nautobot/core/templates/generic/object_retrieve_plugin_full_width.html +3 -0
  79. nautobot/core/templates/inc/footer.html +1 -0
  80. nautobot/core/templates/inc/javascript.html +0 -14
  81. nautobot/core/templates/inc/nav_menu.html +28 -33
  82. nautobot/core/templates/inc/object_details_advanced_panel.html +13 -0
  83. nautobot/core/templates/inc/relationships_table_rows.html +2 -2
  84. nautobot/core/templates/nautobot_config.py.j2 +8 -20
  85. nautobot/core/templates/plugin_template/__init__.py-tpl +1 -2
  86. nautobot/core/templates/rest_framework/api.html +8 -0
  87. nautobot/core/templatetags/buttons.py +32 -28
  88. nautobot/core/testing/__init__.py +47 -44
  89. nautobot/core/testing/api.py +362 -47
  90. nautobot/core/testing/filters.py +1 -1
  91. nautobot/core/testing/migrations.py +2 -0
  92. nautobot/core/testing/mixins.py +22 -9
  93. nautobot/core/testing/schema.py +2 -1
  94. nautobot/core/testing/views.py +21 -46
  95. nautobot/core/tests/integration/test_filters.py +17 -8
  96. nautobot/core/tests/integration/test_navbar.py +11 -34
  97. nautobot/core/tests/integration/test_plugin_navbar.py +9 -103
  98. nautobot/core/tests/nautobot_config.py +2 -3
  99. nautobot/core/tests/test_api.py +290 -21
  100. nautobot/core/tests/test_checks.py +0 -7
  101. nautobot/core/tests/test_filters.py +107 -59
  102. nautobot/core/tests/test_forms.py +26 -92
  103. nautobot/core/tests/test_graphql.py +110 -77
  104. nautobot/core/tests/test_logging.py +4 -0
  105. nautobot/core/tests/test_managers.py +3 -1
  106. nautobot/core/tests/test_models.py +2 -0
  107. nautobot/core/tests/test_paginator.py +3 -1
  108. nautobot/core/tests/test_releases.py +12 -12
  109. nautobot/core/tests/test_templatetags_helpers.py +4 -4
  110. nautobot/core/tests/test_utils.py +32 -68
  111. nautobot/core/tests/test_views.py +12 -15
  112. nautobot/core/utils/data.py +17 -0
  113. nautobot/core/utils/deprecation.py +9 -6
  114. nautobot/core/utils/filtering.py +8 -3
  115. nautobot/core/utils/git.py +12 -4
  116. nautobot/core/utils/lookup.py +3 -1
  117. nautobot/core/utils/requests.py +1 -104
  118. nautobot/core/views/__init__.py +1 -0
  119. nautobot/core/views/generic.py +75 -110
  120. nautobot/core/views/mixins.py +52 -61
  121. nautobot/core/views/renderers.py +6 -7
  122. nautobot/core/views/utils.py +80 -0
  123. nautobot/dcim/api/serializers.py +160 -667
  124. nautobot/dcim/api/urls.py +1 -1
  125. nautobot/dcim/api/views.py +7 -44
  126. nautobot/dcim/choices.py +2 -0
  127. nautobot/dcim/filters/__init__.py +21 -0
  128. nautobot/dcim/form_mixins.py +1 -27
  129. nautobot/dcim/forms.py +19 -765
  130. nautobot/dcim/migrations/0024_alter_device_and_rack_role_add_new_role.py +2 -1
  131. nautobot/dcim/migrations/0025_device_and_rack_roles_data_migrations.py +19 -13
  132. nautobot/dcim/migrations/0027_remove_device_role_and_rack_role.py +1 -1
  133. nautobot/dcim/migrations/0028_rename_foreignkey_fields.py +1 -1
  134. nautobot/dcim/migrations/0030_migrate_region_and_site_data_to_locations.py +2 -2
  135. nautobot/dcim/migrations/0035_related_name_changes.py +1 -1
  136. nautobot/dcim/migrations/0036_remove_region_and_site.py +1 -1
  137. nautobot/dcim/migrations/0040_tagsfield.py +109 -0
  138. nautobot/dcim/migrations/{0040_ipam__namespaces.py → 0041_ipam__namespaces.py} +1 -1
  139. nautobot/dcim/migrations/0042_fixup_null_statuses.py +51 -0
  140. nautobot/dcim/migrations/0043_status_nonnullable.py +72 -0
  141. nautobot/dcim/models/cables.py +3 -33
  142. nautobot/dcim/models/device_component_templates.py +6 -0
  143. nautobot/dcim/models/device_components.py +12 -198
  144. nautobot/dcim/models/devices.py +30 -143
  145. nautobot/dcim/models/locations.py +3 -64
  146. nautobot/dcim/models/power.py +3 -50
  147. nautobot/dcim/models/racks.py +7 -84
  148. nautobot/dcim/navigation.py +141 -467
  149. nautobot/dcim/signals.py +0 -2
  150. nautobot/dcim/tables/locations.py +2 -2
  151. nautobot/dcim/tables/power.py +1 -2
  152. nautobot/dcim/templates/dcim/console_port_connection_list.html +7 -0
  153. nautobot/dcim/templates/dcim/devicetype.html +2 -2
  154. nautobot/dcim/templates/dcim/interface_connection_list.html +7 -0
  155. nautobot/dcim/templates/dcim/location.html +16 -1
  156. nautobot/dcim/templates/dcim/locationtype.html +15 -0
  157. nautobot/dcim/templates/dcim/power_port_connection_list.html +7 -0
  158. nautobot/dcim/templates/dcim/rackgroup.html +0 -12
  159. nautobot/dcim/tests/test_api.py +166 -81
  160. nautobot/dcim/tests/test_cablepaths.py +41 -35
  161. nautobot/dcim/tests/test_filters.py +67 -23
  162. nautobot/dcim/tests/test_forms.py +5 -205
  163. nautobot/dcim/tests/test_graphql.py +7 -2
  164. nautobot/dcim/tests/test_migrations.py +6 -11
  165. nautobot/dcim/tests/test_models.py +182 -110
  166. nautobot/dcim/tests/test_natural_ordering.py +11 -8
  167. nautobot/dcim/tests/test_signals.py +6 -3
  168. nautobot/dcim/tests/test_views.py +197 -175
  169. nautobot/dcim/urls.py +11 -16
  170. nautobot/dcim/views.py +7 -134
  171. nautobot/docs/additional-features/caching.md +6 -87
  172. nautobot/docs/additional-features/job-scheduling-and-approvals.md +3 -0
  173. nautobot/docs/additional-features/jobs.md +177 -195
  174. nautobot/docs/administration/nautobot-server.md +6 -21
  175. nautobot/docs/administration/replicating-nautobot.md +0 -10
  176. nautobot/docs/configuration/optional-settings.md +32 -41
  177. nautobot/docs/configuration/required-settings.md +11 -52
  178. nautobot/docs/development/application-registry.md +2 -13
  179. nautobot/docs/development/extending-models.md +15 -17
  180. nautobot/docs/development/generic-views.md +0 -2
  181. nautobot/docs/development/getting-started.md +55 -5
  182. nautobot/docs/development/navigation-menu.md +22 -93
  183. nautobot/docs/development/react-ui.md +105 -0
  184. nautobot/docs/development/role-internals.md +1 -3
  185. nautobot/docs/development/style-guide.md +6 -4
  186. nautobot/docs/index.md +3 -2
  187. nautobot/docs/installation/migrating-from-netbox.md +11 -42
  188. nautobot/docs/installation/nautobot.md +1 -1
  189. nautobot/docs/installation/tables/v2-api-behavior-changes.yaml +70 -0
  190. nautobot/docs/installation/tables/v2-api-removed-fields.yaml +142 -0
  191. nautobot/docs/installation/tables/v2-api-renamed-fields.yaml +124 -0
  192. nautobot/docs/installation/tables/v2-code-location-changes.yaml +241 -0
  193. nautobot/docs/installation/tables/v2-code-removals.yaml +67 -0
  194. nautobot/docs/installation/tables/v2-database-behavior-changes.yaml +37 -0
  195. nautobot/docs/installation/tables/v2-database-removed-fields.yaml +166 -0
  196. nautobot/docs/installation/tables/v2-database-renamed-fields.yaml +340 -0
  197. nautobot/docs/installation/tables/v2-filters-corrected-fields.yaml +28 -0
  198. nautobot/docs/installation/tables/v2-filters-enhanced-fields.yaml +241 -0
  199. nautobot/docs/installation/tables/v2-filters-removed-fields.yaml +553 -0
  200. nautobot/docs/installation/tables/v2-filters-renamed-fields.yaml +223 -0
  201. nautobot/docs/installation/tables/v2-logging-renamed-loggers.yaml +23 -0
  202. nautobot/docs/installation/upgrading-from-nautobot-v1.md +170 -747
  203. nautobot/docs/models/dcim/device.md +3 -0
  204. nautobot/docs/models/dcim/deviceredundancygroup.md +3 -3
  205. nautobot/docs/models/extras/computedfield.md +4 -4
  206. nautobot/docs/models/extras/gitrepository.md +3 -0
  207. nautobot/docs/models/extras/job.md +1 -0
  208. nautobot/docs/models/extras/jobbutton.md +18 -13
  209. nautobot/docs/models/extras/jobhook.md +7 -4
  210. nautobot/docs/models/extras/jobresult.md +6 -2
  211. nautobot/docs/models/extras/relationship.md +2 -2
  212. nautobot/docs/models/extras/status.md +6 -19
  213. nautobot/docs/models/ipam/ipaddress.md +3 -0
  214. nautobot/docs/models/virtualization/virtualmachine.md +3 -0
  215. nautobot/docs/plugins/development.md +83 -21
  216. nautobot/docs/release-notes/version-1.5.md +53 -0
  217. nautobot/docs/release-notes/version-2.0.md +180 -0
  218. nautobot/docs/requirements.txt +1 -0
  219. nautobot/docs/rest-api/overview.md +384 -215
  220. nautobot/docs/rest-api/ui-related-endpoints.md +9 -0
  221. nautobot/extras/admin.py +3 -5
  222. nautobot/extras/api/customfields.py +15 -39
  223. nautobot/extras/api/fields.py +0 -11
  224. nautobot/extras/api/mixins.py +45 -0
  225. nautobot/extras/api/relationships.py +63 -158
  226. nautobot/extras/api/serializers.py +165 -700
  227. nautobot/extras/api/urls.py +1 -1
  228. nautobot/extras/api/views.py +294 -280
  229. nautobot/extras/apps.py +4 -7
  230. nautobot/extras/choices.py +11 -9
  231. nautobot/extras/constants.py +9 -3
  232. nautobot/extras/datasources/__init__.py +2 -0
  233. nautobot/extras/datasources/git.py +135 -186
  234. nautobot/extras/datasources/registry.py +25 -35
  235. nautobot/extras/filters/__init__.py +20 -19
  236. nautobot/extras/filters/mixins.py +4 -4
  237. nautobot/extras/forms/forms.py +63 -127
  238. nautobot/extras/forms/mixins.py +23 -51
  239. nautobot/extras/health_checks.py +0 -33
  240. nautobot/extras/jobs.py +387 -565
  241. nautobot/extras/management/commands/runjob.py +24 -62
  242. nautobot/extras/managers.py +30 -7
  243. nautobot/extras/migrations/0058_jobresult_add_time_status_idxs.py +38 -0
  244. nautobot/extras/migrations/{0058_joblogentry_scheduledjob_webhook_data_migration.py → 0059_joblogentry_scheduledjob_webhook_data_migration.py} +1 -1
  245. nautobot/extras/migrations/{0059_alter_joblogentry_scheduledjob_webhook_fields.py → 0060_alter_joblogentry_scheduledjob_webhook_fields.py} +1 -1
  246. nautobot/extras/migrations/{0060_role_and_alter_status.py → 0061_role_and_alter_status.py} +1 -7
  247. nautobot/extras/migrations/{0061_collect_roles_from_related_apps_roles.py → 0062_collect_roles_from_related_apps_roles.py} +33 -32
  248. nautobot/extras/migrations/{0062_alter_role_options.py → 0063_alter_role_options.py} +1 -1
  249. nautobot/extras/migrations/{0063_alter_configcontext_and_add_new_role.py → 0064_alter_configcontext_and_add_new_role.py} +1 -1
  250. nautobot/extras/migrations/0065_configcontext_data_migrations.py +44 -0
  251. nautobot/extras/migrations/{0065_rename_configcontext_role.py → 0066_rename_configcontext_role.py} +1 -1
  252. nautobot/extras/migrations/{0066_jobresult__add_celery_fields.py → 0067_jobresult__add_celery_fields.py} +36 -2
  253. nautobot/extras/migrations/{0067_created_datetime.py → 0068_created_datetime.py} +1 -1
  254. nautobot/extras/migrations/{0068_remove_site_and_region_attributes_from_config_context.py → 0069_remove_site_and_region_attributes_from_config_context.py} +1 -1
  255. nautobot/extras/migrations/{0069_replace_related_names.py → 0070_replace_related_names.py} +1 -1
  256. nautobot/extras/migrations/{0070_rename_model_fields.py → 0071_rename_model_fields.py} +1 -1
  257. nautobot/extras/migrations/0072_job__unique_name_data_migration.py +86 -0
  258. nautobot/extras/migrations/{0072_job__unique_name.py → 0073_job__unique_name.py} +13 -9
  259. nautobot/extras/migrations/{0073_remove_gitrepository_fields.py → 0074_remove_gitrepository_fields.py} +1 -1
  260. nautobot/extras/migrations/{0074_rename_slug_to_key_for_custom_field.py → 0075_rename_slug_to_key_for_custom_field.py} +1 -1
  261. nautobot/extras/migrations/{0075_migrate_custom_field_data.py → 0076_migrate_custom_field_data.py} +1 -1
  262. nautobot/extras/migrations/{0076_remove_name_field_and_make_label_field_non_nullable.py → 0077_remove_name_field_and_make_label_field_non_nullable.py} +1 -1
  263. nautobot/extras/migrations/{0077_remove_slug.py → 0078_remove_slug.py} +1 -5
  264. nautobot/extras/migrations/0079_tagsfield.py +28 -0
  265. nautobot/extras/migrations/0080_rename_relationship_slug_to_key.py +17 -0
  266. nautobot/extras/migrations/0081_rename_relationship_name_to_label.py +29 -0
  267. nautobot/extras/migrations/0082_ensure_relationship_keys_are_unique.py +43 -0
  268. nautobot/extras/migrations/0083_rename_computed_field_slug_to_key.py +21 -0
  269. nautobot/extras/migrations/0084_taggeditem_cleanup.py +43 -0
  270. nautobot/extras/migrations/0085_taggeditem_uniqueness.py +22 -0
  271. nautobot/extras/migrations/0086_job__celery_task_fields__dryrun_support.py +81 -0
  272. nautobot/extras/migrations/0087_job__commit_default_data_migration.py +26 -0
  273. nautobot/extras/migrations/0088_joblogentry__log_level_default.py +17 -0
  274. nautobot/extras/migrations/0089_joblogentry__log_level_data_migration.py +34 -0
  275. nautobot/extras/migrations/0090_scheduledjob__data_migration.py +57 -0
  276. nautobot/extras/models/__init__.py +2 -3
  277. nautobot/extras/models/change_logging.py +0 -36
  278. nautobot/extras/models/customfields.py +39 -33
  279. nautobot/extras/models/datasources.py +48 -50
  280. nautobot/extras/models/groups.py +5 -6
  281. nautobot/extras/models/jobs.py +189 -321
  282. nautobot/extras/models/mixins.py +0 -71
  283. nautobot/extras/models/models.py +0 -19
  284. nautobot/extras/models/relationships.py +19 -13
  285. nautobot/extras/models/roles.py +0 -34
  286. nautobot/extras/models/secrets.py +2 -26
  287. nautobot/extras/models/statuses.py +6 -5
  288. nautobot/extras/models/tags.py +2 -17
  289. nautobot/extras/navigation.py +89 -307
  290. nautobot/extras/plugins/__init__.py +3 -120
  291. nautobot/extras/plugins/utils.py +0 -3
  292. nautobot/extras/plugins/validators.py +5 -4
  293. nautobot/extras/plugins/views.py +16 -3
  294. nautobot/extras/querysets.py +1 -7
  295. nautobot/extras/registry.py +3 -0
  296. nautobot/extras/signals.py +26 -60
  297. nautobot/extras/tables.py +34 -40
  298. nautobot/extras/tasks.py +0 -12
  299. nautobot/extras/templates/extras/configcontext.html +1 -1
  300. nautobot/extras/templates/extras/configcontextschema.html +16 -1
  301. nautobot/extras/templates/extras/customfield.html +0 -13
  302. nautobot/extras/templates/extras/gitrepository.html +3 -3
  303. nautobot/extras/templates/extras/inc/jobresult.html +10 -0
  304. nautobot/extras/templates/extras/inc/panel_jobhistory.html +1 -1
  305. nautobot/extras/templates/extras/job.html +35 -25
  306. nautobot/extras/templates/extras/job_approval_request.html +15 -30
  307. nautobot/extras/templates/extras/job_detail.html +13 -31
  308. nautobot/extras/templates/extras/job_edit.html +15 -17
  309. nautobot/extras/templates/extras/jobresult.html +24 -6
  310. nautobot/extras/templates/extras/scheduledjob.html +2 -2
  311. nautobot/extras/templates/extras/secret.html +28 -0
  312. nautobot/extras/templatetags/job_buttons.py +1 -0
  313. nautobot/extras/{tests/example_jobs → test_jobs}/api_test_job.py +13 -6
  314. nautobot/extras/test_jobs/atomic_transaction.py +53 -0
  315. nautobot/extras/test_jobs/dry_run.py +29 -0
  316. nautobot/extras/{tests/example_jobs/test_duplicate_name.py → test_jobs/duplicate_name.py} +4 -0
  317. nautobot/extras/test_jobs/duplicate_name2.py +9 -0
  318. nautobot/extras/test_jobs/fail.py +23 -0
  319. nautobot/extras/{tests/example_jobs/test_field_default.py → test_jobs/field_default.py} +4 -0
  320. nautobot/extras/{tests/example_jobs/test_field_order.py → test_jobs/field_order.py} +4 -0
  321. nautobot/extras/{tests/example_jobs/test_file_upload_fail.py → test_jobs/file_upload_fail.py} +11 -6
  322. nautobot/extras/test_jobs/file_upload_pass.py +25 -0
  323. nautobot/extras/test_jobs/has_sensitive_variables.py +25 -0
  324. nautobot/extras/test_jobs/ipaddress_vars.py +66 -0
  325. nautobot/extras/test_jobs/job_button_receiver.py +28 -0
  326. nautobot/extras/test_jobs/job_hook_receiver.py +29 -0
  327. nautobot/extras/test_jobs/job_variables.py +88 -0
  328. nautobot/extras/test_jobs/location_with_custom_field.py +45 -0
  329. nautobot/extras/test_jobs/log_redaction.py +20 -0
  330. nautobot/extras/test_jobs/log_skip_db_logging.py +17 -0
  331. nautobot/extras/test_jobs/modify_db.py +25 -0
  332. nautobot/extras/{tests/example_jobs/test_no_field_order.py → test_jobs/no_field_order.py} +4 -0
  333. nautobot/extras/test_jobs/object_var_optional.py +21 -0
  334. nautobot/extras/test_jobs/object_var_required.py +21 -0
  335. nautobot/extras/test_jobs/object_vars.py +26 -0
  336. nautobot/extras/test_jobs/pass.py +25 -0
  337. nautobot/extras/test_jobs/profiling.py +32 -0
  338. nautobot/extras/test_jobs/read_only_job.py +15 -0
  339. nautobot/extras/{tests/example_jobs/test_required_args.py → test_jobs/required_args.py} +4 -0
  340. nautobot/extras/{tests/example_jobs/test_soft_time_limit_greater_than_time_limit.py → test_jobs/soft_time_limit_greater_than_time_limit.py} +5 -1
  341. nautobot/extras/{tests/example_jobs/test_task_queues.py → test_jobs/task_queues.py} +5 -1
  342. nautobot/extras/tests/integration/test_computedfields.py +1 -1
  343. nautobot/extras/tests/integration/test_configcontextschema.py +5 -3
  344. nautobot/extras/tests/integration/test_customfields.py +4 -2
  345. nautobot/extras/tests/integration/test_dynamicgroups.py +1 -1
  346. nautobot/extras/tests/integration/test_jobs.py +25 -27
  347. nautobot/extras/tests/integration/test_notes.py +8 -4
  348. nautobot/extras/tests/integration/test_relationships.py +2 -2
  349. nautobot/extras/tests/test_api.py +649 -642
  350. nautobot/extras/tests/test_changelog.py +3 -3
  351. nautobot/extras/tests/test_context_managers.py +5 -3
  352. nautobot/extras/tests/test_customfields.py +92 -50
  353. nautobot/extras/tests/test_datasources.py +189 -112
  354. nautobot/extras/tests/test_dynamicgroups.py +7 -8
  355. nautobot/extras/tests/test_filters.py +137 -89
  356. nautobot/extras/tests/test_forms.py +73 -75
  357. nautobot/extras/tests/{test_scripts.py → test_job_variables.py} +43 -49
  358. nautobot/extras/tests/test_jobs.py +262 -263
  359. nautobot/extras/tests/test_migrations.py +4 -3
  360. nautobot/extras/tests/test_models.py +116 -161
  361. nautobot/extras/tests/test_plugins.py +38 -60
  362. nautobot/extras/tests/test_relationships.py +167 -120
  363. nautobot/extras/tests/test_tags.py +6 -11
  364. nautobot/extras/tests/test_utils.py +31 -1
  365. nautobot/extras/tests/test_views.py +201 -145
  366. nautobot/extras/tests/test_webhooks.py +6 -2
  367. nautobot/extras/urls.py +42 -42
  368. nautobot/extras/utils.py +137 -163
  369. nautobot/extras/views.py +78 -152
  370. nautobot/ipam/api/fields.py +17 -0
  371. nautobot/ipam/api/serializers.py +58 -164
  372. nautobot/ipam/api/urls.py +1 -1
  373. nautobot/ipam/api/views.py +3 -2
  374. nautobot/ipam/apps.py +1 -2
  375. nautobot/ipam/filters.py +1 -10
  376. nautobot/ipam/forms.py +4 -177
  377. nautobot/ipam/lookups.py +1 -0
  378. nautobot/ipam/management/commands/__init__.py +0 -0
  379. nautobot/ipam/management/commands/fix_prefix_broadcast.py +17 -0
  380. nautobot/ipam/migrations/0010_alter_ipam_role_add_new_role.py +1 -1
  381. nautobot/ipam/migrations/0011_migrate_ipam_role_data.py +32 -38
  382. nautobot/ipam/migrations/0020_related_name_changes.py +1 -1
  383. nautobot/ipam/migrations/0022_aggregate_to_prefix_data_migration.py +2 -2
  384. nautobot/ipam/migrations/0028_tagsfield.py +44 -0
  385. nautobot/ipam/migrations/0029_ip_address_to_interface_uniqueness_constraints.py +18 -0
  386. nautobot/ipam/migrations/{0028_ipam__namespaces.py → 0030_ipam__namespaces.py} +77 -28
  387. nautobot/ipam/migrations/0031_ipam__prefix__add_parent.py +58 -0
  388. nautobot/ipam/migrations/0032_ipam__namespaces_finish.py +63 -0
  389. nautobot/ipam/migrations/0033_fixup_null_statuses.py +26 -0
  390. nautobot/ipam/migrations/0034_status_nonnullable.py +36 -0
  391. nautobot/ipam/models.py +100 -236
  392. nautobot/ipam/navigation.py +36 -181
  393. nautobot/ipam/querysets.py +20 -25
  394. nautobot/ipam/signals.py +49 -6
  395. nautobot/ipam/tables.py +10 -3
  396. nautobot/ipam/templates/ipam/namespace_ipaddresses.html +11 -0
  397. nautobot/ipam/templates/ipam/namespace_prefixes.html +11 -0
  398. nautobot/ipam/templates/ipam/namespace_retrieve.html +17 -4
  399. nautobot/ipam/templates/ipam/namespace_vrfs.html +11 -0
  400. nautobot/ipam/templates/ipam/prefix.html +1 -1
  401. nautobot/ipam/templates/ipam/vlangroup.html +0 -13
  402. nautobot/ipam/templates/ipam/vrf_edit.html +6 -0
  403. nautobot/ipam/tests/integration/test_prefixes.py +3 -26
  404. nautobot/ipam/tests/test_api.py +22 -19
  405. nautobot/ipam/tests/test_filters.py +59 -23
  406. nautobot/ipam/tests/test_migrations.py +6 -10
  407. nautobot/ipam/tests/test_models.py +323 -198
  408. nautobot/ipam/tests/test_ordering.py +2 -2
  409. nautobot/ipam/tests/test_querysets.py +44 -24
  410. nautobot/ipam/tests/test_views.py +73 -26
  411. nautobot/ipam/urls.py +16 -0
  412. nautobot/ipam/{utils.py → utils/__init__.py} +2 -2
  413. nautobot/ipam/utils/migrations.py +713 -0
  414. nautobot/ipam/views.py +137 -20
  415. nautobot/project-static/docs/404.html +1178 -10
  416. nautobot/project-static/docs/additional-features/caching.html +1224 -159
  417. nautobot/project-static/docs/additional-features/change-logging.html +1180 -12
  418. nautobot/project-static/docs/additional-features/config-contexts.html +1180 -12
  419. nautobot/project-static/docs/additional-features/graphql.html +1179 -11
  420. nautobot/project-static/docs/additional-features/healthcheck.html +1180 -12
  421. nautobot/project-static/docs/additional-features/job-scheduling-and-approvals.html +1184 -12
  422. nautobot/project-static/docs/additional-features/jobs.html +1514 -328
  423. nautobot/project-static/docs/additional-features/napalm.html +1180 -12
  424. nautobot/project-static/docs/additional-features/prometheus-metrics.html +1180 -12
  425. nautobot/project-static/docs/additional-features/template-filters.html +1180 -12
  426. nautobot/project-static/docs/administration/celery-queues.html +1178 -10
  427. nautobot/project-static/docs/administration/nautobot-server.html +1451 -304
  428. nautobot/project-static/docs/administration/nautobot-shell.html +1178 -10
  429. nautobot/project-static/docs/administration/permissions.html +1178 -10
  430. nautobot/project-static/docs/administration/replicating-nautobot.html +1262 -113
  431. nautobot/project-static/docs/apps/index.html +1178 -10
  432. nautobot/project-static/docs/apps/nautobot-apps.html +1178 -10
  433. nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +1580 -426
  434. nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +1178 -10
  435. nautobot/project-static/docs/code-reference/nautobot/apps/api.html +3481 -1838
  436. nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +1178 -10
  437. nautobot/project-static/docs/code-reference/nautobot/apps/config.html +1178 -10
  438. nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +1185 -11
  439. nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +1719 -551
  440. nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +2062 -930
  441. nautobot/project-static/docs/code-reference/nautobot/apps/models.html +1946 -659
  442. nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +1180 -12
  443. nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +1189 -21
  444. nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +9283 -6218
  445. nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +2734 -2122
  446. nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +1178 -10
  447. nautobot/project-static/docs/code-reference/nautobot/apps/views.html +2337 -1300
  448. nautobot/project-static/docs/configuration/authentication/ldap.html +1178 -10
  449. nautobot/project-static/docs/configuration/authentication/remote.html +1178 -10
  450. nautobot/project-static/docs/configuration/authentication/sso.html +1178 -10
  451. nautobot/project-static/docs/configuration/index.html +1178 -10
  452. nautobot/project-static/docs/configuration/optional-settings.html +1311 -160
  453. nautobot/project-static/docs/configuration/required-settings.html +1312 -211
  454. nautobot/project-static/docs/core-functionality/circuits.html +1178 -10
  455. nautobot/project-static/docs/core-functionality/device-types.html +1178 -10
  456. nautobot/project-static/docs/core-functionality/devices.html +1182 -10
  457. nautobot/project-static/docs/core-functionality/ipam.html +1182 -10
  458. nautobot/project-static/docs/core-functionality/power.html +1178 -10
  459. nautobot/project-static/docs/core-functionality/secrets.html +1178 -10
  460. nautobot/project-static/docs/core-functionality/services.html +1178 -10
  461. nautobot/project-static/docs/core-functionality/sites-and-racks.html +1178 -10
  462. nautobot/project-static/docs/core-functionality/tenancy.html +1178 -10
  463. nautobot/project-static/docs/core-functionality/virtualization.html +1182 -10
  464. nautobot/project-static/docs/core-functionality/vlans.html +1179 -11
  465. nautobot/project-static/docs/development/application-registry.html +1190 -42
  466. nautobot/project-static/docs/development/best-practices.html +1178 -10
  467. nautobot/project-static/docs/development/docker-compose-advanced-use-cases.html +1178 -10
  468. nautobot/project-static/docs/development/extending-models.html +1238 -83
  469. nautobot/project-static/docs/development/generic-views.html +1180 -14
  470. nautobot/project-static/docs/development/getting-started.html +1365 -90
  471. nautobot/project-static/docs/development/homepage.html +1178 -10
  472. nautobot/project-static/docs/development/index.html +1178 -10
  473. nautobot/project-static/docs/development/model-features.html +1178 -10
  474. nautobot/project-static/docs/development/natural-keys.html +1178 -10
  475. nautobot/project-static/docs/development/navigation-menu.html +1215 -125
  476. nautobot/project-static/docs/development/react-ui.html +4199 -0
  477. nautobot/project-static/docs/development/release-checklist.html +1178 -10
  478. nautobot/project-static/docs/development/role-internals.html +1179 -12
  479. nautobot/project-static/docs/development/style-guide.html +1188 -19
  480. nautobot/project-static/docs/development/templates.html +1178 -10
  481. nautobot/project-static/docs/development/testing.html +1178 -10
  482. nautobot/project-static/docs/development/user-preferences.html +1178 -10
  483. nautobot/project-static/docs/docker/index.html +1178 -10
  484. nautobot/project-static/docs/index.html +1183 -12
  485. nautobot/project-static/docs/installation/centos.html +1178 -10
  486. nautobot/project-static/docs/installation/external-authentication.html +1178 -10
  487. nautobot/project-static/docs/installation/http-server.html +1178 -10
  488. nautobot/project-static/docs/installation/index.html +1178 -10
  489. nautobot/project-static/docs/installation/migrating-from-netbox.html +1305 -189
  490. nautobot/project-static/docs/installation/migrating-from-postgresql.html +1178 -10
  491. nautobot/project-static/docs/installation/nautobot.html +1179 -11
  492. nautobot/project-static/docs/installation/region-and-site-data-migration-guide.html +1178 -10
  493. nautobot/project-static/docs/installation/selinux-troubleshooting.html +1178 -10
  494. nautobot/project-static/docs/installation/services.html +1178 -10
  495. nautobot/project-static/docs/installation/tables/v2-api-behavior-changes.yaml +70 -0
  496. nautobot/project-static/docs/installation/tables/v2-api-removed-fields.yaml +142 -0
  497. nautobot/project-static/docs/installation/tables/v2-api-renamed-fields.yaml +124 -0
  498. nautobot/project-static/docs/installation/tables/v2-code-location-changes.yaml +241 -0
  499. nautobot/project-static/docs/installation/tables/v2-code-removals.yaml +67 -0
  500. nautobot/project-static/docs/installation/tables/v2-database-behavior-changes.yaml +37 -0
  501. nautobot/project-static/docs/installation/tables/v2-database-removed-fields.yaml +166 -0
  502. nautobot/project-static/docs/installation/tables/v2-database-renamed-fields.yaml +340 -0
  503. nautobot/project-static/docs/installation/tables/v2-filters-corrected-fields.yaml +28 -0
  504. nautobot/project-static/docs/installation/tables/v2-filters-enhanced-fields.yaml +241 -0
  505. nautobot/project-static/docs/installation/tables/v2-filters-removed-fields.yaml +553 -0
  506. nautobot/project-static/docs/installation/tables/v2-filters-renamed-fields.yaml +223 -0
  507. nautobot/project-static/docs/installation/tables/v2-logging-renamed-loggers.yaml +23 -0
  508. nautobot/project-static/docs/installation/ubuntu.html +1178 -10
  509. nautobot/project-static/docs/installation/upgrading-from-nautobot-v1.html +3823 -2152
  510. nautobot/project-static/docs/installation/upgrading.html +1178 -10
  511. nautobot/project-static/docs/models/circuits/circuit.html +1293 -103
  512. nautobot/project-static/docs/models/circuits/circuittermination.html +1293 -103
  513. nautobot/project-static/docs/models/circuits/circuittype.html +1293 -103
  514. nautobot/project-static/docs/models/circuits/provider.html +1293 -103
  515. nautobot/project-static/docs/models/circuits/providernetwork.html +1293 -103
  516. nautobot/project-static/docs/models/dcim/cable.html +1324 -103
  517. nautobot/project-static/docs/models/dcim/consoleport.html +1293 -103
  518. nautobot/project-static/docs/models/dcim/consoleporttemplate.html +1293 -103
  519. nautobot/project-static/docs/models/dcim/consoleserverport.html +1293 -103
  520. nautobot/project-static/docs/models/dcim/consoleserverporttemplate.html +1293 -103
  521. nautobot/project-static/docs/models/dcim/device.html +1326 -132
  522. nautobot/project-static/docs/models/dcim/devicebay.html +1293 -103
  523. nautobot/project-static/docs/models/dcim/devicebaytemplate.html +1293 -103
  524. nautobot/project-static/docs/models/dcim/deviceredundancygroup.html +1379 -97
  525. nautobot/project-static/docs/models/dcim/devicetype.html +1293 -103
  526. nautobot/project-static/docs/models/dcim/frontport.html +1293 -103
  527. nautobot/project-static/docs/models/dcim/frontporttemplate.html +1293 -103
  528. nautobot/project-static/docs/models/dcim/interface.html +1293 -103
  529. nautobot/project-static/docs/models/dcim/interfacetemplate.html +1293 -103
  530. nautobot/project-static/docs/models/dcim/inventoryitem.html +1293 -103
  531. nautobot/project-static/docs/models/dcim/location.html +1293 -103
  532. nautobot/project-static/docs/models/dcim/locationtype.html +1293 -103
  533. nautobot/project-static/docs/models/dcim/manufacturer.html +1292 -102
  534. nautobot/project-static/docs/models/dcim/platform.html +1272 -82
  535. nautobot/project-static/docs/models/dcim/powerfeed.html +1270 -80
  536. nautobot/project-static/docs/models/dcim/poweroutlet.html +1272 -82
  537. nautobot/project-static/docs/models/dcim/poweroutlettemplate.html +1272 -82
  538. nautobot/project-static/docs/models/dcim/powerpanel.html +1270 -80
  539. nautobot/project-static/docs/models/dcim/powerport.html +1272 -82
  540. nautobot/project-static/docs/models/dcim/powerporttemplate.html +1272 -82
  541. nautobot/project-static/docs/models/dcim/rack.html +1272 -82
  542. nautobot/project-static/docs/models/dcim/rackgroup.html +1272 -82
  543. nautobot/project-static/docs/models/dcim/rackreservation.html +1272 -82
  544. nautobot/project-static/docs/models/dcim/rearport.html +1286 -96
  545. nautobot/project-static/docs/models/dcim/rearporttemplate.html +1286 -96
  546. nautobot/project-static/docs/models/dcim/region.html +1178 -10
  547. nautobot/project-static/docs/models/dcim/site.html +1178 -10
  548. nautobot/project-static/docs/models/dcim/virtualchassis.html +1284 -94
  549. nautobot/project-static/docs/models/extras/computedfield.html +1184 -16
  550. nautobot/project-static/docs/models/extras/configcontext.html +1314 -86
  551. nautobot/project-static/docs/models/extras/configcontextschema.html +1276 -86
  552. nautobot/project-static/docs/models/extras/customfield.html +1180 -12
  553. nautobot/project-static/docs/models/extras/customlink.html +1180 -12
  554. nautobot/project-static/docs/models/extras/dynamicgroup.html +1180 -12
  555. nautobot/project-static/docs/models/extras/exporttemplate.html +1180 -12
  556. nautobot/project-static/docs/models/extras/gitrepository.html +1184 -12
  557. nautobot/project-static/docs/models/extras/graphqlquery.html +1321 -86
  558. nautobot/project-static/docs/models/extras/imageattachment.html +1276 -86
  559. nautobot/project-static/docs/models/extras/job.html +1277 -86
  560. nautobot/project-static/docs/models/extras/jobbutton.html +1201 -29
  561. nautobot/project-static/docs/models/extras/jobhook.html +1188 -16
  562. nautobot/project-static/docs/models/extras/joblogentry.html +1274 -84
  563. nautobot/project-static/docs/models/extras/jobresult.html +1364 -169
  564. nautobot/project-static/docs/models/extras/note.html +1180 -12
  565. nautobot/project-static/docs/models/extras/relationship.html +1182 -14
  566. nautobot/project-static/docs/models/extras/role.html +1320 -86
  567. nautobot/project-static/docs/models/extras/secret.html +1314 -86
  568. nautobot/project-static/docs/models/extras/secretsgroup.html +1276 -86
  569. nautobot/project-static/docs/models/extras/status.html +1188 -59
  570. nautobot/project-static/docs/models/extras/tag.html +1180 -12
  571. nautobot/project-static/docs/models/extras/webhook.html +1180 -12
  572. nautobot/project-static/docs/models/ipam/ipaddress.html +1327 -102
  573. nautobot/project-static/docs/models/ipam/prefix.html +1276 -86
  574. nautobot/project-static/docs/models/ipam/rir.html +1276 -86
  575. nautobot/project-static/docs/models/ipam/routetarget.html +1276 -86
  576. nautobot/project-static/docs/models/ipam/service.html +1276 -86
  577. nautobot/project-static/docs/models/ipam/vlan.html +1276 -86
  578. nautobot/project-static/docs/models/ipam/vlangroup.html +1276 -86
  579. nautobot/project-static/docs/models/ipam/vrf.html +1276 -86
  580. nautobot/project-static/docs/models/tenancy/tenant.html +1276 -86
  581. nautobot/project-static/docs/models/tenancy/tenantgroup.html +1276 -86
  582. nautobot/project-static/docs/models/users/objectpermission.html +1314 -86
  583. nautobot/project-static/docs/models/users/token.html +1276 -86
  584. nautobot/project-static/docs/models/virtualization/cluster.html +1276 -86
  585. nautobot/project-static/docs/models/virtualization/clustergroup.html +1276 -86
  586. nautobot/project-static/docs/models/virtualization/clustertype.html +1276 -86
  587. nautobot/project-static/docs/models/virtualization/virtualmachine.html +1321 -127
  588. nautobot/project-static/docs/models/virtualization/vminterface.html +1276 -86
  589. nautobot/project-static/docs/objects.inv +0 -0
  590. nautobot/project-static/docs/plugins/development.html +1726 -495
  591. nautobot/project-static/docs/plugins/index.html +1178 -10
  592. nautobot/project-static/docs/plugins/porting-from-netbox.html +1178 -10
  593. nautobot/project-static/docs/release-notes/index.html +1178 -10
  594. nautobot/project-static/docs/release-notes/version-1.0.html +1178 -10
  595. nautobot/project-static/docs/release-notes/version-1.1.html +1178 -10
  596. nautobot/project-static/docs/release-notes/version-1.2.html +1178 -10
  597. nautobot/project-static/docs/release-notes/version-1.3.html +1178 -10
  598. nautobot/project-static/docs/release-notes/version-1.4.html +1178 -10
  599. nautobot/project-static/docs/release-notes/version-1.5.html +1608 -225
  600. nautobot/project-static/docs/release-notes/version-2.0.html +1547 -47
  601. nautobot/project-static/docs/requirements.txt +1 -0
  602. nautobot/project-static/docs/rest-api/authentication.html +1179 -11
  603. nautobot/project-static/docs/rest-api/filtering.html +1178 -10
  604. nautobot/project-static/docs/rest-api/overview.html +1841 -446
  605. nautobot/project-static/docs/rest-api/ui-related-endpoints.html +4057 -0
  606. nautobot/project-static/docs/search/search_index.json +1 -1
  607. nautobot/project-static/docs/sitemap.xml +197 -187
  608. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  609. nautobot/project-static/docs/user-guides/custom-fields.html +1178 -10
  610. nautobot/project-static/docs/user-guides/getting-started/creating-devices.html +1178 -10
  611. nautobot/project-static/docs/user-guides/getting-started/index.html +1178 -10
  612. nautobot/project-static/docs/user-guides/getting-started/interfaces.html +1178 -10
  613. nautobot/project-static/docs/user-guides/getting-started/ipam.html +1178 -10
  614. nautobot/project-static/docs/user-guides/getting-started/platforms.html +1178 -10
  615. nautobot/project-static/docs/user-guides/getting-started/regions.html +1178 -10
  616. nautobot/project-static/docs/user-guides/getting-started/search-bar.html +1178 -10
  617. nautobot/project-static/docs/user-guides/getting-started/tenants.html +1178 -10
  618. nautobot/project-static/docs/user-guides/getting-started/vlans-and-vlan-groups.html +1178 -10
  619. nautobot/project-static/docs/user-guides/git-data-source.html +1178 -10
  620. nautobot/project-static/docs/user-guides/graphql.html +1178 -10
  621. nautobot/project-static/docs/user-guides/relationships.html +1178 -10
  622. nautobot/project-static/docs/user-guides/s3-django-storage.html +1178 -10
  623. nautobot/project-static/js/forms.js +16 -9
  624. nautobot/project-static/js/theme.js +5 -0
  625. nautobot/tenancy/api/serializers.py +4 -32
  626. nautobot/tenancy/api/urls.py +1 -1
  627. nautobot/tenancy/forms.py +0 -28
  628. nautobot/tenancy/migrations/0008_tagsfield.py +19 -0
  629. nautobot/tenancy/models.py +0 -25
  630. nautobot/tenancy/navigation.py +6 -39
  631. nautobot/tenancy/templates/tenancy/tenant.html +12 -12
  632. nautobot/tenancy/templates/tenancy/tenantgroup.html +1 -1
  633. nautobot/tenancy/tests/test_api.py +1 -3
  634. nautobot/tenancy/tests/test_filters.py +10 -5
  635. nautobot/tenancy/views.py +0 -2
  636. nautobot/ui/.eslintignore +6 -0
  637. nautobot/ui/.gitignore +10 -0
  638. nautobot/ui/.prettierignore +9 -0
  639. nautobot/ui/.prettierrc +4 -0
  640. nautobot/ui/README.md +33 -0
  641. nautobot/ui/app_imports.js.j2 +7 -0
  642. nautobot/ui/craco.config.js +46 -0
  643. nautobot/ui/jsconfig-base.json +11 -0
  644. nautobot/ui/jsconfig.json +5 -0
  645. nautobot/ui/lib/nautobot-craco-alias-plugin.js +40 -0
  646. nautobot/ui/package-lock.json +21451 -0
  647. nautobot/ui/package.json +70 -0
  648. nautobot/ui/public/index.html +47 -0
  649. nautobot/ui/public/logo192.png +0 -0
  650. nautobot/ui/public/logo512.png +0 -0
  651. nautobot/ui/public/manifest.json +25 -0
  652. nautobot/ui/public/nautobot_logo.svg +131 -0
  653. nautobot/ui/public/robots.txt +3 -0
  654. nautobot/ui/src/App.js +71 -0
  655. nautobot/ui/src/components/AppFullWidthComponents.js +8 -0
  656. nautobot/ui/src/components/AppTab.js +40 -0
  657. nautobot/ui/src/components/Apps.js +60 -0
  658. nautobot/ui/src/components/HomeChangelogPanel.js +98 -0
  659. nautobot/ui/src/components/HomePanel.js +58 -0
  660. nautobot/ui/src/components/JobHistoryTable.js +78 -0
  661. nautobot/ui/src/components/Layout.js +53 -0
  662. nautobot/ui/src/components/LoadingWidget.js +25 -0
  663. nautobot/ui/src/components/Navbar.js +116 -0
  664. nautobot/ui/src/components/NotificationPopover.js +27 -0
  665. nautobot/ui/src/components/ObjectListTable.js +209 -0
  666. nautobot/ui/src/components/ReferenceDataTag.js +35 -0
  667. nautobot/ui/src/components/RouterButton.js +10 -0
  668. nautobot/ui/src/components/RouterLink.js +10 -0
  669. nautobot/ui/src/components/SidebarNav.js +147 -0
  670. nautobot/ui/src/components/Table.js +48 -0
  671. nautobot/ui/src/components/TableItem.js +71 -0
  672. nautobot/ui/src/components/__tests__/AppFullWidthComponents.test.js +16 -0
  673. nautobot/ui/src/components/__tests__/AppTab.test.js +21 -0
  674. nautobot/ui/src/components/__tests__/Apps.test.js +14 -0
  675. nautobot/ui/src/components/__tests__/Layout.test.js +33 -0
  676. nautobot/ui/src/components/__tests__/Table.test.js +36 -0
  677. nautobot/ui/src/components/__tests__/TableItem.test.js +37 -0
  678. nautobot/ui/src/components/__tests__/paginator.test.js +43 -0
  679. nautobot/ui/src/components/__tests__/paginator_form.test.js +13 -0
  680. nautobot/ui/src/components/pagination.js +93 -0
  681. nautobot/ui/src/components/paginator.js +79 -0
  682. nautobot/ui/src/components/paginator_form.js +43 -0
  683. nautobot/ui/src/components/usePagination.js +57 -0
  684. nautobot/ui/src/constants/apiPath.js +10 -0
  685. nautobot/ui/src/constants/icons.js +15 -0
  686. nautobot/ui/src/constants/size.js +15 -0
  687. nautobot/ui/src/index.js +65 -0
  688. nautobot/ui/src/reportWebVitals.js +15 -0
  689. nautobot/ui/src/router.js +77 -0
  690. nautobot/ui/src/utils/api.js +131 -0
  691. nautobot/ui/src/utils/app-import.js +15 -0
  692. nautobot/ui/src/utils/color.js +15 -0
  693. nautobot/ui/src/utils/date.js +14 -0
  694. nautobot/ui/src/utils/index.js +15 -0
  695. nautobot/ui/src/utils/navigation.js +32 -0
  696. nautobot/ui/src/utils/session.js +64 -0
  697. nautobot/ui/src/utils/store.js +242 -0
  698. nautobot/ui/src/utils/string.js +6 -0
  699. nautobot/ui/src/utils/url.js +4 -0
  700. nautobot/ui/src/views/Home.js +138 -0
  701. nautobot/ui/src/views/InstalledApps.js +80 -0
  702. nautobot/ui/src/views/Login.js +48 -0
  703. nautobot/ui/src/views/Logout.js +20 -0
  704. nautobot/ui/src/views/__tests__/BSCreateViewTemplate.test.js +11 -0
  705. nautobot/ui/src/views/__tests__/BSListViewTemplate.test.js +107 -0
  706. nautobot/ui/src/views/__tests__/Login.test.js +15 -0
  707. nautobot/ui/src/views/generic/GenericView.js +142 -0
  708. nautobot/ui/src/views/generic/ObjectCreate.js +96 -0
  709. nautobot/ui/src/views/generic/ObjectList.js +127 -0
  710. nautobot/ui/src/views/generic/ObjectRetrieve.js +551 -0
  711. nautobot/users/admin.py +1 -1
  712. nautobot/users/api/serializers.py +51 -61
  713. nautobot/users/api/urls.py +1 -1
  714. nautobot/users/api/views.py +53 -2
  715. nautobot/users/tests/test_api.py +110 -25
  716. nautobot/virtualization/api/serializers.py +18 -130
  717. nautobot/virtualization/api/urls.py +1 -1
  718. nautobot/virtualization/api/views.py +1 -22
  719. nautobot/virtualization/forms.py +13 -99
  720. nautobot/virtualization/migrations/0012_alter_virtualmachine_role_add_new_role.py +1 -1
  721. nautobot/virtualization/migrations/0013_migrate_virtualmachine_role_data.py +18 -11
  722. nautobot/virtualization/migrations/0015_rename_foreignkey_fields.py +1 -1
  723. nautobot/virtualization/migrations/0018_related_name_changes.py +1 -1
  724. nautobot/virtualization/migrations/0021_tagsfield_and_vminterface_to_primarymodel.py +39 -0
  725. nautobot/virtualization/migrations/0022_vminterface_timestamps_data_migration.py +17 -0
  726. nautobot/virtualization/migrations/{0021_ipam__namespaces.py → 0023_ipam__namespaces.py} +2 -2
  727. nautobot/virtualization/migrations/0024_fixup_null_statuses.py +25 -0
  728. nautobot/virtualization/migrations/0025_status_nonnullable.py +29 -0
  729. nautobot/virtualization/models.py +31 -123
  730. nautobot/virtualization/navigation.py +18 -99
  731. nautobot/virtualization/templates/virtualization/virtualmachine.html +2 -1
  732. nautobot/virtualization/templates/virtualization/virtualmachine_edit.html +6 -0
  733. nautobot/virtualization/tests/test_api.py +25 -26
  734. nautobot/virtualization/tests/test_filters.py +41 -15
  735. nautobot/virtualization/tests/test_models.py +31 -7
  736. nautobot/virtualization/tests/test_views.py +42 -25
  737. nautobot/virtualization/views.py +7 -6
  738. {nautobot-2.0.0a3.dist-info → nautobot-2.0.0b1.dist-info}/METADATA +3 -7
  739. {nautobot-2.0.0a3.dist-info → nautobot-2.0.0b1.dist-info}/RECORD +744 -602
  740. {nautobot-2.0.0a3.dist-info → nautobot-2.0.0b1.dist-info}/WHEEL +1 -1
  741. nautobot/circuits/api/nested_serializers.py +0 -69
  742. nautobot/core/templates/plugin_template/navigation.py-tpl +0 -22
  743. nautobot/dcim/api/nested_serializers.py +0 -356
  744. nautobot/dcim/templates/dcim/device_import.html +0 -5
  745. nautobot/dcim/templates/dcim/device_import_child.html +0 -5
  746. nautobot/dcim/templates/dcim/inc/device_import_header.html +0 -4
  747. nautobot/extras/api/nested_serializers.py +0 -353
  748. nautobot/extras/migrations/0064_configcontext_data_migrations.py +0 -41
  749. nautobot/extras/migrations/0071_job__unique_name_data_migration.py +0 -46
  750. nautobot/extras/reports.py +0 -60
  751. nautobot/extras/scripts.py +0 -72
  752. nautobot/extras/tests/example_jobs/script_variables.py +0 -67
  753. nautobot/extras/tests/example_jobs/test_duplicate_name2.py +0 -5
  754. nautobot/extras/tests/example_jobs/test_fail.py +0 -16
  755. nautobot/extras/tests/example_jobs/test_file_upload_pass.py +0 -20
  756. nautobot/extras/tests/example_jobs/test_ipaddress_vars.py +0 -52
  757. nautobot/extras/tests/example_jobs/test_job_button_receiver.py +0 -21
  758. nautobot/extras/tests/example_jobs/test_job_hook_receiver.py +0 -20
  759. nautobot/extras/tests/example_jobs/test_location_with_custom_field.py +0 -35
  760. nautobot/extras/tests/example_jobs/test_log_redaction.py +0 -14
  761. nautobot/extras/tests/example_jobs/test_modify_db.py +0 -18
  762. nautobot/extras/tests/example_jobs/test_object_var_optional.py +0 -14
  763. nautobot/extras/tests/example_jobs/test_object_var_required.py +0 -14
  764. nautobot/extras/tests/example_jobs/test_object_vars.py +0 -29
  765. nautobot/extras/tests/example_jobs/test_pass.py +0 -19
  766. nautobot/extras/tests/example_jobs/test_read_only_fail.py +0 -24
  767. nautobot/extras/tests/example_jobs/test_read_only_no_commit_field.py +0 -10
  768. nautobot/extras/tests/example_jobs/test_read_only_pass.py +0 -22
  769. nautobot/ipam/api/nested_serializers.py +0 -159
  770. nautobot/ipam/migrations/0029_ipam__prefix__add_parent.py +0 -31
  771. nautobot/ipam/migrations/0030_ipam__prefix__data_migration.py +0 -13
  772. nautobot/ipam/migrations/0031_ipam__ipaddress__add_parent.py +0 -41
  773. nautobot/ipam/migrations/0032_ipam__ipaddress__data_migration.py +0 -11
  774. nautobot/tenancy/api/nested_serializers.py +0 -31
  775. nautobot/users/api/nested_serializers.py +0 -67
  776. nautobot/virtualization/api/nested_serializers.py +0 -65
  777. /nautobot/extras/{tests/example_jobs → test_jobs}/__init__.py +0 -0
  778. /nautobot/{dcim/models/sites.py → ipam/management/__init__.py} +0 -0
  779. {nautobot-2.0.0a3.dist-info → nautobot-2.0.0b1.dist-info}/LICENSE.txt +0 -0
  780. {nautobot-2.0.0a3.dist-info → nautobot-2.0.0b1.dist-info}/entry_points.txt +0 -0
nautobot/extras/jobs.py CHANGED
@@ -1,15 +1,21 @@
1
1
  """Jobs functionality - consolidates and replaces legacy "custom scripts" and "reports" features."""
2
2
  from collections import OrderedDict
3
- import copy
3
+ import functools
4
4
  import inspect
5
5
  import json
6
6
  import logging
7
7
  import os
8
- import shutil
8
+ import tempfile
9
9
  from textwrap import dedent
10
- import traceback
11
10
  import warnings
12
11
 
12
+ from billiard.einfo import ExceptionInfo
13
+ from celery import states
14
+ from celery.exceptions import NotRegistered, Retry
15
+ from celery.result import EagerResult
16
+ from celery.utils.functional import maybe_list
17
+ from celery.utils.log import get_task_logger
18
+ from celery.utils.nodenames import gethostname
13
19
  from db_file_storage.form_widgets import DBClearableFileInput
14
20
  from django import forms
15
21
  from django.conf import settings
@@ -17,24 +23,33 @@ from django.contrib.auth import get_user_model
17
23
  from django.contrib.contenttypes.models import ContentType
18
24
  from django.core.files.uploadedfile import InMemoryUploadedFile
19
25
  from django.core.validators import RegexValidator
20
- from django.db import transaction, IntegrityError
21
26
  from django.db.models import Model
22
27
  from django.db.models.query import QuerySet
28
+ from django.core.exceptions import ObjectDoesNotExist
23
29
  from django.forms import ValidationError
24
- from django.test.client import RequestFactory
25
- from django.utils import timezone
26
30
  from django.utils.functional import classproperty
31
+ from kombu.utils.uuid import uuid
27
32
  import netaddr
28
33
  import yaml
29
34
 
30
- from nautobot.core.celery import nautobot_task
31
- from nautobot.core.exceptions import AbortTransaction
35
+ from nautobot.core.celery import app as celery_app
36
+ from nautobot.core.celery.task import Task
32
37
  from nautobot.core.forms import (
33
38
  DynamicModelChoiceField,
34
39
  DynamicModelMultipleChoiceField,
35
40
  )
36
41
  from nautobot.core.utils.lookup import get_model_from_name
37
- from nautobot.core.utils.requests import copy_safe_request
42
+ from nautobot.extras.choices import ObjectChangeActionChoices, ObjectChangeEventContextChoices
43
+ from nautobot.extras.context_managers import change_logging, JobChangeContext, JobHookChangeContext
44
+ from nautobot.extras.forms import JobForm
45
+ from nautobot.extras.models import (
46
+ FileProxy,
47
+ Job as JobModel,
48
+ JobHook,
49
+ JobResult,
50
+ ObjectChange,
51
+ )
52
+ from nautobot.extras.utils import ChangeLoggedModelsQuery, task_queues_as_choices
38
53
  from nautobot.ipam.formfields import IPAddressFormField, IPNetworkFormField
39
54
  from nautobot.ipam.validators import (
40
55
  MaxPrefixLengthValidator,
@@ -42,14 +57,6 @@ from nautobot.ipam.validators import (
42
57
  prefix_validator,
43
58
  )
44
59
 
45
- from .choices import LogLevelChoices, ObjectChangeActionChoices, ObjectChangeEventContextChoices
46
- from .context_managers import change_logging, JobChangeContext, JobHookChangeContext
47
- from .datasources.git import ensure_git_repository
48
- from .forms import JobForm
49
- from .models import FileProxy, GitRepository, Job as JobModel, JobHook, ObjectChange, ScheduledJob
50
- from .registry import registry
51
- from .utils import ChangeLoggedModelsQuery, get_job_content_type, jobs_in_directory, task_queues_as_choices
52
-
53
60
 
54
61
  User = get_user_model()
55
62
 
@@ -73,17 +80,17 @@ __all__ = [
73
80
  logger = logging.getLogger(__name__)
74
81
 
75
82
 
76
- class BaseJob:
77
- """Base model for jobs (reports, scripts).
83
+ class RunJobTaskFailed(Exception):
84
+ """Celery task failed for some reason."""
85
+
86
+
87
+ class BaseJob(Task):
88
+ """Base model for jobs.
78
89
 
79
90
  Users can subclass this directly if they want to provide their own base class for implementing multiple jobs
80
91
  with shared functionality; if no such sharing is required, use Job class instead.
81
92
 
82
- For backward compatibility with NetBox, this class has several APIs that can be implemented by the user:
83
-
84
- 1. run(self, data, commit) - First method called when invoking a Job, can handle setup and parameter storage.
85
- 2. test_*(self) - Any method matching this pattern will be called next
86
- 3. post_run(self) - Last method called, will be called even in case of an exception during the above methods
93
+ Jobs must define at minimum a run method.
87
94
  """
88
95
 
89
96
  class Meta:
@@ -92,10 +99,8 @@ class BaseJob:
92
99
 
93
100
  - name (str)
94
101
  - description (str)
95
- - commit_default (bool)
96
102
  - hidden (bool)
97
103
  - field_order (list)
98
- - read_only (bool)
99
104
  - approval_required (bool)
100
105
  - soft_time_limit (int)
101
106
  - time_limit (int)
@@ -104,25 +109,274 @@ class BaseJob:
104
109
  """
105
110
 
106
111
  def __init__(self):
107
- self.logger = logging.getLogger(f"{__name__}.{self.__class__.__name__}")
112
+ self.logger = get_task_logger(self.__module__)
108
113
 
109
- self.request = None
110
- self.active_test = "main"
111
- self.failed = False
112
- self._job_result = None
114
+ def __call__(self, *args, **kwargs):
115
+ # Attempt to resolve serialized data back into original form by creating querysets or model instances
116
+ # If we fail to find any objects, we consider this a job execution error, and fail.
117
+ # This might happen when a job sits on the queue for a while (i.e. scheduled) and data has changed
118
+ # or it might be bad input from an API request, or manual execution.
119
+ try:
120
+ deserialized_kwargs = self.deserialize_data(kwargs)
121
+ except Exception as err:
122
+ raise RunJobTaskFailed("Error initializing job") from err
123
+ context_class = JobHookChangeContext if isinstance(self, JobHookReceiver) else JobChangeContext
124
+ change_context = context_class(user=self.user, context_detail=self.class_path)
113
125
 
114
- # Compile test methods and initialize results skeleton
115
- self.test_methods = []
126
+ with change_logging(change_context):
127
+ if self.celery_kwargs.get("nautobot_job_profile", False) is True:
128
+ import cProfile
129
+
130
+ # TODO: This should probably be available as a file download rather than dumped to the hard drive.
131
+ # Pending this: https://github.com/nautobot/nautobot/issues/3352
132
+ profiling_path = f"{tempfile.gettempdir()}/nautobot-jobresult-{self.job_result.id}.pstats"
133
+ self.logger.info(
134
+ "Writing profiling information to %s.", profiling_path, extra={"grouping": "initialization"}
135
+ )
116
136
 
117
- for method_name in dir(self):
118
- if method_name.startswith("test_") and callable(getattr(self, method_name)):
119
- self.test_methods.append(method_name)
137
+ with cProfile.Profile() as pr:
138
+ try:
139
+ output = self.run(*args, **deserialized_kwargs)
140
+ except Exception as err:
141
+ pr.dump_stats(profiling_path)
142
+ raise err
143
+ else:
144
+ pr.dump_stats(profiling_path)
145
+ return output
146
+ else:
147
+ return self.run(*args, **deserialized_kwargs)
120
148
 
121
149
  def __str__(self):
122
150
  return str(self.name)
123
151
 
124
152
  # See https://github.com/PyCQA/pylint-django/issues/240 for why we have a pylint disable on each classproperty below
125
153
 
154
+ # TODO(jathan): Could be interesting for custom stuff when the Job is
155
+ # enabled in the database and then therefore registered in Celery
156
+ @classmethod
157
+ def on_bound(cls, app):
158
+ """Called when the task is bound to an app.
159
+
160
+ Note:
161
+ This class method can be defined to do additional actions when
162
+ the task class is bound to an app.
163
+ """
164
+
165
+ # TODO(jathan): Could be interesting for showing the Job's class path as the
166
+ # shadow name vs. the Celery task_name?
167
+ def shadow_name(self, args, kwargs, options):
168
+ """Override for custom task name in worker logs/monitoring.
169
+
170
+ Example:
171
+ from celery.utils.imports import qualname
172
+
173
+ def shadow_name(task, args, kwargs, options):
174
+ return qualname(args[0])
175
+
176
+ @app.task(shadow_name=shadow_name, serializer='pickle')
177
+ def apply_function_async(fun, *args, **kwargs):
178
+ return fun(*args, **kwargs)
179
+
180
+ Arguments:
181
+ args (Tuple): Task positional arguments.
182
+ kwargs (Dict): Task keyword arguments.
183
+ options (Dict): Task execution options.
184
+ """
185
+
186
+ def before_start(self, task_id, args, kwargs):
187
+ """Handler called before the task starts.
188
+
189
+ Arguments:
190
+ task_id (str): Unique id of the task to execute.
191
+ args (Tuple): Original arguments for the task to execute.
192
+ kwargs (Dict): Original keyword arguments for the task to execute.
193
+
194
+ Returns:
195
+ None: The return value of this handler is ignored.
196
+ """
197
+ self.clear_cache()
198
+
199
+ try:
200
+ self.job_result
201
+ except ObjectDoesNotExist as err:
202
+ raise RunJobTaskFailed(f"Unable to find associated job result for job {task_id}") from err
203
+
204
+ try:
205
+ self.job_model
206
+ except ObjectDoesNotExist as err:
207
+ raise RunJobTaskFailed(f"Unable to find associated job model for job {task_id}") from err
208
+
209
+ if not self.job_model.enabled:
210
+ self.logger.error(
211
+ "Job %s is not enabled to be run!",
212
+ self.job_model,
213
+ extra={"object": self.job_model, "grouping": "initialization"},
214
+ )
215
+ raise RunJobTaskFailed(f"Job {self.job_model} is not enabled to be run!")
216
+
217
+ soft_time_limit = self.job_model.soft_time_limit or settings.CELERY_TASK_SOFT_TIME_LIMIT
218
+ time_limit = self.job_model.time_limit or settings.CELERY_TASK_TIME_LIMIT
219
+ if time_limit <= soft_time_limit:
220
+ self.logger.warning(
221
+ "The hard time limit of %s seconds is less than "
222
+ "or equal to the soft time limit of %s seconds. "
223
+ "This job will fail silently after %s seconds.",
224
+ time_limit,
225
+ soft_time_limit,
226
+ time_limit,
227
+ extra={"grouping": "initialization"},
228
+ )
229
+
230
+ self.logger.info("Running job", extra={"grouping": "initialization"})
231
+
232
+ def run(self, *args, **kwargs):
233
+ """
234
+ Method invoked when this Job is run.
235
+ """
236
+ raise NotImplementedError("Jobs must define the run method.")
237
+
238
+ def on_success(self, retval, task_id, args, kwargs):
239
+ """Success handler.
240
+
241
+ Run by the worker if the task executes successfully.
242
+
243
+ Arguments:
244
+ retval (Any): The return value of the task.
245
+ task_id (str): Unique id of the executed task.
246
+ args (Tuple): Original arguments for the executed task.
247
+ kwargs (Dict): Original keyword arguments for the executed task.
248
+
249
+ Returns:
250
+ None: The return value of this handler is ignored.
251
+ """
252
+
253
+ def on_retry(self, exc, task_id, args, kwargs, einfo):
254
+ """Retry handler.
255
+
256
+ This is run by the worker when the task is to be retried.
257
+
258
+ Arguments:
259
+ exc (Exception): The exception sent to :meth:`retry`.
260
+ task_id (str): Unique id of the retried task.
261
+ args (Tuple): Original arguments for the retried task.
262
+ kwargs (Dict): Original keyword arguments for the retried task.
263
+ einfo (~billiard.einfo.ExceptionInfo): Exception information.
264
+
265
+ Returns:
266
+ None: The return value of this handler is ignored.
267
+ """
268
+
269
+ def on_failure(self, exc, task_id, args, kwargs, einfo):
270
+ """Error handler.
271
+
272
+ This is run by the worker when the task fails.
273
+
274
+ Arguments:
275
+ exc (Exception): The exception raised by the task.
276
+ task_id (str): Unique id of the failed task.
277
+ args (Tuple): Original arguments for the task that failed.
278
+ kwargs (Dict): Original keyword arguments for the task that failed.
279
+ einfo (~billiard.einfo.ExceptionInfo): Exception information.
280
+
281
+ Returns:
282
+ None: The return value of this handler is ignored.
283
+ """
284
+
285
+ def after_return(self, status, retval, task_id, args, kwargs, einfo):
286
+ """
287
+ Handler called after the task returns.
288
+
289
+ Parameters
290
+ status - Current task state.
291
+ retval - Task return value/exception.
292
+ task_id - Unique id of the task.
293
+ args - Original arguments for the task that returned.
294
+ kwargs - Original keyword arguments for the task that returned.
295
+
296
+ Keyword Arguments
297
+ einfo - ExceptionInfo instance, containing the traceback (if any).
298
+
299
+ Returns:
300
+ None: The return value of this handler is ignored.
301
+ """
302
+
303
+ # Cleanup FileProxy objects
304
+ file_fields = list(self._get_file_vars())
305
+ file_ids = [kwargs[f] for f in file_fields]
306
+ if file_ids:
307
+ self.delete_files(*file_ids)
308
+
309
+ self.logger.info("Job completed", extra={"grouping": "post_run"})
310
+
311
+ # TODO(gary): document this in job author docs
312
+ # Super.after_return must be called for chords to function properly
313
+ super().after_return(status, retval, task_id, args, kwargs, einfo=einfo)
314
+
315
+ def apply(
316
+ self,
317
+ args=None,
318
+ kwargs=None,
319
+ link=None,
320
+ link_error=None,
321
+ task_id=None,
322
+ retries=None,
323
+ throw=None,
324
+ logfile=None,
325
+ loglevel=None,
326
+ headers=None,
327
+ **options,
328
+ ):
329
+ """Fix celery's apply method to propagate options to the task result"""
330
+ # trace imports Task, so need to import inline.
331
+ from celery.app.trace import build_tracer
332
+
333
+ app = self._get_app()
334
+ args = args or ()
335
+ kwargs = kwargs or {}
336
+ task_id = task_id or uuid()
337
+ retries = retries or 0
338
+ if throw is None:
339
+ throw = app.conf.task_eager_propagates
340
+
341
+ # Make sure we get the task instance, not class.
342
+ task = app._tasks[self.name]
343
+
344
+ request = {
345
+ "id": task_id,
346
+ "retries": retries,
347
+ "is_eager": True,
348
+ "logfile": logfile,
349
+ "loglevel": loglevel or 0,
350
+ "hostname": gethostname(),
351
+ "callbacks": maybe_list(link),
352
+ "errbacks": maybe_list(link_error),
353
+ "headers": headers,
354
+ "ignore_result": options.get("ignore_result", False),
355
+ "delivery_info": {
356
+ "is_eager": True,
357
+ "exchange": options.get("exchange"),
358
+ "routing_key": options.get("routing_key"),
359
+ "priority": options.get("priority"),
360
+ },
361
+ "properties": options, # one line fix to overloaded method
362
+ }
363
+ tb = None
364
+ tracer = build_tracer(
365
+ task.name,
366
+ task,
367
+ eager=True,
368
+ propagate=throw,
369
+ app=self._get_app(),
370
+ )
371
+ ret = tracer(task_id, args, kwargs, request)
372
+ retval = ret.retval
373
+ if isinstance(retval, ExceptionInfo):
374
+ retval, tb = retval.exception, retval.traceback
375
+ if isinstance(retval, Retry) and retval.sig is not None:
376
+ return retval.sig.apply(retries=retries + 1)
377
+ state = states.SUCCESS if ret.info is None else ret.info.state
378
+ return EagerResult(task_id, retval, state, traceback=tb)
379
+
126
380
  @classproperty
127
381
  def file_path(cls): # pylint: disable=no-self-argument
128
382
  return inspect.getfile(cls)
@@ -130,47 +384,31 @@ class BaseJob:
130
384
  @classproperty
131
385
  def class_path(cls): # pylint: disable=no-self-argument
132
386
  """
133
- Unique identifier of a specific Job class, in the form <source_grouping>/<module_name>/<ClassName>.
387
+ Unique identifier of a specific Job class, in the form <module_name>.<ClassName>.
134
388
 
135
389
  Examples:
136
- local/my_script/MyScript
137
- plugins/my_plugin.jobs/MyPluginJob
138
- git.my-repository/myjob/MyJob
390
+ my_script.MyScript - Local Job
391
+ nautobot.core.jobs.MySystemJob - System Job
392
+ my_plugin.jobs.MyPluginJob - App-provided Job
393
+ git_repository.jobs.myjob.MyJob - GitRepository Job
139
394
  """
140
- # TODO(Glenn): it'd be nice if this were derived more automatically instead of needing this logic
141
- if cls in registry["plugin_jobs"]:
142
- source_grouping = "plugins"
143
- elif cls.file_path.startswith(settings.JOBS_ROOT):
144
- source_grouping = "local"
145
- elif cls.file_path.startswith(settings.GIT_ROOT):
146
- # $GIT_ROOT/<repo_slug>/jobs/job.py -> <repo_slug>
147
- source_grouping = ".".join(
148
- [
149
- "git",
150
- os.path.basename(os.path.dirname(os.path.dirname(cls.file_path))),
151
- ]
152
- )
153
- else:
154
- raise RuntimeError(
155
- f"Unknown/unexpected job file_path {cls.file_path}, should be one of "
156
- + ", ".join([settings.JOBS_ROOT, settings.GIT_ROOT])
157
- )
158
-
159
- return "/".join([source_grouping, cls.__module__, cls.__name__])
395
+ return f"{cls.__module__}.{cls.__name__}"
160
396
 
161
397
  @classproperty
162
398
  def class_path_dotted(cls): # pylint: disable=no-self-argument
163
399
  """
164
400
  Dotted class_path, suitable for use in things like Python logger names.
401
+
402
+ Deprecated as of Nautobot 2.0: just use .class_path instead.
165
403
  """
166
- return cls.class_path.replace("/", ".")
404
+ return cls.class_path
167
405
 
168
406
  @classproperty
169
407
  def class_path_js_escaped(cls): # pylint: disable=no-self-argument
170
408
  """
171
409
  Escape various characters so that the class_path can be used as a jQuery selector.
172
410
  """
173
- return cls.class_path.replace("/", r"\/").replace(".", r"\.")
411
+ return cls.class_path.replace(".", r"\.")
174
412
 
175
413
  @classproperty
176
414
  def grouping(cls): # pylint: disable=no-self-argument
@@ -192,8 +430,8 @@ class BaseJob:
192
430
  return ""
193
431
 
194
432
  @classproperty
195
- def commit_default(cls): # pylint: disable=no-self-argument
196
- return getattr(cls.Meta, "commit_default", True)
433
+ def dryrun_default(cls): # pylint: disable=no-self-argument
434
+ return getattr(cls.Meta, "dryrun_default", False)
197
435
 
198
436
  @classproperty
199
437
  def hidden(cls): # pylint: disable=no-self-argument
@@ -223,6 +461,10 @@ class BaseJob:
223
461
  def has_sensitive_variables(cls): # pylint: disable=no-self-argument
224
462
  return getattr(cls.Meta, "has_sensitive_variables", True)
225
463
 
464
+ @classproperty
465
+ def supports_dryrun(cls): # pylint: disable=no-self-argument
466
+ return isinstance(getattr(cls, "dryrun", None), DryRunVar)
467
+
226
468
  @classproperty
227
469
  def task_queues(cls): # pylint: disable=no-self-argument
228
470
  return getattr(cls.Meta, "task_queues", [])
@@ -239,15 +481,17 @@ class BaseJob:
239
481
  "grouping": cls.grouping,
240
482
  "description": cls.description,
241
483
  "approval_required": cls.approval_required,
242
- "commit_default": cls.commit_default,
243
484
  "hidden": cls.hidden,
244
- "read_only": cls.read_only,
245
485
  "soft_time_limit": cls.soft_time_limit,
246
486
  "time_limit": cls.time_limit,
247
487
  "has_sensitive_variables": cls.has_sensitive_variables,
248
488
  "task_queues": cls.task_queues,
249
489
  }
250
490
 
491
+ @classproperty
492
+ def registered_name(cls): # pylint: disable=no-self-argument
493
+ return f"{cls.__module__}.{cls.__name__}"
494
+
251
495
  @classmethod
252
496
  def _get_vars(cls):
253
497
  """
@@ -276,33 +520,6 @@ class BaseJob:
276
520
 
277
521
  return file_vars
278
522
 
279
- @property
280
- def job_result(self):
281
- return self._job_result
282
-
283
- @job_result.setter
284
- def job_result(self, value):
285
- # Initialize job_result data format for our usage
286
- value.data = OrderedDict()
287
-
288
- self._job_result = value
289
-
290
- @property
291
- def results(self):
292
- """
293
- The results generated by this job.
294
- ** If you need the logs, you will need to filter on JobLogEntry **
295
- Ex.
296
- from nautobot.extras.models import JogLogEntry
297
-
298
- JobLogEntry.objects.filter(job_result=self.job_result, <other criteria>)
299
-
300
- {
301
- "output": "...",
302
- }
303
- """
304
- return self.job_result.data if self.job_result else None
305
-
306
523
  def as_form_class(self):
307
524
  """
308
525
  Dynamically generate a Django form class corresponding to the variables in this Job.
@@ -324,27 +541,22 @@ class BaseJob:
324
541
 
325
542
  try:
326
543
  job_model = JobModel.objects.get_for_class_path(self.class_path)
327
- read_only = job_model.read_only if job_model.read_only_override else self.read_only
328
- commit_default = job_model.commit_default if job_model.commit_default_override else self.commit_default
544
+ dryrun_default = job_model.dryrun_default if job_model.dryrun_default_override else self.dryrun_default
329
545
  task_queues = job_model.task_queues if job_model.task_queues_override else self.task_queues
330
546
  except JobModel.DoesNotExist:
331
- # 2.0 TODO: remove this fallback, Job records should always exist.
332
547
  logger.error("No Job instance found in the database corresponding to %s", self.class_path)
333
- read_only = self.read_only
334
- commit_default = self.commit_default
548
+ dryrun_default = self.dryrun_default
335
549
  task_queues = self.task_queues
336
550
 
337
- if read_only:
338
- # Hide the commit field for read only jobs
339
- form.fields["_commit"].widget = forms.HiddenInput()
340
- form.fields["_commit"].initial = False
341
- elif not initial or "_commit" not in initial:
342
- # Set initial "commit" checkbox state based on the Meta parameter
343
- form.fields["_commit"].initial = commit_default
344
-
345
551
  # Update task queue choices
346
552
  form.fields["_task_queue"].choices = task_queues_as_choices(task_queues)
347
553
 
554
+ if self.supports_dryrun and (not initial or "dryrun" not in initial):
555
+ # Set initial "dryrun" checkbox state based on the Meta parameter
556
+ form.fields["dryrun"].initial = dryrun_default
557
+ if not settings.DEBUG:
558
+ form.fields["_profile"].widget = forms.HiddenInput()
559
+
348
560
  # https://github.com/PyCQA/pylint/issues/3484
349
561
  if self.field_order: # pylint: disable=using-constant-test
350
562
  form.order_fields(self.field_order)
@@ -354,11 +566,42 @@ class BaseJob:
354
566
  for _, field in form.fields.items():
355
567
  field.disabled = True
356
568
 
357
- # Alter the commit help text to avoid confusion concerning approval dry-runs
358
- form.fields["_commit"].help_text = "Commit changes to the database"
359
-
360
569
  return form
361
570
 
571
+ def clear_cache(self):
572
+ """
573
+ Clear all cached properties on this instance without accessing them. This is required because
574
+ celery reuses task instances for multiple runs.
575
+ """
576
+ try:
577
+ del self.celery_kwargs
578
+ except AttributeError:
579
+ pass
580
+ try:
581
+ del self.job_result
582
+ except AttributeError:
583
+ pass
584
+ try:
585
+ del self.job_model
586
+ except AttributeError:
587
+ pass
588
+
589
+ @functools.cached_property
590
+ def job_model(self):
591
+ return JobModel.objects.get(module_name=self.__module__, job_class_name=self.__name__)
592
+
593
+ @functools.cached_property
594
+ def job_result(self):
595
+ return JobResult.objects.get(id=self.request.id)
596
+
597
+ @functools.cached_property
598
+ def celery_kwargs(self):
599
+ return self.job_result.celery_kwargs or {}
600
+
601
+ @property
602
+ def user(self):
603
+ return getattr(self.job_result, "user", None)
604
+
362
605
  @staticmethod
363
606
  def serialize_data(data):
364
607
  """
@@ -390,6 +633,7 @@ class BaseJob:
390
633
 
391
634
  return return_data
392
635
 
636
+ # TODO: can the deserialize_data logic be moved to NautobotKombuJSONEncoder?
393
637
  @classmethod
394
638
  def deserialize_data(cls, data):
395
639
  """
@@ -409,7 +653,7 @@ class BaseJob:
409
653
  raise TypeError("Data should be a dictionary.")
410
654
 
411
655
  for field_name, value in data.items():
412
- # If a field isn't a var, skip it (e.g. `_commit`).
656
+ # If a field isn't a var, skip it (e.g. `_task_queue`).
413
657
  try:
414
658
  var = cls_vars[field_name]
415
659
  except KeyError:
@@ -467,6 +711,12 @@ class BaseJob:
467
711
 
468
712
  return f.cleaned_data
469
713
 
714
+ @classmethod
715
+ def prepare_job_kwargs(cls, job_kwargs):
716
+ """Process dict and return kwargs that exist as ScriptVariables on this job."""
717
+ job_vars = cls._get_vars()
718
+ return {k: v for k, v in job_kwargs.items() if k in job_vars}
719
+
470
720
  @staticmethod
471
721
  def load_file(pk):
472
722
  """Load a file proxy stored in the database by primary key.
@@ -495,8 +745,7 @@ class BaseJob:
495
745
  fp = FileProxy.objects.create(name=uploaded_file.name, file=uploaded_file)
496
746
  return fp.pk
497
747
 
498
- @staticmethod
499
- def delete_files(*files_to_delete):
748
+ def delete_files(self, *files_to_delete):
500
749
  """Given an unpacked list of primary keys for `FileProxy` objects, delete them.
501
750
 
502
751
  Args:
@@ -510,86 +759,9 @@ class BaseJob:
510
759
  for fp in files:
511
760
  fp.delete() # Call delete() on each, so `FileAttachment` is reaped
512
761
  num += 1
513
- logger.debug(f"Deleted {num} file proxies")
762
+ self.logger.debug("Deleted %d file proxies", num, extra={"grouping": "post_run"})
514
763
  return num
515
764
 
516
- def run(self, data, commit):
517
- """
518
- Method invoked when this Job is run, before any "test_*" methods.
519
- """
520
-
521
- def post_run(self):
522
- """
523
- Method invoked after "run()" and all "test_*" methods.
524
- """
525
-
526
- # Logging
527
-
528
- def _log(self, obj, message, level_choice=LogLevelChoices.LOG_DEFAULT):
529
- """
530
- Log a message. Do not call this method directly; use one of the log_* wrappers below.
531
- """
532
- self.job_result.log(
533
- message,
534
- obj=obj,
535
- level_choice=level_choice,
536
- grouping=self.active_test,
537
- logger=self.logger,
538
- )
539
-
540
- def log(self, message):
541
- """
542
- Log a generic message which is not associated with a particular object.
543
- """
544
- self._log(None, message, level_choice=LogLevelChoices.LOG_DEFAULT)
545
-
546
- def log_debug(self, message):
547
- """
548
- Log a debug message which is not associated with a particular object.
549
- """
550
- self._log(None, message, level_choice=LogLevelChoices.LOG_DEFAULT)
551
-
552
- def log_success(self, obj=None, message=None):
553
- """
554
- Record a successful test against an object. Logging a message is optional.
555
- If the object provided is a string, treat it as a message. This is a carryover of Netbox Report API
556
- """
557
- if isinstance(obj, str) and message is None:
558
- self._log(obj=None, message=obj, level_choice=LogLevelChoices.LOG_SUCCESS)
559
- else:
560
- self._log(obj, message, level_choice=LogLevelChoices.LOG_SUCCESS)
561
-
562
- def log_info(self, obj=None, message=None):
563
- """
564
- Log an informational message.
565
- If the object provided is a string, treat it as a message. This is a carryover of Netbox Report API
566
- """
567
- if isinstance(obj, str) and message is None:
568
- self._log(obj=None, message=obj, level_choice=LogLevelChoices.LOG_INFO)
569
- else:
570
- self._log(obj, message, level_choice=LogLevelChoices.LOG_INFO)
571
-
572
- def log_warning(self, obj=None, message=None):
573
- """
574
- Log a warning.
575
- If the object provided is a string, treat it as a message. This is a carryover of Netbox Report API
576
- """
577
- if isinstance(obj, str) and message is None:
578
- self._log(obj=None, message=obj, level_choice=LogLevelChoices.LOG_WARNING)
579
- else:
580
- self._log(obj, message, level_choice=LogLevelChoices.LOG_WARNING)
581
-
582
- def log_failure(self, obj=None, message=None):
583
- """
584
- Log a failure. Calling this method will automatically mark the overall job as failed.
585
- If the object provided is a string, treat it as a message. This is a carryover of Netbox Report API
586
- """
587
- if isinstance(obj, str) and message is None:
588
- self._log(obj=None, message=obj, level_choice=LogLevelChoices.LOG_FAILURE)
589
- else:
590
- self._log(obj, message, level_choice=LogLevelChoices.LOG_FAILURE)
591
- raise RunJobTaskFailed(message)
592
-
593
765
  # Convenience functions
594
766
 
595
767
  def load_yaml(self, filename):
@@ -728,6 +900,23 @@ class BooleanVar(ScriptVariable):
728
900
  self.field_attrs["required"] = False
729
901
 
730
902
 
903
+ class DryRunVar(BooleanVar):
904
+ """
905
+ Special boolean variable that bypasses approval requirements if this is set to True on job execution.
906
+ """
907
+
908
+ description = "Check to run job in dryrun mode."
909
+
910
+ def __init__(self, *args, **kwargs):
911
+ # Default must be false unless overridden through `dryrun_default` meta attribute
912
+ kwargs["default"] = False
913
+
914
+ # Default description if one was not provided
915
+ kwargs.setdefault("description", self.description)
916
+
917
+ super().__init__(*args, **kwargs)
918
+
919
+
731
920
  class ChoiceVar(ScriptVariable):
732
921
  """
733
922
  Select one of several predefined static choices, passed as a list of two-tuples. Example:
@@ -866,9 +1055,8 @@ class JobHookReceiver(Job):
866
1055
 
867
1056
  object_change = ObjectVar(model=ObjectChange)
868
1057
 
869
- def run(self, data, commit):
1058
+ def run(self, object_change):
870
1059
  """JobHookReceiver subclasses generally shouldn't need to override this method."""
871
- object_change = data["object_change"]
872
1060
  self.receive_job_hook(
873
1061
  change=object_change,
874
1062
  action=object_change.action,
@@ -895,11 +1083,8 @@ class JobButtonReceiver(Job):
895
1083
  object_pk = StringVar()
896
1084
  object_model_name = StringVar()
897
1085
 
898
- def run(self, data, commit):
1086
+ def run(self, object_pk, object_model_name):
899
1087
  """JobButtonReceiver subclasses generally shouldn't need to override this method."""
900
- object_pk = data["object_pk"]
901
- object_model_name = data["object_model_name"]
902
-
903
1088
  model = get_model_from_name(object_model_name)
904
1089
  obj = model.objects.get(pk=object_pk)
905
1090
 
@@ -918,11 +1103,8 @@ def is_job(obj):
918
1103
  """
919
1104
  Returns True if the given object is a Job subclass.
920
1105
  """
921
- from .scripts import Script, BaseScript
922
- from .reports import Report
923
-
924
1106
  try:
925
- return issubclass(obj, Job) and obj not in [Job, Script, BaseScript, Report, JobHookReceiver, JobButtonReceiver]
1107
+ return issubclass(obj, Job) and obj not in [Job, JobHookReceiver, JobButtonReceiver]
926
1108
  except TypeError:
927
1109
  return False
928
1110
 
@@ -934,372 +1116,23 @@ def is_variable(obj):
934
1116
  return isinstance(obj, ScriptVariable)
935
1117
 
936
1118
 
937
- def get_jobs():
938
- """
939
- Compile a dictionary of all jobs available across all modules in the jobs path(s).
940
-
941
- Returns an OrderedDict:
942
-
943
- {
944
- "local": {
945
- <module_name>: {
946
- "name": <human-readable module name>,
947
- "jobs": {
948
- <class_name>: <job_class>,
949
- <class_name>: <job_class>,
950
- ...
951
- },
952
- },
953
- <module_name>: { ... },
954
- ...
955
- },
956
- "git.<repository-slug>": {
957
- <module_name>: { ... },
958
- },
959
- ...
960
- "plugins": {
961
- <module_name>: { ... },
962
- }
963
- }
964
- """
965
- jobs = OrderedDict()
966
-
967
- paths = _get_job_source_paths()
968
-
969
- # Iterate over all filesystem sources (local, git.<slug1>, git.<slug2>, etc.)
970
- for source, path in paths.items():
971
- for job_info in jobs_in_directory(path):
972
- jobs.setdefault(source, {})
973
- if job_info.module_name not in jobs[source]:
974
- jobs[source][job_info.module_name] = {"name": job_info.job_class.grouping, "jobs": OrderedDict()}
975
- jobs[source][job_info.module_name]["jobs"][job_info.job_class_name] = job_info.job_class
976
-
977
- # Add jobs from plugins (which were already imported at startup)
978
- for cls in registry["plugin_jobs"]:
979
- module = inspect.getmodule(cls)
980
- jobs.setdefault("plugins", {}).setdefault(module.__name__, {"name": cls.grouping, "jobs": OrderedDict()})
981
- jobs["plugins"][module.__name__]["jobs"][cls.__name__] = cls
982
-
983
- return jobs
984
-
985
-
986
- def _get_job_source_paths():
987
- """
988
- Helper function to get_jobs().
989
-
990
- Constructs a dict of {"grouping": filesystem_path, ...}.
991
- Current groupings are "local", "git.<repository_slug>".
992
- Plugin jobs aren't loaded dynamically from a source_path and so are not included in this function
993
- """
994
- paths = {}
995
- # Locally installed jobs
996
- if settings.JOBS_ROOT and os.path.exists(settings.JOBS_ROOT):
997
- paths["local"] = settings.JOBS_ROOT
998
-
999
- # Jobs derived from Git repositories
1000
- if settings.GIT_ROOT and os.path.isdir(settings.GIT_ROOT):
1001
- for repository_record in GitRepository.objects.all():
1002
- if "extras.job" not in repository_record.provided_contents:
1003
- # This repository isn't marked as containing jobs that we should use.
1004
- continue
1005
-
1006
- try:
1007
- # In the case where we have multiple Nautobot instances, or multiple worker instances,
1008
- # they are not required to share a common filesystem; therefore, we may need to refresh our local clone
1009
- # of the Git repository to ensure that it is in sync with the latest repository clone from any instance.
1010
- ensure_git_repository(
1011
- repository_record,
1012
- head=repository_record.current_head,
1013
- logger=logger,
1014
- )
1015
- except Exception as exc:
1016
- logger.error(f"Error during local clone of Git repository {repository_record}: {exc}")
1017
- continue
1018
-
1019
- jobs_path = os.path.join(repository_record.filesystem_path, "jobs")
1020
- if os.path.isdir(jobs_path):
1021
- paths[f"git.{repository_record.slug}"] = jobs_path
1022
- else:
1023
- logger.warning(f"Git repository {repository_record} is configured to provide jobs, but none are found!")
1024
-
1025
- # TODO(Glenn): when a Git repo is deleted or its slug is changed, we update the local filesystem
1026
- # (see extras/signals.py, extras/models/datasources.py), but as noted above, there may be multiple filesystems
1027
- # involved, so not all local clones of deleted Git repositories may have been deleted yet.
1028
- # For now, if we encounter a "leftover" Git repo here, we delete it now.
1029
- for git_slug in os.listdir(settings.GIT_ROOT):
1030
- git_path = os.path.join(settings.GIT_ROOT, git_slug)
1031
- if not os.path.isdir(git_path):
1032
- logger.warning(
1033
- f"Found non-directory {git_slug} in {settings.GIT_ROOT}. Only Git repositories should exist here."
1034
- )
1035
- elif not os.path.isdir(os.path.join(git_path, ".git")):
1036
- logger.warning(f"Directory {git_slug} in {settings.GIT_ROOT} does not appear to be a Git repository.")
1037
- elif not GitRepository.objects.filter(slug=git_slug):
1038
- logger.warning(f"Deleting unmanaged (leftover?) repository at {git_path}")
1039
- shutil.rmtree(git_path)
1040
-
1041
- return paths
1042
-
1043
-
1044
- def get_job_classpaths():
1045
- """
1046
- Get a list of all known Job class_path strings.
1047
-
1048
- This is used as a cacheable, light-weight alternative to calling get_jobs() or get_job()
1049
- when all that's needed is to verify whether a given job exists.
1050
- """
1051
- jobs_dict = get_jobs()
1052
- result = set()
1053
- for grouping_name, modules_dict in jobs_dict.items():
1054
- for module_name in modules_dict:
1055
- for class_name in modules_dict[module_name]["jobs"]:
1056
- result.add(f"{grouping_name}/{module_name}/{class_name}")
1057
- return result
1058
-
1059
-
1060
1119
  def get_job(class_path):
1061
1120
  """
1062
- Retrieve a specific job class by its class_path.
1121
+ Retrieve a specific job class by its class_path (<module_name>.<JobClassName>).
1063
1122
 
1064
- Note that this is built atop get_jobs() and so is not a particularly light-weight API;
1065
- if all you need to do is to verify whether a given class_path exists, use get_job_classpaths() instead.
1066
-
1067
- Returns None if not found.
1123
+ May return None if the job isn't properly registered with Celery at this time.
1068
1124
  """
1069
1125
  try:
1070
- grouping_name, module_name, class_name = class_path.split("/", 2)
1071
- except ValueError:
1072
- logger.error(f'Invalid class_path value "{class_path}"')
1126
+ return celery_app.tasks[class_path].__class__
1127
+ except NotRegistered:
1073
1128
  return None
1074
1129
 
1075
- jobs = get_jobs()
1076
- return jobs.get(grouping_name, {}).get(module_name, {}).get("jobs", {}).get(class_name, None)
1077
-
1078
-
1079
- class RunJobTaskFailed(Exception):
1080
- """Celery task failed for some reason."""
1081
-
1082
-
1083
- @nautobot_task
1084
- def run_job(data, request, job_result_pk, commit=True, *args, **kwargs):
1085
- """
1086
- Helper function to call the "run()", "test_*()", and "post_run" methods on a Job.
1087
-
1088
- This function is responsible for setting up the job execution, handing the DB tranaction
1089
- and rollback conditions, plus post execution cleanup and saving the JobResult record.
1090
- """
1091
- from nautobot.extras.models import JobResult # avoid circular import
1092
-
1093
- # Getting the correct job result can fail if the stored data cannot be serialized.
1094
- # Catching `TypeError: the JSON object must be str, bytes or bytearray, not int`
1095
- job_result = JobResult.objects.get(pk=job_result_pk)
1096
-
1097
- job_model = job_result.job_model
1098
- initialization_failure = None
1099
- job_model = JobModel.objects.get_for_class_path(job_result.name)
1100
-
1101
- if not job_model.enabled:
1102
- initialization_failure = f"Job {job_model} is not enabled to be run!"
1103
- else:
1104
- job_class = job_model.job_class
1105
-
1106
- if not job_model.installed or not job_class:
1107
- initialization_failure = f'Unable to locate job "{job_result.name}" to run it!'
1108
-
1109
- if initialization_failure:
1110
- job_result.log(
1111
- message=initialization_failure,
1112
- obj=job_model,
1113
- level_choice=LogLevelChoices.LOG_FAILURE,
1114
- grouping="initialization",
1115
- logger=logger,
1116
- )
1117
- raise RunJobTaskFailed(initialization_failure)
1118
-
1119
- job = job_class()
1120
- job.active_test = "initialization"
1121
- job.job_result = job_result
1122
-
1123
- soft_time_limit = job_model.soft_time_limit or settings.CELERY_TASK_SOFT_TIME_LIMIT
1124
- time_limit = job_model.time_limit or settings.CELERY_TASK_TIME_LIMIT
1125
- if time_limit <= soft_time_limit:
1126
- job_result.log(
1127
- f"The hard time limit of {time_limit} seconds is less than "
1128
- f"or equal to the soft time limit of {soft_time_limit} seconds. "
1129
- f"This job will fail silently after {time_limit} seconds.",
1130
- level_choice=LogLevelChoices.LOG_WARNING,
1131
- grouping="initialization",
1132
- logger=logger,
1133
- )
1134
-
1135
- file_ids = None
1136
- try:
1137
- # Capture the file IDs for any FileProxy objects created so we can cleanup later.
1138
- file_fields = list(job._get_file_vars())
1139
- file_ids = [data[f] for f in file_fields]
1140
-
1141
- # Attempt to resolve serialized data back into original form by creating querysets or model instances
1142
- # If we fail to find any objects, we consider this a job execution error, and fail.
1143
- # This might happen when a job sits on the queue for a while (i.e. scheduled) and data has changed
1144
- # or it might be bad input from an API request, or manual execution.
1145
-
1146
- data = job_class.deserialize_data(data)
1147
- # TODO(jathan): Another place where because `log()` is called which mutates `.data`, we must
1148
- # explicitly call `save()` again. We need to see if we can move more of this to `NauotbotTask`
1149
- # and/or the DB backend as well.
1150
- except Exception:
1151
- stacktrace = traceback.format_exc()
1152
- job_result.log(
1153
- f"Error initializing job:\n```\n{stacktrace}\n```",
1154
- level_choice=LogLevelChoices.LOG_FAILURE,
1155
- grouping="initialization",
1156
- logger=logger,
1157
- )
1158
- job_result.save()
1159
- if file_ids:
1160
- # Cleanup FileProxy objects
1161
- job.delete_files(*file_ids) # pylint: disable=not-an-iterable
1162
- raise
1163
-
1164
- if job_model.read_only:
1165
- # Force commit to false for read only jobs.
1166
- commit = False
1167
-
1168
- # TODO(Glenn): validate that all args required by this job are set in the data or else log helpful errors?
1169
-
1170
- job.logger.info(f"Running job (commit={commit})")
1171
-
1172
- # Add the current request as a property of the job
1173
- job.request = request
1174
-
1175
- def _run_job():
1176
- """
1177
- Core job execution task.
1178
-
1179
- We capture this within a subfunction to allow for conditionally wrapping it with the change_logging
1180
- context manager (which is only relevant if commit == True).
1181
-
1182
- If the job is marked as read_only == True, then commit is forced to False and no log messages will be
1183
- emitted related to reverting database changes.
1184
- """
1185
- started = timezone.now()
1186
- job.results["output"] = ""
1187
- try:
1188
- with transaction.atomic():
1189
- # Script-like behavior
1190
- job.active_test = "run"
1191
- output = job.run(data=data, commit=commit)
1192
- if output:
1193
- job.results["output"] += "\n" + str(output)
1194
-
1195
- # Report-like behavior
1196
- for method_name in job.test_methods:
1197
- job.active_test = method_name
1198
- output = getattr(job, method_name)()
1199
- if output:
1200
- job.results["output"] += "\n" + str(output)
1201
-
1202
- job.logger.info("job completed successfully")
1203
-
1204
- if not commit:
1205
- raise AbortTransaction("Database changes have been reverted automatically.")
1206
-
1207
- except AbortTransaction:
1208
- if not job_model.read_only:
1209
- job.log_info(message="Database changes have been reverted automatically.")
1210
-
1211
- except Exception:
1212
- if not job_model.read_only:
1213
- job.log_info(message="Database changes have been reverted due to error.")
1214
- raise
1215
-
1216
- # TODO(jathan): For now we still need to call `save()` so that any output data from the job
1217
- # that was stored gets saved to the `JobResult`. We need to consider where this should be
1218
- # moved as we get closer to eliminating `run_job()` entirely. Hint: Probably inside of
1219
- # `NautobotTask` class.
1220
- finally:
1221
- _data = copy.deepcopy(job_result.data)
1222
- job_result.refresh_from_db()
1223
- job_result.data = _data
1224
- try:
1225
- job_result.save()
1226
- except IntegrityError:
1227
- # handle job_model deleted while job was running
1228
- job_result.job_model = None
1229
- job_result.save()
1230
- if file_ids:
1231
- job.delete_files(*file_ids) # Cleanup FileProxy objects
1232
-
1233
- # TODO(jathan): Pretty sure this can also be handled by the backend, but
1234
- # leaving it for now.
1235
- # record data about this jobrun in the scheduled_job
1236
- if job_result.scheduled_job:
1237
- job_result.scheduled_job.total_run_count += 1
1238
- job_result.scheduled_job.last_run_at = started
1239
- job_result.scheduled_job.save()
1240
-
1241
- # Perform any post-run tasks
1242
- # 2.0 TODO Remove post_run() method entirely
1243
- job.active_test = "post_run"
1244
- output = job.post_run()
1245
- # TODO(jathan): We need to call `save()` here too so that any appended output from
1246
- # `post_run` gets stored on the `JobResult`. We need to move this out of here as well.
1247
- if output:
1248
- job.results["output"] += "\n" + str(output)
1249
- job_result.save()
1250
-
1251
- job_result.refresh_from_db()
1252
- job.logger.info(f"Job completed in {job_result.duration}")
1253
-
1254
- # TODO(jathan): For now this is only output from `post_run()` which is not straightforward.
1255
- # We need to think about what we want to be returned from job runs and stored as
1256
- # `JobResult.result`, otherwise it will always be `None`.
1257
- return output
1258
-
1259
- # Execute the job. If commit == True, wrap it with the change_logging context manager to ensure we
1260
- # process change logs, webhooks, etc.
1261
- if commit:
1262
- context_class = JobHookChangeContext if job_model.is_job_hook_receiver else JobChangeContext
1263
- change_context = context_class(user=request.user, context_detail=job_model.slug)
1264
- with change_logging(change_context):
1265
- output = _run_job()
1266
- else:
1267
- output = _run_job()
1268
-
1269
- # This is just passing through the return value from `post_run()` which for now will always be
1270
- # `None` (see above).
1271
- return output
1272
-
1273
-
1274
- @nautobot_task
1275
- def scheduled_job_handler(*args, **kwargs):
1276
- """
1277
- A thin wrapper around JobResult.enqueue_job() that allows for it to be called as an async task
1278
- for the purposes of enqueuing scheduled jobs at their recurring intervals. Thus, JobResult.enqueue_job()
1279
- is responsible for enqueuing the actual job for execution and this method is the task executed
1280
- by the scheduler to kick off the job execution on a recurring interval.
1281
- """
1282
- from nautobot.extras.models import JobResult # avoid circular import
1283
-
1284
- user_pk = kwargs.pop("user")
1285
- user = User.objects.get(pk=user_pk)
1286
- name = kwargs.pop("name")
1287
- scheduled_job_pk = kwargs.pop("scheduled_job_pk")
1288
- celery_kwargs = kwargs.pop("celery_kwargs", {})
1289
- schedule = ScheduledJob.objects.get(pk=scheduled_job_pk)
1290
-
1291
- job_content_type = get_job_content_type()
1292
- JobResult.enqueue_job(
1293
- run_job, name, job_content_type, user, celery_kwargs=celery_kwargs, schedule=schedule, **kwargs
1294
- )
1295
-
1296
1130
 
1297
1131
  def enqueue_job_hooks(object_change):
1298
1132
  """
1299
1133
  Find job hook(s) assigned to this changed object type + action and enqueue them
1300
1134
  to be processed
1301
1135
  """
1302
- from nautobot.extras.models import JobResult # avoid circular import
1303
1136
 
1304
1137
  # Job hooks cannot trigger other job hooks
1305
1138
  if object_change.change_context == ObjectChangeEventContextChoices.CONTEXT_JOB_HOOK:
@@ -1321,16 +1154,5 @@ def enqueue_job_hooks(object_change):
1321
1154
 
1322
1155
  # Enqueue the jobs related to the job_hooks
1323
1156
  for job_hook in job_hooks:
1324
- job_content_type = get_job_content_type()
1325
1157
  job_model = job_hook.job
1326
- request = RequestFactory().request(SERVER_NAME="job_hook")
1327
- request.user = object_change.user
1328
- JobResult.enqueue_job(
1329
- run_job,
1330
- job_model.class_path,
1331
- job_content_type,
1332
- object_change.user,
1333
- data=job_model.job_class.serialize_data({"object_change": object_change}),
1334
- request=copy_safe_request(request),
1335
- commit=True,
1336
- )
1158
+ JobResult.enqueue_job(job_model, object_change.user, object_change=object_change.pk)