nautobot 2.0.0a2__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 (1029) hide show
  1. nautobot/__init__.py +1 -5
  2. nautobot/apps/api.py +6 -8
  3. nautobot/apps/forms.py +0 -2
  4. nautobot/apps/ui.py +0 -8
  5. nautobot/circuits/api/serializers.py +9 -119
  6. nautobot/circuits/api/urls.py +1 -1
  7. nautobot/circuits/api/views.py +0 -1
  8. nautobot/circuits/choices.py +0 -2
  9. nautobot/circuits/filters.py +7 -6
  10. nautobot/circuits/forms.py +3 -73
  11. nautobot/circuits/migrations/0001_initial_part_1.py +0 -1
  12. nautobot/circuits/migrations/0002_initial_part_2.py +0 -1
  13. nautobot/circuits/migrations/0003_auto_slug.py +0 -1
  14. nautobot/circuits/migrations/0004_increase_provider_account_length.py +0 -1
  15. nautobot/circuits/migrations/0005_providernetwork.py +0 -1
  16. nautobot/circuits/migrations/0006_cache_circuit_terminations.py +0 -1
  17. nautobot/circuits/migrations/0007_circuitterminations_primary_model.py +0 -1
  18. nautobot/circuits/migrations/0008_add_natural_indexing.py +0 -1
  19. nautobot/circuits/migrations/0009_circuittermination_location.py +0 -1
  20. nautobot/circuits/migrations/0010_rename_foreign_keys_and_related_names.py +0 -1
  21. nautobot/circuits/migrations/0011_remove_site_foreign_key_from_circuit_termination_class.py +0 -1
  22. nautobot/circuits/migrations/0012_created_datetime.py +0 -1
  23. nautobot/circuits/migrations/0013_alter_circuittermination__path.py +0 -1
  24. nautobot/circuits/migrations/0014_related_name_changes.py +1 -2
  25. nautobot/circuits/migrations/0015_remove_circuittype_provider_slug.py +20 -0
  26. nautobot/circuits/migrations/0016_tagsfield.py +34 -0
  27. nautobot/circuits/migrations/0017_fixup_null_statuses.py +22 -0
  28. nautobot/circuits/migrations/0018_status_nonnullable.py +22 -0
  29. nautobot/circuits/models.py +3 -93
  30. nautobot/circuits/navigation.py +14 -69
  31. nautobot/circuits/signals.py +0 -2
  32. nautobot/circuits/tables.py +42 -5
  33. nautobot/circuits/templates/circuits/circuit_retrieve.html +1 -1
  34. nautobot/circuits/templates/circuits/circuittermination_retrieve.html +1 -1
  35. nautobot/circuits/templates/circuits/circuittype_retrieve.html +1 -1
  36. nautobot/circuits/templates/circuits/provider_create.html +0 -1
  37. nautobot/circuits/templates/circuits/provider_retrieve.html +1 -1
  38. nautobot/circuits/tests/integration/test_relationships.py +13 -16
  39. nautobot/circuits/tests/test_api.py +13 -43
  40. nautobot/circuits/tests/test_filters.py +20 -15
  41. nautobot/circuits/tests/test_models.py +7 -3
  42. nautobot/circuits/tests/test_views.py +57 -67
  43. nautobot/circuits/views.py +18 -9
  44. nautobot/core/api/__init__.py +8 -2
  45. nautobot/core/api/authentication.py +0 -3
  46. nautobot/core/api/fields.py +15 -6
  47. nautobot/core/api/filter_backends.py +3 -2
  48. nautobot/core/api/metadata.py +237 -30
  49. nautobot/core/api/mixins.py +94 -0
  50. nautobot/core/api/pagination.py +3 -3
  51. nautobot/core/api/parsers.py +154 -0
  52. nautobot/core/api/renderers.py +153 -2
  53. nautobot/core/api/schema.py +47 -3
  54. nautobot/core/api/serializers.py +377 -37
  55. nautobot/core/api/urls.py +11 -3
  56. nautobot/core/api/utils.py +174 -2
  57. nautobot/core/api/versioning.py +32 -10
  58. nautobot/core/api/views.py +266 -75
  59. nautobot/core/apps/__init__.py +138 -221
  60. nautobot/core/celery/__init__.py +112 -41
  61. nautobot/core/celery/backends.py +19 -13
  62. nautobot/core/celery/control.py +46 -0
  63. nautobot/core/celery/encoders.py +53 -0
  64. nautobot/core/celery/log.py +38 -0
  65. nautobot/core/celery/schedulers.py +23 -4
  66. nautobot/core/celery/task.py +1 -16
  67. nautobot/core/checks.py +0 -27
  68. nautobot/core/choices.py +21 -113
  69. nautobot/core/{cli.py → cli/__init__.py} +1 -2
  70. nautobot/core/cli/__main__.py +3 -0
  71. nautobot/core/constants.py +25 -43
  72. nautobot/core/context_processors.py +12 -0
  73. nautobot/core/filters.py +2 -2
  74. nautobot/core/forms/__init__.py +0 -4
  75. nautobot/core/forms/fields.py +39 -68
  76. nautobot/core/forms/forms.py +27 -27
  77. nautobot/core/forms/utils.py +7 -59
  78. nautobot/core/forms/widgets.py +0 -1
  79. nautobot/core/graphql/__init__.py +2 -2
  80. nautobot/core/graphql/schema.py +4 -27
  81. nautobot/core/jobs/__init__.py +75 -0
  82. nautobot/core/management/commands/build_ui.py +255 -0
  83. nautobot/core/management/commands/celery.py +0 -1
  84. nautobot/core/management/commands/generate_test_data.py +18 -13
  85. nautobot/core/management/commands/post_upgrade.py +24 -24
  86. nautobot/core/management/commands/validate_models.py +0 -1
  87. nautobot/core/middleware.py +0 -1
  88. nautobot/core/models/__init__.py +26 -1
  89. nautobot/core/models/fields.py +24 -5
  90. nautobot/core/models/generics.py +2 -46
  91. nautobot/core/models/managers.py +5 -0
  92. nautobot/core/models/name_color_content_types.py +1 -19
  93. nautobot/core/models/tree_queries.py +14 -4
  94. nautobot/core/models/utils.py +9 -10
  95. nautobot/core/models/validators.py +17 -8
  96. nautobot/core/releases.py +8 -10
  97. nautobot/core/settings.py +81 -53
  98. nautobot/core/tables.py +5 -5
  99. nautobot/core/tasks.py +4 -7
  100. nautobot/core/templates/base.html +1 -49
  101. nautobot/core/templates/base_django.html +49 -0
  102. nautobot/core/templates/base_react.html +55 -0
  103. nautobot/core/templates/buttons/export.html +6 -4
  104. nautobot/core/templates/generic/object_bulk_create.html +10 -21
  105. nautobot/core/templates/generic/object_list.html +4 -1
  106. nautobot/core/templates/generic/object_retrieve_plugin_full_width.html +3 -0
  107. nautobot/core/templates/inc/footer.html +1 -0
  108. nautobot/core/templates/inc/javascript.html +0 -14
  109. nautobot/core/templates/inc/nav_menu.html +28 -33
  110. nautobot/core/templates/inc/object_details_advanced_panel.html +13 -0
  111. nautobot/core/templates/inc/relationships_table_rows.html +2 -2
  112. nautobot/core/templates/nautobot_config.py.j2 +8 -25
  113. nautobot/core/templates/plugin_template/__init__.py-tpl +1 -2
  114. nautobot/core/templates/rest_framework/api.html +8 -0
  115. nautobot/core/templatetags/buttons.py +32 -29
  116. nautobot/core/templatetags/helpers.py +1 -1
  117. nautobot/core/testing/__init__.py +47 -44
  118. nautobot/core/testing/api.py +365 -47
  119. nautobot/core/testing/filters.py +12 -7
  120. nautobot/core/testing/integration.py +1 -1
  121. nautobot/core/testing/migrations.py +2 -0
  122. nautobot/core/testing/mixins.py +22 -12
  123. nautobot/core/testing/schema.py +2 -1
  124. nautobot/core/testing/views.py +28 -51
  125. nautobot/core/tests/integration/test_filters.py +17 -8
  126. nautobot/core/tests/integration/test_navbar.py +11 -34
  127. nautobot/core/tests/integration/test_plugin_navbar.py +9 -103
  128. nautobot/core/tests/nautobot_config.py +2 -3
  129. nautobot/core/tests/runner.py +0 -1
  130. nautobot/core/tests/test_api.py +290 -24
  131. nautobot/core/tests/test_authentication.py +57 -14
  132. nautobot/core/tests/test_checks.py +0 -7
  133. nautobot/core/tests/test_choices.py +0 -1
  134. nautobot/core/tests/test_filters.py +117 -110
  135. nautobot/core/tests/test_forms.py +47 -110
  136. nautobot/core/tests/test_graphql.py +158 -135
  137. nautobot/core/tests/test_logging.py +4 -1
  138. nautobot/core/tests/test_managers.py +3 -5
  139. nautobot/core/tests/test_models.py +2 -0
  140. nautobot/core/tests/test_ordering.py +0 -2
  141. nautobot/core/tests/test_paginator.py +3 -1
  142. nautobot/core/tests/test_releases.py +12 -12
  143. nautobot/core/tests/test_templatetags_helpers.py +7 -4
  144. nautobot/core/tests/test_utils.py +112 -78
  145. nautobot/core/tests/test_views.py +12 -17
  146. nautobot/core/tests/test_views_utils.py +6 -9
  147. nautobot/core/utils/data.py +17 -0
  148. nautobot/core/utils/deprecation.py +13 -20
  149. nautobot/core/utils/filtering.py +53 -9
  150. nautobot/core/utils/git.py +12 -4
  151. nautobot/core/utils/lookup.py +3 -1
  152. nautobot/core/utils/requests.py +23 -116
  153. nautobot/core/views/__init__.py +1 -2
  154. nautobot/core/views/generic.py +131 -119
  155. nautobot/core/views/mixins.py +53 -62
  156. nautobot/core/views/paginator.py +0 -1
  157. nautobot/core/views/renderers.py +14 -12
  158. nautobot/core/views/utils.py +87 -4
  159. nautobot/dcim/api/serializers.py +160 -672
  160. nautobot/dcim/api/urls.py +1 -1
  161. nautobot/dcim/api/views.py +7 -46
  162. nautobot/dcim/choices.py +2 -25
  163. nautobot/dcim/elevations.py +0 -1
  164. nautobot/dcim/factory.py +15 -4
  165. nautobot/dcim/filters/__init__.py +42 -13
  166. nautobot/dcim/form_mixins.py +1 -27
  167. nautobot/dcim/forms.py +58 -797
  168. nautobot/dcim/management/commands/trace_paths.py +0 -1
  169. nautobot/dcim/migrations/0001_initial_part_1.py +0 -1
  170. nautobot/dcim/migrations/0002_initial_part_2.py +0 -1
  171. nautobot/dcim/migrations/0003_initial_part_3.py +0 -1
  172. nautobot/dcim/migrations/0004_initial_part_4.py +0 -1
  173. nautobot/dcim/migrations/0005_device_local_context_schema.py +0 -1
  174. nautobot/dcim/migrations/0006_auto_slug.py +0 -1
  175. nautobot/dcim/migrations/0007_device_secrets_group.py +0 -1
  176. nautobot/dcim/migrations/0008_increase_all_serial_lengths.py +0 -1
  177. nautobot/dcim/migrations/0009_add_natural_indexing.py +0 -1
  178. nautobot/dcim/migrations/0010_interface_status.py +0 -1
  179. nautobot/dcim/migrations/0011_interface_status_data_migration.py +0 -1
  180. nautobot/dcim/migrations/0012_interface_parent_bridge.py +0 -1
  181. nautobot/dcim/migrations/0013_location_location_type.py +0 -1
  182. nautobot/dcim/migrations/0014_location_status_data_migration.py +0 -1
  183. nautobot/dcim/migrations/0015_device_components__changeloggedmodel.py +0 -1
  184. nautobot/dcim/migrations/0016_device_components__timestamp_data_migration.py +0 -1
  185. nautobot/dcim/migrations/0017_locationtype_nestable.py +0 -1
  186. nautobot/dcim/migrations/0018_device_redundancy_group.py +0 -1
  187. nautobot/dcim/migrations/0019_device_redundancy_group_data_migration.py +0 -1
  188. nautobot/dcim/migrations/0020_move_site_fields_to_location_model.py +0 -1
  189. nautobot/dcim/migrations/0021_mptt_to_tree_queries.py +0 -1
  190. nautobot/dcim/migrations/0022_interface_mac_address_data_migration.py +0 -1
  191. nautobot/dcim/migrations/0023_alter_interface_mac_address.py +0 -1
  192. nautobot/dcim/migrations/0024_alter_device_and_rack_role_add_new_role.py +2 -2
  193. nautobot/dcim/migrations/0025_device_and_rack_roles_data_migrations.py +19 -14
  194. nautobot/dcim/migrations/0026_rename_device_and_rack_role.py +0 -1
  195. nautobot/dcim/migrations/0027_remove_device_role_and_rack_role.py +1 -2
  196. nautobot/dcim/migrations/0028_rename_foreignkey_fields.py +1 -2
  197. nautobot/dcim/migrations/0029_add_tree_managers_and_foreign_keys_pre_data_migration.py +0 -1
  198. nautobot/dcim/migrations/0030_migrate_region_and_site_data_to_locations.py +2 -2
  199. nautobot/dcim/migrations/0031_rename_path_end_point_related_name.py +0 -1
  200. nautobot/dcim/migrations/0032_remove_site_foreign_key_from_dcim_models.py +0 -1
  201. nautobot/dcim/migrations/0033_created_datetime.py +0 -1
  202. nautobot/dcim/migrations/0034_fixup_fks_and_related_names.py +0 -1
  203. nautobot/dcim/migrations/0035_related_name_changes.py +1 -2
  204. nautobot/dcim/migrations/0036_remove_region_and_site.py +1 -2
  205. nautobot/dcim/migrations/0037_interface_ip_addresses_m2m.py +0 -1
  206. nautobot/dcim/migrations/0038_alter_location_managers.py +0 -1
  207. nautobot/dcim/migrations/0039_remove_slug.py +24 -0
  208. nautobot/dcim/migrations/0040_tagsfield.py +109 -0
  209. nautobot/dcim/migrations/0041_ipam__namespaces.py +25 -0
  210. nautobot/dcim/migrations/0042_fixup_null_statuses.py +51 -0
  211. nautobot/dcim/migrations/0043_status_nonnullable.py +72 -0
  212. nautobot/dcim/models/cables.py +4 -35
  213. nautobot/dcim/models/device_component_templates.py +7 -2
  214. nautobot/dcim/models/device_components.py +26 -203
  215. nautobot/dcim/models/devices.py +30 -152
  216. nautobot/dcim/models/locations.py +3 -64
  217. nautobot/dcim/models/power.py +3 -51
  218. nautobot/dcim/models/racks.py +7 -86
  219. nautobot/dcim/navigation.py +141 -467
  220. nautobot/dcim/signals.py +0 -2
  221. nautobot/dcim/tables/devices.py +8 -5
  222. nautobot/dcim/tables/devicetypes.py +1 -1
  223. nautobot/dcim/tables/locations.py +2 -2
  224. nautobot/dcim/tables/power.py +2 -2
  225. nautobot/dcim/templates/dcim/console_port_connection_list.html +7 -0
  226. nautobot/dcim/templates/dcim/device.html +15 -4
  227. nautobot/dcim/templates/dcim/device_edit.html +6 -0
  228. nautobot/dcim/templates/dcim/deviceredundancygroup_create.html +0 -1
  229. nautobot/dcim/templates/dcim/devicetype.html +2 -2
  230. nautobot/dcim/templates/dcim/interface.html +4 -0
  231. nautobot/dcim/templates/dcim/interface_connection_list.html +7 -0
  232. nautobot/dcim/templates/dcim/interface_edit.html +1 -0
  233. nautobot/dcim/templates/dcim/location.html +16 -1
  234. nautobot/dcim/templates/dcim/locationtype.html +15 -0
  235. nautobot/dcim/templates/dcim/power_port_connection_list.html +7 -0
  236. nautobot/dcim/templates/dcim/rackgroup.html +0 -12
  237. nautobot/dcim/tests/integration/test_cable_connect_form.py +4 -4
  238. nautobot/dcim/tests/test_api.py +202 -130
  239. nautobot/dcim/tests/test_cablepaths.py +47 -42
  240. nautobot/dcim/tests/test_filters.py +156 -134
  241. nautobot/dcim/tests/test_forms.py +12 -213
  242. nautobot/dcim/tests/test_graphql.py +8 -3
  243. nautobot/dcim/tests/test_migrations.py +6 -11
  244. nautobot/dcim/tests/test_models.py +208 -158
  245. nautobot/dcim/tests/test_natural_ordering.py +12 -14
  246. nautobot/dcim/tests/test_signals.py +7 -4
  247. nautobot/dcim/tests/test_views.py +270 -264
  248. nautobot/dcim/urls.py +21 -26
  249. nautobot/dcim/views.py +14 -156
  250. nautobot/docs/additional-features/caching.md +6 -87
  251. nautobot/docs/additional-features/job-scheduling-and-approvals.md +3 -0
  252. nautobot/docs/additional-features/jobs.md +179 -197
  253. nautobot/docs/administration/nautobot-server.md +9 -24
  254. nautobot/docs/administration/nautobot-shell.md +6 -6
  255. nautobot/docs/administration/replicating-nautobot.md +0 -10
  256. nautobot/docs/configuration/index.md +9 -9
  257. nautobot/docs/configuration/optional-settings.md +32 -61
  258. nautobot/docs/configuration/required-settings.md +11 -52
  259. nautobot/docs/development/application-registry.md +2 -13
  260. nautobot/docs/development/best-practices.md +2 -1
  261. nautobot/docs/development/docker-compose-advanced-use-cases.md +1 -1
  262. nautobot/docs/development/extending-models.md +15 -17
  263. nautobot/docs/development/generic-views.md +0 -2
  264. nautobot/docs/development/getting-started.md +56 -6
  265. nautobot/docs/development/navigation-menu.md +22 -93
  266. nautobot/docs/development/react-ui.md +105 -0
  267. nautobot/docs/development/release-checklist.md +3 -3
  268. nautobot/docs/development/role-internals.md +1 -3
  269. nautobot/docs/development/style-guide.md +6 -4
  270. nautobot/docs/development/templates.md +2 -1
  271. nautobot/docs/docker/index.md +16 -14
  272. nautobot/docs/index.md +7 -3
  273. nautobot/docs/installation/index.md +4 -1
  274. nautobot/docs/installation/migrating-from-netbox.md +12 -43
  275. nautobot/docs/installation/migrating-from-postgresql.md +1 -1
  276. nautobot/docs/installation/nautobot.md +1 -1
  277. nautobot/docs/installation/tables/v2-api-behavior-changes.yaml +70 -0
  278. nautobot/docs/installation/tables/v2-api-removed-fields.yaml +142 -0
  279. nautobot/docs/installation/tables/v2-api-renamed-fields.yaml +124 -0
  280. nautobot/docs/installation/tables/v2-code-location-changes.yaml +241 -0
  281. nautobot/docs/installation/tables/v2-code-removals.yaml +67 -0
  282. nautobot/docs/installation/tables/v2-database-behavior-changes.yaml +37 -0
  283. nautobot/docs/installation/tables/v2-database-removed-fields.yaml +166 -0
  284. nautobot/docs/installation/tables/v2-database-renamed-fields.yaml +340 -0
  285. nautobot/docs/installation/tables/v2-filters-corrected-fields.yaml +28 -0
  286. nautobot/docs/installation/tables/v2-filters-enhanced-fields.yaml +241 -0
  287. nautobot/docs/installation/tables/v2-filters-removed-fields.yaml +553 -0
  288. nautobot/docs/installation/tables/v2-filters-renamed-fields.yaml +223 -0
  289. nautobot/docs/installation/tables/v2-logging-renamed-loggers.yaml +23 -0
  290. nautobot/docs/installation/upgrading-from-nautobot-v1.md +190 -636
  291. nautobot/docs/installation/upgrading.md +5 -2
  292. nautobot/docs/models/dcim/device.md +3 -0
  293. nautobot/docs/models/dcim/deviceredundancygroup.md +3 -3
  294. nautobot/docs/models/extras/computedfield.md +4 -4
  295. nautobot/docs/models/extras/dynamicgroup.md +9 -9
  296. nautobot/docs/models/extras/gitrepository.md +3 -0
  297. nautobot/docs/models/extras/job.md +1 -0
  298. nautobot/docs/models/extras/jobbutton.md +18 -13
  299. nautobot/docs/models/extras/jobhook.md +7 -4
  300. nautobot/docs/models/extras/jobresult.md +6 -2
  301. nautobot/docs/models/extras/relationship.md +2 -2
  302. nautobot/docs/models/extras/status.md +6 -19
  303. nautobot/docs/models/ipam/ipaddress.md +3 -0
  304. nautobot/docs/models/ipam/vrf.md +0 -3
  305. nautobot/docs/models/virtualization/virtualmachine.md +3 -0
  306. nautobot/docs/plugins/development.md +92 -24
  307. nautobot/docs/release-notes/version-1.5.md +96 -0
  308. nautobot/docs/release-notes/version-2.0.md +216 -0
  309. nautobot/docs/requirements.txt +5 -4
  310. nautobot/docs/rest-api/overview.md +384 -215
  311. nautobot/docs/rest-api/ui-related-endpoints.md +9 -0
  312. nautobot/extras/admin.py +3 -5
  313. nautobot/extras/api/customfields.py +15 -39
  314. nautobot/extras/api/fields.py +0 -11
  315. nautobot/extras/api/mixins.py +45 -0
  316. nautobot/extras/api/relationships.py +63 -159
  317. nautobot/extras/api/serializers.py +165 -706
  318. nautobot/extras/api/urls.py +1 -1
  319. nautobot/extras/api/views.py +295 -282
  320. nautobot/extras/apps.py +4 -7
  321. nautobot/extras/choices.py +11 -22
  322. nautobot/extras/constants.py +9 -3
  323. nautobot/extras/datasources/__init__.py +2 -0
  324. nautobot/extras/datasources/git.py +135 -186
  325. nautobot/extras/datasources/registry.py +25 -35
  326. nautobot/extras/factory.py +1 -3
  327. nautobot/extras/filters/__init__.py +49 -47
  328. nautobot/extras/filters/mixins.py +10 -8
  329. nautobot/extras/forms/forms.py +72 -148
  330. nautobot/extras/forms/mixins.py +34 -57
  331. nautobot/extras/health_checks.py +0 -33
  332. nautobot/extras/jobs.py +387 -566
  333. nautobot/extras/management/__init__.py +55 -48
  334. nautobot/extras/management/commands/renaturalize.py +0 -1
  335. nautobot/extras/management/commands/runjob.py +24 -62
  336. nautobot/extras/management/commands/webhook_receiver.py +0 -1
  337. nautobot/extras/managers.py +30 -7
  338. nautobot/extras/migrations/0001_initial_part_1.py +0 -1
  339. nautobot/extras/migrations/0002_initial_part_2.py +0 -1
  340. nautobot/extras/migrations/0003_initial_part_3.py +0 -1
  341. nautobot/extras/migrations/0004_populate_default_status_records.py +0 -1
  342. nautobot/extras/migrations/0005_configcontext_device_types.py +0 -1
  343. nautobot/extras/migrations/0006_graphqlquery.py +0 -1
  344. nautobot/extras/migrations/0007_configcontextschema.py +0 -1
  345. nautobot/extras/migrations/0008_jobresult__custom_field_data.py +0 -1
  346. nautobot/extras/migrations/0009_computedfield.py +0 -1
  347. nautobot/extras/migrations/0010_change_cf_validation_max_min_field_to_bigint.py +0 -1
  348. nautobot/extras/migrations/0011_fileattachment_fileproxy.py +0 -1
  349. nautobot/extras/migrations/0012_healthchecktestmodel.py +0 -1
  350. nautobot/extras/migrations/0013_default_fallback_value_computedfield.py +0 -1
  351. nautobot/extras/migrations/0014_auto_slug.py +0 -1
  352. nautobot/extras/migrations/0015_scheduled_job.py +0 -1
  353. nautobot/extras/migrations/0016_secret.py +0 -1
  354. nautobot/extras/migrations/0017_joblogentry.py +0 -1
  355. nautobot/extras/migrations/0018_joblog_data_migration.py +0 -2
  356. nautobot/extras/migrations/0019_joblogentry__meta_options__related_name.py +0 -1
  357. nautobot/extras/migrations/0020_customfield_changelog.py +0 -1
  358. nautobot/extras/migrations/0021_customfield_changelog_data.py +0 -1
  359. nautobot/extras/migrations/0022_objectchange_object_datav2.py +0 -1
  360. nautobot/extras/migrations/0023_job_model.py +0 -1
  361. nautobot/extras/migrations/0024_job_data_migration.py +0 -1
  362. nautobot/extras/migrations/0025_add_advanced_ui_boolean_to_customfield_conputedfield_and_relationship.py +0 -1
  363. nautobot/extras/migrations/0026_job_add_gitrepository_fk.py +0 -1
  364. nautobot/extras/migrations/0027_job_gitrepository_data_migration.py +0 -1
  365. nautobot/extras/migrations/0028_job_reduce_source.py +0 -1
  366. nautobot/extras/migrations/0029_dynamicgroup.py +0 -1
  367. nautobot/extras/migrations/0030_webhook_alter_unique_together.py +0 -1
  368. nautobot/extras/migrations/0031_tag_content_types.py +0 -1
  369. nautobot/extras/migrations/0032_tag_content_types_data_migration.py +0 -1
  370. nautobot/extras/migrations/0033_add__optimized_indexing.py +0 -1
  371. nautobot/extras/migrations/0034_alter_fileattachment_mimetype.py +0 -1
  372. nautobot/extras/migrations/0035_scheduledjob_crontab.py +0 -1
  373. nautobot/extras/migrations/0036_job_add_has_sensitive_variables.py +0 -1
  374. nautobot/extras/migrations/0037_configcontextschema__remove_name_unique__create_constraint_unique_name_owner.py +0 -1
  375. nautobot/extras/migrations/0038_configcontext_locations.py +0 -1
  376. nautobot/extras/migrations/0039_objectchange__add_change_context.py +0 -1
  377. nautobot/extras/migrations/0040_dynamicgroup__dynamicgroupmembership.py +0 -1
  378. nautobot/extras/migrations/0041_jobresult_job_kwargs.py +0 -1
  379. nautobot/extras/migrations/0042_job__add_is_job_hook_receiver.py +0 -1
  380. nautobot/extras/migrations/0043_note.py +0 -1
  381. nautobot/extras/migrations/0044_add_job_hook.py +0 -1
  382. nautobot/extras/migrations/0045_add_custom_field_slug.py +0 -1
  383. nautobot/extras/migrations/0046_populate_custom_field_slug_label.py +0 -1
  384. nautobot/extras/migrations/0047_enforce_custom_field_slug.py +0 -1
  385. nautobot/extras/migrations/0048_alter_objectchange_change_context_detail.py +0 -1
  386. nautobot/extras/migrations/0049_alter_tag_slug.py +0 -1
  387. nautobot/extras/migrations/0050_customfield_grouping.py +0 -1
  388. nautobot/extras/migrations/0051_add_job_task_queues.py +0 -1
  389. nautobot/extras/migrations/0052_configcontext_device_redundancy_groups.py +0 -1
  390. nautobot/extras/migrations/0053_relationship_required_on.py +0 -1
  391. nautobot/extras/migrations/0054_scheduledjob_kwargs_request_user_change.py +0 -1
  392. nautobot/extras/migrations/0055_configcontext_dynamic_groups.py +0 -1
  393. nautobot/extras/migrations/0056_objectchange_add_reverse_time_idx.py +0 -1
  394. nautobot/extras/migrations/0057_jobbutton.py +0 -1
  395. nautobot/extras/migrations/0058_jobresult_add_time_status_idxs.py +38 -0
  396. nautobot/extras/migrations/{0058_joblogentry_scheduledjob_webhook_data_migration.py → 0059_joblogentry_scheduledjob_webhook_data_migration.py} +1 -2
  397. nautobot/extras/migrations/{0059_alter_joblogentry_scheduledjob_webhook_fields.py → 0060_alter_joblogentry_scheduledjob_webhook_fields.py} +1 -2
  398. nautobot/extras/migrations/{0060_role_and_alter_status.py → 0061_role_and_alter_status.py} +1 -8
  399. nautobot/extras/migrations/{0061_collect_roles_from_related_apps_roles.py → 0062_collect_roles_from_related_apps_roles.py} +33 -33
  400. nautobot/extras/migrations/{0062_alter_role_options.py → 0063_alter_role_options.py} +1 -2
  401. nautobot/extras/migrations/{0063_alter_configcontext_and_add_new_role.py → 0064_alter_configcontext_and_add_new_role.py} +1 -2
  402. nautobot/extras/migrations/0065_configcontext_data_migrations.py +44 -0
  403. nautobot/extras/migrations/{0065_rename_configcontext_role.py → 0066_rename_configcontext_role.py} +1 -2
  404. nautobot/extras/migrations/{0066_jobresult__add_celery_fields.py → 0067_jobresult__add_celery_fields.py} +36 -3
  405. nautobot/extras/migrations/{0067_created_datetime.py → 0068_created_datetime.py} +1 -2
  406. nautobot/extras/migrations/{0068_remove_site_and_region_attributes_from_config_context.py → 0069_remove_site_and_region_attributes_from_config_context.py} +1 -2
  407. nautobot/extras/migrations/{0069_replace_related_names.py → 0070_replace_related_names.py} +1 -1
  408. nautobot/extras/migrations/{0070_rename_model_fields.py → 0071_rename_model_fields.py} +1 -2
  409. nautobot/extras/migrations/0072_job__unique_name_data_migration.py +86 -0
  410. nautobot/extras/migrations/{0072_job__unique_name.py → 0073_job__unique_name.py} +13 -10
  411. nautobot/extras/migrations/{0073_remove_gitrepository_fields.py → 0074_remove_gitrepository_fields.py} +1 -2
  412. nautobot/extras/migrations/{0074_rename_slug_to_key_for_custom_field.py → 0075_rename_slug_to_key_for_custom_field.py} +1 -1
  413. nautobot/extras/migrations/{0075_migrate_custom_field_data.py → 0076_migrate_custom_field_data.py} +1 -1
  414. 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
  415. nautobot/extras/migrations/0078_remove_slug.py +45 -0
  416. nautobot/extras/migrations/0079_tagsfield.py +28 -0
  417. nautobot/extras/migrations/0080_rename_relationship_slug_to_key.py +17 -0
  418. nautobot/extras/migrations/0081_rename_relationship_name_to_label.py +29 -0
  419. nautobot/extras/migrations/0082_ensure_relationship_keys_are_unique.py +43 -0
  420. nautobot/extras/migrations/0083_rename_computed_field_slug_to_key.py +21 -0
  421. nautobot/extras/migrations/0084_taggeditem_cleanup.py +43 -0
  422. nautobot/extras/migrations/0085_taggeditem_uniqueness.py +22 -0
  423. nautobot/extras/migrations/0086_job__celery_task_fields__dryrun_support.py +81 -0
  424. nautobot/extras/migrations/0087_job__commit_default_data_migration.py +26 -0
  425. nautobot/extras/migrations/0088_joblogentry__log_level_default.py +17 -0
  426. nautobot/extras/migrations/0089_joblogentry__log_level_data_migration.py +34 -0
  427. nautobot/extras/migrations/0090_scheduledjob__data_migration.py +57 -0
  428. nautobot/extras/models/__init__.py +2 -3
  429. nautobot/extras/models/change_logging.py +0 -36
  430. nautobot/extras/models/customfields.py +39 -33
  431. nautobot/extras/models/datasources.py +48 -50
  432. nautobot/extras/models/groups.py +5 -12
  433. nautobot/extras/models/jobs.py +190 -323
  434. nautobot/extras/models/mixins.py +0 -71
  435. nautobot/extras/models/models.py +1 -22
  436. nautobot/extras/models/relationships.py +20 -21
  437. nautobot/extras/models/roles.py +0 -23
  438. nautobot/extras/models/secrets.py +2 -31
  439. nautobot/extras/models/statuses.py +6 -5
  440. nautobot/extras/models/tags.py +2 -17
  441. nautobot/extras/navigation.py +89 -307
  442. nautobot/extras/plugins/__init__.py +3 -121
  443. nautobot/extras/plugins/utils.py +0 -3
  444. nautobot/extras/plugins/validators.py +5 -4
  445. nautobot/extras/plugins/views.py +16 -4
  446. nautobot/extras/querysets.py +1 -7
  447. nautobot/extras/registry.py +3 -0
  448. nautobot/extras/signals.py +26 -60
  449. nautobot/extras/tables.py +42 -49
  450. nautobot/extras/tasks.py +0 -12
  451. nautobot/extras/templates/extras/configcontext.html +1 -1
  452. nautobot/extras/templates/extras/configcontextschema.html +16 -1
  453. nautobot/extras/templates/extras/customfield.html +0 -13
  454. nautobot/extras/templates/extras/dynamicgroup_edit.html +0 -1
  455. nautobot/extras/templates/extras/gitrepository.html +3 -3
  456. nautobot/extras/templates/extras/inc/jobresult.html +10 -0
  457. nautobot/extras/templates/extras/inc/panel_jobhistory.html +1 -1
  458. nautobot/extras/templates/extras/job.html +35 -25
  459. nautobot/extras/templates/extras/job_approval_request.html +15 -30
  460. nautobot/extras/templates/extras/job_detail.html +13 -31
  461. nautobot/extras/templates/extras/job_edit.html +14 -17
  462. nautobot/extras/templates/extras/jobresult.html +24 -6
  463. nautobot/extras/templates/extras/objectchange_list.html +1 -1
  464. nautobot/extras/templates/extras/scheduledjob.html +2 -2
  465. nautobot/extras/templates/extras/secret.html +28 -0
  466. nautobot/extras/templates/extras/secret_edit.html +0 -1
  467. nautobot/extras/templates/extras/secretsgroup_edit.html +0 -1
  468. nautobot/extras/templatetags/custom_links.py +0 -2
  469. nautobot/extras/templatetags/job_buttons.py +1 -0
  470. nautobot/extras/templatetags/plugins.py +0 -1
  471. nautobot/extras/{tests/example_jobs → test_jobs}/api_test_job.py +13 -6
  472. nautobot/extras/test_jobs/atomic_transaction.py +53 -0
  473. nautobot/extras/test_jobs/dry_run.py +29 -0
  474. nautobot/extras/{tests/example_jobs/test_duplicate_name.py → test_jobs/duplicate_name.py} +4 -0
  475. nautobot/extras/test_jobs/duplicate_name2.py +9 -0
  476. nautobot/extras/test_jobs/fail.py +23 -0
  477. nautobot/extras/{tests/example_jobs/test_field_default.py → test_jobs/field_default.py} +4 -0
  478. nautobot/extras/{tests/example_jobs/test_field_order.py → test_jobs/field_order.py} +4 -0
  479. nautobot/extras/{tests/example_jobs/test_file_upload_fail.py → test_jobs/file_upload_fail.py} +11 -6
  480. nautobot/extras/test_jobs/file_upload_pass.py +25 -0
  481. nautobot/extras/test_jobs/has_sensitive_variables.py +25 -0
  482. nautobot/extras/test_jobs/ipaddress_vars.py +66 -0
  483. nautobot/extras/test_jobs/job_button_receiver.py +28 -0
  484. nautobot/extras/test_jobs/job_hook_receiver.py +29 -0
  485. nautobot/extras/test_jobs/job_variables.py +88 -0
  486. nautobot/extras/test_jobs/location_with_custom_field.py +45 -0
  487. nautobot/extras/test_jobs/log_redaction.py +20 -0
  488. nautobot/extras/test_jobs/log_skip_db_logging.py +17 -0
  489. nautobot/extras/test_jobs/modify_db.py +25 -0
  490. nautobot/extras/{tests/example_jobs/test_no_field_order.py → test_jobs/no_field_order.py} +4 -0
  491. nautobot/extras/test_jobs/object_var_optional.py +21 -0
  492. nautobot/extras/test_jobs/object_var_required.py +21 -0
  493. nautobot/extras/test_jobs/object_vars.py +26 -0
  494. nautobot/extras/test_jobs/pass.py +25 -0
  495. nautobot/extras/test_jobs/profiling.py +32 -0
  496. nautobot/extras/test_jobs/read_only_job.py +15 -0
  497. nautobot/extras/{tests/example_jobs/test_required_args.py → test_jobs/required_args.py} +4 -0
  498. 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
  499. nautobot/extras/{tests/example_jobs/test_task_queues.py → test_jobs/task_queues.py} +5 -1
  500. nautobot/extras/tests/integration/__init__.py +3 -3
  501. nautobot/extras/tests/integration/test_computedfields.py +1 -1
  502. nautobot/extras/tests/integration/test_configcontextschema.py +7 -5
  503. nautobot/extras/tests/integration/test_customfields.py +4 -2
  504. nautobot/extras/tests/integration/test_dynamicgroups.py +2 -2
  505. nautobot/extras/tests/integration/test_jobs.py +25 -27
  506. nautobot/extras/tests/integration/test_notes.py +8 -4
  507. nautobot/extras/tests/integration/test_plugins.py +4 -4
  508. nautobot/extras/tests/integration/test_relationships.py +2 -2
  509. nautobot/extras/tests/test_api.py +371 -381
  510. nautobot/extras/tests/test_changelog.py +17 -16
  511. nautobot/extras/tests/test_context_managers.py +5 -6
  512. nautobot/extras/tests/test_customfields.py +112 -73
  513. nautobot/extras/tests/test_datasources.py +191 -117
  514. nautobot/extras/tests/test_dynamicgroups.py +45 -68
  515. nautobot/extras/tests/test_filters.py +170 -130
  516. nautobot/extras/tests/test_forms.py +107 -109
  517. nautobot/extras/tests/{test_scripts.py → test_job_variables.py} +43 -49
  518. nautobot/extras/tests/test_jobs.py +271 -273
  519. nautobot/extras/tests/test_management.py +3 -6
  520. nautobot/extras/tests/test_migrations.py +5 -3
  521. nautobot/extras/tests/test_models.py +121 -173
  522. nautobot/extras/tests/test_notes.py +0 -1
  523. nautobot/extras/tests/test_plugins.py +55 -89
  524. nautobot/extras/tests/test_relationships.py +174 -130
  525. nautobot/extras/tests/test_tags.py +6 -12
  526. nautobot/extras/tests/test_utils.py +31 -1
  527. nautobot/extras/tests/test_views.py +223 -184
  528. nautobot/extras/tests/test_webhooks.py +16 -15
  529. nautobot/extras/urls.py +69 -69
  530. nautobot/extras/utils.py +137 -163
  531. nautobot/extras/views.py +81 -153
  532. nautobot/ipam/api/fields.py +17 -0
  533. nautobot/ipam/api/serializers.py +77 -164
  534. nautobot/ipam/api/urls.py +4 -1
  535. nautobot/ipam/api/views.py +28 -19
  536. nautobot/ipam/apps.py +1 -0
  537. nautobot/ipam/choices.py +5 -12
  538. nautobot/ipam/constants.py +1 -0
  539. nautobot/ipam/factory.py +41 -30
  540. nautobot/ipam/filters.py +58 -25
  541. nautobot/ipam/forms.py +82 -211
  542. nautobot/ipam/graphql/types.py +0 -9
  543. nautobot/ipam/lookups.py +13 -8
  544. nautobot/ipam/management/commands/__init__.py +0 -0
  545. nautobot/ipam/management/commands/fix_prefix_broadcast.py +17 -0
  546. nautobot/ipam/migrations/0001_initial_part_1.py +0 -1
  547. nautobot/ipam/migrations/0002_initial_part_2.py +0 -1
  548. nautobot/ipam/migrations/0003_remove_max_length.py +0 -1
  549. nautobot/ipam/migrations/0004_fixup_p2p_broadcast.py +0 -1
  550. nautobot/ipam/migrations/0005_auto_slug.py +0 -1
  551. nautobot/ipam/migrations/0006_ipaddress_nat_outside_list.py +0 -1
  552. nautobot/ipam/migrations/0007_add_natural_indexing.py +0 -1
  553. nautobot/ipam/migrations/0008_prefix_vlan_vlangroup_location.py +0 -1
  554. nautobot/ipam/migrations/0009_alter_vlan_name.py +0 -1
  555. nautobot/ipam/migrations/0010_alter_ipam_role_add_new_role.py +1 -2
  556. nautobot/ipam/migrations/0011_migrate_ipam_role_data.py +32 -39
  557. nautobot/ipam/migrations/0012_rename_ipam_roles.py +0 -1
  558. nautobot/ipam/migrations/0013_delete_role.py +0 -1
  559. nautobot/ipam/migrations/0014_rename_foreign_keys_and_related_names.py +0 -1
  560. nautobot/ipam/migrations/0015_prefix_add_type.py +0 -1
  561. nautobot/ipam/migrations/0016_prefix_type_data_migration.py +0 -3
  562. nautobot/ipam/migrations/0017_prefix_remove_is_pool.py +0 -1
  563. nautobot/ipam/migrations/0018_remove_site_foreign_key_from_ipam_models.py +0 -1
  564. nautobot/ipam/migrations/0019_created_datetime.py +0 -1
  565. nautobot/ipam/migrations/0020_related_name_changes.py +1 -2
  566. nautobot/ipam/migrations/0021_prefix_add_rir_and_date_allocated.py +0 -1
  567. nautobot/ipam/migrations/0022_aggregate_to_prefix_data_migration.py +3 -5
  568. nautobot/ipam/migrations/0023_delete_aggregate.py +0 -1
  569. nautobot/ipam/migrations/0024_interface_to_ipaddress_m2m.py +0 -1
  570. nautobot/ipam/migrations/0025_interface_ipaddress_m2m_data_migration.py +0 -1
  571. nautobot/ipam/migrations/0026_ipaddress_remove_assigned_object.py +0 -1
  572. nautobot/ipam/migrations/0027_remove_rir_slug.py +16 -0
  573. nautobot/ipam/migrations/0028_tagsfield.py +44 -0
  574. nautobot/ipam/migrations/0029_ip_address_to_interface_uniqueness_constraints.py +18 -0
  575. nautobot/ipam/migrations/0030_ipam__namespaces.py +231 -0
  576. nautobot/ipam/migrations/0031_ipam__prefix__add_parent.py +58 -0
  577. nautobot/ipam/migrations/0032_ipam__namespaces_finish.py +63 -0
  578. nautobot/ipam/migrations/0033_fixup_null_statuses.py +26 -0
  579. nautobot/ipam/migrations/0034_status_nonnullable.py +36 -0
  580. nautobot/ipam/models.py +579 -368
  581. nautobot/ipam/navigation.py +36 -159
  582. nautobot/ipam/querysets.py +117 -90
  583. nautobot/ipam/signals.py +89 -0
  584. nautobot/ipam/tables.py +86 -28
  585. nautobot/ipam/templates/ipam/ipaddress.html +14 -30
  586. nautobot/ipam/templates/ipam/ipaddress_edit.html +1 -0
  587. nautobot/ipam/templates/ipam/namespace_ipaddresses.html +11 -0
  588. nautobot/ipam/templates/ipam/namespace_prefixes.html +11 -0
  589. nautobot/ipam/templates/ipam/namespace_retrieve.html +42 -0
  590. nautobot/ipam/templates/ipam/namespace_vrfs.html +11 -0
  591. nautobot/ipam/templates/ipam/prefix.html +27 -33
  592. nautobot/ipam/templates/ipam/prefix_edit.html +7 -1
  593. nautobot/ipam/templates/ipam/vlangroup.html +0 -13
  594. nautobot/ipam/templates/ipam/vrf.html +6 -4
  595. nautobot/ipam/templates/ipam/vrf_edit.html +20 -2
  596. nautobot/ipam/tests/integration/test_prefixes.py +4 -27
  597. nautobot/ipam/tests/test_api.py +60 -61
  598. nautobot/ipam/tests/test_filters.py +187 -126
  599. nautobot/ipam/tests/test_forms.py +12 -6
  600. nautobot/ipam/tests/test_graphql.py +8 -6
  601. nautobot/ipam/tests/test_migrations.py +8 -13
  602. nautobot/ipam/tests/test_models.py +426 -274
  603. nautobot/ipam/tests/test_ordering.py +6 -3
  604. nautobot/ipam/tests/test_querysets.py +340 -96
  605. nautobot/ipam/tests/test_views.py +100 -55
  606. nautobot/ipam/urls.py +28 -5
  607. nautobot/ipam/{utils.py → utils/__init__.py} +2 -2
  608. nautobot/ipam/utils/migrations.py +713 -0
  609. nautobot/ipam/views.py +237 -122
  610. nautobot/project-static/docs/404.html +1399 -166
  611. nautobot/project-static/docs/additional-features/caching.html +1416 -320
  612. nautobot/project-static/docs/additional-features/change-logging.html +1389 -190
  613. nautobot/project-static/docs/additional-features/config-contexts.html +1389 -190
  614. nautobot/project-static/docs/additional-features/graphql.html +1389 -190
  615. nautobot/project-static/docs/additional-features/healthcheck.html +1389 -190
  616. nautobot/project-static/docs/additional-features/job-scheduling-and-approvals.html +1393 -190
  617. nautobot/project-static/docs/additional-features/jobs.html +1677 -460
  618. nautobot/project-static/docs/additional-features/napalm.html +1389 -190
  619. nautobot/project-static/docs/additional-features/prometheus-metrics.html +1389 -190
  620. nautobot/project-static/docs/additional-features/template-filters.html +1389 -190
  621. nautobot/project-static/docs/administration/celery-queues.html +1389 -190
  622. nautobot/project-static/docs/administration/nautobot-server.html +1553 -375
  623. nautobot/project-static/docs/administration/nautobot-shell.html +1395 -196
  624. nautobot/project-static/docs/administration/permissions.html +1389 -190
  625. nautobot/project-static/docs/administration/replicating-nautobot.html +1387 -207
  626. nautobot/project-static/docs/apps/index.html +1389 -190
  627. nautobot/project-static/docs/apps/nautobot-apps.html +1387 -175
  628. nautobot/project-static/docs/assets/javascripts/bundle.51198bba.min.js +29 -0
  629. nautobot/project-static/docs/assets/javascripts/bundle.51198bba.min.js.map +8 -0
  630. nautobot/project-static/docs/assets/javascripts/workers/{search.16e2a7d4.min.js → search.208ed371.min.js} +9 -15
  631. nautobot/project-static/docs/assets/javascripts/workers/{search.16e2a7d4.min.js.map → search.208ed371.min.js.map} +4 -4
  632. nautobot/project-static/docs/assets/stylesheets/main.ded33207.min.css +1 -0
  633. nautobot/project-static/docs/assets/stylesheets/main.ded33207.min.css.map +1 -0
  634. nautobot/project-static/docs/assets/stylesheets/palette.a0c5b2b5.min.css +1 -0
  635. nautobot/project-static/docs/assets/stylesheets/palette.a0c5b2b5.min.css.map +1 -0
  636. nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +1775 -590
  637. nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +1389 -190
  638. nautobot/project-static/docs/code-reference/nautobot/apps/api.html +3588 -1922
  639. nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +1461 -262
  640. nautobot/project-static/docs/code-reference/nautobot/apps/config.html +1401 -170
  641. nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +1396 -191
  642. nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +2095 -894
  643. nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +2357 -1194
  644. nautobot/project-static/docs/code-reference/nautobot/apps/models.html +2258 -940
  645. nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +1389 -190
  646. nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +1400 -201
  647. nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +11068 -7861
  648. nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +2867 -2224
  649. nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +1389 -190
  650. nautobot/project-static/docs/code-reference/nautobot/apps/views.html +2641 -1573
  651. nautobot/project-static/docs/configuration/authentication/ldap.html +1389 -190
  652. nautobot/project-static/docs/configuration/authentication/remote.html +1389 -190
  653. nautobot/project-static/docs/configuration/authentication/sso.html +1389 -190
  654. nautobot/project-static/docs/configuration/index.html +1398 -199
  655. nautobot/project-static/docs/configuration/optional-settings.html +1418 -274
  656. nautobot/project-static/docs/configuration/required-settings.html +1419 -287
  657. nautobot/project-static/docs/core-functionality/circuits.html +1446 -247
  658. nautobot/project-static/docs/core-functionality/device-types.html +1448 -249
  659. nautobot/project-static/docs/core-functionality/devices.html +1452 -249
  660. nautobot/project-static/docs/core-functionality/ipam.html +1452 -253
  661. nautobot/project-static/docs/core-functionality/power.html +1448 -249
  662. nautobot/project-static/docs/core-functionality/secrets.html +1448 -249
  663. nautobot/project-static/docs/core-functionality/services.html +1448 -249
  664. nautobot/project-static/docs/core-functionality/sites-and-racks.html +1448 -249
  665. nautobot/project-static/docs/core-functionality/tenancy.html +1448 -249
  666. nautobot/project-static/docs/core-functionality/virtualization.html +1452 -249
  667. nautobot/project-static/docs/core-functionality/vlans.html +1448 -249
  668. nautobot/project-static/docs/development/application-registry.html +1393 -214
  669. nautobot/project-static/docs/development/best-practices.html +1392 -192
  670. nautobot/project-static/docs/development/docker-compose-advanced-use-cases.html +1390 -191
  671. nautobot/project-static/docs/development/extending-models.html +1443 -257
  672. nautobot/project-static/docs/development/generic-views.html +1403 -174
  673. nautobot/project-static/docs/development/getting-started.html +1568 -262
  674. nautobot/project-static/docs/development/homepage.html +1389 -190
  675. nautobot/project-static/docs/development/index.html +1389 -190
  676. nautobot/project-static/docs/development/model-features.html +1389 -190
  677. nautobot/project-static/docs/development/natural-keys.html +1389 -190
  678. nautobot/project-static/docs/development/navigation-menu.html +1451 -330
  679. nautobot/project-static/docs/development/react-ui.html +4199 -0
  680. nautobot/project-static/docs/development/release-checklist.html +1392 -193
  681. nautobot/project-static/docs/development/role-internals.html +1402 -172
  682. nautobot/project-static/docs/development/style-guide.html +1399 -199
  683. nautobot/project-static/docs/development/templates.html +1391 -191
  684. nautobot/project-static/docs/development/testing.html +1389 -190
  685. nautobot/project-static/docs/development/user-preferences.html +1389 -190
  686. nautobot/project-static/docs/docker/index.html +1408 -206
  687. nautobot/project-static/docs/index.html +1397 -180
  688. nautobot/project-static/docs/installation/centos.html +1401 -170
  689. nautobot/project-static/docs/installation/external-authentication.html +1389 -190
  690. nautobot/project-static/docs/installation/http-server.html +1389 -190
  691. nautobot/project-static/docs/installation/index.html +1394 -191
  692. nautobot/project-static/docs/installation/migrating-from-netbox.html +1452 -305
  693. nautobot/project-static/docs/installation/migrating-from-postgresql.html +1390 -191
  694. nautobot/project-static/docs/installation/nautobot.html +1390 -191
  695. nautobot/project-static/docs/installation/region-and-site-data-migration-guide.html +1389 -190
  696. nautobot/project-static/docs/installation/selinux-troubleshooting.html +1401 -170
  697. nautobot/project-static/docs/installation/services.html +1389 -190
  698. nautobot/project-static/docs/installation/tables/v2-api-behavior-changes.yaml +70 -0
  699. nautobot/project-static/docs/installation/tables/v2-api-removed-fields.yaml +142 -0
  700. nautobot/project-static/docs/installation/tables/v2-api-renamed-fields.yaml +124 -0
  701. nautobot/project-static/docs/installation/tables/v2-code-location-changes.yaml +241 -0
  702. nautobot/project-static/docs/installation/tables/v2-code-removals.yaml +67 -0
  703. nautobot/project-static/docs/installation/tables/v2-database-behavior-changes.yaml +37 -0
  704. nautobot/project-static/docs/installation/tables/v2-database-removed-fields.yaml +166 -0
  705. nautobot/project-static/docs/installation/tables/v2-database-renamed-fields.yaml +340 -0
  706. nautobot/project-static/docs/installation/tables/v2-filters-corrected-fields.yaml +28 -0
  707. nautobot/project-static/docs/installation/tables/v2-filters-enhanced-fields.yaml +241 -0
  708. nautobot/project-static/docs/installation/tables/v2-filters-removed-fields.yaml +553 -0
  709. nautobot/project-static/docs/installation/tables/v2-filters-renamed-fields.yaml +223 -0
  710. nautobot/project-static/docs/installation/tables/v2-logging-renamed-loggers.yaml +23 -0
  711. nautobot/project-static/docs/installation/ubuntu.html +1401 -170
  712. nautobot/project-static/docs/installation/upgrading-from-nautobot-v1.html +4254 -1923
  713. nautobot/project-static/docs/installation/upgrading.html +1395 -192
  714. nautobot/project-static/docs/models/circuits/circuit.html +1427 -174
  715. nautobot/project-static/docs/models/circuits/circuittermination.html +1427 -174
  716. nautobot/project-static/docs/models/circuits/circuittype.html +1427 -174
  717. nautobot/project-static/docs/models/circuits/provider.html +1427 -174
  718. nautobot/project-static/docs/models/circuits/providernetwork.html +1427 -174
  719. nautobot/project-static/docs/models/dcim/cable.html +1458 -174
  720. nautobot/project-static/docs/models/dcim/consoleport.html +1427 -174
  721. nautobot/project-static/docs/models/dcim/consoleporttemplate.html +1427 -174
  722. nautobot/project-static/docs/models/dcim/consoleserverport.html +1427 -174
  723. nautobot/project-static/docs/models/dcim/consoleserverporttemplate.html +1427 -174
  724. nautobot/project-static/docs/models/dcim/device.html +1431 -174
  725. nautobot/project-static/docs/models/dcim/devicebay.html +1427 -174
  726. nautobot/project-static/docs/models/dcim/devicebaytemplate.html +1427 -174
  727. nautobot/project-static/docs/models/dcim/deviceredundancygroup.html +1522 -177
  728. nautobot/project-static/docs/models/dcim/devicetype.html +1427 -174
  729. nautobot/project-static/docs/models/dcim/frontport.html +1427 -174
  730. nautobot/project-static/docs/models/dcim/frontporttemplate.html +1427 -174
  731. nautobot/project-static/docs/models/dcim/interface.html +1427 -174
  732. nautobot/project-static/docs/models/dcim/interfacetemplate.html +1427 -174
  733. nautobot/project-static/docs/models/dcim/inventoryitem.html +1427 -174
  734. nautobot/project-static/docs/models/dcim/location.html +1427 -174
  735. nautobot/project-static/docs/models/dcim/locationtype.html +1427 -174
  736. nautobot/project-static/docs/models/dcim/manufacturer.html +1427 -174
  737. nautobot/project-static/docs/models/dcim/platform.html +1427 -174
  738. nautobot/project-static/docs/models/dcim/powerfeed.html +1425 -172
  739. nautobot/project-static/docs/models/dcim/poweroutlet.html +1427 -174
  740. nautobot/project-static/docs/models/dcim/poweroutlettemplate.html +1427 -174
  741. nautobot/project-static/docs/models/dcim/powerpanel.html +1425 -172
  742. nautobot/project-static/docs/models/dcim/powerport.html +1427 -174
  743. nautobot/project-static/docs/models/dcim/powerporttemplate.html +1427 -174
  744. nautobot/project-static/docs/models/dcim/rack.html +1427 -174
  745. nautobot/project-static/docs/models/dcim/rackgroup.html +1427 -174
  746. nautobot/project-static/docs/models/dcim/rackreservation.html +1427 -174
  747. nautobot/project-static/docs/models/dcim/rearport.html +1427 -174
  748. nautobot/project-static/docs/models/dcim/rearporttemplate.html +1427 -174
  749. nautobot/project-static/docs/models/dcim/region.html +1401 -170
  750. nautobot/project-static/docs/models/dcim/site.html +1401 -170
  751. nautobot/project-static/docs/models/dcim/virtualchassis.html +1425 -172
  752. nautobot/project-static/docs/models/extras/computedfield.html +1393 -194
  753. nautobot/project-static/docs/models/extras/configcontext.html +1465 -174
  754. nautobot/project-static/docs/models/extras/configcontextschema.html +1421 -168
  755. nautobot/project-static/docs/models/extras/customfield.html +1389 -190
  756. nautobot/project-static/docs/models/extras/customlink.html +1389 -190
  757. nautobot/project-static/docs/models/extras/dynamicgroup.html +1398 -199
  758. nautobot/project-static/docs/models/extras/exporttemplate.html +1389 -190
  759. nautobot/project-static/docs/models/extras/gitrepository.html +1393 -190
  760. nautobot/project-static/docs/models/extras/graphqlquery.html +1469 -171
  761. nautobot/project-static/docs/models/extras/imageattachment.html +1434 -181
  762. nautobot/project-static/docs/models/extras/job.html +1411 -157
  763. nautobot/project-static/docs/models/extras/jobbutton.html +1410 -207
  764. nautobot/project-static/docs/models/extras/jobhook.html +1397 -194
  765. nautobot/project-static/docs/models/extras/joblogentry.html +1408 -155
  766. nautobot/project-static/docs/models/extras/jobresult.html +1417 -159
  767. nautobot/project-static/docs/models/extras/note.html +1389 -190
  768. nautobot/project-static/docs/models/extras/relationship.html +1391 -192
  769. nautobot/project-static/docs/models/extras/role.html +1495 -198
  770. nautobot/project-static/docs/models/extras/secret.html +1492 -201
  771. nautobot/project-static/docs/models/extras/secretsgroup.html +1410 -157
  772. nautobot/project-static/docs/models/extras/status.html +1381 -221
  773. nautobot/project-static/docs/models/extras/tag.html +1389 -190
  774. nautobot/project-static/docs/models/extras/webhook.html +1389 -190
  775. nautobot/project-static/docs/models/ipam/ipaddress.html +1488 -200
  776. nautobot/project-static/docs/models/ipam/prefix.html +1410 -157
  777. nautobot/project-static/docs/models/ipam/rir.html +1410 -157
  778. nautobot/project-static/docs/models/ipam/routetarget.html +1410 -157
  779. nautobot/project-static/docs/models/ipam/service.html +1410 -157
  780. nautobot/project-static/docs/models/ipam/vlan.html +1410 -157
  781. nautobot/project-static/docs/models/ipam/vlangroup.html +1410 -157
  782. nautobot/project-static/docs/models/ipam/vrf.html +1410 -161
  783. nautobot/project-static/docs/models/tenancy/tenant.html +1412 -159
  784. nautobot/project-static/docs/models/tenancy/tenantgroup.html +1412 -159
  785. nautobot/project-static/docs/models/users/objectpermission.html +1462 -171
  786. nautobot/project-static/docs/models/users/token.html +1410 -157
  787. nautobot/project-static/docs/models/virtualization/cluster.html +1410 -157
  788. nautobot/project-static/docs/models/virtualization/clustergroup.html +1410 -157
  789. nautobot/project-static/docs/models/virtualization/clustertype.html +1410 -157
  790. nautobot/project-static/docs/models/virtualization/virtualmachine.html +1414 -157
  791. nautobot/project-static/docs/models/virtualization/vminterface.html +1410 -157
  792. nautobot/project-static/docs/objects.inv +0 -0
  793. nautobot/project-static/docs/plugins/development.html +1916 -646
  794. nautobot/project-static/docs/plugins/index.html +1389 -190
  795. nautobot/project-static/docs/plugins/porting-from-netbox.html +1389 -190
  796. nautobot/project-static/docs/release-notes/index.html +1389 -190
  797. nautobot/project-static/docs/release-notes/version-1.0.html +1389 -190
  798. nautobot/project-static/docs/release-notes/version-1.1.html +1389 -190
  799. nautobot/project-static/docs/release-notes/version-1.2.html +1389 -190
  800. nautobot/project-static/docs/release-notes/version-1.3.html +1389 -190
  801. nautobot/project-static/docs/release-notes/version-1.4.html +1389 -190
  802. nautobot/project-static/docs/release-notes/version-1.5.html +2016 -397
  803. nautobot/project-static/docs/release-notes/version-2.0.html +1935 -287
  804. nautobot/project-static/docs/requirements.txt +5 -4
  805. nautobot/project-static/docs/rest-api/authentication.html +1389 -190
  806. nautobot/project-static/docs/rest-api/filtering.html +1389 -190
  807. nautobot/project-static/docs/rest-api/overview.html +2002 -576
  808. nautobot/project-static/docs/rest-api/ui-related-endpoints.html +4057 -0
  809. nautobot/project-static/docs/search/search_index.json +1 -1
  810. nautobot/project-static/docs/sitemap.xml +197 -187
  811. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  812. nautobot/project-static/docs/user-guides/custom-fields.html +1390 -191
  813. nautobot/project-static/docs/user-guides/getting-started/creating-devices.html +1392 -193
  814. nautobot/project-static/docs/user-guides/getting-started/index.html +1388 -189
  815. nautobot/project-static/docs/user-guides/getting-started/interfaces.html +1388 -189
  816. nautobot/project-static/docs/user-guides/getting-started/ipam.html +1386 -187
  817. nautobot/project-static/docs/user-guides/getting-started/platforms.html +1448 -249
  818. nautobot/project-static/docs/user-guides/getting-started/regions.html +1411 -212
  819. nautobot/project-static/docs/user-guides/getting-started/search-bar.html +1395 -196
  820. nautobot/project-static/docs/user-guides/getting-started/tenants.html +1448 -249
  821. nautobot/project-static/docs/user-guides/getting-started/vlans-and-vlan-groups.html +1448 -249
  822. nautobot/project-static/docs/user-guides/git-data-source.html +1405 -206
  823. nautobot/project-static/docs/user-guides/graphql.html +1402 -203
  824. nautobot/project-static/docs/user-guides/relationships.html +1448 -249
  825. nautobot/project-static/docs/user-guides/s3-django-storage.html +1448 -249
  826. nautobot/project-static/js/forms.js +16 -9
  827. nautobot/project-static/js/theme.js +5 -0
  828. nautobot/tenancy/api/serializers.py +4 -34
  829. nautobot/tenancy/api/urls.py +1 -1
  830. nautobot/tenancy/filters/__init__.py +9 -7
  831. nautobot/tenancy/filters/mixins.py +3 -2
  832. nautobot/tenancy/forms.py +3 -36
  833. nautobot/tenancy/migrations/0001_initial.py +0 -1
  834. nautobot/tenancy/migrations/0002_auto_slug.py +0 -1
  835. nautobot/tenancy/migrations/0003_mptt_to_tree_queries.py +0 -1
  836. nautobot/tenancy/migrations/0004_change_tree_manager_on_tree_models.py +0 -1
  837. nautobot/tenancy/migrations/0005_rename_foreign_keys_and_related_names.py +0 -1
  838. nautobot/tenancy/migrations/0006_created_datetime.py +0 -1
  839. nautobot/tenancy/migrations/0007_remove_tenant_tenantgroup_slug.py +20 -0
  840. nautobot/tenancy/migrations/0008_tagsfield.py +19 -0
  841. nautobot/tenancy/models.py +0 -30
  842. nautobot/tenancy/navigation.py +6 -39
  843. nautobot/tenancy/tables.py +4 -4
  844. nautobot/tenancy/templates/tenancy/tenant.html +12 -12
  845. nautobot/tenancy/templates/tenancy/tenant_edit.html +0 -1
  846. nautobot/tenancy/templates/tenancy/tenantgroup.html +1 -1
  847. nautobot/tenancy/tests/test_api.py +1 -12
  848. nautobot/tenancy/tests/test_filters.py +20 -12
  849. nautobot/tenancy/tests/test_views.py +11 -29
  850. nautobot/tenancy/urls.py +10 -10
  851. nautobot/tenancy/views.py +0 -3
  852. nautobot/ui/.eslintignore +6 -0
  853. nautobot/ui/.gitignore +10 -0
  854. nautobot/ui/.prettierignore +9 -0
  855. nautobot/ui/.prettierrc +4 -0
  856. nautobot/ui/README.md +33 -0
  857. nautobot/ui/app_imports.js.j2 +7 -0
  858. nautobot/ui/craco.config.js +46 -0
  859. nautobot/ui/jsconfig-base.json +11 -0
  860. nautobot/ui/jsconfig.json +5 -0
  861. nautobot/ui/lib/nautobot-craco-alias-plugin.js +40 -0
  862. nautobot/ui/package-lock.json +21451 -0
  863. nautobot/ui/package.json +70 -0
  864. nautobot/ui/public/index.html +47 -0
  865. nautobot/ui/public/logo192.png +0 -0
  866. nautobot/ui/public/logo512.png +0 -0
  867. nautobot/ui/public/manifest.json +25 -0
  868. nautobot/ui/public/nautobot_logo.svg +131 -0
  869. nautobot/ui/public/robots.txt +3 -0
  870. nautobot/ui/src/App.js +71 -0
  871. nautobot/ui/src/components/AppFullWidthComponents.js +8 -0
  872. nautobot/ui/src/components/AppTab.js +40 -0
  873. nautobot/ui/src/components/Apps.js +60 -0
  874. nautobot/ui/src/components/HomeChangelogPanel.js +98 -0
  875. nautobot/ui/src/components/HomePanel.js +58 -0
  876. nautobot/ui/src/components/JobHistoryTable.js +78 -0
  877. nautobot/ui/src/components/Layout.js +53 -0
  878. nautobot/ui/src/components/LoadingWidget.js +25 -0
  879. nautobot/ui/src/components/Navbar.js +116 -0
  880. nautobot/ui/src/components/NotificationPopover.js +27 -0
  881. nautobot/ui/src/components/ObjectListTable.js +209 -0
  882. nautobot/ui/src/components/ReferenceDataTag.js +35 -0
  883. nautobot/ui/src/components/RouterButton.js +10 -0
  884. nautobot/ui/src/components/RouterLink.js +10 -0
  885. nautobot/ui/src/components/SidebarNav.js +147 -0
  886. nautobot/ui/src/components/Table.js +48 -0
  887. nautobot/ui/src/components/TableItem.js +71 -0
  888. nautobot/ui/src/components/__tests__/AppFullWidthComponents.test.js +16 -0
  889. nautobot/ui/src/components/__tests__/AppTab.test.js +21 -0
  890. nautobot/ui/src/components/__tests__/Apps.test.js +14 -0
  891. nautobot/ui/src/components/__tests__/Layout.test.js +33 -0
  892. nautobot/ui/src/components/__tests__/Table.test.js +36 -0
  893. nautobot/ui/src/components/__tests__/TableItem.test.js +37 -0
  894. nautobot/ui/src/components/__tests__/paginator.test.js +43 -0
  895. nautobot/ui/src/components/__tests__/paginator_form.test.js +13 -0
  896. nautobot/ui/src/components/pagination.js +93 -0
  897. nautobot/ui/src/components/paginator.js +79 -0
  898. nautobot/ui/src/components/paginator_form.js +43 -0
  899. nautobot/ui/src/components/usePagination.js +57 -0
  900. nautobot/ui/src/constants/apiPath.js +10 -0
  901. nautobot/ui/src/constants/icons.js +15 -0
  902. nautobot/ui/src/constants/size.js +15 -0
  903. nautobot/ui/src/index.js +65 -0
  904. nautobot/ui/src/reportWebVitals.js +15 -0
  905. nautobot/ui/src/router.js +77 -0
  906. nautobot/ui/src/utils/api.js +131 -0
  907. nautobot/ui/src/utils/app-import.js +15 -0
  908. nautobot/ui/src/utils/color.js +15 -0
  909. nautobot/ui/src/utils/date.js +14 -0
  910. nautobot/ui/src/utils/index.js +15 -0
  911. nautobot/ui/src/utils/navigation.js +32 -0
  912. nautobot/ui/src/utils/session.js +64 -0
  913. nautobot/ui/src/utils/store.js +242 -0
  914. nautobot/ui/src/utils/string.js +6 -0
  915. nautobot/ui/src/utils/url.js +4 -0
  916. nautobot/ui/src/views/Home.js +138 -0
  917. nautobot/ui/src/views/InstalledApps.js +80 -0
  918. nautobot/ui/src/views/Login.js +48 -0
  919. nautobot/ui/src/views/Logout.js +20 -0
  920. nautobot/ui/src/views/__tests__/BSCreateViewTemplate.test.js +11 -0
  921. nautobot/ui/src/views/__tests__/BSListViewTemplate.test.js +107 -0
  922. nautobot/ui/src/views/__tests__/Login.test.js +15 -0
  923. nautobot/ui/src/views/generic/GenericView.js +142 -0
  924. nautobot/ui/src/views/generic/ObjectCreate.js +96 -0
  925. nautobot/ui/src/views/generic/ObjectList.js +127 -0
  926. nautobot/ui/src/views/generic/ObjectRetrieve.js +551 -0
  927. nautobot/users/admin.py +1 -1
  928. nautobot/users/api/serializers.py +51 -61
  929. nautobot/users/api/urls.py +1 -1
  930. nautobot/users/api/views.py +53 -2
  931. nautobot/users/migrations/0001_initial.py +0 -1
  932. nautobot/users/migrations/0002_token_ordering_by_created.py +0 -1
  933. nautobot/users/migrations/0003_alter_user_options.py +0 -1
  934. nautobot/users/migrations/0004_alter_user_managers.py +0 -1
  935. nautobot/users/tests/test_api.py +109 -28
  936. nautobot/users/tests/test_filters.py +0 -4
  937. nautobot/users/tests/test_models.py +0 -1
  938. nautobot/users/views.py +0 -7
  939. nautobot/virtualization/api/serializers.py +18 -132
  940. nautobot/virtualization/api/urls.py +1 -1
  941. nautobot/virtualization/api/views.py +1 -22
  942. nautobot/virtualization/choices.py +0 -2
  943. nautobot/virtualization/filters.py +12 -7
  944. nautobot/virtualization/forms.py +21 -117
  945. nautobot/virtualization/migrations/0001_initial.py +0 -1
  946. nautobot/virtualization/migrations/0002_virtualmachine_local_context_schema.py +0 -1
  947. nautobot/virtualization/migrations/0003_vminterface_verbose_name.py +0 -1
  948. nautobot/virtualization/migrations/0004_auto_slug.py +0 -1
  949. nautobot/virtualization/migrations/0005_add_natural_indexing.py +0 -1
  950. nautobot/virtualization/migrations/0006_vminterface_status.py +0 -1
  951. nautobot/virtualization/migrations/0007_vminterface_status_data_migration.py +0 -1
  952. nautobot/virtualization/migrations/0008_vminterface_parent.py +0 -1
  953. nautobot/virtualization/migrations/0009_cluster_location.py +0 -1
  954. nautobot/virtualization/migrations/0010_vminterface_mac_address_data_migration.py +0 -1
  955. nautobot/virtualization/migrations/0011_alter_vminterface_mac_address.py +0 -1
  956. nautobot/virtualization/migrations/0012_alter_virtualmachine_role_add_new_role.py +1 -2
  957. nautobot/virtualization/migrations/0013_migrate_virtualmachine_role_data.py +18 -12
  958. nautobot/virtualization/migrations/0014_rename_virtualmachine_roles.py +0 -1
  959. nautobot/virtualization/migrations/0015_rename_foreignkey_fields.py +1 -2
  960. nautobot/virtualization/migrations/0016_remove_site_foreign_key_from_cluster_class.py +0 -1
  961. nautobot/virtualization/migrations/0017_created_datetime.py +0 -1
  962. nautobot/virtualization/migrations/0018_related_name_changes.py +1 -2
  963. nautobot/virtualization/migrations/0019_vminterface_ip_addresses_m2m.py +0 -1
  964. nautobot/virtualization/migrations/0020_remove_clustergroup_clustertype_slug.py +20 -0
  965. nautobot/virtualization/migrations/0021_tagsfield_and_vminterface_to_primarymodel.py +39 -0
  966. nautobot/virtualization/migrations/0022_vminterface_timestamps_data_migration.py +17 -0
  967. nautobot/virtualization/migrations/0023_ipam__namespaces.py +25 -0
  968. nautobot/virtualization/migrations/0024_fixup_null_statuses.py +25 -0
  969. nautobot/virtualization/migrations/0025_status_nonnullable.py +29 -0
  970. nautobot/virtualization/models.py +39 -131
  971. nautobot/virtualization/navigation.py +18 -99
  972. nautobot/virtualization/tables.py +4 -4
  973. nautobot/virtualization/templates/virtualization/virtualmachine.html +13 -2
  974. nautobot/virtualization/templates/virtualization/virtualmachine_edit.html +6 -0
  975. nautobot/virtualization/tests/test_api.py +42 -52
  976. nautobot/virtualization/tests/test_filters.py +98 -75
  977. nautobot/virtualization/tests/test_models.py +36 -13
  978. nautobot/virtualization/tests/test_views.py +68 -73
  979. nautobot/virtualization/urls.py +10 -10
  980. nautobot/virtualization/views.py +8 -14
  981. {nautobot-2.0.0a2.dist-info → nautobot-2.0.0b1.dist-info}/METADATA +15 -22
  982. {nautobot-2.0.0a2.dist-info → nautobot-2.0.0b1.dist-info}/RECORD +987 -834
  983. {nautobot-2.0.0a2.dist-info → nautobot-2.0.0b1.dist-info}/WHEEL +1 -1
  984. nautobot/circuits/api/nested_serializers.py +0 -69
  985. nautobot/core/templates/plugin_template/navigation.py-tpl +0 -22
  986. nautobot/dcim/api/nested_serializers.py +0 -356
  987. nautobot/dcim/templates/dcim/device_import.html +0 -5
  988. nautobot/dcim/templates/dcim/device_import_child.html +0 -5
  989. nautobot/dcim/templates/dcim/inc/device_import_header.html +0 -4
  990. nautobot/extras/api/nested_serializers.py +0 -353
  991. nautobot/extras/migrations/0064_configcontext_data_migrations.py +0 -42
  992. nautobot/extras/migrations/0071_job__unique_name_data_migration.py +0 -47
  993. nautobot/extras/reports.py +0 -60
  994. nautobot/extras/scripts.py +0 -72
  995. nautobot/extras/tests/example_jobs/script_variables.py +0 -67
  996. nautobot/extras/tests/example_jobs/test_duplicate_name2.py +0 -5
  997. nautobot/extras/tests/example_jobs/test_fail.py +0 -16
  998. nautobot/extras/tests/example_jobs/test_file_upload_pass.py +0 -20
  999. nautobot/extras/tests/example_jobs/test_ipaddress_vars.py +0 -52
  1000. nautobot/extras/tests/example_jobs/test_job_button_receiver.py +0 -21
  1001. nautobot/extras/tests/example_jobs/test_job_hook_receiver.py +0 -20
  1002. nautobot/extras/tests/example_jobs/test_location_with_custom_field.py +0 -35
  1003. nautobot/extras/tests/example_jobs/test_log_redaction.py +0 -14
  1004. nautobot/extras/tests/example_jobs/test_modify_db.py +0 -19
  1005. nautobot/extras/tests/example_jobs/test_object_var_optional.py +0 -14
  1006. nautobot/extras/tests/example_jobs/test_object_var_required.py +0 -14
  1007. nautobot/extras/tests/example_jobs/test_object_vars.py +0 -29
  1008. nautobot/extras/tests/example_jobs/test_pass.py +0 -19
  1009. nautobot/extras/tests/example_jobs/test_read_only_fail.py +0 -24
  1010. nautobot/extras/tests/example_jobs/test_read_only_no_commit_field.py +0 -10
  1011. nautobot/extras/tests/example_jobs/test_read_only_pass.py +0 -22
  1012. nautobot/ipam/api/nested_serializers.py +0 -143
  1013. nautobot/project-static/docs/assets/javascripts/bundle.5a2dcb6a.min.js +0 -29
  1014. nautobot/project-static/docs/assets/javascripts/bundle.5a2dcb6a.min.js.map +0 -8
  1015. nautobot/project-static/docs/assets/javascripts/extra/bundle.5f09fbc3.min.js +0 -18
  1016. nautobot/project-static/docs/assets/javascripts/extra/bundle.5f09fbc3.min.js.map +0 -8
  1017. nautobot/project-static/docs/assets/stylesheets/extra.0d2c79a8.min.css +0 -1
  1018. nautobot/project-static/docs/assets/stylesheets/extra.0d2c79a8.min.css.map +0 -1
  1019. nautobot/project-static/docs/assets/stylesheets/main.975780f9.min.css +0 -1
  1020. nautobot/project-static/docs/assets/stylesheets/main.975780f9.min.css.map +0 -1
  1021. nautobot/project-static/docs/assets/stylesheets/palette.2505c338.min.css +0 -1
  1022. nautobot/project-static/docs/assets/stylesheets/palette.2505c338.min.css.map +0 -1
  1023. nautobot/tenancy/api/nested_serializers.py +0 -31
  1024. nautobot/users/api/nested_serializers.py +0 -67
  1025. nautobot/virtualization/api/nested_serializers.py +0 -65
  1026. /nautobot/extras/{tests/example_jobs → test_jobs}/__init__.py +0 -0
  1027. /nautobot/{dcim/models/sites.py → ipam/management/__init__.py} +0 -0
  1028. {nautobot-2.0.0a2.dist-info → nautobot-2.0.0b1.dist-info}/LICENSE.txt +0 -0
  1029. {nautobot-2.0.0a2.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):
@@ -632,7 +804,6 @@ class ScriptVariable:
632
804
  form_field = forms.CharField
633
805
 
634
806
  def __init__(self, label="", description="", default=None, required=True, widget=None):
635
-
636
807
  # Initialize field attributes
637
808
  if not hasattr(self, "field_attrs"):
638
809
  self.field_attrs = {}
@@ -729,6 +900,23 @@ class BooleanVar(ScriptVariable):
729
900
  self.field_attrs["required"] = False
730
901
 
731
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
+
732
920
  class ChoiceVar(ScriptVariable):
733
921
  """
734
922
  Select one of several predefined static choices, passed as a list of two-tuples. Example:
@@ -867,9 +1055,8 @@ class JobHookReceiver(Job):
867
1055
 
868
1056
  object_change = ObjectVar(model=ObjectChange)
869
1057
 
870
- def run(self, data, commit):
1058
+ def run(self, object_change):
871
1059
  """JobHookReceiver subclasses generally shouldn't need to override this method."""
872
- object_change = data["object_change"]
873
1060
  self.receive_job_hook(
874
1061
  change=object_change,
875
1062
  action=object_change.action,
@@ -896,11 +1083,8 @@ class JobButtonReceiver(Job):
896
1083
  object_pk = StringVar()
897
1084
  object_model_name = StringVar()
898
1085
 
899
- def run(self, data, commit):
1086
+ def run(self, object_pk, object_model_name):
900
1087
  """JobButtonReceiver subclasses generally shouldn't need to override this method."""
901
- object_pk = data["object_pk"]
902
- object_model_name = data["object_model_name"]
903
-
904
1088
  model = get_model_from_name(object_model_name)
905
1089
  obj = model.objects.get(pk=object_pk)
906
1090
 
@@ -919,11 +1103,8 @@ def is_job(obj):
919
1103
  """
920
1104
  Returns True if the given object is a Job subclass.
921
1105
  """
922
- from .scripts import Script, BaseScript
923
- from .reports import Report
924
-
925
1106
  try:
926
- 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]
927
1108
  except TypeError:
928
1109
  return False
929
1110
 
@@ -935,372 +1116,23 @@ def is_variable(obj):
935
1116
  return isinstance(obj, ScriptVariable)
936
1117
 
937
1118
 
938
- def get_jobs():
939
- """
940
- Compile a dictionary of all jobs available across all modules in the jobs path(s).
941
-
942
- Returns an OrderedDict:
943
-
944
- {
945
- "local": {
946
- <module_name>: {
947
- "name": <human-readable module name>,
948
- "jobs": {
949
- <class_name>: <job_class>,
950
- <class_name>: <job_class>,
951
- ...
952
- },
953
- },
954
- <module_name>: { ... },
955
- ...
956
- },
957
- "git.<repository-slug>": {
958
- <module_name>: { ... },
959
- },
960
- ...
961
- "plugins": {
962
- <module_name>: { ... },
963
- }
964
- }
965
- """
966
- jobs = OrderedDict()
967
-
968
- paths = _get_job_source_paths()
969
-
970
- # Iterate over all filesystem sources (local, git.<slug1>, git.<slug2>, etc.)
971
- for source, path in paths.items():
972
- for job_info in jobs_in_directory(path):
973
- jobs.setdefault(source, {})
974
- if job_info.module_name not in jobs[source]:
975
- jobs[source][job_info.module_name] = {"name": job_info.job_class.grouping, "jobs": OrderedDict()}
976
- jobs[source][job_info.module_name]["jobs"][job_info.job_class_name] = job_info.job_class
977
-
978
- # Add jobs from plugins (which were already imported at startup)
979
- for cls in registry["plugin_jobs"]:
980
- module = inspect.getmodule(cls)
981
- jobs.setdefault("plugins", {}).setdefault(module.__name__, {"name": cls.grouping, "jobs": OrderedDict()})
982
- jobs["plugins"][module.__name__]["jobs"][cls.__name__] = cls
983
-
984
- return jobs
985
-
986
-
987
- def _get_job_source_paths():
988
- """
989
- Helper function to get_jobs().
990
-
991
- Constructs a dict of {"grouping": filesystem_path, ...}.
992
- Current groupings are "local", "git.<repository_slug>".
993
- Plugin jobs aren't loaded dynamically from a source_path and so are not included in this function
994
- """
995
- paths = {}
996
- # Locally installed jobs
997
- if settings.JOBS_ROOT and os.path.exists(settings.JOBS_ROOT):
998
- paths["local"] = settings.JOBS_ROOT
999
-
1000
- # Jobs derived from Git repositories
1001
- if settings.GIT_ROOT and os.path.isdir(settings.GIT_ROOT):
1002
- for repository_record in GitRepository.objects.all():
1003
- if "extras.job" not in repository_record.provided_contents:
1004
- # This repository isn't marked as containing jobs that we should use.
1005
- continue
1006
-
1007
- try:
1008
- # In the case where we have multiple Nautobot instances, or multiple worker instances,
1009
- # they are not required to share a common filesystem; therefore, we may need to refresh our local clone
1010
- # of the Git repository to ensure that it is in sync with the latest repository clone from any instance.
1011
- ensure_git_repository(
1012
- repository_record,
1013
- head=repository_record.current_head,
1014
- logger=logger,
1015
- )
1016
- except Exception as exc:
1017
- logger.error(f"Error during local clone of Git repository {repository_record}: {exc}")
1018
- continue
1019
-
1020
- jobs_path = os.path.join(repository_record.filesystem_path, "jobs")
1021
- if os.path.isdir(jobs_path):
1022
- paths[f"git.{repository_record.slug}"] = jobs_path
1023
- else:
1024
- logger.warning(f"Git repository {repository_record} is configured to provide jobs, but none are found!")
1025
-
1026
- # TODO(Glenn): when a Git repo is deleted or its slug is changed, we update the local filesystem
1027
- # (see extras/signals.py, extras/models/datasources.py), but as noted above, there may be multiple filesystems
1028
- # involved, so not all local clones of deleted Git repositories may have been deleted yet.
1029
- # For now, if we encounter a "leftover" Git repo here, we delete it now.
1030
- for git_slug in os.listdir(settings.GIT_ROOT):
1031
- git_path = os.path.join(settings.GIT_ROOT, git_slug)
1032
- if not os.path.isdir(git_path):
1033
- logger.warning(
1034
- f"Found non-directory {git_slug} in {settings.GIT_ROOT}. Only Git repositories should exist here."
1035
- )
1036
- elif not os.path.isdir(os.path.join(git_path, ".git")):
1037
- logger.warning(f"Directory {git_slug} in {settings.GIT_ROOT} does not appear to be a Git repository.")
1038
- elif not GitRepository.objects.filter(slug=git_slug):
1039
- logger.warning(f"Deleting unmanaged (leftover?) repository at {git_path}")
1040
- shutil.rmtree(git_path)
1041
-
1042
- return paths
1043
-
1044
-
1045
- def get_job_classpaths():
1046
- """
1047
- Get a list of all known Job class_path strings.
1048
-
1049
- This is used as a cacheable, light-weight alternative to calling get_jobs() or get_job()
1050
- when all that's needed is to verify whether a given job exists.
1051
- """
1052
- jobs_dict = get_jobs()
1053
- result = set()
1054
- for grouping_name, modules_dict in jobs_dict.items():
1055
- for module_name in modules_dict:
1056
- for class_name in modules_dict[module_name]["jobs"]:
1057
- result.add(f"{grouping_name}/{module_name}/{class_name}")
1058
- return result
1059
-
1060
-
1061
1119
  def get_job(class_path):
1062
1120
  """
1063
- Retrieve a specific job class by its class_path.
1121
+ Retrieve a specific job class by its class_path (<module_name>.<JobClassName>).
1064
1122
 
1065
- Note that this is built atop get_jobs() and so is not a particularly light-weight API;
1066
- if all you need to do is to verify whether a given class_path exists, use get_job_classpaths() instead.
1067
-
1068
- Returns None if not found.
1123
+ May return None if the job isn't properly registered with Celery at this time.
1069
1124
  """
1070
1125
  try:
1071
- grouping_name, module_name, class_name = class_path.split("/", 2)
1072
- except ValueError:
1073
- logger.error(f'Invalid class_path value "{class_path}"')
1126
+ return celery_app.tasks[class_path].__class__
1127
+ except NotRegistered:
1074
1128
  return None
1075
1129
 
1076
- jobs = get_jobs()
1077
- return jobs.get(grouping_name, {}).get(module_name, {}).get("jobs", {}).get(class_name, None)
1078
-
1079
-
1080
- class RunJobTaskFailed(Exception):
1081
- """Celery task failed for some reason."""
1082
-
1083
-
1084
- @nautobot_task
1085
- def run_job(data, request, job_result_pk, commit=True, *args, **kwargs):
1086
- """
1087
- Helper function to call the "run()", "test_*()", and "post_run" methods on a Job.
1088
-
1089
- This function is responsible for setting up the job execution, handing the DB tranaction
1090
- and rollback conditions, plus post execution cleanup and saving the JobResult record.
1091
- """
1092
- from nautobot.extras.models import JobResult # avoid circular import
1093
-
1094
- # Getting the correct job result can fail if the stored data cannot be serialized.
1095
- # Catching `TypeError: the JSON object must be str, bytes or bytearray, not int`
1096
- job_result = JobResult.objects.get(pk=job_result_pk)
1097
-
1098
- job_model = job_result.job_model
1099
- initialization_failure = None
1100
- job_model = JobModel.objects.get_for_class_path(job_result.name)
1101
-
1102
- if not job_model.enabled:
1103
- initialization_failure = f"Job {job_model} is not enabled to be run!"
1104
- else:
1105
- job_class = job_model.job_class
1106
-
1107
- if not job_model.installed or not job_class:
1108
- initialization_failure = f'Unable to locate job "{job_result.name}" to run it!'
1109
-
1110
- if initialization_failure:
1111
- job_result.log(
1112
- message=initialization_failure,
1113
- obj=job_model,
1114
- level_choice=LogLevelChoices.LOG_FAILURE,
1115
- grouping="initialization",
1116
- logger=logger,
1117
- )
1118
- raise RunJobTaskFailed(initialization_failure)
1119
-
1120
- job = job_class()
1121
- job.active_test = "initialization"
1122
- job.job_result = job_result
1123
-
1124
- soft_time_limit = job_model.soft_time_limit or settings.CELERY_TASK_SOFT_TIME_LIMIT
1125
- time_limit = job_model.time_limit or settings.CELERY_TASK_TIME_LIMIT
1126
- if time_limit <= soft_time_limit:
1127
- job_result.log(
1128
- f"The hard time limit of {time_limit} seconds is less than "
1129
- f"or equal to the soft time limit of {soft_time_limit} seconds. "
1130
- f"This job will fail silently after {time_limit} seconds.",
1131
- level_choice=LogLevelChoices.LOG_WARNING,
1132
- grouping="initialization",
1133
- logger=logger,
1134
- )
1135
-
1136
- file_ids = None
1137
- try:
1138
- # Capture the file IDs for any FileProxy objects created so we can cleanup later.
1139
- file_fields = list(job._get_file_vars())
1140
- file_ids = [data[f] for f in file_fields]
1141
-
1142
- # Attempt to resolve serialized data back into original form by creating querysets or model instances
1143
- # If we fail to find any objects, we consider this a job execution error, and fail.
1144
- # This might happen when a job sits on the queue for a while (i.e. scheduled) and data has changed
1145
- # or it might be bad input from an API request, or manual execution.
1146
-
1147
- data = job_class.deserialize_data(data)
1148
- # TODO(jathan): Another place where because `log()` is called which mutates `.data`, we must
1149
- # explicitly call `save()` again. We need to see if we can move more of this to `NauotbotTask`
1150
- # and/or the DB backend as well.
1151
- except Exception:
1152
- stacktrace = traceback.format_exc()
1153
- job_result.log(
1154
- f"Error initializing job:\n```\n{stacktrace}\n```",
1155
- level_choice=LogLevelChoices.LOG_FAILURE,
1156
- grouping="initialization",
1157
- logger=logger,
1158
- )
1159
- job_result.save()
1160
- if file_ids:
1161
- # Cleanup FileProxy objects
1162
- job.delete_files(*file_ids) # pylint: disable=not-an-iterable
1163
- raise
1164
-
1165
- if job_model.read_only:
1166
- # Force commit to false for read only jobs.
1167
- commit = False
1168
-
1169
- # TODO(Glenn): validate that all args required by this job are set in the data or else log helpful errors?
1170
-
1171
- job.logger.info(f"Running job (commit={commit})")
1172
-
1173
- # Add the current request as a property of the job
1174
- job.request = request
1175
-
1176
- def _run_job():
1177
- """
1178
- Core job execution task.
1179
-
1180
- We capture this within a subfunction to allow for conditionally wrapping it with the change_logging
1181
- context manager (which is only relevant if commit == True).
1182
-
1183
- If the job is marked as read_only == True, then commit is forced to False and no log messages will be
1184
- emitted related to reverting database changes.
1185
- """
1186
- started = timezone.now()
1187
- job.results["output"] = ""
1188
- try:
1189
- with transaction.atomic():
1190
- # Script-like behavior
1191
- job.active_test = "run"
1192
- output = job.run(data=data, commit=commit)
1193
- if output:
1194
- job.results["output"] += "\n" + str(output)
1195
-
1196
- # Report-like behavior
1197
- for method_name in job.test_methods:
1198
- job.active_test = method_name
1199
- output = getattr(job, method_name)()
1200
- if output:
1201
- job.results["output"] += "\n" + str(output)
1202
-
1203
- job.logger.info("job completed successfully")
1204
-
1205
- if not commit:
1206
- raise AbortTransaction("Database changes have been reverted automatically.")
1207
-
1208
- except AbortTransaction:
1209
- if not job_model.read_only:
1210
- job.log_info(message="Database changes have been reverted automatically.")
1211
-
1212
- except Exception:
1213
- if not job_model.read_only:
1214
- job.log_info(message="Database changes have been reverted due to error.")
1215
- raise
1216
-
1217
- # TODO(jathan): For now we still need to call `save()` so that any output data from the job
1218
- # that was stored gets saved to the `JobResult`. We need to consider where this should be
1219
- # moved as we get closer to eliminating `run_job()` entirely. Hint: Probably inside of
1220
- # `NautobotTask` class.
1221
- finally:
1222
- _data = copy.deepcopy(job_result.data)
1223
- job_result.refresh_from_db()
1224
- job_result.data = _data
1225
- try:
1226
- job_result.save()
1227
- except IntegrityError:
1228
- # handle job_model deleted while job was running
1229
- job_result.job_model = None
1230
- job_result.save()
1231
- if file_ids:
1232
- job.delete_files(*file_ids) # Cleanup FileProxy objects
1233
-
1234
- # TODO(jathan): Pretty sure this can also be handled by the backend, but
1235
- # leaving it for now.
1236
- # record data about this jobrun in the scheduled_job
1237
- if job_result.scheduled_job:
1238
- job_result.scheduled_job.total_run_count += 1
1239
- job_result.scheduled_job.last_run_at = started
1240
- job_result.scheduled_job.save()
1241
-
1242
- # Perform any post-run tasks
1243
- # 2.0 TODO Remove post_run() method entirely
1244
- job.active_test = "post_run"
1245
- output = job.post_run()
1246
- # TODO(jathan): We need to call `save()` here too so that any appended output from
1247
- # `post_run` gets stored on the `JobResult`. We need to move this out of here as well.
1248
- if output:
1249
- job.results["output"] += "\n" + str(output)
1250
- job_result.save()
1251
-
1252
- job_result.refresh_from_db()
1253
- job.logger.info(f"Job completed in {job_result.duration}")
1254
-
1255
- # TODO(jathan): For now this is only output from `post_run()` which is not straightforward.
1256
- # We need to think about what we want to be returned from job runs and stored as
1257
- # `JobResult.result`, otherwise it will always be `None`.
1258
- return output
1259
-
1260
- # Execute the job. If commit == True, wrap it with the change_logging context manager to ensure we
1261
- # process change logs, webhooks, etc.
1262
- if commit:
1263
- context_class = JobHookChangeContext if job_model.is_job_hook_receiver else JobChangeContext
1264
- change_context = context_class(user=request.user, context_detail=job_model.slug)
1265
- with change_logging(change_context):
1266
- output = _run_job()
1267
- else:
1268
- output = _run_job()
1269
-
1270
- # This is just passing through the return value from `post_run()` which for now will always be
1271
- # `None` (see above).
1272
- return output
1273
-
1274
-
1275
- @nautobot_task
1276
- def scheduled_job_handler(*args, **kwargs):
1277
- """
1278
- A thin wrapper around JobResult.enqueue_job() that allows for it to be called as an async task
1279
- for the purposes of enqueuing scheduled jobs at their recurring intervals. Thus, JobResult.enqueue_job()
1280
- is responsible for enqueuing the actual job for execution and this method is the task executed
1281
- by the scheduler to kick off the job execution on a recurring interval.
1282
- """
1283
- from nautobot.extras.models import JobResult # avoid circular import
1284
-
1285
- user_pk = kwargs.pop("user")
1286
- user = User.objects.get(pk=user_pk)
1287
- name = kwargs.pop("name")
1288
- scheduled_job_pk = kwargs.pop("scheduled_job_pk")
1289
- celery_kwargs = kwargs.pop("celery_kwargs", {})
1290
- schedule = ScheduledJob.objects.get(pk=scheduled_job_pk)
1291
-
1292
- job_content_type = get_job_content_type()
1293
- JobResult.enqueue_job(
1294
- run_job, name, job_content_type, user, celery_kwargs=celery_kwargs, schedule=schedule, **kwargs
1295
- )
1296
-
1297
1130
 
1298
1131
  def enqueue_job_hooks(object_change):
1299
1132
  """
1300
1133
  Find job hook(s) assigned to this changed object type + action and enqueue them
1301
1134
  to be processed
1302
1135
  """
1303
- from nautobot.extras.models import JobResult # avoid circular import
1304
1136
 
1305
1137
  # Job hooks cannot trigger other job hooks
1306
1138
  if object_change.change_context == ObjectChangeEventContextChoices.CONTEXT_JOB_HOOK:
@@ -1322,16 +1154,5 @@ def enqueue_job_hooks(object_change):
1322
1154
 
1323
1155
  # Enqueue the jobs related to the job_hooks
1324
1156
  for job_hook in job_hooks:
1325
- job_content_type = get_job_content_type()
1326
1157
  job_model = job_hook.job
1327
- request = RequestFactory().request(SERVER_NAME="job_hook")
1328
- request.user = object_change.user
1329
- JobResult.enqueue_job(
1330
- run_job,
1331
- job_model.class_path,
1332
- job_content_type,
1333
- object_change.user,
1334
- data=job_model.job_class.serialize_data({"object_change": object_change}),
1335
- request=copy_safe_request(request),
1336
- commit=True,
1337
- )
1158
+ JobResult.enqueue_job(job_model, object_change.user, object_change=object_change.pk)