mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-16 04:02:52 -06:00
PR review updates
This commit is contained in:
parent
f98fa364c0
commit
f092c107b5
@ -258,11 +258,7 @@ class JobResultSerializer(serializers.ModelSerializer):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class ReportSerializer(serializers.Serializer):
|
class ReportSerializer(serializers.Serializer):
|
||||||
url = serializers.HyperlinkedIdentityField(
|
id = serializers.CharField(read_only=True, source="full_name")
|
||||||
view_name='extras-api:report-detail',
|
|
||||||
lookup_field='full_name',
|
|
||||||
lookup_url_kwarg='pk'
|
|
||||||
)
|
|
||||||
module = serializers.CharField(max_length=255)
|
module = serializers.CharField(max_length=255)
|
||||||
name = serializers.CharField(max_length=255)
|
name = serializers.CharField(max_length=255)
|
||||||
description = serializers.CharField(max_length=255, required=False)
|
description = serializers.CharField(max_length=255, required=False)
|
||||||
@ -279,12 +275,8 @@ class ReportDetailSerializer(ReportSerializer):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class ScriptSerializer(serializers.Serializer):
|
class ScriptSerializer(serializers.Serializer):
|
||||||
url = serializers.HyperlinkedIdentityField(
|
|
||||||
view_name='extras-api:script-detail',
|
|
||||||
lookup_field='full_name',
|
|
||||||
lookup_url_kwarg='pk'
|
|
||||||
)
|
|
||||||
id = serializers.CharField(read_only=True, source="full_name")
|
id = serializers.CharField(read_only=True, source="full_name")
|
||||||
|
module = serializers.CharField(max_length=255)
|
||||||
name = serializers.CharField(read_only=True)
|
name = serializers.CharField(read_only=True)
|
||||||
description = serializers.CharField(read_only=True)
|
description = serializers.CharField(read_only=True)
|
||||||
vars = serializers.SerializerMethodField(read_only=True)
|
vars = serializers.SerializerMethodField(read_only=True)
|
||||||
|
@ -14,7 +14,7 @@ from extras.choices import JobResultStatusChoices
|
|||||||
from extras.models import (
|
from extras.models import (
|
||||||
ConfigContext, CustomFieldChoice, ExportTemplate, Graph, ImageAttachment, ObjectChange, JobResult, Tag,
|
ConfigContext, CustomFieldChoice, ExportTemplate, Graph, ImageAttachment, ObjectChange, JobResult, Tag,
|
||||||
)
|
)
|
||||||
from extras.reports import get_report, get_reports
|
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.utils import copy_safe_request
|
from utilities.utils import copy_safe_request
|
||||||
|
@ -129,25 +129,23 @@ class JobResultStatusChoices(ChoiceSet):
|
|||||||
STATUS_PENDING = 'pending'
|
STATUS_PENDING = 'pending'
|
||||||
STATUS_RUNNING = 'running'
|
STATUS_RUNNING = 'running'
|
||||||
STATUS_COMPLETED = 'completed'
|
STATUS_COMPLETED = 'completed'
|
||||||
|
STATUS_ERRORED = 'errored'
|
||||||
STATUS_FAILED = 'failed'
|
STATUS_FAILED = 'failed'
|
||||||
|
|
||||||
CHOICES = (
|
CHOICES = (
|
||||||
(STATUS_PENDING, 'Pending'),
|
(STATUS_PENDING, 'Pending'),
|
||||||
(STATUS_RUNNING, 'Running'),
|
(STATUS_RUNNING, 'Running'),
|
||||||
(STATUS_COMPLETED, 'Completed'),
|
(STATUS_COMPLETED, 'Completed'),
|
||||||
|
(STATUS_ERRORED, 'Errored'),
|
||||||
(STATUS_FAILED, 'Failed'),
|
(STATUS_FAILED, 'Failed'),
|
||||||
)
|
)
|
||||||
|
|
||||||
TERMINAL_STATE_CHOICES = (
|
TERMINAL_STATE_CHOICES = (
|
||||||
STATUS_COMPLETED,
|
STATUS_COMPLETED,
|
||||||
|
STATUS_ERRORED,
|
||||||
STATUS_FAILED,
|
STATUS_FAILED,
|
||||||
)
|
)
|
||||||
|
|
||||||
NON_TERMINAL_STATE_CHOICES = (
|
|
||||||
STATUS_PENDING,
|
|
||||||
STATUS_RUNNING,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Webhooks
|
# Webhooks
|
||||||
|
22
netbox/extras/migrations/0043_report_model.py
Normal file
22
netbox/extras/migrations/0043_report_model.py
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
# Generated by Django 3.0.7 on 2020-06-23 02:28
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('extras', '0042_customfield_manager'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Report',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'managed': False,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
]
|
@ -13,15 +13,11 @@ def convert_job_results(apps, schema_editor):
|
|||||||
"""
|
"""
|
||||||
Convert ReportResult objects to JobResult objects
|
Convert ReportResult objects to JobResult objects
|
||||||
"""
|
"""
|
||||||
from django.contrib.contenttypes.management import create_contenttypes
|
|
||||||
from extras.choices import JobResultStatusChoices
|
from extras.choices import JobResultStatusChoices
|
||||||
|
|
||||||
ReportResult = apps.get_model('extras', 'ReportResult')
|
ReportResult = apps.get_model('extras', 'ReportResult')
|
||||||
JobResult = apps.get_model('extras', 'JobResult')
|
JobResult = apps.get_model('extras', 'JobResult')
|
||||||
ContentType = apps.get_model('contenttypes', 'ContentType')
|
ContentType = apps.get_model('contenttypes', 'ContentType')
|
||||||
app_config = apps.get_app_config('extras')
|
|
||||||
app_config.models_module = app_config.models_module or True
|
|
||||||
create_contenttypes(app_config)
|
|
||||||
report_content_type = ContentType.objects.get(app_label='extras', model='report')
|
report_content_type = ContentType.objects.get(app_label='extras', model='report')
|
||||||
|
|
||||||
job_results = []
|
job_results = []
|
||||||
@ -50,19 +46,10 @@ class Migration(migrations.Migration):
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
('contenttypes', '0002_remove_content_type_name'),
|
('contenttypes', '0002_remove_content_type_name'),
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
('extras', '0042_customfield_manager'),
|
('extras', '0043_report_model'),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.CreateModel(
|
|
||||||
name='Report',
|
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'managed': False,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='JobResult',
|
name='JobResult',
|
||||||
fields=[
|
fields=[
|
@ -12,6 +12,7 @@ from django.db import models
|
|||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.template import Template, Context
|
from django.template import Template, Context
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from django.utils import timezone
|
||||||
from rest_framework.utils.encoders import JSONEncoder
|
from rest_framework.utils.encoders import JSONEncoder
|
||||||
|
|
||||||
from utilities.querysets import RestrictedQuerySet
|
from utilities.querysets import RestrictedQuerySet
|
||||||
@ -650,6 +651,18 @@ class JobResult(models.Model):
|
|||||||
|
|
||||||
return f"{int(minutes)} minutes, {seconds:.2f} seconds"
|
return f"{int(minutes)} minutes, {seconds:.2f} seconds"
|
||||||
|
|
||||||
|
def set_status(self, status):
|
||||||
|
"""
|
||||||
|
Helper method to change the status of the job result and save. If the target status is terminal, the
|
||||||
|
completion time is also set.
|
||||||
|
"""
|
||||||
|
self.status = status
|
||||||
|
if status in JobResultStatusChoices.TERMINAL_STATE_CHOICES:
|
||||||
|
self.completed = timezone.now()
|
||||||
|
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def enqueue_job(cls, func, name, obj_type, user, *args, **kwargs):
|
def enqueue_job(cls, func, name, obj_type, user, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
@ -14,6 +14,9 @@ from .constants import *
|
|||||||
from .models import JobResult
|
from .models import JobResult
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def is_report(obj):
|
def is_report(obj):
|
||||||
"""
|
"""
|
||||||
Returns True if the given object is a Report.
|
Returns True if the given object is a Report.
|
||||||
@ -71,13 +74,18 @@ def run_report(job_result, *args, **kwargs):
|
|||||||
"""
|
"""
|
||||||
module_name, report_name = job_result.name.split('.', 1)
|
module_name, report_name = job_result.name.split('.', 1)
|
||||||
report = get_report(module_name, report_name)
|
report = get_report(module_name, report_name)
|
||||||
report.run(job_result)
|
|
||||||
|
try:
|
||||||
|
report.run(job_result)
|
||||||
|
except Exception:
|
||||||
|
job_result.set_status(JobResultStatusChoices.STATUS_ERRORED)
|
||||||
|
logging.error(f"Error during execution of report {job_result.name}")
|
||||||
|
|
||||||
# Delete any previous terminal state results
|
# Delete any previous terminal state results
|
||||||
JobResult.objects.filter(
|
JobResult.objects.filter(
|
||||||
obj_type=job_result.obj_type,
|
obj_type=job_result.obj_type,
|
||||||
name=job_result.name,
|
name=job_result.name,
|
||||||
status=JobResultStatusChoices.TERMINAL_STATE_CHOICES
|
status__in=JobResultStatusChoices.TERMINAL_STATE_CHOICES
|
||||||
).exclude(
|
).exclude(
|
||||||
pk=job_result.pk
|
pk=job_result.pk
|
||||||
).delete()
|
).delete()
|
||||||
|
@ -399,10 +399,6 @@ def run_script(data, request, commit=True, *args, **kwargs):
|
|||||||
A wrapper for calling Script.run(). This performs error handling and provides a hook for committing changes. It
|
A wrapper for calling Script.run(). This performs error handling and provides a hook for committing changes. It
|
||||||
exists outside of the Script class to ensure it cannot be overridden by a script author.
|
exists outside of the Script class to ensure it cannot be overridden by a script author.
|
||||||
"""
|
"""
|
||||||
output = None
|
|
||||||
start_time = None
|
|
||||||
end_time = None
|
|
||||||
|
|
||||||
job_result = kwargs.pop('job_result')
|
job_result = kwargs.pop('job_result')
|
||||||
module, script_name = job_result.name.split('.', 1)
|
module, script_name = job_result.name.split('.', 1)
|
||||||
|
|
||||||
@ -463,7 +459,7 @@ def run_script(data, request, commit=True, *args, **kwargs):
|
|||||||
JobResult.objects.filter(
|
JobResult.objects.filter(
|
||||||
obj_type=job_result.obj_type,
|
obj_type=job_result.obj_type,
|
||||||
name=job_result.name,
|
name=job_result.name,
|
||||||
status=JobResultStatusChoices.TERMINAL_STATE_CHOICES
|
status__in=JobResultStatusChoices.TERMINAL_STATE_CHOICES
|
||||||
).exclude(
|
).exclude(
|
||||||
pk=job_result.pk
|
pk=job_result.pk
|
||||||
).delete()
|
).delete()
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import django_tables2 as tables
|
import django_tables2 as tables
|
||||||
from django_tables2.utils import Accessor
|
from django_tables2.utils import Accessor
|
||||||
|
|
||||||
from utilities.tables import BaseTable, BooleanColumn, ButtonsColumn, ColorColumn, ToggleColumn
|
from utilities.tables import BaseTable, BooleanColumn, ColorColumn, ToggleColumn
|
||||||
from .models import ConfigContext, ObjectChange, JobResult, Tag, TaggedItem
|
from .models import ConfigContext, ObjectChange, Tag, TaggedItem
|
||||||
|
|
||||||
TAGGED_ITEM = """
|
TAGGED_ITEM = """
|
||||||
{% if value.get_absolute_url %}
|
{% if value.get_absolute_url %}
|
||||||
|
@ -8,9 +8,9 @@ from rest_framework import status
|
|||||||
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Rack, RackGroup, RackRole, Site
|
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Rack, RackGroup, RackRole, Site
|
||||||
from extras.api.views import ScriptViewSet
|
from extras.api.views import ScriptViewSet
|
||||||
from extras.models import ConfigContext, Graph, ExportTemplate, Tag
|
from extras.models import ConfigContext, Graph, ExportTemplate, Tag
|
||||||
|
from extras.reports import Report
|
||||||
from extras.scripts import BooleanVar, IntegerVar, Script, StringVar
|
from extras.scripts import BooleanVar, IntegerVar, Script, StringVar
|
||||||
from utilities.testing import APITestCase, APIViewTestCases
|
from utilities.testing import APITestCase, APIViewTestCases
|
||||||
from utilities.utils import copy_safe_request
|
|
||||||
|
|
||||||
|
|
||||||
class AppTest(APITestCase):
|
class AppTest(APITestCase):
|
||||||
@ -208,6 +208,37 @@ class ConfigContextTest(APIViewTestCases.APIViewTestCase):
|
|||||||
self.assertEqual(rendered_context['bar'], 456)
|
self.assertEqual(rendered_context['bar'], 456)
|
||||||
|
|
||||||
|
|
||||||
|
class ReportTest(APITestCase):
|
||||||
|
|
||||||
|
class TestReport(Report):
|
||||||
|
pass # The report is not actually run, we are testing that the view enqueues the job
|
||||||
|
|
||||||
|
def get_test_report(self, *args):
|
||||||
|
return self.TestReport
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
|
||||||
|
super().setUp()
|
||||||
|
|
||||||
|
# Monkey-patch the API viewset's _get_script method to return our test script above
|
||||||
|
ReportViewSet._get_report = self.get_test_report
|
||||||
|
|
||||||
|
def test_get_report(self):
|
||||||
|
|
||||||
|
url = reverse('extras-api:report-detail', kwargs={'pk': None})
|
||||||
|
response = self.client.get(url, **self.header)
|
||||||
|
|
||||||
|
self.assertEqual(response.data['name'], self.TestReport.__name__)
|
||||||
|
|
||||||
|
def test_run_report(self):
|
||||||
|
|
||||||
|
url = reverse('extras-api:report-detail', kwargs={'pk': None})
|
||||||
|
response = self.client.post(url, {}, format='json', **self.header)
|
||||||
|
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||||
|
|
||||||
|
self.assertEqual(response.data['result']['status']['value'], 'pending')
|
||||||
|
|
||||||
|
|
||||||
class ScriptTest(APITestCase):
|
class ScriptTest(APITestCase):
|
||||||
|
|
||||||
class TestScript(Script):
|
class TestScript(Script):
|
||||||
|
@ -36,12 +36,12 @@ urlpatterns = [
|
|||||||
|
|
||||||
# Reports
|
# Reports
|
||||||
path('reports/', views.ReportListView.as_view(), name='report_list'),
|
path('reports/', views.ReportListView.as_view(), name='report_list'),
|
||||||
path('reports/<str:name>/', views.ReportView.as_view(), name='report'),
|
path('reports/<str:module>.<str:name>/', views.ReportView.as_view(), name='report'),
|
||||||
path('reports/<str:name>/run/', views.ReportRunView.as_view(), name='report_run'),
|
path('reports/results/<int:job_result_pk>/', views.ReportResultView.as_view(), name='report_result'),
|
||||||
|
|
||||||
# Scripts
|
# Scripts
|
||||||
path('scripts/', views.ScriptListView.as_view(), name='script_list'),
|
path('scripts/', views.ScriptListView.as_view(), name='script_list'),
|
||||||
path('scripts/<str:module>/<str:name>/', views.ScriptView.as_view(), name='script'),
|
path('scripts/<str:module>.<str:name>/', views.ScriptView.as_view(), name='script'),
|
||||||
path('scripts/<str:module>/<str:name>/result/<int:job_result_pk>/', views.ScriptResultView.as_view(), name='script_result'),
|
path('scripts/results/<int:job_result_pk>/', views.ScriptResultView.as_view(), name='script_result'),
|
||||||
|
|
||||||
]
|
]
|
||||||
|
@ -350,12 +350,10 @@ class ReportListView(ContentTypePermissionRequiredMixin, View):
|
|||||||
|
|
||||||
|
|
||||||
class GetReportMixin:
|
class GetReportMixin:
|
||||||
def get_report(self, name):
|
def _get_report(self, name, module=None):
|
||||||
if '.' not in name:
|
if module is None:
|
||||||
raise Http404
|
module, name = name.split('.', 1)
|
||||||
# Retrieve the Report by "<module>.<report>"
|
report = get_report(module, name)
|
||||||
module_name, report_name = name.split('.', 1)
|
|
||||||
report = get_report(module_name, report_name)
|
|
||||||
if report is None:
|
if report is None:
|
||||||
raise Http404
|
raise Http404
|
||||||
|
|
||||||
@ -369,43 +367,29 @@ class ReportView(GetReportMixin, ContentTypePermissionRequiredMixin, View):
|
|||||||
def get_required_permission(self):
|
def get_required_permission(self):
|
||||||
return 'extras.view_reportresult'
|
return 'extras.view_reportresult'
|
||||||
|
|
||||||
def get(self, request, name):
|
def get(self, request, module, name):
|
||||||
|
|
||||||
report = self.get_report(name)
|
report = self._get_report(name, module)
|
||||||
|
|
||||||
report_content_type = ContentType.objects.get(app_label='extras', model='report')
|
report_content_type = ContentType.objects.get(app_label='extras', model='report')
|
||||||
latest_run_result = JobResult.objects.filter(
|
report.result = JobResult.objects.filter(
|
||||||
obj_type=report_content_type,
|
obj_type=report_content_type,
|
||||||
name=report.full_name,
|
name=report.full_name,
|
||||||
status__in=JobResultStatusChoices.TERMINAL_STATE_CHOICES
|
status__in=JobResultStatusChoices.TERMINAL_STATE_CHOICES
|
||||||
).first()
|
).first()
|
||||||
pending_run_result = JobResult.objects.filter(
|
|
||||||
obj_type=report_content_type,
|
|
||||||
name=report.full_name,
|
|
||||||
status__in=JobResultStatusChoices.NON_TERMINAL_STATE_CHOICES
|
|
||||||
).order_by(
|
|
||||||
'created'
|
|
||||||
).first()
|
|
||||||
|
|
||||||
report.result = latest_run_result
|
|
||||||
report.pending_result = pending_run_result
|
|
||||||
|
|
||||||
return render(request, 'extras/report.html', {
|
return render(request, 'extras/report.html', {
|
||||||
'report': report,
|
'report': report,
|
||||||
'run_form': ConfirmationForm(),
|
'run_form': ConfirmationForm(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
def post(self, request, module, name):
|
||||||
|
|
||||||
class ReportRunView(GetReportMixin, ContentTypePermissionRequiredMixin, View):
|
# Permissions check
|
||||||
"""
|
if not request.user.has_perm('extras.run_report'):
|
||||||
Run a Report and record a new ReportResult.
|
return HttpResponseForbidden()
|
||||||
"""
|
|
||||||
def get_required_permission(self):
|
|
||||||
return 'extras.add_reportresult'
|
|
||||||
|
|
||||||
def post(self, request, name):
|
report = self._get_report(name, module)
|
||||||
|
|
||||||
report = self.get_report(name)
|
|
||||||
|
|
||||||
form = ConfirmationForm(request.POST)
|
form = ConfirmationForm(request.POST)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
@ -419,15 +403,42 @@ class ReportRunView(GetReportMixin, ContentTypePermissionRequiredMixin, View):
|
|||||||
request.user
|
request.user
|
||||||
)
|
)
|
||||||
|
|
||||||
return redirect('extras:report', name=report.full_name)
|
return redirect('extras:report_result', job_result_pk=job_result.pk)
|
||||||
|
|
||||||
|
return render(request, 'extras/report.html', {
|
||||||
|
'report': report,
|
||||||
|
'run_form': form,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
class ReportResultView(ContentTypePermissionRequiredMixin, GetReportMixin, View):
|
||||||
|
|
||||||
|
def get_required_permission(self):
|
||||||
|
return 'extras.view_report'
|
||||||
|
|
||||||
|
def get(self, request, job_result_pk):
|
||||||
|
result = get_object_or_404(JobResult.objects.all(), pk=job_result_pk)
|
||||||
|
report_content_type = ContentType.objects.get(app_label='extras', model='report')
|
||||||
|
if result.obj_type != report_content_type:
|
||||||
|
raise Http404
|
||||||
|
|
||||||
|
report = self._get_report(result.name)
|
||||||
|
|
||||||
|
return render(request, 'extras/report_result.html', {
|
||||||
|
'report': report,
|
||||||
|
'result': result,
|
||||||
|
'class_name': report.name,
|
||||||
|
'run_form': ConfirmationForm(),
|
||||||
|
})
|
||||||
|
|
||||||
#
|
#
|
||||||
# Scripts
|
# Scripts
|
||||||
#
|
#
|
||||||
|
|
||||||
class GetScriptMixin:
|
class GetScriptMixin:
|
||||||
def _get_script(self, module, name):
|
def _get_script(self, name, module=None):
|
||||||
|
if module is None:
|
||||||
|
module, name = name.split('.', 1)
|
||||||
scripts = get_scripts()
|
scripts = get_scripts()
|
||||||
try:
|
try:
|
||||||
return scripts[module][name]()
|
return scripts[module][name]()
|
||||||
@ -453,7 +464,7 @@ class ScriptView(ContentTypePermissionRequiredMixin, GetScriptMixin, View):
|
|||||||
return 'extras.view_script'
|
return 'extras.view_script'
|
||||||
|
|
||||||
def get(self, request, module, name):
|
def get(self, request, module, name):
|
||||||
script = self._get_script(module, name)
|
script = self._get_script(name, module)
|
||||||
form = script.as_form(initial=request.GET)
|
form = script.as_form(initial=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)
|
||||||
@ -461,7 +472,8 @@ class ScriptView(ContentTypePermissionRequiredMixin, GetScriptMixin, View):
|
|||||||
script.result = JobResult.objects.filter(
|
script.result = JobResult.objects.filter(
|
||||||
obj_type=script_content_type,
|
obj_type=script_content_type,
|
||||||
name=script.full_name,
|
name=script.full_name,
|
||||||
status__in=JobResultStatusChoices.NON_TERMINAL_STATE_CHOICES
|
).exclude(
|
||||||
|
status__in=JobResultStatusChoices.TERMINAL_STATE_CHOICES
|
||||||
).first()
|
).first()
|
||||||
|
|
||||||
return render(request, 'extras/script.html', {
|
return render(request, 'extras/script.html', {
|
||||||
@ -476,10 +488,8 @@ class ScriptView(ContentTypePermissionRequiredMixin, GetScriptMixin, View):
|
|||||||
if not request.user.has_perm('extras.run_script'):
|
if not request.user.has_perm('extras.run_script'):
|
||||||
return HttpResponseForbidden()
|
return HttpResponseForbidden()
|
||||||
|
|
||||||
script = self._get_script(module, name)
|
script = self._get_script(name, module)
|
||||||
form = script.as_form(request.POST, request.FILES)
|
form = script.as_form(request.POST, request.FILES)
|
||||||
output = None
|
|
||||||
execution_time = None
|
|
||||||
|
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
commit = form.cleaned_data.pop('_commit')
|
commit = form.cleaned_data.pop('_commit')
|
||||||
@ -495,7 +505,13 @@ class ScriptView(ContentTypePermissionRequiredMixin, GetScriptMixin, View):
|
|||||||
commit=commit
|
commit=commit
|
||||||
)
|
)
|
||||||
|
|
||||||
return redirect('extras:script_result', module=module, name=name, job_result_pk=job_result.pk)
|
return redirect('extras:script_result', job_result_pk=job_result.pk)
|
||||||
|
|
||||||
|
return render(request, 'extras/script.html', {
|
||||||
|
'module': module,
|
||||||
|
'script': script,
|
||||||
|
'form': form,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
class ScriptResultView(ContentTypePermissionRequiredMixin, GetScriptMixin, View):
|
class ScriptResultView(ContentTypePermissionRequiredMixin, GetScriptMixin, View):
|
||||||
@ -503,19 +519,16 @@ class ScriptResultView(ContentTypePermissionRequiredMixin, GetScriptMixin, View)
|
|||||||
def get_required_permission(self):
|
def get_required_permission(self):
|
||||||
return 'extras.view_script'
|
return 'extras.view_script'
|
||||||
|
|
||||||
def get(self, request, module, name, job_result_pk):
|
def get(self, request, job_result_pk):
|
||||||
script = self._get_script(module, name)
|
|
||||||
form = script.as_form(initial=request.GET)
|
|
||||||
|
|
||||||
# Look for a pending JobResult (use the latest one by creation timestamp)
|
|
||||||
script_content_type = ContentType.objects.get(app_label='extras', model='script')
|
|
||||||
result = get_object_or_404(JobResult.objects.all(), pk=job_result_pk)
|
result = get_object_or_404(JobResult.objects.all(), pk=job_result_pk)
|
||||||
|
script_content_type = ContentType.objects.get(app_label='extras', model='script')
|
||||||
if result.obj_type != script_content_type:
|
if result.obj_type != script_content_type:
|
||||||
raise Http404
|
raise Http404
|
||||||
|
|
||||||
|
script = self._get_script(result.name)
|
||||||
|
|
||||||
return render(request, 'extras/script_result.html', {
|
return render(request, 'extras/script_result.html', {
|
||||||
'module': module,
|
|
||||||
'script': script,
|
'script': script,
|
||||||
'result': result,
|
'result': result,
|
||||||
'class_name': name
|
'class_name': script.__class__.__name__
|
||||||
})
|
})
|
||||||
|
@ -21,7 +21,7 @@
|
|||||||
{% for report in module_reports %}
|
{% for report in module_reports %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<a href="{% url 'extras:report' name=report.full_name %}" name="report.{{ report.name }}"><strong>{{ report.name }}</strong></a>
|
<a href="{% url 'extras:report_result' job_result_pk=report.result.pk %}" name="report.{{ report.name }}"><strong>{{ report.name }}</strong></a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{% include 'extras/inc/job_label.html' with result=report.result %}
|
{% include 'extras/inc/job_label.html' with result=report.result %}
|
||||||
|
@ -16,36 +16,35 @@
|
|||||||
</div>
|
</div>
|
||||||
{% if perms.extras.add_reportresult %}
|
{% if perms.extras.add_reportresult %}
|
||||||
<div class="pull-right noprint">
|
<div class="pull-right noprint">
|
||||||
<form action="{% url 'extras:report_run' name=report.full_name %}" method="post">
|
<form action="{% url 'extras:report' module=report.module name=report.name %}" method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{{ run_form }}
|
{{ run_form }}
|
||||||
<button type="submit" name="_run" class="btn btn-primary"><i class="fa fa-play"></i> Run Report</button>
|
<button type="submit" name="_run" class="btn btn-primary"><i class="fa fa-play"></i> Run Report</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<h1>{{ report.name }}{% include 'extras/inc/job_label.html' with result=report.result %}</h1>
|
<h1>{{ report.name }}</h1>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
{% if report.description %}
|
{% if report.description %}
|
||||||
<p class="lead">{{ report.description }}</p>
|
<p class="lead">{{ report.description }}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if report.result %}
|
<p>
|
||||||
<p>Last run: <strong>{{ report.result.created }}</strong> {% if report.result.completed %} Duration: <strong>{{ report.result.duration }}</strong>{% endif %}</p>
|
Run: <strong>{{ result.created }}</strong>
|
||||||
{% endif %}
|
{% if result.completed %}
|
||||||
{% if report.pending_result %}
|
Duration: <strong>{{ result.duration }}</strong>
|
||||||
<p>
|
{% else %}
|
||||||
Pending run: <strong>{{ report.pending_result.created }}</strong>
|
|
||||||
<span id="pending-result-label">{% include 'extras/inc/job_label.html' with result=report.pending_result %}</span>
|
|
||||||
<img id="pending-result-loader" src="{% static 'img/ajax-loader.gif' %}" />
|
<img id="pending-result-loader" src="{% static 'img/ajax-loader.gif' %}" />
|
||||||
</p>
|
{% endif %}
|
||||||
{% endif %}
|
<span id="pending-result-label">{% include 'extras/inc/job_label.html' with result=result %}</span>
|
||||||
{% if report.result %}
|
</p>
|
||||||
|
{% if result.completed %}
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<strong>Report Methods</strong>
|
<strong>Report Methods</strong>
|
||||||
</div>
|
</div>
|
||||||
<table class="table table-hover panel-body">
|
<table class="table table-hover panel-body">
|
||||||
{% for method, data in report.result.data.items %}
|
{% for method, data in result.data.items %}
|
||||||
<tr>
|
<tr>
|
||||||
<td><code><a href="#{{ method }}">{{ method }}</a></code></td>
|
<td><code><a href="#{{ method }}">{{ method }}</a></code></td>
|
||||||
<td class="text-right report-stats">
|
<td class="text-right report-stats">
|
||||||
@ -72,7 +71,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for method, data in report.result.data.items %}
|
{% for method, data in result.data.items %}
|
||||||
<tr>
|
<tr>
|
||||||
<th colspan="4" style="font-family: monospace">
|
<th colspan="4" style="font-family: monospace">
|
||||||
<a name="{{ method }}"></a>{{ method }}
|
<a name="{{ method }}"></a>{{ method }}
|
||||||
@ -99,11 +98,7 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="well">No results are available for this report. Please run the report first.</div>
|
<div class="well">Pending results</div>
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3">
|
|
||||||
{% if report.result %}
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -111,19 +106,14 @@
|
|||||||
|
|
||||||
{% block javascript %}
|
{% block javascript %}
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
{% if report.pending_result %}
|
{% if not result.completed %}
|
||||||
var pending_result_id = {{ report.pending_result.pk }};
|
var pending_result_id = {{ result.pk }};
|
||||||
{% else %}
|
{% else %}
|
||||||
var pending_result_id = null;
|
var pending_result_id = null;
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
function jobTerminatedAction(){
|
function jobTerminatedAction(){
|
||||||
$('#pending-result-loader').hide();
|
refreshWindow();
|
||||||
var refreshButton = document.createElement('button');
|
|
||||||
refreshButton.className = 'btn btn-xs btn-primary';
|
|
||||||
refreshButton.onclick = refreshWindow;
|
|
||||||
refreshButton.innerHTML = '<i class="fa fa-refresh"></i> Refresh';
|
|
||||||
$('#pending-result-loader').parents('p').append(refreshButton)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
@ -11,7 +11,7 @@
|
|||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<ol class="breadcrumb">
|
<ol class="breadcrumb">
|
||||||
<li><a href="{% url 'extras:script_list' %}">Scripts</a></li>
|
<li><a href="{% url 'extras:script_list' %}">Scripts</a></li>
|
||||||
<li><a href="{% url 'extras:script_list' %}#module.{{ module }}">{{ module|bettertitle }}</a></li>
|
<li><a href="{% url 'extras:script_list' %}#module.{{ script.module }}">{{ script.module|bettertitle }}</a></li>
|
||||||
<li><a href="{% url 'extras:script' module=script.module name=class_name %}">{{ script }}</a></li>
|
<li><a href="{% url 'extras:script' module=script.module name=class_name %}">{{ script }}</a></li>
|
||||||
<li>{{ result.created }}</li>
|
<li>{{ result.created }}</li>
|
||||||
</ol>
|
</ol>
|
||||||
@ -36,9 +36,9 @@
|
|||||||
{% if result.completed %}
|
{% if result.completed %}
|
||||||
Duration: <strong>{{ result.duration }}</strong>
|
Duration: <strong>{{ result.duration }}</strong>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span id="pending-result-label">{% include 'extras/inc/job_label.html' with result=result %}</span>
|
|
||||||
<img id="pending-result-loader" src="{% static 'img/ajax-loader.gif' %}" />
|
<img id="pending-result-loader" src="{% static 'img/ajax-loader.gif' %}" />
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<span id="pending-result-label">{% include 'extras/inc/job_label.html' with result=result %}</span>
|
||||||
</p>
|
</p>
|
||||||
<div role="tabpanel" class="tab-pane active" id="log">
|
<div role="tabpanel" class="tab-pane active" id="log">
|
||||||
{% if result.completed %}
|
{% if result.completed %}
|
||||||
@ -76,6 +76,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="well">Pending results</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div role="tabpanel" class="tab-pane" id="output">
|
<div role="tabpanel" class="tab-pane" id="output">
|
||||||
|
@ -280,7 +280,7 @@
|
|||||||
<table class="table table-hover panel-body">
|
<table class="table table-hover panel-body">
|
||||||
{% for result in report_results %}
|
{% for result in report_results %}
|
||||||
<tr>
|
<tr>
|
||||||
<td><a href="{% url 'extras:report' name=result.name %}">{{ result.name }}</a></td>
|
<td><a href="{% url 'extras:report_result' job_result_pk=result.pk %}">{{ result.name }}</a></td>
|
||||||
<td class="text-right"><span title="{{ result.created }}">{% include 'extras/inc/job_label.html' %}</span></td>
|
<td class="text-right"><span title="{{ result.created }}">{% include 'extras/inc/job_label.html' %}</span></td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@ -270,9 +270,8 @@ class NetBoxFakeRequest:
|
|||||||
A fake request object which is explicitly defined at the module level so it is able to be pickled. It simply
|
A fake request object which is explicitly defined at the module level so it is able to be pickled. It simply
|
||||||
takes what is passed to it as kwargs on init and sets them as instance variables.
|
takes what is passed to it as kwargs on init and sets them as instance variables.
|
||||||
"""
|
"""
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, _dict):
|
||||||
for k, v in kwargs.items():
|
self.__dict__ = _dict
|
||||||
setattr(self, k, v)
|
|
||||||
|
|
||||||
|
|
||||||
def copy_safe_request(request):
|
def copy_safe_request(request):
|
||||||
@ -285,7 +284,7 @@ def copy_safe_request(request):
|
|||||||
for k in HTTP_REQUEST_META_SAFE_COPY
|
for k in HTTP_REQUEST_META_SAFE_COPY
|
||||||
if k in request.META and isinstance(request.META[k], str)
|
if k in request.META and isinstance(request.META[k], str)
|
||||||
}
|
}
|
||||||
return NetBoxFakeRequest(**{
|
return NetBoxFakeRequest({
|
||||||
'META': meta,
|
'META': meta,
|
||||||
'POST': request.POST,
|
'POST': request.POST,
|
||||||
'GET': request.GET,
|
'GET': request.GET,
|
||||||
|
Loading…
Reference in New Issue
Block a user