1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

#2006: Prevent script/report execution if no RQ worker is running

This commit is contained in:
Jeremy Stretch
2020-07-13 15:11:58 -04:00
parent e53839ca2a
commit 05aa008ce1
3 changed files with 40 additions and 7 deletions

View File

@ -3,11 +3,13 @@ from collections import OrderedDict
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.db.models import Count from django.db.models import Count
from django.http import Http404 from django.http import Http404
from django_rq.queues import get_connection
from rest_framework import status from rest_framework import status
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.exceptions import PermissionDenied from rest_framework.exceptions import PermissionDenied
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.viewsets import ReadOnlyModelViewSet, ViewSet from rest_framework.viewsets import ReadOnlyModelViewSet, ViewSet
from rq import Worker
from extras import filters from extras import filters
from extras.choices import JobResultStatusChoices from extras.choices import JobResultStatusChoices
@ -17,6 +19,7 @@ from extras.models import (
from extras.reports import get_report, get_reports, run_report from extras.reports import get_report, get_reports, run_report
from extras.scripts import get_script, get_scripts, run_script from extras.scripts import get_script, get_scripts, run_script
from utilities.api import IsAuthenticatedOrLoginNotRequired, ModelViewSet from utilities.api import IsAuthenticatedOrLoginNotRequired, ModelViewSet
from utilities.exceptions import RQWorkerNotRunningException
from utilities.metadata import ContentTypeMetadata from utilities.metadata import ContentTypeMetadata
from utilities.utils import copy_safe_request from utilities.utils import copy_safe_request
from . import serializers from . import serializers
@ -219,11 +222,14 @@ class ReportViewSet(ViewSet):
""" """
Run a Report identified as "<module>.<script>" and return the pending JobResult as the result Run a Report identified as "<module>.<script>" and return the pending JobResult as the result
""" """
# Check that the user has permission to run reports. # Check that the user has permission to run reports.
if not request.user.has_perm('extras.run_script'): if not request.user.has_perm('extras.run_script'):
raise PermissionDenied("This user does not have permission to run reports.") raise PermissionDenied("This user does not have permission to run reports.")
# Check that at least one RQ worker is running
if not Worker.count(get_connection('default')):
raise RQWorkerNotRunningException()
# Retrieve and run the Report. This will create a new JobResult. # Retrieve and run the Report. This will create a new JobResult.
report = self._retrieve_report(pk) report = self._retrieve_report(pk)
report_content_type = ContentType.objects.get(app_label='extras', model='report') report_content_type = ContentType.objects.get(app_label='extras', model='report')
@ -299,6 +305,10 @@ class ScriptViewSet(ViewSet):
script = self._get_script(pk)() script = self._get_script(pk)()
input_serializer = serializers.ScriptInputSerializer(data=request.data) input_serializer = serializers.ScriptInputSerializer(data=request.data)
# Check that at least one RQ worker is running
if not Worker.count(get_connection('default')):
raise RQWorkerNotRunningException()
if input_serializer.is_valid(): if input_serializer.is_valid():
data = input_serializer.data['data'] data = input_serializer.data['data']
commit = input_serializer.data['commit'] commit = input_serializer.data['commit']

View File

@ -1,13 +1,14 @@
import time
from django import template from django import template
from django.conf import settings from django.conf import settings
from django.contrib import messages
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.db.models import Count, Prefetch, Q from django.db.models import Count, Prefetch, Q
from django.http import Http404, HttpResponseForbidden from django.http import Http404, HttpResponseForbidden
from django.shortcuts import get_object_or_404, redirect, render from django.shortcuts import get_object_or_404, redirect, render
from django.views.generic import View from django.views.generic import View
from django_rq.queues import get_connection
from django_tables2 import RequestConfig from django_tables2 import RequestConfig
from rq import Worker
from dcim.models import DeviceRole, Platform, Region, Site from dcim.models import DeviceRole, Platform, Region, Site
from tenancy.models import Tenant, TenantGroup from tenancy.models import Tenant, TenantGroup
@ -21,7 +22,7 @@ from utilities.views import (
from virtualization.models import Cluster, ClusterGroup from virtualization.models import Cluster, ClusterGroup
from . import filters, forms, tables from . import filters, forms, tables
from .choices import JobResultStatusChoices from .choices import JobResultStatusChoices
from .models import ConfigContext, ImageAttachment, ObjectChange, Report, JobResult, Script, Tag, TaggedItem from .models import ConfigContext, ImageAttachment, ObjectChange, JobResult, Tag
from .reports import get_report, get_reports, run_report from .reports import get_report, get_reports, run_report
from .scripts import get_scripts, run_script from .scripts import get_scripts, run_script
@ -388,9 +389,13 @@ class ReportView(GetReportMixin, ContentTypePermissionRequiredMixin, View):
return HttpResponseForbidden() return HttpResponseForbidden()
report = self._get_report(name, module) report = self._get_report(name, module)
form = ConfirmationForm(request.POST) form = ConfirmationForm(request.POST)
if form.is_valid():
# Allow execution only if RQ worker process is running
if not Worker.count(get_connection('default')):
messages.error(request, "Unable to run report: RQ worker process not running.")
elif form.is_valid():
# 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') report_content_type = ContentType.objects.get(app_label='extras', model='report')
@ -504,7 +509,11 @@ class ScriptView(ContentTypePermissionRequiredMixin, GetScriptMixin, View):
script = self._get_script(name, module) script = self._get_script(name, module)
form = script.as_form(request.POST, request.FILES) form = script.as_form(request.POST, request.FILES)
if form.is_valid(): # Allow execution only if RQ worker process is running
if not Worker.count(get_connection('default')):
messages.error(request, "Unable to run script: RQ worker process not running.")
elif form.is_valid():
commit = form.cleaned_data.pop('_commit') commit = form.cleaned_data.pop('_commit')
script_content_type = ContentType.objects.get(app_label='extras', model='script') script_content_type = ContentType.objects.get(app_label='extras', model='script')

View File

@ -1,5 +1,19 @@
from rest_framework import status
from rest_framework.exceptions import APIException
class AbortTransaction(Exception): class AbortTransaction(Exception):
""" """
A dummy exception used to trigger a database transaction rollback. A dummy exception used to trigger a database transaction rollback.
""" """
pass pass
class RQWorkerNotRunningException(APIException):
"""
Indicates the temporary inability to enqueue a new task (e.g. custom script execution) because no RQ worker
processes are currently running.
"""
status_code = status.HTTP_503_SERVICE_UNAVAILABLE
default_detail = 'Unable to process request: RQ worker process not running.'
default_code = 'rq_worker_not_running'