=== modified file 'doc/changes.rst'
@@ -2,6 +2,12 @@
***************
+Version 0.13
+============
+
+* Allow job files to specify addresses to email on completion
+ (possibly only unsuccessful completion).
+
.. _version_0_12_1:
Version 0.12.1
=== modified file 'lava_scheduler_app/models.py'
@@ -1,13 +1,19 @@
+import logging
import simplejson
+from django.conf import settings
from django.contrib.auth.models import User
-from django.core.exceptions import ValidationError
+from django.contrib.sites.models import Site
+from django.core.exceptions import ImproperlyConfigured, ValidationError
+from django.core.mail import send_mail
+from django.core.validators import validate_email
from django.db import models
+from django.template.loader import render_to_string
from django.utils.translation import ugettext as _
from django_restricted_resource.models import RestrictedResource
-from dashboard_app.models import BundleStream
+from dashboard_app.models import Bundle, BundleStream
from lava_dispatcher.job import validate_job_data
@@ -237,20 +243,41 @@
blank = True,
editable = False
)
+
+ @property
+ def duration(self):
+ if self.end_time is None:
+ return None
+ return self.end_time - self.start_time
+
status = models.IntegerField(
choices = STATUS_CHOICES,
default = SUBMITTED,
verbose_name = _(u"Status"),
)
+
definition = models.TextField(
editable = False,
)
+
log_file = models.FileField(
upload_to='lava-logs', default=None, null=True, blank=True)
results_link = models.CharField(
max_length=400, default=None, null=True, blank=True)
+ @property
+ def results_bundle(self):
+ # XXX So this is clearly appalling (it depends on the format of bundle
+ # links, for example). We should just have a fkey to Bundle.
+ if not self.results_link:
+ return None
+ sha1 = self.results_link.strip('/').split('/')[-1]
+ try:
+ return Bundle.objects.get(content_sha1=sha1)
+ except Bundle.DoesNotExist:
+ return None
+
def __unicode__(self):
r = "%s test job" % self.get_status_display()
if self.requested_device:
@@ -274,6 +301,23 @@
else:
raise JSONDataError(
"Neither 'target' nor 'device_type' found in job data.")
+
+ for email_field in 'notify', 'notify_on_incomplete':
+ if email_field in job_data:
+ value = job_data[email_field]
+ msg = ("%r must be a list of email addresses if present"
+ % email_field)
+ if not isinstance(value, list):
+ raise ValueError(msg)
+ for address in value:
+ if not isinstance(address, basestring):
+ raise ValueError(msg)
+ try:
+ validate_email(address)
+ except ValidationError:
+ raise ValueError(
+ "%r is not a valid email address." % address)
+
job_name = job_data.get('job_name', '')
is_check = job_data.get('health_check', False)
@@ -324,6 +368,40 @@
self.status = TestJob.CANCELED
self.save()
+ def _generate_summary_mail(self):
+ domain = '???'
+ try:
+ site = Site.objects.get_current()
+ except (Site.DoesNotExist, ImproperlyConfigured):
+ pass
+ else:
+ domain = site.domain
+ url_prefix = 'http://%s' % domain
+ return render_to_string(
+ 'lava_scheduler_app/job_summary_mail.txt',
+ {'job': self, 'url_prefix': url_prefix})
+
+ def _get_notification_recipients(self):
+ job_data = simplejson.loads(self.definition)
+ recipients = job_data.get('notify', [])
+ if self.status != self.COMPLETE:
+ recipients.extend(job_data.get('notify_on_incomplete', []))
+ return recipients
+
+ def send_summary_mails(self):
+ recipients = self._get_notification_recipients()
+ if not recipients:
+ return
+ mail = self._generate_summary_mail()
+ description = self.description.splitlines()[0]
+ if len(description) > 200:
+ description = description[197:] + '...'
+ logger = logging.getLogger(self.__class__.__name__ + '.' + str(self.pk))
+ logger.info("sending mail to %s", recipients)
+ send_mail(
+ "LAVA job notification: " + description, mail,
+ settings.SERVER_EMAIL, recipients)
+
class DeviceStateTransition(models.Model):
created_on = models.DateTimeField(auto_now_add=True)
=== added file 'lava_scheduler_app/templates/lava_scheduler_app/job_summary_mail.txt'
@@ -0,0 +1,27 @@
+Hi,
+
+The job with id {{ job.id }} has finished. It took {{ job.start_time|timesince:job.end_time }}.
+
+The final status was {{ job.get_status_display }}.
+
+You can see more details at:
+
+ {{ url_prefix }}{{ job.get_absolute_url }}
+{% if job.results_bundle %}
+The results can be summarized as:
+
+ +----------------------+--------+--------+
+ | Test run | Passes | Total |
+ +----------------------+--------+--------+
+{% for run in job.results_bundle.test_runs.all %}{% with results=run.get_summary_results %} | {{ run.test.test_id|ljust:20 }} | {{ results.pass|default:0|rjust:6 }} | {{ results.total|default:0|rjust:6 }} |
+{% endwith %}{% endfor %} +----------------------+--------+--------+
+
+For more details, please see:
+
+ {{ url_prefix }}{{ job.results_bundle.get_absolute_url }}
+
+{% else %}
+No results were reported to the dashboard for this run.
+
+{% endif %}LAVA
+Linaro Automated Validation
=== modified file 'lava_scheduler_daemon/dbjobsource.py'
@@ -33,7 +33,8 @@
implements(IJobSource)
- logger = logging.getLogger(__name__ + '.DatabaseJobSource')
+ def __init__(self):
+ self.logger = logging.getLogger(__name__ + '.DatabaseJobSource')
deferToThread = staticmethod(deferToThread)
@@ -235,7 +236,7 @@
created_by=None, device=device, old_state=old_device_status,
new_state=device.status, message=None, job=job).save()
- if job.health_check is True:
+ if job.health_check:
device.last_health_report_job = job
if job.status == TestJob.INCOMPLETE:
device.health_status = Device.HEALTH_FAIL
@@ -249,6 +250,13 @@
device.save()
job.save()
token.delete()
+ try:
+ job.send_summary_mails()
+ except:
+ # Better to catch all exceptions here and log it than have this
+ # method fail.
+ self.logger.exception(
+ 'sending job summary mails for job %r failed', job.pk)
transaction.commit()
def jobCompleted(self, board_name, exit_code):