nautobot 2.4.3__py3-none-any.whl → 2.4.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of nautobot might be problematic. Click here for more details.
- nautobot/__init__.py +19 -3
- nautobot/apps/filters.py +2 -0
- nautobot/circuits/filters.py +1 -1
- nautobot/circuits/tests/test_models.py +5 -3
- nautobot/cloud/filters.py +3 -6
- nautobot/cloud/tests/test_filters.py +21 -0
- nautobot/core/admin.py +2 -0
- nautobot/core/celery/__init__.py +5 -3
- nautobot/core/jobs/__init__.py +5 -3
- nautobot/core/management/commands/generate_performance_test_endpoints.py +9 -6
- nautobot/core/models/utils.py +6 -1
- nautobot/core/templates/inc/javascript.html +1 -0
- nautobot/core/templatetags/ui_framework.py +20 -4
- nautobot/core/testing/__init__.py +2 -0
- nautobot/core/testing/forms.py +1 -1
- nautobot/core/testing/mixins.py +9 -0
- nautobot/core/tests/test_api.py +1 -1
- nautobot/core/tests/test_graphql.py +3 -3
- nautobot/core/tests/test_jobs.py +30 -28
- nautobot/core/ui/object_detail.py +1 -1
- nautobot/dcim/api/serializers.py +36 -0
- nautobot/dcim/api/views.py +1 -1
- nautobot/dcim/elevations.py +17 -4
- nautobot/dcim/factory.py +9 -1
- nautobot/dcim/filters/__init__.py +27 -1
- nautobot/dcim/forms.py +13 -1
- nautobot/dcim/models/devices.py +11 -5
- nautobot/dcim/signals.py +26 -0
- nautobot/dcim/templates/dcim/virtualdevicecontext_retrieve.html +0 -62
- nautobot/dcim/templates/dcim/virtualdevicecontext_update.html +6 -0
- nautobot/dcim/tests/test_api.py +176 -0
- nautobot/dcim/tests/test_filters.py +56 -3
- nautobot/dcim/tests/test_jobs.py +4 -6
- nautobot/dcim/tests/test_models.py +40 -0
- nautobot/dcim/views.py +24 -14
- nautobot/extras/api/mixins.py +1 -1
- nautobot/extras/api/views.py +2 -2
- nautobot/extras/choices.py +8 -3
- nautobot/extras/filters/__init__.py +4 -0
- nautobot/extras/jobs.py +181 -103
- nautobot/extras/management/utils.py +13 -2
- nautobot/extras/models/datasources.py +11 -4
- nautobot/extras/models/jobs.py +20 -17
- nautobot/extras/plugins/__init__.py +26 -1
- nautobot/extras/tables.py +25 -29
- nautobot/extras/templates/extras/inc/jobresult.html +12 -13
- nautobot/extras/templates/extras/objectchange.html +28 -12
- nautobot/extras/test_jobs/atomic_transaction.py +6 -6
- nautobot/extras/test_jobs/fail.py +75 -1
- nautobot/extras/tests/test_api.py +17 -16
- nautobot/extras/tests/test_datasources.py +64 -54
- nautobot/extras/tests/test_filters.py +2 -0
- nautobot/extras/tests/test_jobs.py +69 -62
- nautobot/extras/tests/test_models.py +1 -1
- nautobot/extras/tests/test_plugins.py +32 -1
- nautobot/extras/tests/test_relationships.py +5 -5
- nautobot/extras/tests/test_views.py +12 -2
- nautobot/extras/views.py +10 -1
- nautobot/ipam/api/serializers.py +7 -8
- nautobot/ipam/api/views.py +2 -2
- nautobot/ipam/factory.py +27 -8
- nautobot/ipam/filters.py +67 -29
- nautobot/ipam/formfields.py +51 -0
- nautobot/ipam/forms.py +28 -1
- nautobot/ipam/migrations/0051_added_optional_vrf_relationship_to_vdc.py +41 -0
- nautobot/ipam/models.py +63 -5
- nautobot/ipam/querysets.py +6 -0
- nautobot/ipam/tables.py +21 -7
- nautobot/ipam/templates/ipam/rir.html +1 -43
- nautobot/ipam/tests/test_api.py +107 -66
- nautobot/ipam/tests/test_filters.py +145 -5
- nautobot/ipam/tests/test_models.py +16 -0
- nautobot/ipam/tests/test_views.py +15 -2
- nautobot/ipam/urls.py +1 -21
- nautobot/ipam/views.py +24 -41
- nautobot/project-static/css/base.css +11 -0
- nautobot/project-static/css/dark.css +2 -1
- nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +62 -0
- nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +43 -5
- nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +35 -0
- nautobot/project-static/docs/development/apps/api/configuration-view.html +0 -3
- nautobot/project-static/docs/development/apps/api/models/graphql.html +0 -4
- nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +94 -1
- nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +0 -3
- nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +0 -3
- nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +0 -3
- nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +0 -3
- nautobot/project-static/docs/development/apps/api/prometheus.html +0 -3
- nautobot/project-static/docs/development/apps/api/testing.html +0 -6
- nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +0 -3
- nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +0 -3
- nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +0 -3
- nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +0 -3
- nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +1 -7
- nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +0 -7
- nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +0 -4
- nautobot/project-static/docs/development/apps/api/views/notes.html +0 -3
- nautobot/project-static/docs/development/apps/index.html +2 -35
- nautobot/project-static/docs/development/apps/migration/code-updates.html +1 -1
- nautobot/project-static/docs/development/core/application-registry.html +0 -6
- nautobot/project-static/docs/development/core/best-practices.html +0 -27
- nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +58 -4
- nautobot/project-static/docs/development/core/getting-started.html +12 -16
- nautobot/project-static/docs/development/core/homepage.html +0 -3
- nautobot/project-static/docs/development/core/style-guide.html +0 -5
- nautobot/project-static/docs/development/core/templates.html +0 -3
- nautobot/project-static/docs/development/core/testing.html +0 -9
- nautobot/project-static/docs/development/jobs/index.html +30 -43
- nautobot/project-static/docs/objects.inv +0 -0
- nautobot/project-static/docs/overview/application_stack.html +0 -18
- nautobot/project-static/docs/release-notes/version-2.4.html +374 -0
- nautobot/project-static/docs/requirements.txt +2 -2
- nautobot/project-static/docs/search/search_index.json +1 -1
- nautobot/project-static/docs/sitemap.xml +290 -290
- nautobot/project-static/docs/sitemap.xml.gz +0 -0
- nautobot/project-static/docs/user-guide/administration/configuration/settings.html +0 -10
- nautobot/project-static/docs/user-guide/administration/guides/docker.html +0 -15
- nautobot/project-static/docs/user-guide/administration/installation/index.html +0 -16
- nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +1 -4
- nautobot/project-static/docs/user-guide/administration/installation/services.html +0 -11
- nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +3 -3
- nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +5 -35
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/tables/v2-code-location-changes.yaml +1 -1
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +1 -1
- nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +0 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +0 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +1 -17
- nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +0 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +0 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +1 -7
- nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +0 -6
- nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +0 -3
- nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +0 -8
- nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +3 -3
- nautobot/project-static/docs/user-guide/feature-guides/graphql.html +0 -6
- nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +0 -3
- nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +3 -15
- nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +0 -26
- nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +0 -8
- nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +0 -3
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +0 -8
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +0 -7
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +0 -3
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +0 -3
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +0 -14
- nautobot/project-static/docs/user-guide/platform-functionality/note.html +0 -3
- nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +1 -10
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +0 -3
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +0 -14
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +0 -19
- nautobot/project-static/docs/user-guide/platform-functionality/secret.html +3 -9
- nautobot/project-static/docs/user-guide/platform-functionality/status.html +0 -8
- nautobot/project-static/docs/user-guide/platform-functionality/tag.html +0 -4
- nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +1 -13
- nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +0 -5
- nautobot/project-static/js/editor.js +292 -0
- nautobot/project-static/monaco-editor-0.52.2/README.md +81 -0
- nautobot/project-static/monaco-editor-0.52.2/vs/base/browser/ui/codicons/codicon/codicon.ttf +0 -0
- nautobot/project-static/monaco-editor-0.52.2/vs/base/worker/workerMain.js +31 -0
- nautobot/project-static/monaco-editor-0.52.2/vs/basic-languages/xml/xml.js +10 -0
- nautobot/project-static/monaco-editor-0.52.2/vs/basic-languages/yaml/yaml.js +10 -0
- nautobot/project-static/monaco-editor-0.52.2/vs/editor/editor.main.css +8 -0
- nautobot/project-static/monaco-editor-0.52.2/vs/editor/editor.main.js +798 -0
- nautobot/project-static/monaco-editor-0.52.2/vs/language/json/jsonMode.js +19 -0
- nautobot/project-static/monaco-editor-0.52.2/vs/language/json/jsonWorker.js +42 -0
- nautobot/project-static/monaco-editor-0.52.2/vs/loader.js +11 -0
- nautobot/tenancy/filters/__init__.py +3 -5
- nautobot/tenancy/tests/test_filters.py +10 -0
- nautobot/virtualization/views.py +0 -1
- nautobot/wireless/tables.py +9 -4
- nautobot/wireless/tests/test_api.py +0 -9
- {nautobot-2.4.3.dist-info → nautobot-2.4.5.dist-info}/METADATA +4 -4
- {nautobot-2.4.3.dist-info → nautobot-2.4.5.dist-info}/RECORD +198 -186
- {nautobot-2.4.3.dist-info → nautobot-2.4.5.dist-info}/LICENSE.txt +0 -0
- {nautobot-2.4.3.dist-info → nautobot-2.4.5.dist-info}/NOTICE +0 -0
- {nautobot-2.4.3.dist-info → nautobot-2.4.5.dist-info}/WHEEL +0 -0
- {nautobot-2.4.3.dist-info → nautobot-2.4.5.dist-info}/entry_points.txt +0 -0
nautobot/extras/jobs.py
CHANGED
|
@@ -13,13 +13,13 @@ from typing import final
|
|
|
13
13
|
import warnings
|
|
14
14
|
|
|
15
15
|
from billiard.einfo import ExceptionInfo
|
|
16
|
+
from celery.exceptions import Ignore, Reject
|
|
16
17
|
from celery.utils.log import get_task_logger
|
|
17
18
|
from db_file_storage.form_widgets import DBClearableFileInput
|
|
18
19
|
from django import forms
|
|
19
20
|
from django.conf import settings
|
|
20
21
|
from django.contrib.auth import get_user_model
|
|
21
22
|
from django.core.cache import cache
|
|
22
|
-
from django.core.exceptions import ObjectDoesNotExist
|
|
23
23
|
from django.core.files.base import ContentFile
|
|
24
24
|
from django.core.files.uploadedfile import UploadedFile
|
|
25
25
|
from django.core.validators import RegexValidator
|
|
@@ -121,6 +121,7 @@ class BaseJob:
|
|
|
121
121
|
|
|
122
122
|
def __init__(self):
|
|
123
123
|
self.logger = get_task_logger(self.__module__)
|
|
124
|
+
self._failed = False
|
|
124
125
|
|
|
125
126
|
def __call__(self, *args, **kwargs):
|
|
126
127
|
# Attempt to resolve serialized data back into original form by creating querysets or model instances
|
|
@@ -130,8 +131,9 @@ class BaseJob:
|
|
|
130
131
|
try:
|
|
131
132
|
deserialized_kwargs = self.deserialize_data(kwargs)
|
|
132
133
|
except Exception as err:
|
|
133
|
-
self.logger.
|
|
134
|
+
self.logger.exception("Error deserializing kwargs")
|
|
134
135
|
raise RunJobTaskFailed("Error initializing job") from err
|
|
136
|
+
|
|
135
137
|
if isinstance(self, JobHookReceiver):
|
|
136
138
|
change_context = ObjectChangeEventContextChoices.CONTEXT_JOB_HOOK
|
|
137
139
|
else:
|
|
@@ -163,7 +165,11 @@ class BaseJob:
|
|
|
163
165
|
def __str__(self):
|
|
164
166
|
return str(self.name)
|
|
165
167
|
|
|
166
|
-
|
|
168
|
+
def fail(self, msg, *args, **kwargs):
|
|
169
|
+
"""Mark this job as failed without immediately raising an exception and aborting."""
|
|
170
|
+
# Instead of failing in "fail" grouping, fail in the parent function's grouping by default
|
|
171
|
+
self.logger.failure(msg, *args, stacklevel=2, **kwargs)
|
|
172
|
+
self._failed = True
|
|
167
173
|
|
|
168
174
|
def before_start(self, task_id, args, kwargs):
|
|
169
175
|
"""Handler called before the task starts.
|
|
@@ -174,65 +180,10 @@ class BaseJob:
|
|
|
174
180
|
kwargs (Dict): Original keyword arguments for the task to execute.
|
|
175
181
|
|
|
176
182
|
Returns:
|
|
177
|
-
(
|
|
183
|
+
(Any): The return value of this handler is ignored normally, **except** if `self.fail()` is called herein,
|
|
184
|
+
in which case the return value will be used as the overall JobResult return value
|
|
185
|
+
since `self.run()` will **not** be called in such a case.
|
|
178
186
|
"""
|
|
179
|
-
try:
|
|
180
|
-
self.job_result
|
|
181
|
-
except ObjectDoesNotExist as err:
|
|
182
|
-
raise RunJobTaskFailed(f"Unable to find associated job result for job {task_id}") from err
|
|
183
|
-
|
|
184
|
-
try:
|
|
185
|
-
self.job_model
|
|
186
|
-
except ObjectDoesNotExist as err:
|
|
187
|
-
raise RunJobTaskFailed(f"Unable to find associated job model for job {task_id}") from err
|
|
188
|
-
|
|
189
|
-
if not self.job_model.enabled:
|
|
190
|
-
self.logger.error(
|
|
191
|
-
"Job %s is not enabled to be run!",
|
|
192
|
-
self.job_model,
|
|
193
|
-
extra={"object": self.job_model, "grouping": "initialization"},
|
|
194
|
-
)
|
|
195
|
-
raise RunJobTaskFailed(f"Job {self.job_model} is not enabled to be run!")
|
|
196
|
-
|
|
197
|
-
ignore_singleton_lock = self.celery_kwargs.get("nautobot_job_ignore_singleton_lock", False)
|
|
198
|
-
if self.job_model.is_singleton:
|
|
199
|
-
is_running = cache.get(self.singleton_cache_key)
|
|
200
|
-
if is_running:
|
|
201
|
-
if ignore_singleton_lock:
|
|
202
|
-
self.logger.info(
|
|
203
|
-
"Job %s is a singleton and already running, but singleton will be ignored because"
|
|
204
|
-
" `ignore_singleton_lock` is set.",
|
|
205
|
-
self.job_model,
|
|
206
|
-
extra={"object": self.job_model, "grouping": "initialization"},
|
|
207
|
-
)
|
|
208
|
-
else:
|
|
209
|
-
self.logger.error(
|
|
210
|
-
"Job %s is a singleton and already running.",
|
|
211
|
-
self.job_model,
|
|
212
|
-
extra={"object": self.job_model, "grouping": "initialization"},
|
|
213
|
-
)
|
|
214
|
-
raise RunJobTaskFailed(f"Job '{self.job_model}' is a singleton and already running.")
|
|
215
|
-
cache_parameters = {
|
|
216
|
-
"key": self.singleton_cache_key,
|
|
217
|
-
"value": 1,
|
|
218
|
-
"timeout": self.job_model.time_limit or settings.CELERY_TASK_TIME_LIMIT,
|
|
219
|
-
}
|
|
220
|
-
cache.set(**cache_parameters)
|
|
221
|
-
|
|
222
|
-
soft_time_limit = self.job_model.soft_time_limit or settings.CELERY_TASK_SOFT_TIME_LIMIT
|
|
223
|
-
time_limit = self.job_model.time_limit or settings.CELERY_TASK_TIME_LIMIT
|
|
224
|
-
if time_limit <= soft_time_limit:
|
|
225
|
-
self.logger.warning(
|
|
226
|
-
"The hard time limit of %s seconds is less than "
|
|
227
|
-
"or equal to the soft time limit of %s seconds. "
|
|
228
|
-
"This job will fail silently after %s seconds.",
|
|
229
|
-
time_limit,
|
|
230
|
-
soft_time_limit,
|
|
231
|
-
time_limit,
|
|
232
|
-
extra={"grouping": "initialization"},
|
|
233
|
-
)
|
|
234
|
-
|
|
235
|
-
self.logger.info("Running job", extra={"grouping": "initialization", "object": self.job_model})
|
|
236
187
|
|
|
237
188
|
def run(self, *args, **kwargs):
|
|
238
189
|
"""
|
|
@@ -277,11 +228,12 @@ class BaseJob:
|
|
|
277
228
|
This is run by the worker when the task fails.
|
|
278
229
|
|
|
279
230
|
Arguments:
|
|
280
|
-
exc (
|
|
231
|
+
exc (Any): Exception raised by the task (if any) **or** return value from the task, if it failed cleanly,
|
|
232
|
+
such as if the Job called `self.fail()` rather than raising an exception.
|
|
281
233
|
task_id (str): Unique id of the failed task.
|
|
282
234
|
args (Tuple): Original arguments for the task that failed.
|
|
283
235
|
kwargs (Dict): Original keyword arguments for the task that failed.
|
|
284
|
-
einfo (~billiard.einfo.ExceptionInfo): Exception information.
|
|
236
|
+
einfo (~billiard.einfo.ExceptionInfo): Exception information, or None.
|
|
285
237
|
|
|
286
238
|
Returns:
|
|
287
239
|
(None): The return value of this handler is ignored.
|
|
@@ -303,16 +255,7 @@ class BaseJob:
|
|
|
303
255
|
(None): The return value of this handler is ignored.
|
|
304
256
|
"""
|
|
305
257
|
|
|
306
|
-
|
|
307
|
-
file_fields = list(self._get_file_vars())
|
|
308
|
-
file_ids = [kwargs[f] for f in file_fields if f in kwargs]
|
|
309
|
-
if file_ids:
|
|
310
|
-
self._delete_file_proxies(*file_ids)
|
|
311
|
-
|
|
312
|
-
if status == JobResultStatusChoices.STATUS_SUCCESS:
|
|
313
|
-
self.logger.success("Job completed", extra={"grouping": "post_run"})
|
|
314
|
-
|
|
315
|
-
cache.delete(self.singleton_cache_key)
|
|
258
|
+
# See https://github.com/PyCQA/pylint-django/issues/240 for why we have a pylint disable on each classproperty below
|
|
316
259
|
|
|
317
260
|
@final
|
|
318
261
|
@classproperty
|
|
@@ -1205,6 +1148,114 @@ def get_job(class_path, reload=False):
|
|
|
1205
1148
|
return jobs.get(class_path, None)
|
|
1206
1149
|
|
|
1207
1150
|
|
|
1151
|
+
def _prepare_job(job_class_path, request, kwargs) -> tuple[Job, dict]:
|
|
1152
|
+
"""Helper method to run_job task, handling initial data setup and initialization before running a Job."""
|
|
1153
|
+
logger.debug("Preparing to run job %s for task %s", job_class_path, request.id)
|
|
1154
|
+
|
|
1155
|
+
# Get the job code
|
|
1156
|
+
job_class = get_job(job_class_path, reload=True)
|
|
1157
|
+
if job_class is None:
|
|
1158
|
+
raise KeyError(f"Job class not found for class path {job_class_path}")
|
|
1159
|
+
job = job_class()
|
|
1160
|
+
job.request = request
|
|
1161
|
+
|
|
1162
|
+
# Get the JobResult record to record results to
|
|
1163
|
+
try:
|
|
1164
|
+
job_result = JobResult.objects.get(id=request.id)
|
|
1165
|
+
except JobResult.DoesNotExist:
|
|
1166
|
+
job.logger.exception("Unable to find JobResult %s", request.id, extra={"grouping": "initialization"})
|
|
1167
|
+
raise
|
|
1168
|
+
|
|
1169
|
+
# Get the JobModel record associated with this job
|
|
1170
|
+
try:
|
|
1171
|
+
job.job_model
|
|
1172
|
+
except JobModel.DoesNotExist:
|
|
1173
|
+
job.logger.exception(
|
|
1174
|
+
"Unable to find Job database record %s", job_class_path, extra={"grouping": "initialization"}
|
|
1175
|
+
)
|
|
1176
|
+
raise
|
|
1177
|
+
|
|
1178
|
+
# Make sure it's valid to run this job - 1) is it enabled?
|
|
1179
|
+
if not job.job_model.enabled:
|
|
1180
|
+
job.logger.error(
|
|
1181
|
+
"Job %s is not enabled to be run!",
|
|
1182
|
+
job.job_model,
|
|
1183
|
+
extra={"object": job.job_model, "grouping": "initialization"},
|
|
1184
|
+
)
|
|
1185
|
+
raise RunJobTaskFailed(f"Job {job.job_model} is not enabled to be run!")
|
|
1186
|
+
# 2) if it's a singleton, is there any existing lock to be aware of?
|
|
1187
|
+
if job.job_model.is_singleton:
|
|
1188
|
+
is_running = cache.get(job.singleton_cache_key)
|
|
1189
|
+
if is_running:
|
|
1190
|
+
ignore_singleton_lock = job.celery_kwargs.get("nautobot_job_ignore_singleton_lock", False)
|
|
1191
|
+
if ignore_singleton_lock:
|
|
1192
|
+
job.logger.warning(
|
|
1193
|
+
"Job %s is a singleton and already running, but singleton will be ignored because"
|
|
1194
|
+
" `ignore_singleton_lock` is set.",
|
|
1195
|
+
job.job_model,
|
|
1196
|
+
extra={"object": job.job_model, "grouping": "initialization"},
|
|
1197
|
+
)
|
|
1198
|
+
else:
|
|
1199
|
+
# TODO 3.0: maybe change to logger.failure() and return cleanly, as this is an "acceptable" failure?
|
|
1200
|
+
job.logger.error(
|
|
1201
|
+
"Job %s is a singleton and already running.",
|
|
1202
|
+
job.job_model,
|
|
1203
|
+
extra={"object": job.job_model, "grouping": "initialization"},
|
|
1204
|
+
)
|
|
1205
|
+
raise RunJobTaskFailed(f"Job '{job.job_model}' is a singleton and already running.")
|
|
1206
|
+
cache_parameters = {
|
|
1207
|
+
"key": job.singleton_cache_key,
|
|
1208
|
+
"value": 1,
|
|
1209
|
+
"timeout": job.job_model.time_limit or settings.CELERY_TASK_TIME_LIMIT,
|
|
1210
|
+
}
|
|
1211
|
+
cache.set(**cache_parameters)
|
|
1212
|
+
|
|
1213
|
+
# Check for validity of the soft/hard time limits for the job.
|
|
1214
|
+
# TODO: this is a bit out of place?
|
|
1215
|
+
soft_time_limit = job.job_model.soft_time_limit or settings.CELERY_TASK_SOFT_TIME_LIMIT
|
|
1216
|
+
time_limit = job.job_model.time_limit or settings.CELERY_TASK_TIME_LIMIT
|
|
1217
|
+
if time_limit <= soft_time_limit:
|
|
1218
|
+
job.logger.warning(
|
|
1219
|
+
"The hard time limit of %s seconds is less than "
|
|
1220
|
+
"or equal to the soft time limit of %s seconds. "
|
|
1221
|
+
"This job will fail silently after %s seconds.",
|
|
1222
|
+
time_limit,
|
|
1223
|
+
soft_time_limit,
|
|
1224
|
+
time_limit,
|
|
1225
|
+
extra={"grouping": "initialization", "object": job.job_model},
|
|
1226
|
+
)
|
|
1227
|
+
|
|
1228
|
+
# Send notice that the job is running
|
|
1229
|
+
event_payload = {
|
|
1230
|
+
"job_result_id": request.id,
|
|
1231
|
+
"job_name": job.name, # TODO: should this be job.job_model.name instead? Possible breaking change
|
|
1232
|
+
"user_name": job_result.user.username,
|
|
1233
|
+
}
|
|
1234
|
+
if not job.job_model.has_sensitive_variables:
|
|
1235
|
+
event_payload["job_kwargs"] = kwargs
|
|
1236
|
+
publish_event(topic="nautobot.jobs.job.started", payload=event_payload)
|
|
1237
|
+
job.logger.info("Running job", extra={"grouping": "initialization", "object": job.job_model})
|
|
1238
|
+
|
|
1239
|
+
# Return the job, ready to run
|
|
1240
|
+
return job, event_payload
|
|
1241
|
+
|
|
1242
|
+
|
|
1243
|
+
def _cleanup_job(job, event_payload, status, kwargs):
|
|
1244
|
+
"""Helper method to run_job task, handling cleanup after running a Job."""
|
|
1245
|
+
# Cleanup FileProxy objects
|
|
1246
|
+
file_fields = list(job._get_file_vars())
|
|
1247
|
+
file_ids = [kwargs[f] for f in file_fields if f in kwargs]
|
|
1248
|
+
if file_ids:
|
|
1249
|
+
job._delete_file_proxies(*file_ids)
|
|
1250
|
+
|
|
1251
|
+
if status == JobResultStatusChoices.STATUS_SUCCESS:
|
|
1252
|
+
job.logger.success("Job completed", extra={"grouping": "post_run"})
|
|
1253
|
+
|
|
1254
|
+
publish_event(topic="nautobot.jobs.job.completed", payload=event_payload)
|
|
1255
|
+
|
|
1256
|
+
cache.delete(job.singleton_cache_key)
|
|
1257
|
+
|
|
1258
|
+
|
|
1208
1259
|
@nautobot_task(bind=True)
|
|
1209
1260
|
def run_job(self, job_class_path, *args, **kwargs):
|
|
1210
1261
|
"""
|
|
@@ -1212,49 +1263,76 @@ def run_job(self, job_class_path, *args, **kwargs):
|
|
|
1212
1263
|
|
|
1213
1264
|
This calls the following Job APIs in the following order:
|
|
1214
1265
|
|
|
1215
|
-
- `__init__()`
|
|
1216
|
-
- `before_start()`
|
|
1217
|
-
- `__call__()` (which calls `run()`)
|
|
1218
|
-
- If no exceptions have been raised
|
|
1219
|
-
|
|
1266
|
+
- `Job.__init__()`
|
|
1267
|
+
- `Job.before_start(self.request.id, args, kwargs)`
|
|
1268
|
+
- `Job.__call__(*args, **kwargs)` (which calls `run(*args, **kwargs)`)
|
|
1269
|
+
- If no exceptions have been raised (and `Job.fail()` was not called):
|
|
1270
|
+
- `Job.on_success(result, self.request.id, args, kwargs)`
|
|
1271
|
+
- Else:
|
|
1272
|
+
- `Job.on_failure(result_or_exception, self.request.id, args, kwargs, einfo)`
|
|
1273
|
+
- `Job.after_return(status, result_or_exception, self.request.id, args, kwargs, einfo)`
|
|
1220
1274
|
|
|
1221
|
-
Finally, it either returns
|
|
1275
|
+
Finally, it either returns any data returned from `Job.run()` or re-raises any exception encountered.
|
|
1222
1276
|
"""
|
|
1223
|
-
logger.debug("Running job %s", job_class_path)
|
|
1224
1277
|
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
job.request = self.request
|
|
1230
|
-
job_result = JobResult.objects.get(id=self.request.id)
|
|
1231
|
-
payload = {
|
|
1232
|
-
"job_result_id": self.request.id,
|
|
1233
|
-
"job_name": job.name,
|
|
1234
|
-
"user_name": job_result.user.username,
|
|
1235
|
-
}
|
|
1236
|
-
if not job.job_model.has_sensitive_variables:
|
|
1237
|
-
payload["job_kwargs"] = kwargs
|
|
1278
|
+
job, event_payload = _prepare_job(job_class_path, self.request, kwargs)
|
|
1279
|
+
|
|
1280
|
+
result = None
|
|
1281
|
+
status = None
|
|
1238
1282
|
try:
|
|
1239
|
-
|
|
1240
|
-
job.
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1283
|
+
before_start_result = job.before_start(self.request.id, args, kwargs)
|
|
1284
|
+
if not job._failed:
|
|
1285
|
+
# Call job(), which automatically calls job.run():
|
|
1286
|
+
result = job(*args, **kwargs)
|
|
1287
|
+
else:
|
|
1288
|
+
# don't run the job if before_start() reported a failure, and report the before_start() return value
|
|
1289
|
+
result = before_start_result
|
|
1290
|
+
|
|
1291
|
+
event_payload["job_output"] = result
|
|
1292
|
+
status = JobResultStatusChoices.STATUS_SUCCESS if not job._failed else JobResultStatusChoices.STATUS_FAILURE
|
|
1293
|
+
|
|
1294
|
+
if status == JobResultStatusChoices.STATUS_SUCCESS:
|
|
1295
|
+
job.on_success(result, self.request.id, args, kwargs)
|
|
1296
|
+
else:
|
|
1297
|
+
job.on_failure(result, self.request.id, args, kwargs, None)
|
|
1298
|
+
|
|
1299
|
+
job.after_return(status, result, self.request.id, args, kwargs, None)
|
|
1300
|
+
|
|
1301
|
+
if status == JobResultStatusChoices.STATUS_SUCCESS:
|
|
1302
|
+
return result
|
|
1303
|
+
|
|
1304
|
+
# Report a failure, but with a result rather than an exception and einfo:
|
|
1305
|
+
self.update_state(
|
|
1306
|
+
state=status,
|
|
1307
|
+
meta=result,
|
|
1308
|
+
)
|
|
1309
|
+
# If we return a result, Celery automatically applies STATUS_SUCCESS.
|
|
1310
|
+
# If we raise an exception *other than* `Ignore` or `Reject`, Celery automatically applies STATUS_FAILURE.
|
|
1311
|
+
# We don't want to overwrite the manual state update that we did above, so:
|
|
1312
|
+
raise Ignore()
|
|
1313
|
+
|
|
1314
|
+
except Reject:
|
|
1315
|
+
status = status or JobResultStatusChoices.STATUS_REJECTED
|
|
1316
|
+
raise
|
|
1317
|
+
|
|
1318
|
+
except Ignore:
|
|
1319
|
+
status = status or JobResultStatusChoices.STATUS_IGNORED
|
|
1320
|
+
raise
|
|
1321
|
+
|
|
1247
1322
|
except Exception as exc:
|
|
1323
|
+
status = JobResultStatusChoices.STATUS_FAILURE
|
|
1248
1324
|
einfo = ExceptionInfo(sys.exc_info())
|
|
1249
1325
|
job.on_failure(exc, self.request.id, args, kwargs, einfo)
|
|
1250
1326
|
job.after_return(JobResultStatusChoices.STATUS_FAILURE, exc, self.request.id, args, kwargs, einfo)
|
|
1251
|
-
|
|
1327
|
+
event_payload["einfo"] = {
|
|
1252
1328
|
"exc_type": type(exc).__name__,
|
|
1253
1329
|
"exc_message": sanitize(str(exc)),
|
|
1254
1330
|
}
|
|
1255
|
-
publish_event(topic="nautobot.jobs.job.completed", payload=payload)
|
|
1256
1331
|
raise
|
|
1257
1332
|
|
|
1333
|
+
finally:
|
|
1334
|
+
_cleanup_job(job, event_payload, status, kwargs)
|
|
1335
|
+
|
|
1258
1336
|
|
|
1259
1337
|
def enqueue_job_hooks(object_change, may_reload_jobs=True, jobhook_queryset=None):
|
|
1260
1338
|
"""
|
|
@@ -47,12 +47,21 @@ def report_job_status(command, job_result):
|
|
|
47
47
|
logs = JobLogEntry.objects.filter(job_result__pk=job_result.pk, grouping=group)
|
|
48
48
|
debug_count = logs.filter(log_level=LogLevelChoices.LOG_DEBUG).count()
|
|
49
49
|
info_count = logs.filter(log_level=LogLevelChoices.LOG_INFO).count()
|
|
50
|
+
success_count = logs.filter(log_level=LogLevelChoices.LOG_SUCCESS).count()
|
|
50
51
|
warning_count = logs.filter(log_level=LogLevelChoices.LOG_WARNING).count()
|
|
52
|
+
failure_count = logs.filter(log_level=LogLevelChoices.LOG_FAILURE).count()
|
|
51
53
|
error_count = logs.filter(log_level=LogLevelChoices.LOG_ERROR).count()
|
|
52
54
|
critical_count = logs.filter(log_level=LogLevelChoices.LOG_CRITICAL).count()
|
|
53
55
|
|
|
54
56
|
command.stdout.write(
|
|
55
|
-
f"\t{group}:
|
|
57
|
+
f"\t{group}: "
|
|
58
|
+
f"{debug_count} debug, "
|
|
59
|
+
f"{info_count} info, "
|
|
60
|
+
f"{success_count} success, "
|
|
61
|
+
f"{warning_count} warning, "
|
|
62
|
+
f"{failure_count} failure, "
|
|
63
|
+
f"{error_count} error, "
|
|
64
|
+
f"{critical_count} critical"
|
|
56
65
|
)
|
|
57
66
|
|
|
58
67
|
for log_entry in logs:
|
|
@@ -63,7 +72,7 @@ def report_job_status(command, job_result):
|
|
|
63
72
|
status = status
|
|
64
73
|
elif status == "warning":
|
|
65
74
|
status = command.style.WARNING(status)
|
|
66
|
-
elif status
|
|
75
|
+
elif status in ["failure", "error", "critical"]:
|
|
67
76
|
status = command.style.NOTICE(status)
|
|
68
77
|
|
|
69
78
|
if log_entry.log_object:
|
|
@@ -73,6 +82,8 @@ def report_job_status(command, job_result):
|
|
|
73
82
|
|
|
74
83
|
if job_result.result:
|
|
75
84
|
command.stdout.write(str(job_result.result))
|
|
85
|
+
if job_result.traceback:
|
|
86
|
+
command.stdout.write(command.style.ERROR(job_result.traceback))
|
|
76
87
|
|
|
77
88
|
if job_result.status == JobResultStatusChoices.STATUS_FAILURE:
|
|
78
89
|
status = command.style.ERROR("FAILURE")
|
|
@@ -131,16 +131,20 @@ class GitRepository(PrimaryModel):
|
|
|
131
131
|
|
|
132
132
|
def get_latest_sync(self):
|
|
133
133
|
"""
|
|
134
|
-
Return a `JobResult` for the latest sync operation.
|
|
134
|
+
Return a `JobResult` for the latest sync operation if one has occurred.
|
|
135
135
|
|
|
136
136
|
Returns:
|
|
137
|
-
JobResult
|
|
137
|
+
Returns a `JobResult` if the repo has been synced before, otherwise returns None.
|
|
138
138
|
"""
|
|
139
139
|
from nautobot.extras.models import JobResult
|
|
140
140
|
|
|
141
141
|
# This will match all "GitRepository" jobs (pull/refresh, dry-run, etc.)
|
|
142
142
|
prefix = "nautobot.core.jobs.GitRepository"
|
|
143
|
-
|
|
143
|
+
|
|
144
|
+
if JobResult.objects.filter(task_name__startswith=prefix, task_kwargs__repository=self.pk).exists():
|
|
145
|
+
return JobResult.objects.filter(task_name__startswith=prefix, task_kwargs__repository=self.pk).latest()
|
|
146
|
+
else:
|
|
147
|
+
return None
|
|
144
148
|
|
|
145
149
|
def to_csv(self):
|
|
146
150
|
return (
|
|
@@ -216,6 +220,8 @@ class GitRepository(PrimaryModel):
|
|
|
216
220
|
Returns:
|
|
217
221
|
Returns the absolute path of the cloned repo if clone was successful, otherwise returns None.
|
|
218
222
|
"""
|
|
223
|
+
from nautobot.extras.datasources import get_repo_access_url
|
|
224
|
+
|
|
219
225
|
if branch and head:
|
|
220
226
|
raise ValueError("Cannot specify both branch and head")
|
|
221
227
|
|
|
@@ -229,7 +235,8 @@ class GitRepository(PrimaryModel):
|
|
|
229
235
|
branch = self.branch
|
|
230
236
|
|
|
231
237
|
try:
|
|
232
|
-
|
|
238
|
+
remote_url = get_repo_access_url(self)
|
|
239
|
+
repo_helper = GitRepo(path_name, remote_url, depth=depth, branch=branch)
|
|
233
240
|
if head:
|
|
234
241
|
repo_helper.checkout(branch, head)
|
|
235
242
|
except Exception as e:
|
nautobot/extras/models/jobs.py
CHANGED
|
@@ -897,25 +897,28 @@ class JobResult(BaseModel, CustomFieldModel):
|
|
|
897
897
|
# Cancel the scheduled SIGALRM if it hasn't fired already
|
|
898
898
|
signal.alarm(0)
|
|
899
899
|
|
|
900
|
-
# copy fields from eager result to job result
|
|
901
900
|
job_result.refresh_from_db()
|
|
902
|
-
#
|
|
903
|
-
if
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
901
|
+
# copy from eager result to job result if and only if the job result isn't already in a proper state.
|
|
902
|
+
if JobResultStatusChoices.precedence(job_result.status) > JobResultStatusChoices.precedence(
|
|
903
|
+
eager_result.status
|
|
904
|
+
):
|
|
905
|
+
if eager_result.status in JobResultStatusChoices.EXCEPTION_STATES and isinstance(
|
|
906
|
+
eager_result.result, Exception
|
|
907
|
+
):
|
|
908
|
+
job_result.result = {
|
|
909
|
+
"exc_type": type(eager_result.result).__name__,
|
|
910
|
+
"exc_message": sanitize(str(eager_result.result)),
|
|
911
|
+
}
|
|
912
|
+
elif eager_result.result is not None:
|
|
910
913
|
job_result.result = sanitize(eager_result.result)
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
914
|
+
job_result.status = eager_result.status
|
|
915
|
+
if (
|
|
916
|
+
eager_result.status in JobResultStatusChoices.EXCEPTION_STATES
|
|
917
|
+
and eager_result.traceback is not None
|
|
918
|
+
):
|
|
919
|
+
job_result.traceback = sanitize(eager_result.traceback)
|
|
920
|
+
if not job_result.date_done:
|
|
921
|
+
job_result.date_done = timezone.now()
|
|
919
922
|
job_result.save()
|
|
920
923
|
else:
|
|
921
924
|
# Jobs queued inside of a transaction need to run after the transaction completes and the JobResult is saved to the database
|
|
@@ -728,6 +728,31 @@ def register_plugin_menu_items(section_name, menu_items):
|
|
|
728
728
|
#
|
|
729
729
|
|
|
730
730
|
|
|
731
|
+
class CustomValidatorContext(dict):
|
|
732
|
+
def __init__(self, obj):
|
|
733
|
+
"""
|
|
734
|
+
If there is an active change context, meaning we are in a web request context,
|
|
735
|
+
we have access to the current user object. Otherwise, we are likely running inside
|
|
736
|
+
a management command or other non-web or non-Job context, and we should use an AnonymousUser.
|
|
737
|
+
This ensures people's custom validators don't outright break when running in non-web
|
|
738
|
+
contexts, and should generally provide a sane default, given validation based on the
|
|
739
|
+
user is commonly going to be least-privelege based, and thus the AnonymousUser will
|
|
740
|
+
cause such validation logic to fail closed.
|
|
741
|
+
"""
|
|
742
|
+
from django.contrib.auth.models import AnonymousUser
|
|
743
|
+
|
|
744
|
+
from nautobot.extras.signals import change_context_state
|
|
745
|
+
|
|
746
|
+
change_context = change_context_state.get()
|
|
747
|
+
user = None
|
|
748
|
+
if change_context:
|
|
749
|
+
user = change_context.get_user()
|
|
750
|
+
if user is None:
|
|
751
|
+
user = AnonymousUser()
|
|
752
|
+
|
|
753
|
+
super().__init__(object=obj, user=user)
|
|
754
|
+
|
|
755
|
+
|
|
731
756
|
class CustomValidator:
|
|
732
757
|
"""
|
|
733
758
|
This class is used to register plugin custom model validators which act on specified models. It contains the clean
|
|
@@ -742,7 +767,7 @@ class CustomValidator:
|
|
|
742
767
|
model = None
|
|
743
768
|
|
|
744
769
|
def __init__(self, obj):
|
|
745
|
-
self.context =
|
|
770
|
+
self.context = CustomValidatorContext(obj)
|
|
746
771
|
|
|
747
772
|
def validation_error(self, message):
|
|
748
773
|
"""
|
nautobot/extras/tables.py
CHANGED
|
@@ -112,6 +112,27 @@ JOB_BUTTONS = """
|
|
|
112
112
|
<a href="{% url 'extras:jobresult_list' %}?job_model={{ record.name | urlencode }}" class="btn btn-default btn-xs" title="Job Results"><i class="mdi mdi-format-list-bulleted" aria-hidden="true"></i></a>
|
|
113
113
|
"""
|
|
114
114
|
|
|
115
|
+
JOB_RESULT_BUTTONS = """
|
|
116
|
+
{% load helpers %}
|
|
117
|
+
{% if perms.extras.run_job %}
|
|
118
|
+
{% if record.job_model and record.task_kwargs %}
|
|
119
|
+
<a href="{% url 'extras:job_run' pk=record.job_model.pk %}?kwargs_from_job_result={{ record.pk }}"
|
|
120
|
+
class="btn btn-xs btn-success" title="Re-run job with same arguments.">
|
|
121
|
+
<i class="mdi mdi-repeat"></i>
|
|
122
|
+
</a>
|
|
123
|
+
{% elif record.job_model is not None %}
|
|
124
|
+
<a href="{% url 'extras:job_run' pk=record.job_model.pk %}" class="btn btn-primary btn-xs"
|
|
125
|
+
title="Run job">
|
|
126
|
+
<i class="mdi mdi-play"></i>
|
|
127
|
+
</a>
|
|
128
|
+
{% else %}
|
|
129
|
+
<a href="#" class="btn btn-xs btn-default disabled" title="Job is not available, cannot be re-run">
|
|
130
|
+
<i class="mdi mdi-repeat-off"></i>
|
|
131
|
+
</a>
|
|
132
|
+
{% endif %}
|
|
133
|
+
{% endif %}
|
|
134
|
+
"""
|
|
135
|
+
|
|
115
136
|
SCHEDULED_JOB_BUTTONS = """
|
|
116
137
|
<a href="{% url 'extras:jobresult_list' %}?scheduled_job={{ record.name | urlencode }}" class="btn btn-default btn-xs" title="Job Results"><i class="mdi mdi-format-list-bulleted" aria-hidden="true"></i></a>
|
|
117
138
|
"""
|
|
@@ -675,7 +696,7 @@ def log_object_link(value, record):
|
|
|
675
696
|
|
|
676
697
|
|
|
677
698
|
def log_entry_color_css(record):
|
|
678
|
-
if record.log_level.lower() in ("error", "critical"):
|
|
699
|
+
if record.log_level.lower() in ("failure", "error", "critical"):
|
|
679
700
|
return "danger"
|
|
680
701
|
return record.log_level.lower()
|
|
681
702
|
|
|
@@ -811,7 +832,7 @@ class JobLogEntryTable(BaseTable):
|
|
|
811
832
|
def render_log_level(self, value):
|
|
812
833
|
log_level = value.lower()
|
|
813
834
|
# The css is label-danger for failure items.
|
|
814
|
-
if log_level in ["error", "critical"]:
|
|
835
|
+
if log_level in ["failure", "error", "critical"]:
|
|
815
836
|
log_level = "danger"
|
|
816
837
|
elif log_level == "debug":
|
|
817
838
|
log_level = "default"
|
|
@@ -877,32 +898,7 @@ class JobResultTable(BaseTable):
|
|
|
877
898
|
linkify=True,
|
|
878
899
|
verbose_name="Scheduled Job",
|
|
879
900
|
)
|
|
880
|
-
actions =
|
|
881
|
-
template_code="""
|
|
882
|
-
{% load helpers %}
|
|
883
|
-
{% if perms.extras.run_job %}
|
|
884
|
-
{% if record.job_model and record.task_kwargs %}
|
|
885
|
-
<a href="{% url 'extras:job_run' pk=record.job_model.pk %}?kwargs_from_job_result={{ record.pk }}"
|
|
886
|
-
class="btn btn-xs btn-success" title="Re-run job with same arguments.">
|
|
887
|
-
<i class="mdi mdi-repeat"></i>
|
|
888
|
-
</a>
|
|
889
|
-
{% elif record.job_model is not None %}
|
|
890
|
-
<a href="{% url 'extras:job_run' pk=record.job_model.pk %}" class="btn btn-primary btn-xs"
|
|
891
|
-
title="Run job">
|
|
892
|
-
<i class="mdi mdi-play"></i>
|
|
893
|
-
</a>
|
|
894
|
-
{% else %}
|
|
895
|
-
<a href="#" class="btn btn-xs btn-default disabled" title="Job is not available, cannot be re-run">
|
|
896
|
-
<i class="mdi mdi-repeat-off"></i>
|
|
897
|
-
</a>
|
|
898
|
-
{% endif %}
|
|
899
|
-
{% endif %}
|
|
900
|
-
<a href="{% url 'extras:jobresult_delete' pk=record.pk %}" class="btn btn-xs btn-danger"
|
|
901
|
-
title="Delete this job result.">
|
|
902
|
-
<i class="mdi mdi-trash-can-outline"></i>
|
|
903
|
-
</a>
|
|
904
|
-
"""
|
|
905
|
-
)
|
|
901
|
+
actions = ButtonsColumn(JobResult, buttons=("delete",), prepend_template=JOB_RESULT_BUTTONS)
|
|
906
902
|
|
|
907
903
|
def render_summary(self, record):
|
|
908
904
|
"""
|
|
@@ -1094,7 +1090,7 @@ class ScheduledJobTable(BaseTable):
|
|
|
1094
1090
|
last_run_at = tables.DateTimeColumn(verbose_name="Most Recent Run", format=settings.SHORT_DATETIME_FORMAT)
|
|
1095
1091
|
crontab = tables.Column()
|
|
1096
1092
|
total_run_count = tables.Column(verbose_name="Total Run Count")
|
|
1097
|
-
actions = ButtonsColumn(ScheduledJob, buttons=("delete"), prepend_template=SCHEDULED_JOB_BUTTONS)
|
|
1093
|
+
actions = ButtonsColumn(ScheduledJob, buttons=("delete",), prepend_template=SCHEDULED_JOB_BUTTONS)
|
|
1098
1094
|
|
|
1099
1095
|
class Meta(BaseTable.Meta):
|
|
1100
1096
|
model = ScheduledJob
|