From cbcf2290d88461fc80a706c172e40a808a688beb Mon Sep 17 00:00:00 2001 From: donkirkby Date: Tue, 4 Dec 2018 09:57:26 -0800 Subject: [PATCH] Display stdout and stderr from container runs for #739. --- .../management/commands/runcontainer.py | 26 +++++++++++-- kive/container/models.py | 3 ++ .../container/containerlog_detail.html | 39 +++++++++++++++++++ .../container/containerrun_form.html | 10 ++--- kive/container/views.py | 29 +++++++++++++- kive/fleet/tests_slurmlib.py | 3 +- kive/kive/urls.py | 3 ++ 7 files changed, 102 insertions(+), 11 deletions(-) create mode 100644 kive/container/templates/container/containerlog_detail.html diff --git a/kive/container/management/commands/runcontainer.py b/kive/container/management/commands/runcontainer.py index a2bcfba70..885d0e075 100644 --- a/kive/container/management/commands/runcontainer.py +++ b/kive/container/management/commands/runcontainer.py @@ -1,15 +1,17 @@ from __future__ import print_function + +import errno import logging import os import shutil from subprocess import call from tempfile import mkdtemp -import errno from django.conf import settings from django.core.management.base import BaseCommand, CommandError +from django.utils import timezone -from container.models import ContainerRun, ContainerArgument +from container.models import ContainerRun, ContainerArgument, ContainerLog from librarian.models import Dataset logger = logging.getLogger(__name__) @@ -50,6 +52,11 @@ def update_state(self, run_id, old_state, new_state): def create_sandbox(self, run): sandbox_root = os.path.join(settings.MEDIA_ROOT, settings.SANDBOX_PATH) + try: + os.mkdir(sandbox_root) + except OSError as ex: + if ex.errno != errno.EEXIST: + raise prefix = 'user{}_run{}_'.format(run.user.username, run.pk) run.sandbox_path = mkdtemp(prefix=prefix, dir=sandbox_root) @@ -97,5 +104,16 @@ def save_outputs(self, run): user=run.user) run.datasets.create(dataset=dataset, argument=argument) - - run.state = ContainerRun.COMPLETE + logs_path = os.path.join(run.sandbox_path, 'logs') + for file_name, log_type in (('stdout.txt', ContainerLog.STDOUT), + ('stderr.txt', ContainerLog.STDERR)): + # noinspection PyUnresolvedReferences,PyProtectedMember + chunk_size = ContainerLog._meta.get_field('short_text').max_length + with open(os.path.join(logs_path, file_name)) as f: + chunk = f.read(chunk_size) + run.logs.create(type=log_type, short_text=chunk) + + run.state = (ContainerRun.COMPLETE + if run.return_code == 0 + else ContainerRun.FAILED) + run.end_time = timezone.now() diff --git a/kive/container/models.py b/kive/container/models.py index cfb1310c3..84f4300c1 100644 --- a/kive/container/models.py +++ b/kive/container/models.py @@ -472,3 +472,6 @@ class ContainerLog(models.Model): help_text="Holds the log text if it's shorter than the max length.") long_text = models.FileField( help_text="Holds the log text if it's longer than the max length.") + + def get_absolute_url(self): + return reverse('container_log_detail', kwargs=dict(pk=self.pk)) diff --git a/kive/container/templates/container/containerlog_detail.html b/kive/container/templates/container/containerlog_detail.html new file mode 100644 index 000000000..a0de991fc --- /dev/null +++ b/kive/container/templates/container/containerlog_detail.html @@ -0,0 +1,39 @@ + + +{% extends "portal/base.html" %} + +{% block title %}Container Log{% endblock %} + +{% block javascript %} + + + +{% endblock %} + +{% block stylesheets %} + +{% endblock %} + +{% block widget_media %} +{{ form.media }} +{% endblock %} + +{% block content %} + + + +

Container Log - +{% if object.type == 'E' %} + stderr +{% else %} + stdout +{% endif %} +

+ +
+
+{{ object.short_text }}
+
+
+ +{% endblock %} diff --git a/kive/container/templates/container/containerrun_form.html b/kive/container/templates/container/containerrun_form.html index da7a6cbb1..d8a710405 100644 --- a/kive/container/templates/container/containerrun_form.html +++ b/kive/container/templates/container/containerrun_form.html @@ -156,11 +156,11 @@

Groups allowed

Size Date -{% for run_dataset in object.datasets.all %} - {{ run_dataset.argument.type }} - {{ run_dataset.argument.name }} - {{ run_dataset.dataset.get_formatted_filesize }} - {{ run_dataset.dataset.date_created }} +{% for data_entry in data_entries %} + {{ data_entry.type }} + {{ data_entry.name }} + {{ data_entry.size }} + {{ data_entry.created }} {% endfor %} diff --git a/kive/container/views.py b/kive/container/views.py index b9ae1c41b..b61e9dc98 100644 --- a/kive/container/views.py +++ b/kive/container/views.py @@ -12,13 +12,14 @@ from django.urls import reverse from django.utils.decorators import method_decorator from django.views.generic.base import TemplateView +from django.views.generic.detail import DetailView from django.views.generic.edit import CreateView, UpdateView, ModelFormMixin import rest_framework.reverse from container.forms import ContainerFamilyForm, ContainerForm, \ ContainerUpdateForm, ContainerAppForm, ContainerRunForm, BatchForm from container.models import ContainerFamily, Container, ContainerApp, \ - ContainerRun, ContainerArgument + ContainerRun, ContainerArgument, ContainerLog from file_access_utils import compute_md5 from portal.views import developer_check, AdminViewMixin @@ -249,7 +250,33 @@ def get_context_data(self, **kwargs): context['is_dev'] = developer_check(self.request.user) state_names = dict(ContainerRun.STATES) context['state_name'] = state_names.get(self.object.state) + data_entries = [] + type_names = dict(ContainerArgument.TYPES) + input_count = 0 + for run_dataset in self.object.datasets.all(): + data_entries.append(dict( + type=type_names[run_dataset.argument.type], + url=run_dataset.dataset.get_view_url, + name=run_dataset.argument.name, + size=run_dataset.dataset.get_formatted_filesize, + created=run_dataset.dataset.date_created)) + if run_dataset.argument.type == ContainerArgument.INPUT: + input_count += 1 + log_names = dict(ContainerLog.TYPES) + for log in self.object.logs.order_by('type'): + data_entries.insert(input_count, dict( + type='Log', + url=log.get_absolute_url(), + name=log_names[log.type], + size=len(log.short_text), + created=self.object.end_time)) + context['data_entries'] = data_entries return context def get_success_url(self): return reverse('container_runs') + + +@method_decorator(login_required, name='dispatch') +class ContainerLogDetail(DetailView): + model = ContainerLog diff --git a/kive/fleet/tests_slurmlib.py b/kive/fleet/tests_slurmlib.py index b49645a09..c2bd147f7 100644 --- a/kive/fleet/tests_slurmlib.py +++ b/kive/fleet/tests_slurmlib.py @@ -16,7 +16,7 @@ import fleet.slurmlib as slurmlib from django.conf import settings -from django.test import TestCase +from django.test import TestCase, skipIfDBFeature # NOTE: Here, select which SlurmScheduler to test. # we select the DummySlurmScheduler by default, so that the automatic tests @@ -87,6 +87,7 @@ def get_accounting_info(jhandles=None, sched_cls=None): return curstates +@skipIfDBFeature('is_mocked') # Doesn't use the database, but this test is slow. class SlurmDummyTests(TestCase): def setUp(self): self.addTypeEqualityFunc(str, self.assertMultiLineEqual) diff --git a/kive/kive/urls.py b/kive/kive/urls.py index fe6eb4a6f..8fc2403cd 100644 --- a/kive/kive/urls.py +++ b/kive/kive/urls.py @@ -108,6 +108,9 @@ url(r'^container_runs/(?P\d+)/$', container.views.ContainerRunUpdate.as_view(), name='container_run_detail'), + url(r'^container_logs/(?P\d+)/$', + container.views.ContainerLogDetail.as_view(), + name='container_log_detail'), url(r'^datatypes$', metadata.views.datatypes, name='datatypes'), url(r'^datatypes/(?P\d+)/$', metadata.views.datatype_detail, name='datatype_detail'),