From patchwork Fri Jan 4 16:26:13 2013 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Andy Doan X-Patchwork-Id: 13803 Return-Path: X-Original-To: patchwork@peony.canonical.com Delivered-To: patchwork@peony.canonical.com Received: from fiordland.canonical.com (fiordland.canonical.com [91.189.94.145]) by peony.canonical.com (Postfix) with ESMTP id 9EF1023F74 for ; Fri, 4 Jan 2013 16:26:19 +0000 (UTC) Received: from mail-vc0-f172.google.com (mail-vc0-f172.google.com [209.85.220.172]) by fiordland.canonical.com (Postfix) with ESMTP id 231A4A18128 for ; Fri, 4 Jan 2013 16:26:19 +0000 (UTC) Received: by mail-vc0-f172.google.com with SMTP id fw7so16699994vcb.17 for ; Fri, 04 Jan 2013 08:26:18 -0800 (PST) X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=20120113; h=x-received:x-forwarded-to:x-forwarded-for:delivered-to:x-received :received-spf:content-type:mime-version:x-launchpad-project :x-launchpad-branch:x-launchpad-message-rationale :x-launchpad-branch-revision-number:x-launchpad-notification-type:to :from:subject:message-id:date:reply-to:sender:errors-to:precedence :x-generated-by:x-launchpad-hash:x-gm-message-state; bh=6iB2jaDR2w+gsAfi8X3v1EXicGob9rEz6Lw8rnZJxXg=; b=NYTCAYLEoyffZclya/rU1dVYyKKA87IN+2gQEhernht6+QKjT1rOPi+TnnygLZvVyW KCpJrqGCt1hRJX1ZcJk3QwULHSBHTWTlaycyYqIMZiNSM+cdbBxp4h49rQZez8J5+GD2 e2CN1EjCXDs67rI3YhUXZALwY9Khoc865J++piySxFiiAb62LzOZVuFubSktymG9R8qW kr6cWZ4HKYY0SkzZfP7msJFtnH56WfqJuv2gft2zPLFct3AqcxCDqNU8ZKeYQZzScYQ2 oR9d9tnDuFDoNO92z3oRecOpB9ar2hUHG7e96iPok0Pt4qCve/bYzpjWjKs1d9C7R5VR QaFQ== X-Received: by 10.58.74.196 with SMTP id w4mr172283vev.7.1357316778514; Fri, 04 Jan 2013 08:26:18 -0800 (PST) X-Forwarded-To: linaro-patchwork@canonical.com X-Forwarded-For: patch@linaro.org linaro-patchwork@canonical.com Delivered-To: patches@linaro.org Received: by 10.58.145.101 with SMTP id st5csp144869veb; Fri, 4 Jan 2013 08:26:15 -0800 (PST) X-Received: by 10.180.102.230 with SMTP id fr6mr82432933wib.4.1357316774548; Fri, 04 Jan 2013 08:26:14 -0800 (PST) Received: from indium.canonical.com (indium.canonical.com. [91.189.90.7]) by mx.google.com with ESMTPS id l6si76883115wjy.65.2013.01.04.08.26.14 (version=TLSv1/SSLv3 cipher=OTHER); Fri, 04 Jan 2013 08:26:14 -0800 (PST) Received-SPF: pass (google.com: best guess record for domain of bounces@canonical.com designates 91.189.90.7 as permitted sender) client-ip=91.189.90.7; Authentication-Results: mx.google.com; spf=pass (google.com: best guess record for domain of bounces@canonical.com designates 91.189.90.7 as permitted sender) smtp.mail=bounces@canonical.com Received: from ackee.canonical.com ([91.189.89.26]) by indium.canonical.com with esmtp (Exim 4.71 #1 (Debian)) id 1TrA61-0001BF-VC for ; Fri, 04 Jan 2013 16:26:13 +0000 Received: from ackee.canonical.com (localhost [127.0.0.1]) by ackee.canonical.com (Postfix) with ESMTP id D88B4E02EA for ; Fri, 4 Jan 2013 16:26:13 +0000 (UTC) MIME-Version: 1.0 X-Launchpad-Project: lava-scheduler X-Launchpad-Branch: ~linaro-validation/lava-scheduler/trunk X-Launchpad-Message-Rationale: Subscriber X-Launchpad-Branch-Revision-Number: 233 X-Launchpad-Notification-Type: branch-revision To: Linaro Patch Tracker From: noreply@launchpad.net Subject: [Branch ~linaro-validation/lava-scheduler/trunk] Rev 233: add ability to annotate/report failures Message-Id: <20130104162613.6222.59847.launchpad@ackee.canonical.com> Date: Fri, 04 Jan 2013 16:26:13 -0000 Reply-To: noreply@launchpad.net Sender: bounces@canonical.com Errors-To: bounces@canonical.com Precedence: bulk X-Generated-By: Launchpad (canonical.com); Revision="16393"; Instance="launchpad-lazr.conf" X-Launchpad-Hash: 1b78188a9713f343ce470ac813fade5263a8cee3 X-Gm-Message-State: ALoCoQlWVPcLyqE8guFmrTx/wIk2gtZ5wjTxMB36CDUdWlP/g9vtQzSktdLdBnpwaXMPhF7Tt66O Merge authors: Andy Doan (doanac) Related merge proposals: https://code.launchpad.net/~doanac/lava-scheduler/failure-reporting/+merge/141679 proposed by: Andy Doan (doanac) ------------------------------------------------------------ revno: 233 [merge] committer: Andy Doan branch nick: lava-scheduler timestamp: Fri 2013-01-04 10:25:09 -0600 message: add ability to annotate/report failures added: lava_scheduler_app/migrations/0029_auto__add_jobfailuretag__add_field_testjob_failure_comment.py lava_scheduler_app/templates/lava_scheduler_app/failure_report.html lava_scheduler_app/templates/lava_scheduler_app/job_annotate_failure.html modified: doc/changes.rst lava_scheduler_app/admin.py lava_scheduler_app/models.py lava_scheduler_app/templates/lava_scheduler_app/job_sidebar.html lava_scheduler_app/templates/lava_scheduler_app/reports.html lava_scheduler_app/urls.py lava_scheduler_app/views.py --- lp:lava-scheduler https://code.launchpad.net/~linaro-validation/lava-scheduler/trunk You are subscribed to branch lp:lava-scheduler. To unsubscribe from this branch go to https://code.launchpad.net/~linaro-validation/lava-scheduler/trunk/+edit-subscription === modified file 'doc/changes.rst' --- doc/changes.rst 2012-12-13 23:58:29 +0000 +++ doc/changes.rst 2013-01-04 16:25:09 +0000 @@ -6,6 +6,7 @@ Version 0.26 ============= * Unreleased +* Added ability to annotate failures .. _version_0_25: === modified file 'lava_scheduler_app/admin.py' --- lava_scheduler_app/admin.py 2012-10-30 08:27:20 +0000 +++ lava_scheduler_app/admin.py 2013-01-02 19:27:56 +0000 @@ -1,6 +1,6 @@ from django.contrib import admin from lava_scheduler_app.models import ( - Device, DeviceStateTransition, DeviceType, TestJob, Tag, + Device, DeviceStateTransition, DeviceType, TestJob, Tag, JobFailureTag, ) # XXX These actions should really go to another screen that asks for a reason. @@ -55,3 +55,4 @@ admin.site.register(DeviceType) admin.site.register(TestJob, TestJobAdmin) admin.site.register(Tag) +admin.site.register(JobFailureTag) === added file 'lava_scheduler_app/migrations/0029_auto__add_jobfailuretag__add_field_testjob_failure_comment.py' --- lava_scheduler_app/migrations/0029_auto__add_jobfailuretag__add_field_testjob_failure_comment.py 1970-01-01 00:00:00 +0000 +++ lava_scheduler_app/migrations/0029_auto__add_jobfailuretag__add_field_testjob_failure_comment.py 2013-01-02 19:27:56 +0000 @@ -0,0 +1,181 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding model 'JobFailureTag' + db.create_table('lava_scheduler_app_jobfailuretag', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('name', self.gf('django.db.models.fields.CharField')(unique=True, max_length=256)), + ('description', self.gf('django.db.models.fields.TextField')(null=True, blank=True)), + )) + db.send_create_signal('lava_scheduler_app', ['JobFailureTag']) + + # Adding field 'TestJob.failure_comment' + db.add_column('lava_scheduler_app_testjob', 'failure_comment', + self.gf('django.db.models.fields.TextField')(null=True, blank=True), + keep_default=False) + + # Adding M2M table for field failure_tags on 'TestJob' + db.create_table('lava_scheduler_app_testjob_failure_tags', ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('testjob', models.ForeignKey(orm['lava_scheduler_app.testjob'], null=False)), + ('jobfailuretag', models.ForeignKey(orm['lava_scheduler_app.jobfailuretag'], null=False)) + )) + db.create_unique('lava_scheduler_app_testjob_failure_tags', ['testjob_id', 'jobfailuretag_id']) + + + def backwards(self, orm): + # Deleting model 'JobFailureTag' + db.delete_table('lava_scheduler_app_jobfailuretag') + + # Deleting field 'TestJob.failure_comment' + db.delete_column('lava_scheduler_app_testjob', 'failure_comment') + + # Removing M2M table for field failure_tags on 'TestJob' + db.delete_table('lava_scheduler_app_testjob_failure_tags') + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'dashboard_app.bundle': { + 'Meta': {'ordering': "['-uploaded_on']", 'object_name': 'Bundle'}, + '_gz_content': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'null': 'True', 'db_column': "'gz_content'"}), + '_raw_content': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'null': 'True', 'db_column': "'content'"}), + 'bundle_stream': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'bundles'", 'to': "orm['dashboard_app.BundleStream']"}), + 'content_filename': ('django.db.models.fields.CharField', [], {'max_length': '256'}), + 'content_sha1': ('django.db.models.fields.CharField', [], {'max_length': '40', 'unique': 'True', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_deserialized': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'uploaded_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'uploaded_bundles'", 'null': 'True', 'to': "orm['auth.User']"}), + 'uploaded_on': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.utcnow'}) + }, + 'dashboard_app.bundlestream': { + 'Meta': {'object_name': 'BundleStream'}, + 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.Group']", 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_anonymous': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_public': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}), + 'pathname': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '128'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}) + }, + 'lava_scheduler_app.device': { + 'Meta': {'object_name': 'Device'}, + 'current_job': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['lava_scheduler_app.TestJob']", 'blank': 'True', 'unique': 'True'}), + 'device_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['lava_scheduler_app.DeviceType']"}), + 'device_version': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '200', 'null': 'True', 'blank': 'True'}), + 'health_status': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'hostname': ('django.db.models.fields.CharField', [], {'max_length': '200', 'primary_key': 'True'}), + 'last_health_report_job': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['lava_scheduler_app.TestJob']", 'blank': 'True', 'unique': 'True'}), + 'status': ('django.db.models.fields.IntegerField', [], {'default': '1'}), + 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['lava_scheduler_app.Tag']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'lava_scheduler_app.devicestatetransition': { + 'Meta': {'object_name': 'DeviceStateTransition'}, + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}), + 'created_on': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'device': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'transitions'", 'to': "orm['lava_scheduler_app.Device']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'job': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['lava_scheduler_app.TestJob']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}), + 'message': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'new_state': ('django.db.models.fields.IntegerField', [], {}), + 'old_state': ('django.db.models.fields.IntegerField', [], {}) + }, + 'lava_scheduler_app.devicetype': { + 'Meta': {'object_name': 'DeviceType'}, + 'display': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'health_check_job': ('django.db.models.fields.TextField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'name': ('django.db.models.fields.SlugField', [], {'max_length': '50', 'primary_key': 'True'}) + }, + 'lava_scheduler_app.jobfailuretag': { + 'Meta': {'object_name': 'JobFailureTag'}, + 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '256'}) + }, + 'lava_scheduler_app.tag': { + 'Meta': {'object_name': 'Tag'}, + 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '50'}) + }, + 'lava_scheduler_app.testjob': { + 'Meta': {'object_name': 'TestJob'}, + '_results_bundle': ('django.db.models.fields.related.OneToOneField', [], {'null': 'True', 'db_column': "'results_bundle_id'", 'on_delete': 'models.SET_NULL', 'to': "orm['dashboard_app.Bundle']", 'blank': 'True', 'unique': 'True'}), + '_results_link': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '400', 'null': 'True', 'db_column': "'results_link'", 'blank': 'True'}), + 'actual_device': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'+'", 'null': 'True', 'blank': 'True', 'to': "orm['lava_scheduler_app.Device']"}), + 'definition': ('django.db.models.fields.TextField', [], {}), + 'description': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '200', 'null': 'True', 'blank': 'True'}), + 'end_time': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'failure_comment': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'failure_tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'failure_tags'", 'blank': 'True', 'to': "orm['lava_scheduler_app.JobFailureTag']"}), + 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.Group']", 'null': 'True', 'blank': 'True'}), + 'health_check': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_public': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'log_file': ('django.db.models.fields.files.FileField', [], {'default': 'None', 'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'priority': ('django.db.models.fields.IntegerField', [], {'default': '50'}), + 'requested_device': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'+'", 'null': 'True', 'blank': 'True', 'to': "orm['lava_scheduler_app.Device']"}), + 'requested_device_type': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'+'", 'null': 'True', 'blank': 'True', 'to': "orm['lava_scheduler_app.DeviceType']"}), + 'start_time': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'status': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'submit_time': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'submit_token': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['linaro_django_xmlrpc.AuthToken']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}), + 'submitter': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'+'", 'to': "orm['auth.User']"}), + 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['lava_scheduler_app.Tag']", 'symmetrical': 'False', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}) + }, + 'linaro_django_xmlrpc.authtoken': { + 'Meta': {'object_name': 'AuthToken'}, + 'created_on': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_used_on': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'secret': ('django.db.models.fields.CharField', [], {'default': "'gz3f80buhio70b6ptm90c0bly6640oiylkimx0t3okbuq5ckezltlfyiz0ndcmgd8osaqu9h9mc8224108zatlq2hs8drzq0cgbqc22ia6f4lf7bg98r0i12nhti33yj'", 'unique': 'True', 'max_length': '128'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'auth_tokens'", 'to': "orm['auth.User']"}) + } + } + + complete_apps = ['lava_scheduler_app'] \ No newline at end of file === modified file 'lava_scheduler_app/models.py' --- lava_scheduler_app/models.py 2012-12-03 05:12:52 +0000 +++ lava_scheduler_app/models.py 2013-01-03 16:12:15 +0000 @@ -201,6 +201,18 @@ # return device_type.device_set.all() +class JobFailureTag(models.Model): + """ + Allows us to maintain a set of common ways jobs fail. These can then be + associated with a TestJob so we can do easy data mining + """ + name = models.CharField(unique=True, max_length=256) + + description = models.TextField(null=True, blank=True) + + def __unicode__(self): + return self.name + class TestJob(RestrictedResource): """ @@ -313,6 +325,10 @@ log_file = models.FileField( upload_to='lava-logs', default=None, null=True, blank=True) + failure_tags = models.ManyToManyField( + JobFailureTag, blank=True, related_name='failure_tags') + failure_comment = models.TextField(null=True, blank=True) + _results_link = models.CharField( max_length=400, default=None, null=True, blank=True, db_column="results_link") @@ -436,8 +452,20 @@ job.tags.add(tag) return job + def _can_admin(self, user): + """ used to check for things like if the user can cancel or annotate + a job failure + """ + return user.is_superuser or user == self.submitter + + def can_annotate(self, user): + """ + Permission required for user to add failure information to a job + """ + return self._can_admin(user) + def can_cancel(self, user): - return user.is_superuser or user == self.submitter + return self._can_admin(user) def cancel(self): if self.status == TestJob.RUNNING: === added file 'lava_scheduler_app/templates/lava_scheduler_app/failure_report.html' --- lava_scheduler_app/templates/lava_scheduler_app/failure_report.html 1970-01-01 00:00:00 +0000 +++ lava_scheduler_app/templates/lava_scheduler_app/failure_report.html 2013-01-03 16:12:15 +0000 @@ -0,0 +1,9 @@ +{% extends "lava_scheduler_app/_content.html" %} + +{% load django_tables2 %} + +{% block content %} +

Failure Report

+{% render_table failed_job_table %} + +{% endblock %} === added file 'lava_scheduler_app/templates/lava_scheduler_app/job_annotate_failure.html' --- lava_scheduler_app/templates/lava_scheduler_app/job_annotate_failure.html 1970-01-01 00:00:00 +0000 +++ lava_scheduler_app/templates/lava_scheduler_app/job_annotate_failure.html 2013-01-03 16:12:15 +0000 @@ -0,0 +1,16 @@ +{% extends "lava_scheduler_app/job_sidebar.html" %} + +{% block content %} +

Annotate Job Failure - {{ job.id }}

+ +{% if form.errors %} +

Errors found in submission

+{{ form.errors }} +{% endif %} + +
+{% csrf_token %} +{{ form }} + +
+{% endblock %} === modified file 'lava_scheduler_app/templates/lava_scheduler_app/job_sidebar.html' --- lava_scheduler_app/templates/lava_scheduler_app/job_sidebar.html 2012-11-13 20:44:35 +0000 +++ lava_scheduler_app/templates/lava_scheduler_app/job_sidebar.html 2013-01-02 19:27:56 +0000 @@ -83,14 +83,21 @@ {% endif %} +{% if show_cancel or show_failure %} +

Actions

{% if show_cancel %} -

Actions

{% csrf_token %}
{% endif %} +{% if show_failure %} + +{% endif %} +{% endif %} {% endblock %} === modified file 'lava_scheduler_app/templates/lava_scheduler_app/reports.html' --- lava_scheduler_app/templates/lava_scheduler_app/reports.html 2012-07-20 20:29:27 +0000 +++ lava_scheduler_app/templates/lava_scheduler_app/reports.html 2013-01-02 22:33:19 +0000 @@ -13,7 +13,7 @@ var ddates= []; {% for day in health_day_report %} dpass.push([{{forloop.counter0}}, 100*{{day.pass}}/({{day.pass}}+{{day.fail}})]); - ddates.push([{{forloop.counter0}}, "{{day.date}}
Pass: {{day.pass}}
Fail: {{day.fail}}"]); + ddates.push([{{forloop.counter0}}, "{{day.date}}
Pass: {{day.pass}}
Fail: {{day.fail}}"]); {% endfor %} var ddata = [ @@ -46,7 +46,7 @@ var wdates= []; {% for week in health_week_report %} wpass.push([{{forloop.counter0}}, 100*{{week.pass}}/({{week.pass}}+{{week.fail}})]); - wdates.push([{{forloop.counter0}}, "{{week.date}}
Pass: {{week.pass}}
Fail: {{week.fail}}"]); + wdates.push([{{forloop.counter0}}, "{{week.date}}
Pass: {{week.pass}}
Fail: {{week.fail}}"]); {% endfor %} var wdata = [ @@ -80,7 +80,7 @@ var jddates= []; {% for day in job_day_report %} jdpass.push([{{forloop.counter0}}, 100*{{day.pass}}/({{day.pass}}+{{day.fail}})]); - jddates.push([{{forloop.counter0}}, "{{day.date}}
Pass: {{day.pass}}
Fail: {{day.fail}}"]); + jddates.push([{{forloop.counter0}}, "{{day.date}}
Pass: {{day.pass}}
Fail: {{day.fail}}"]); {% endfor %} var jddata = [ @@ -113,7 +113,7 @@ var jwdates= []; {% for week in job_week_report %} jwpass.push([{{forloop.counter0}}, 100*{{week.pass}}/({{week.pass}}+{{week.fail}})]); - jwdates.push([{{forloop.counter0}}, "{{week.date}}
Pass: {{week.pass}}
Fail: {{week.fail}}"]); + jwdates.push([{{forloop.counter0}}, "{{week.date}}
Pass: {{week.pass}}
Fail: {{week.fail}}"]); {% endfor %} var jwdata = [ === modified file 'lava_scheduler_app/urls.py' --- lava_scheduler_app/urls.py 2012-06-16 03:04:57 +0000 +++ lava_scheduler_app/urls.py 2013-01-02 22:07:30 +0000 @@ -9,6 +9,12 @@ url(r'^reports$', 'reports', name='lava.scheduler.reports'), + url(r'^reports/failures$', + 'failure_report', + name='lava.scheduler.failure_report'), + url(r'^reports/failures_json$', + 'failed_jobs_json', + name='lava.scheduler.failed_jobs_json'), url(r'^active_jobs_json$', 'index_active_jobs_json', name='lava.scheduler.active_jobs_json'), @@ -81,6 +87,9 @@ url(r'^job/(?P[0-9]+)/cancel$', 'job_cancel', name='lava.scheduler.job.cancel'), + url(r'^job/(?P[0-9]+)/annotate_failure$', + 'job_annotate_failure', + name='lava.scheduler.job.annotate_failure'), url(r'^job/(?P[0-9]+)/json$', 'job_json', name='lava.scheduler.job.json'), === modified file 'lava_scheduler_app/views.py' --- lava_scheduler_app/views.py 2012-11-22 03:18:31 +0000 +++ lava_scheduler_app/views.py 2013-01-03 16:15:48 +0000 @@ -6,6 +6,8 @@ import datetime from dateutil.relativedelta import relativedelta +from django import forms + from django.conf import settings from django.core.exceptions import PermissionDenied from django.core.urlresolvers import reverse @@ -48,6 +50,7 @@ Device, DeviceType, DeviceStateTransition, + JobFailureTag, TestJob, ) @@ -215,10 +218,13 @@ ).values( 'status' ) + url = reverse('lava.scheduler.failure_report') + params = 'start=%s&end=%s&health_check=%d' % (start_day, end_day, health_check) return { 'pass': res.filter(status=TestJob.COMPLETE).count(), 'fail': res.exclude(status=TestJob.COMPLETE).count(), 'date': start_date.strftime('%m-%d'), + 'failure_url': '%s?%s' % (url, params), } @BreadCrumb("Reports", parent=lava_index) @@ -250,6 +256,71 @@ }, RequestContext(request)) + +class TagsColumn(Column): + + def render(self, value): + return ', '.join([x.name for x in value.all()]) + + +class FailedJobTable(JobTable): + failure_tags = TagsColumn() + failure_comment = Column() + + def get_queryset(self, request): + failures = [TestJob.INCOMPLETE, TestJob.CANCELED, TestJob.CANCELING] + jobs = TestJob.objects.filter(status__in=failures) + + health = request.GET.get('health_check', None) + if health: + jobs = jobs.filter(health_check=_str_to_bool(health)) + + dt = request.GET.get('device_type', None) + if dt: + jobs = jobs.filter(actual_device__device_type__name=dt) + + device = request.GET.get('device', None) + if device: + jobs = jobs.filter(actual_device__hostname=device) + + start = request.GET.get('start', None) + if start: + now = datetime.datetime.now() + start = now + datetime.timedelta(int(start)) + + end = request.GET.get('end', None) + if end: + end = now + datetime.timedelta(int(end)) + jobs = jobs.filter(start_time__range=(start, end)) + return jobs + + class Meta: + exclude = ('status', 'submitter', 'end_time', 'priority', 'description') + + +def failed_jobs_json(request): + return FailedJobTable.json(request, params=(request,)) + + +def _str_to_bool(str): + return str.lower() in ['1', 'true', 'yes'] + + +@BreadCrumb("Failure Report", parent=reports) +def failure_report(request): + return render_to_response( + "lava_scheduler_app/failure_report.html", + { + 'failed_job_table': FailedJobTable( + 'failure_report', + reverse(failed_jobs_json), + params=(request,) + ), + 'bread_crumb_trail': BreadCrumbTrail.leading_to(reports), + }, + RequestContext(request)) + + @BreadCrumb("All Devices", parent=index) def device_list(request): return render_to_response( @@ -490,8 +561,9 @@ data = { 'job': job, 'show_cancel': job.status <= TestJob.RUNNING and job.can_cancel(request.user), + 'show_failure': job.status > TestJob.COMPLETE and job.can_annotate(request.user), 'bread_crumb_trail': BreadCrumbTrail.leading_to(job_detail, pk=pk), - 'show_reload_page' : job.status <= TestJob.RUNNING, + 'show_reload_page': job.status <= TestJob.RUNNING, } log_file = job.log_file @@ -656,6 +728,34 @@ "you cannot cancel this job", content_type="text/plain") +class FailureForm(forms.ModelForm): + class Meta: + model = TestJob + fields = ('failure_tags', 'failure_comment') + + +def job_annotate_failure(request, pk): + job = get_restricted_job(request.user, pk) + if not job.can_annotate(request.user): + raise PermissionDenied() + + if request.method == 'POST': + form = FailureForm(request.POST, instance=job) + if form.is_valid(): + form.save() + return redirect(job) + else: + form = FailureForm(instance=job) + + return render_to_response( + "lava_scheduler_app/job_annotate_failure.html", + { + 'form': form, + 'job': job, + }, + RequestContext(request)) + + def job_json(request, pk): job = get_restricted_job(request.user, pk) json_text = simplejson.dumps({