Accept a recurrence interval when executing scripts & reports

This commit is contained in:
jeremystretch 2022-12-02 16:48:18 -05:00
parent d505c723dc
commit 05780891ed
7 changed files with 75 additions and 44 deletions

View File

@ -414,6 +414,7 @@ class ReportDetailSerializer(ReportSerializer):
class ReportInputSerializer(serializers.Serializer): class ReportInputSerializer(serializers.Serializer):
schedule_at = serializers.DateTimeField(required=False, allow_null=True) schedule_at = serializers.DateTimeField(required=False, allow_null=True)
interval = serializers.IntegerField(required=False, allow_null=True)
# #
@ -448,6 +449,7 @@ class ScriptInputSerializer(serializers.Serializer):
data = serializers.JSONField() data = serializers.JSONField()
commit = serializers.BooleanField() commit = serializers.BooleanField()
schedule_at = serializers.DateTimeField(required=False, allow_null=True) schedule_at = serializers.DateTimeField(required=False, allow_null=True)
interval = serializers.IntegerField(required=False, allow_null=True)
class ScriptLogMessageSerializer(serializers.Serializer): class ScriptLogMessageSerializer(serializers.Serializer):

View File

@ -1,5 +1,4 @@
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.db.models import Q
from django.http import Http404 from django.http import Http404
from django_rq.queues import get_connection from django_rq.queues import get_connection
from rest_framework import status from rest_framework import status
@ -246,16 +245,14 @@ class ReportViewSet(ViewSet):
input_serializer = serializers.ReportInputSerializer(data=request.data) input_serializer = serializers.ReportInputSerializer(data=request.data)
if input_serializer.is_valid(): if input_serializer.is_valid():
schedule_at = input_serializer.validated_data.get('schedule_at')
report_content_type = ContentType.objects.get(app_label='extras', model='report')
job_result = JobResult.enqueue_job( job_result = JobResult.enqueue_job(
run_report, run_report,
report.full_name, name=report.full_name,
report_content_type, obj_type=ContentType.objects.get_for_model(Report),
request.user, user=request.user,
job_timeout=report.job_timeout, job_timeout=report.job_timeout,
schedule_at=schedule_at, schedule_at=input_serializer.validated_data.get('schedule_at'),
interval=input_serializer.validated_data.get('interval')
) )
report.result = job_result report.result = job_result
@ -329,21 +326,17 @@ class ScriptViewSet(ViewSet):
raise RQWorkerNotRunningException() raise RQWorkerNotRunningException()
if input_serializer.is_valid(): if input_serializer.is_valid():
data = input_serializer.data['data']
commit = input_serializer.data['commit']
schedule_at = input_serializer.validated_data.get('schedule_at')
script_content_type = ContentType.objects.get(app_label='extras', model='script')
job_result = JobResult.enqueue_job( job_result = JobResult.enqueue_job(
run_script, run_script,
script.full_name, name=script.full_name,
script_content_type, obj_type=ContentType.objects.get_for_model(Script),
request.user, user=request.user,
data=data, data=input_serializer.data['data'],
request=copy_safe_request(request), request=copy_safe_request(request),
commit=commit, commit=input_serializer.data['commit'],
job_timeout=script.job_timeout, job_timeout=script.job_timeout,
schedule_at=schedule_at, schedule_at=input_serializer.validated_data.get('schedule_at'),
interval=input_serializer.validated_data.get('interval')
) )
script.result = job_result script.result = job_result
serializer = serializers.ScriptDetailSerializer(script, context={'request': request}) serializer = serializers.ScriptDetailSerializer(script, context={'request': request})

View File

@ -15,3 +15,8 @@ class ReportForm(BootstrapMixin, forms.Form):
label=_("Schedule at"), label=_("Schedule at"),
help_text=_("Schedule execution of report to a set time"), help_text=_("Schedule execution of report to a set time"),
) )
interval = forms.IntegerField(
required=False,
label=_("Recurs every"),
help_text=_("Interval at which this report is re-run (in minutes)")
)

View File

@ -21,19 +21,26 @@ class ScriptForm(BootstrapMixin, forms.Form):
label=_("Schedule at"), label=_("Schedule at"),
help_text=_("Schedule execution of script to a set time"), help_text=_("Schedule execution of script to a set time"),
) )
_interval = forms.IntegerField(
required=False,
label=_("Recurs every"),
help_text=_("Interval at which this script is re-run (in minutes)")
)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
# Move _commit and _schedule_at to the end of the form # Move _commit and _schedule_at to the end of the form
schedule_at = self.fields.pop('_schedule_at') schedule_at = self.fields.pop('_schedule_at')
interval = self.fields.pop('_interval')
commit = self.fields.pop('_commit') commit = self.fields.pop('_commit')
self.fields['_schedule_at'] = schedule_at self.fields['_schedule_at'] = schedule_at
self.fields['_interval'] = interval
self.fields['_commit'] = commit self.fields['_commit'] = commit
@property @property
def requires_input(self): def requires_input(self):
""" """
A boolean indicating whether the form requires user input (ignore the _commit and _schedule_at fields). A boolean indicating whether the form requires user input (ignore the built-in fields).
""" """
return bool(len(self.fields) > 2) return bool(len(self.fields) > 3)

View File

@ -1,8 +1,8 @@
import importlib
import inspect import inspect
import logging import logging
import pkgutil import pkgutil
import traceback import traceback
from datetime import timedelta
from django.conf import settings from django.conf import settings
from django.utils import timezone from django.utils import timezone
@ -11,7 +11,6 @@ from django_rq import job
from .choices import JobResultStatusChoices, LogLevelChoices from .choices import JobResultStatusChoices, LogLevelChoices
from .models import JobResult from .models import JobResult
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -85,10 +84,23 @@ def run_report(job_result, *args, **kwargs):
try: try:
job_result.start() job_result.start()
report.run(job_result) report.run(job_result)
except Exception as e: except Exception:
job_result.set_status(JobResultStatusChoices.STATUS_ERRORED) job_result.set_status(JobResultStatusChoices.STATUS_ERRORED)
job_result.save() job_result.save()
logging.error(f"Error during execution of report {job_result.name}") logging.error(f"Error during execution of report {job_result.name}")
finally:
# Schedule the next job if an interval has been set
if job_result.interval:
new_scheduled_time = job_result.scheduled + timedelta(minutes=job_result.interval)
JobResult.enqueue_job(
run_report,
name=job_result.name,
obj_type=job_result.obj_type,
user=job_result.user,
job_timeout=report.job_timeout,
schedule_at=new_scheduled_time,
interval=job_result.interval
)
class Report(object): class Report(object):

View File

@ -4,8 +4,9 @@ import logging
import os import os
import pkgutil import pkgutil
import sys import sys
import traceback
import threading import threading
import traceback
from datetime import timedelta
import yaml import yaml
from django import forms from django import forms
@ -16,6 +17,7 @@ from django.utils.functional import classproperty
from extras.api.serializers import ScriptOutputSerializer from extras.api.serializers import ScriptOutputSerializer
from extras.choices import JobResultStatusChoices, LogLevelChoices from extras.choices import JobResultStatusChoices, LogLevelChoices
from extras.models import JobResult
from extras.signals import clear_webhooks from extras.signals import clear_webhooks
from ipam.formfields import IPAddressFormField, IPNetworkFormField from ipam.formfields import IPAddressFormField, IPNetworkFormField
from ipam.validators import MaxPrefixLengthValidator, MinPrefixLengthValidator, prefix_validator from ipam.validators import MaxPrefixLengthValidator, MinPrefixLengthValidator, prefix_validator
@ -491,6 +493,22 @@ def run_script(data, request, commit=True, *args, **kwargs):
else: else:
_run_script() _run_script()
# Schedule the next job if an interval has been set
if job_result.interval:
new_scheduled_time = job_result.scheduled + timedelta(minutes=job_result.interval)
JobResult.enqueue_job(
run_script,
name=job_result.name,
obj_type=job_result.obj_type,
user=job_result.user,
schedule_at=new_scheduled_time,
interval=job_result.interval,
job_timeout=script.job_timeout,
data=data,
request=request,
commit=commit
)
def get_scripts(use_names=False): def get_scripts(use_names=False):
""" """

View File

@ -676,7 +676,6 @@ class ReportView(ContentTypePermissionRequiredMixin, View):
form = ReportForm(request.POST) form = ReportForm(request.POST)
if form.is_valid(): if form.is_valid():
schedule_at = form.cleaned_data.get("schedule_at")
# Allow execution only if RQ worker process is running # Allow execution only if RQ worker process is running
if not Worker.count(get_connection('default')): if not Worker.count(get_connection('default')):
@ -686,14 +685,14 @@ class ReportView(ContentTypePermissionRequiredMixin, View):
}) })
# Run the Report. A new JobResult is created. # Run the Report. A new JobResult is created.
report_content_type = ContentType.objects.get(app_label='extras', model='report')
job_result = JobResult.enqueue_job( job_result = JobResult.enqueue_job(
run_report, run_report,
report.full_name, name=report.full_name,
report_content_type, obj_type=ContentType.objects.get_for_model(Report),
request.user, user=request.user,
job_timeout=report.job_timeout, schedule_at=form.cleaned_data.get('schedule_at'),
schedule_at=schedule_at, interval=form.cleaned_data.get('interval'),
job_timeout=report.job_timeout
) )
return redirect('extras:report_result', job_result_pk=job_result.pk) return redirect('extras:report_result', job_result_pk=job_result.pk)
@ -787,9 +786,8 @@ class ScriptView(ContentTypePermissionRequiredMixin, GetScriptMixin, View):
form = script.as_form(initial=normalize_querydict(request.GET)) form = script.as_form(initial=normalize_querydict(request.GET))
# Look for a pending JobResult (use the latest one by creation timestamp) # Look for a pending JobResult (use the latest one by creation timestamp)
script_content_type = ContentType.objects.get(app_label='extras', model='script')
script.result = JobResult.objects.filter( script.result = JobResult.objects.filter(
obj_type=script_content_type, obj_type=ContentType.objects.get_for_model(Script),
name=script.full_name, name=script.full_name,
).exclude( ).exclude(
status__in=JobResultStatusChoices.TERMINAL_STATE_CHOICES status__in=JobResultStatusChoices.TERMINAL_STATE_CHOICES
@ -815,21 +813,17 @@ class ScriptView(ContentTypePermissionRequiredMixin, GetScriptMixin, View):
messages.error(request, "Unable to run script: RQ worker process not running.") messages.error(request, "Unable to run script: RQ worker process not running.")
elif form.is_valid(): elif form.is_valid():
commit = form.cleaned_data.pop('_commit')
schedule_at = form.cleaned_data.pop("_schedule_at")
script_content_type = ContentType.objects.get(app_label='extras', model='script')
job_result = JobResult.enqueue_job( job_result = JobResult.enqueue_job(
run_script, run_script,
script.full_name, name=script.full_name,
script_content_type, obj_type=ContentType.objects.get_for_model(Script),
request.user, user=request.user,
schedule_at=form.cleaned_data.pop('_schedule_at'),
interval=form.cleaned_data.pop('_interval'),
data=form.cleaned_data, data=form.cleaned_data,
request=copy_safe_request(request), request=copy_safe_request(request),
commit=commit,
job_timeout=script.job_timeout, job_timeout=script.job_timeout,
schedule_at=schedule_at, commit=form.cleaned_data.pop('_commit')
) )
return redirect('extras:script_result', job_result_pk=job_result.pk) return redirect('extras:script_result', job_result_pk=job_result.pk)