Merge lp:~doanac/lava-scheduler/failure-reporting into lp:lava-scheduler

Proposed by Andy Doan
Status: Merged
Approved by: Michael Hudson-Doyle
Approved revision: 237
Merged at revision: 233
Proposed branch: lp:~doanac/lava-scheduler/failure-reporting
Merge into: lp:lava-scheduler
Diff against target: 541 lines (+359/-8)
9 files modified
lava_scheduler_app/admin.py (+2/-1)
lava_scheduler_app/migrations/0029_auto__add_jobfailuretag__add_field_testjob_failure_comment.py (+181/-0)
lava_scheduler_app/models.py (+29/-1)
lava_scheduler_app/templates/lava_scheduler_app/failure_report.html (+9/-0)
lava_scheduler_app/templates/lava_scheduler_app/job_annotate_failure.html (+16/-0)
lava_scheduler_app/templates/lava_scheduler_app/job_sidebar.html (+8/-1)
lava_scheduler_app/templates/lava_scheduler_app/reports.html (+4/-4)
lava_scheduler_app/urls.py (+9/-0)
lava_scheduler_app/views.py (+101/-1)
To merge this branch: bzr merge lp:~doanac/lava-scheduler/failure-reporting
Reviewer Review Type Date Requested Status
Linaro Validation Team Pending
Review via email: mp+141679@code.launchpad.net

Description of the change

This change is an attempt to rid us of the google doc:

 https://docs.google.com/a/linaro.org/spreadsheet/ccc?key=0AnxpY5uv-BlNdG9zYTdDLWZWRVFGaWFxQzRLNWtaNmc

Since this updates views, I thought I'd walk you throught the basic usage with some screenshots:

1) Adding "failure tags". This can be used for common issues like "downloading root.tgz"
 http://people.linaro.org/~doanac/failure-pics/admin_panel.png

2) Drill-down
The main way I see this working is through our current "reports" view. I've added hyper-links to each date range, that will link to what you see in item 3:

 http://people.linaro.org/~doanac/failure-pics/reports-main.png

3) Reports view:
When you first look at this type of view, it might not have failures annotated. it would look like:

 http://people.linaro.org/~doanac/failure-pics/failure-view-empty.png

This page is pretty dynamic and allows narrowing down results via some GET parameters (documented in commit message)

4) "Annotating" a failure
When you look at a job, if its INCOMPLETE, CANCELED, or CANCELING, you'll be given an "annotate failure" link (assuming you are a super user):

 http://people.linaro.org/~doanac/failure-pics/failure-action.png

This page is then pretty simple:

 http://people.linaro.org/~doanac/failure-pics/failure-annotate.png

The styling is ugly but it should work for us

5) After annotating some failures the reports view will now be a little nicer:
 http://people.linaro.org/~doanac/failure-pics/failure-view-data.png

To post a comment you must log in.
Revision history for this message
Michael Hudson-Doyle (mwhudson) wrote :
Download full text (8.6 KiB)

Andy Doan <email address hidden> writes:

> Andy Doan has proposed merging lp:~doanac/lava-scheduler/failure-reporting into lp:lava-scheduler.
>
> Requested reviews:
> Linaro Validation Team (linaro-validation)
>
> For more details, see:
> https://code.launchpad.net/~doanac/lava-scheduler/failure-reporting/+merge/141679
>
> This change is an attempt to rid us of the google doc:
>
> https://docs.google.com/a/linaro.org/spreadsheet/ccc?key=0AnxpY5uv-BlNdG9zYTdDLWZWRVFGaWFxQzRLNWtaNmc
>
> Since this updates views, I thought I'd walk you throught the basic usage with some screenshots:
>
> 1) Adding "failure tags". This can be used for common issues like "downloading root.tgz"
> http://people.linaro.org/~doanac/failure-pics/admin_panel.png
>
> 2) Drill-down
> The main way I see this working is through our current "reports" view. I've added hyper-links to each date range, that will link to what you see in item 3:
>
> http://people.linaro.org/~doanac/failure-pics/reports-main.png
>
> 3) Reports view:
> When you first look at this type of view, it might not have failures annotated. it would look like:
>
> http://people.linaro.org/~doanac/failure-pics/failure-view-empty.png
>
> This page is pretty dynamic and allows narrowing down results via some GET parameters (documented in commit message)
>
> 4) "Annotating" a failure
> When you look at a job, if its INCOMPLETE, CANCELED, or CANCELING, you'll be given an "annotate failure" link (assuming you are a super user):
>
> http://people.linaro.org/~doanac/failure-pics/failure-action.png
>
> This page is then pretty simple:
>
> http://people.linaro.org/~doanac/failure-pics/failure-annotate.png
>
> The styling is ugly but it should work for us
>
> 5) After annotating some failures the reports view will now be a little nicer:
> http://people.linaro.org/~doanac/failure-pics/failure-view-data.png

This is a super awesome surprise! A few comments below, but mostly I'm
a massive +1.

>
> === 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-02 22:49:23 +0000
> @@ -0,0 +1,12 @@
> +{% extends "lava_scheduler_app/_content.html" %}
> +
> +{% load django_tables2 %}
> +
> +{% block content %}
> +<h2>Failure Report</h2>
> +
> +{% block content_columns %}

I don't think you need this.

> +{% render_table failed_job_table %}
> +{% endblock %}
> +
> +{% 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-02 22:49:23 +0000
> @@ -0,0 +1,16 @@
> +{% extends "lava_scheduler_app/job_sidebar.html" %}
> +
> +{% block content %}
> +<h2>Annotate Job Failure - {{ job.id }} </h2>
> +
> +{% if form.errors %}
> +<h3>Errors found in submission</h3>
> +{{ form.errors }}
> +{% endif %}
> +
> +<form action="{% url lava.scheduler.job.annotate_failure job.pk %}" method="post"...

Read more...

236. By Andy Doan

address code review issues

237. By Andy Doan

one more code review fix

The code works, but just because of some django coersion. This makes
it explicit

Revision history for this message
Andy Doan (doanac) wrote :

All comments addressed in latest push.

On 01/02/2013 07:46 PM, Michael Hudson-Doyle wrote:
>> + dt = request.GET.get('device_type', None)
>> >+ if dt:
>> >+ jobs = jobs.filter(actual_device__device_type=dt)
> Does this work? I'd have thought that dt would need to be an actual
> DeviceType object here. Or that you'd have to say
> actual_device__device_type__name=dt or something.

It did actually work, but I went ahead and fixed it. I guess django did
some coercion for me, but making this explicit is best.

>> >+ 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))
> So if you specify start but not end, there is no filtering? I guess
> that's OK.

Yeah - that sucks. I thought about making the parameter be "range", but
then you wind up with some odd syntax requirement in a URL.

Revision history for this message
Michael Hudson-Doyle (mwhudson) wrote :

Cool, looks good to go. By "not using as_p" I meant doing the form rendering more manually, but never mind that for now :-)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lava_scheduler_app/admin.py'
2--- lava_scheduler_app/admin.py 2012-10-30 08:27:20 +0000
3+++ lava_scheduler_app/admin.py 2013-01-03 16:18:24 +0000
4@@ -1,6 +1,6 @@
5 from django.contrib import admin
6 from lava_scheduler_app.models import (
7- Device, DeviceStateTransition, DeviceType, TestJob, Tag,
8+ Device, DeviceStateTransition, DeviceType, TestJob, Tag, JobFailureTag,
9 )
10
11 # XXX These actions should really go to another screen that asks for a reason.
12@@ -55,3 +55,4 @@
13 admin.site.register(DeviceType)
14 admin.site.register(TestJob, TestJobAdmin)
15 admin.site.register(Tag)
16+admin.site.register(JobFailureTag)
17
18=== added file 'lava_scheduler_app/migrations/0029_auto__add_jobfailuretag__add_field_testjob_failure_comment.py'
19--- lava_scheduler_app/migrations/0029_auto__add_jobfailuretag__add_field_testjob_failure_comment.py 1970-01-01 00:00:00 +0000
20+++ lava_scheduler_app/migrations/0029_auto__add_jobfailuretag__add_field_testjob_failure_comment.py 2013-01-03 16:18:24 +0000
21@@ -0,0 +1,181 @@
22+# -*- coding: utf-8 -*-
23+import datetime
24+from south.db import db
25+from south.v2 import SchemaMigration
26+from django.db import models
27+
28+
29+class Migration(SchemaMigration):
30+
31+ def forwards(self, orm):
32+ # Adding model 'JobFailureTag'
33+ db.create_table('lava_scheduler_app_jobfailuretag', (
34+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
35+ ('name', self.gf('django.db.models.fields.CharField')(unique=True, max_length=256)),
36+ ('description', self.gf('django.db.models.fields.TextField')(null=True, blank=True)),
37+ ))
38+ db.send_create_signal('lava_scheduler_app', ['JobFailureTag'])
39+
40+ # Adding field 'TestJob.failure_comment'
41+ db.add_column('lava_scheduler_app_testjob', 'failure_comment',
42+ self.gf('django.db.models.fields.TextField')(null=True, blank=True),
43+ keep_default=False)
44+
45+ # Adding M2M table for field failure_tags on 'TestJob'
46+ db.create_table('lava_scheduler_app_testjob_failure_tags', (
47+ ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
48+ ('testjob', models.ForeignKey(orm['lava_scheduler_app.testjob'], null=False)),
49+ ('jobfailuretag', models.ForeignKey(orm['lava_scheduler_app.jobfailuretag'], null=False))
50+ ))
51+ db.create_unique('lava_scheduler_app_testjob_failure_tags', ['testjob_id', 'jobfailuretag_id'])
52+
53+
54+ def backwards(self, orm):
55+ # Deleting model 'JobFailureTag'
56+ db.delete_table('lava_scheduler_app_jobfailuretag')
57+
58+ # Deleting field 'TestJob.failure_comment'
59+ db.delete_column('lava_scheduler_app_testjob', 'failure_comment')
60+
61+ # Removing M2M table for field failure_tags on 'TestJob'
62+ db.delete_table('lava_scheduler_app_testjob_failure_tags')
63+
64+
65+ models = {
66+ 'auth.group': {
67+ 'Meta': {'object_name': 'Group'},
68+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
69+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
70+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
71+ },
72+ 'auth.permission': {
73+ 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
74+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
75+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
76+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
77+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
78+ },
79+ 'auth.user': {
80+ 'Meta': {'object_name': 'User'},
81+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
82+ 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
83+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
84+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
85+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
86+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
87+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
88+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
89+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
90+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
91+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
92+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
93+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
94+ },
95+ 'contenttypes.contenttype': {
96+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
97+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
98+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
99+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
100+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
101+ },
102+ 'dashboard_app.bundle': {
103+ 'Meta': {'ordering': "['-uploaded_on']", 'object_name': 'Bundle'},
104+ '_gz_content': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'null': 'True', 'db_column': "'gz_content'"}),
105+ '_raw_content': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'null': 'True', 'db_column': "'content'"}),
106+ 'bundle_stream': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'bundles'", 'to': "orm['dashboard_app.BundleStream']"}),
107+ 'content_filename': ('django.db.models.fields.CharField', [], {'max_length': '256'}),
108+ 'content_sha1': ('django.db.models.fields.CharField', [], {'max_length': '40', 'unique': 'True', 'null': 'True'}),
109+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
110+ 'is_deserialized': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
111+ 'uploaded_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'uploaded_bundles'", 'null': 'True', 'to': "orm['auth.User']"}),
112+ 'uploaded_on': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.utcnow'})
113+ },
114+ 'dashboard_app.bundlestream': {
115+ 'Meta': {'object_name': 'BundleStream'},
116+ 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.Group']", 'null': 'True', 'blank': 'True'}),
117+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
118+ 'is_anonymous': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
119+ 'is_public': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
120+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}),
121+ 'pathname': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '128'}),
122+ 'slug': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}),
123+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'})
124+ },
125+ 'lava_scheduler_app.device': {
126+ 'Meta': {'object_name': 'Device'},
127+ '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'}),
128+ 'device_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['lava_scheduler_app.DeviceType']"}),
129+ 'device_version': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '200', 'null': 'True', 'blank': 'True'}),
130+ 'health_status': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
131+ 'hostname': ('django.db.models.fields.CharField', [], {'max_length': '200', 'primary_key': 'True'}),
132+ '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'}),
133+ 'status': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
134+ 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['lava_scheduler_app.Tag']", 'symmetrical': 'False', 'blank': 'True'})
135+ },
136+ 'lava_scheduler_app.devicestatetransition': {
137+ 'Meta': {'object_name': 'DeviceStateTransition'},
138+ 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
139+ 'created_on': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
140+ 'device': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'transitions'", 'to': "orm['lava_scheduler_app.Device']"}),
141+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
142+ 'job': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['lava_scheduler_app.TestJob']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
143+ 'message': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
144+ 'new_state': ('django.db.models.fields.IntegerField', [], {}),
145+ 'old_state': ('django.db.models.fields.IntegerField', [], {})
146+ },
147+ 'lava_scheduler_app.devicetype': {
148+ 'Meta': {'object_name': 'DeviceType'},
149+ 'display': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
150+ 'health_check_job': ('django.db.models.fields.TextField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
151+ 'name': ('django.db.models.fields.SlugField', [], {'max_length': '50', 'primary_key': 'True'})
152+ },
153+ 'lava_scheduler_app.jobfailuretag': {
154+ 'Meta': {'object_name': 'JobFailureTag'},
155+ 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
156+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
157+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '256'})
158+ },
159+ 'lava_scheduler_app.tag': {
160+ 'Meta': {'object_name': 'Tag'},
161+ 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
162+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
163+ 'name': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '50'})
164+ },
165+ 'lava_scheduler_app.testjob': {
166+ 'Meta': {'object_name': 'TestJob'},
167+ '_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'}),
168+ '_results_link': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '400', 'null': 'True', 'db_column': "'results_link'", 'blank': 'True'}),
169+ 'actual_device': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'+'", 'null': 'True', 'blank': 'True', 'to': "orm['lava_scheduler_app.Device']"}),
170+ 'definition': ('django.db.models.fields.TextField', [], {}),
171+ 'description': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '200', 'null': 'True', 'blank': 'True'}),
172+ 'end_time': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
173+ 'failure_comment': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
174+ 'failure_tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'failure_tags'", 'blank': 'True', 'to': "orm['lava_scheduler_app.JobFailureTag']"}),
175+ 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.Group']", 'null': 'True', 'blank': 'True'}),
176+ 'health_check': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
177+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
178+ 'is_public': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
179+ 'log_file': ('django.db.models.fields.files.FileField', [], {'default': 'None', 'max_length': '100', 'null': 'True', 'blank': 'True'}),
180+ 'priority': ('django.db.models.fields.IntegerField', [], {'default': '50'}),
181+ 'requested_device': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'+'", 'null': 'True', 'blank': 'True', 'to': "orm['lava_scheduler_app.Device']"}),
182+ 'requested_device_type': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'+'", 'null': 'True', 'blank': 'True', 'to': "orm['lava_scheduler_app.DeviceType']"}),
183+ 'start_time': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
184+ 'status': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
185+ 'submit_time': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
186+ 'submit_token': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['linaro_django_xmlrpc.AuthToken']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
187+ 'submitter': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'+'", 'to': "orm['auth.User']"}),
188+ 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['lava_scheduler_app.Tag']", 'symmetrical': 'False', 'blank': 'True'}),
189+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'})
190+ },
191+ 'linaro_django_xmlrpc.authtoken': {
192+ 'Meta': {'object_name': 'AuthToken'},
193+ 'created_on': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
194+ 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
195+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
196+ 'last_used_on': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
197+ 'secret': ('django.db.models.fields.CharField', [], {'default': "'gz3f80buhio70b6ptm90c0bly6640oiylkimx0t3okbuq5ckezltlfyiz0ndcmgd8osaqu9h9mc8224108zatlq2hs8drzq0cgbqc22ia6f4lf7bg98r0i12nhti33yj'", 'unique': 'True', 'max_length': '128'}),
198+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'auth_tokens'", 'to': "orm['auth.User']"})
199+ }
200+ }
201+
202+ complete_apps = ['lava_scheduler_app']
203\ No newline at end of file
204
205=== modified file 'lava_scheduler_app/models.py'
206--- lava_scheduler_app/models.py 2012-12-03 05:12:52 +0000
207+++ lava_scheduler_app/models.py 2013-01-03 16:18:24 +0000
208@@ -201,6 +201,18 @@
209 # return device_type.device_set.all()
210
211
212+class JobFailureTag(models.Model):
213+ """
214+ Allows us to maintain a set of common ways jobs fail. These can then be
215+ associated with a TestJob so we can do easy data mining
216+ """
217+ name = models.CharField(unique=True, max_length=256)
218+
219+ description = models.TextField(null=True, blank=True)
220+
221+ def __unicode__(self):
222+ return self.name
223+
224
225 class TestJob(RestrictedResource):
226 """
227@@ -313,6 +325,10 @@
228 log_file = models.FileField(
229 upload_to='lava-logs', default=None, null=True, blank=True)
230
231+ failure_tags = models.ManyToManyField(
232+ JobFailureTag, blank=True, related_name='failure_tags')
233+ failure_comment = models.TextField(null=True, blank=True)
234+
235 _results_link = models.CharField(
236 max_length=400, default=None, null=True, blank=True, db_column="results_link")
237
238@@ -436,8 +452,20 @@
239 job.tags.add(tag)
240 return job
241
242+ def _can_admin(self, user):
243+ """ used to check for things like if the user can cancel or annotate
244+ a job failure
245+ """
246+ return user.is_superuser or user == self.submitter
247+
248+ def can_annotate(self, user):
249+ """
250+ Permission required for user to add failure information to a job
251+ """
252+ return self._can_admin(user)
253+
254 def can_cancel(self, user):
255- return user.is_superuser or user == self.submitter
256+ return self._can_admin(user)
257
258 def cancel(self):
259 if self.status == TestJob.RUNNING:
260
261=== added file 'lava_scheduler_app/templates/lava_scheduler_app/failure_report.html'
262--- lava_scheduler_app/templates/lava_scheduler_app/failure_report.html 1970-01-01 00:00:00 +0000
263+++ lava_scheduler_app/templates/lava_scheduler_app/failure_report.html 2013-01-03 16:18:24 +0000
264@@ -0,0 +1,9 @@
265+{% extends "lava_scheduler_app/_content.html" %}
266+
267+{% load django_tables2 %}
268+
269+{% block content %}
270+<h2>Failure Report</h2>
271+{% render_table failed_job_table %}
272+
273+{% endblock %}
274
275=== added file 'lava_scheduler_app/templates/lava_scheduler_app/job_annotate_failure.html'
276--- lava_scheduler_app/templates/lava_scheduler_app/job_annotate_failure.html 1970-01-01 00:00:00 +0000
277+++ lava_scheduler_app/templates/lava_scheduler_app/job_annotate_failure.html 2013-01-03 16:18:24 +0000
278@@ -0,0 +1,16 @@
279+{% extends "lava_scheduler_app/job_sidebar.html" %}
280+
281+{% block content %}
282+<h2>Annotate Job Failure - {{ job.id }} </h2>
283+
284+{% if form.errors %}
285+<h3>Errors found in submission</h3>
286+{{ form.errors }}
287+{% endif %}
288+
289+<form action="{% url lava.scheduler.job.annotate_failure job.pk %}" method="post">
290+{% csrf_token %}
291+{{ form }}
292+<input type="submit" value="Submit" />
293+</form>
294+{% endblock %}
295
296=== modified file 'lava_scheduler_app/templates/lava_scheduler_app/job_sidebar.html'
297--- lava_scheduler_app/templates/lava_scheduler_app/job_sidebar.html 2012-11-13 20:44:35 +0000
298+++ lava_scheduler_app/templates/lava_scheduler_app/job_sidebar.html 2013-01-03 16:18:24 +0000
299@@ -83,14 +83,21 @@
300 {% endif %}
301
302 </ul>
303+{% if show_cancel or show_failure %}
304+<h2>Actions</h2>
305 {% if show_cancel %}
306-<h2>Actions</h2>
307 <form method="POST"
308 action="{% url lava.scheduler.job.cancel job.pk %}">
309 {% csrf_token %}
310 <button id="cancel-button">Cancel Job</button>
311 </form>
312 {% endif %}
313+{% if show_failure %}
314+<ul>
315+ <li><a href="{% url lava.scheduler.job.annotate_failure job.pk %}">Annotate Failure</a></li>
316+</ul>
317+{% endif %}
318+{% endif %}
319
320 {% endblock %}
321
322
323=== modified file 'lava_scheduler_app/templates/lava_scheduler_app/reports.html'
324--- lava_scheduler_app/templates/lava_scheduler_app/reports.html 2012-07-20 20:29:27 +0000
325+++ lava_scheduler_app/templates/lava_scheduler_app/reports.html 2013-01-03 16:18:24 +0000
326@@ -13,7 +13,7 @@
327 var ddates= [];
328 {% for day in health_day_report %}
329 dpass.push([{{forloop.counter0}}, 100*{{day.pass}}/({{day.pass}}+{{day.fail}})]);
330- ddates.push([{{forloop.counter0}}, "{{day.date}}<br/>Pass: {{day.pass}}<br/>Fail: {{day.fail}}"]);
331+ ddates.push([{{forloop.counter0}}, "<a href='{{day.failure_url}}'>{{day.date}}</a><br/>Pass: {{day.pass}}<br/>Fail: {{day.fail}}"]);
332 {% endfor %}
333
334 var ddata = [
335@@ -46,7 +46,7 @@
336 var wdates= [];
337 {% for week in health_week_report %}
338 wpass.push([{{forloop.counter0}}, 100*{{week.pass}}/({{week.pass}}+{{week.fail}})]);
339- wdates.push([{{forloop.counter0}}, "{{week.date}}<br/>Pass: {{week.pass}}<br/>Fail: {{week.fail}}"]);
340+ wdates.push([{{forloop.counter0}}, "<a href='{{week.failure_url}}'>{{week.date}}</a><br/>Pass: {{week.pass}}<br/>Fail: {{week.fail}}"]);
341 {% endfor %}
342
343 var wdata = [
344@@ -80,7 +80,7 @@
345 var jddates= [];
346 {% for day in job_day_report %}
347 jdpass.push([{{forloop.counter0}}, 100*{{day.pass}}/({{day.pass}}+{{day.fail}})]);
348- jddates.push([{{forloop.counter0}}, "{{day.date}}<br/>Pass: {{day.pass}}<br/>Fail: {{day.fail}}"]);
349+ jddates.push([{{forloop.counter0}}, "<a href='{{day.failure_url}}'>{{day.date}}</a><br/>Pass: {{day.pass}}<br/>Fail: {{day.fail}}"]);
350 {% endfor %}
351
352 var jddata = [
353@@ -113,7 +113,7 @@
354 var jwdates= [];
355 {% for week in job_week_report %}
356 jwpass.push([{{forloop.counter0}}, 100*{{week.pass}}/({{week.pass}}+{{week.fail}})]);
357- jwdates.push([{{forloop.counter0}}, "{{week.date}}<br/>Pass: {{week.pass}}<br/>Fail: {{week.fail}}"]);
358+ jwdates.push([{{forloop.counter0}}, "<a href='{{week.failure_url}}'>{{week.date}}</a><br/>Pass: {{week.pass}}<br/>Fail: {{week.fail}}"]);
359 {% endfor %}
360
361 var jwdata = [
362
363=== modified file 'lava_scheduler_app/urls.py'
364--- lava_scheduler_app/urls.py 2012-06-16 03:04:57 +0000
365+++ lava_scheduler_app/urls.py 2013-01-03 16:18:24 +0000
366@@ -9,6 +9,12 @@
367 url(r'^reports$',
368 'reports',
369 name='lava.scheduler.reports'),
370+ url(r'^reports/failures$',
371+ 'failure_report',
372+ name='lava.scheduler.failure_report'),
373+ url(r'^reports/failures_json$',
374+ 'failed_jobs_json',
375+ name='lava.scheduler.failed_jobs_json'),
376 url(r'^active_jobs_json$',
377 'index_active_jobs_json',
378 name='lava.scheduler.active_jobs_json'),
379@@ -81,6 +87,9 @@
380 url(r'^job/(?P<pk>[0-9]+)/cancel$',
381 'job_cancel',
382 name='lava.scheduler.job.cancel'),
383+ url(r'^job/(?P<pk>[0-9]+)/annotate_failure$',
384+ 'job_annotate_failure',
385+ name='lava.scheduler.job.annotate_failure'),
386 url(r'^job/(?P<pk>[0-9]+)/json$',
387 'job_json',
388 name='lava.scheduler.job.json'),
389
390=== modified file 'lava_scheduler_app/views.py'
391--- lava_scheduler_app/views.py 2012-11-22 03:18:31 +0000
392+++ lava_scheduler_app/views.py 2013-01-03 16:18:24 +0000
393@@ -6,6 +6,8 @@
394 import datetime
395 from dateutil.relativedelta import relativedelta
396
397+from django import forms
398+
399 from django.conf import settings
400 from django.core.exceptions import PermissionDenied
401 from django.core.urlresolvers import reverse
402@@ -48,6 +50,7 @@
403 Device,
404 DeviceType,
405 DeviceStateTransition,
406+ JobFailureTag,
407 TestJob,
408 )
409
410@@ -215,10 +218,13 @@
411 ).values(
412 'status'
413 )
414+ url = reverse('lava.scheduler.failure_report')
415+ params = 'start=%s&end=%s&health_check=%d' % (start_day, end_day, health_check)
416 return {
417 'pass': res.filter(status=TestJob.COMPLETE).count(),
418 'fail': res.exclude(status=TestJob.COMPLETE).count(),
419 'date': start_date.strftime('%m-%d'),
420+ 'failure_url': '%s?%s' % (url, params),
421 }
422
423 @BreadCrumb("Reports", parent=lava_index)
424@@ -250,6 +256,71 @@
425 },
426 RequestContext(request))
427
428+
429+class TagsColumn(Column):
430+
431+ def render(self, value):
432+ return ', '.join([x.name for x in value.all()])
433+
434+
435+class FailedJobTable(JobTable):
436+ failure_tags = TagsColumn()
437+ failure_comment = Column()
438+
439+ def get_queryset(self, request):
440+ failures = [TestJob.INCOMPLETE, TestJob.CANCELED, TestJob.CANCELING]
441+ jobs = TestJob.objects.filter(status__in=failures)
442+
443+ health = request.GET.get('health_check', None)
444+ if health:
445+ jobs = jobs.filter(health_check=_str_to_bool(health))
446+
447+ dt = request.GET.get('device_type', None)
448+ if dt:
449+ jobs = jobs.filter(actual_device__device_type__name=dt)
450+
451+ device = request.GET.get('device', None)
452+ if device:
453+ jobs = jobs.filter(actual_device__hostname=device)
454+
455+ start = request.GET.get('start', None)
456+ if start:
457+ now = datetime.datetime.now()
458+ start = now + datetime.timedelta(int(start))
459+
460+ end = request.GET.get('end', None)
461+ if end:
462+ end = now + datetime.timedelta(int(end))
463+ jobs = jobs.filter(start_time__range=(start, end))
464+ return jobs
465+
466+ class Meta:
467+ exclude = ('status', 'submitter', 'end_time', 'priority', 'description')
468+
469+
470+def failed_jobs_json(request):
471+ return FailedJobTable.json(request, params=(request,))
472+
473+
474+def _str_to_bool(str):
475+ return str.lower() in ['1', 'true', 'yes']
476+
477+
478+@BreadCrumb("Failure Report", parent=reports)
479+def failure_report(request):
480+ return render_to_response(
481+ "lava_scheduler_app/failure_report.html",
482+ {
483+ 'failed_job_table': FailedJobTable(
484+ 'failure_report',
485+ reverse(failed_jobs_json),
486+ params=(request,)
487+ ),
488+ 'bread_crumb_trail': BreadCrumbTrail.leading_to(reports),
489+ },
490+ RequestContext(request))
491+
492+
493 @BreadCrumb("All Devices", parent=index)
494 def device_list(request):
495 return render_to_response(
496@@ -490,8 +561,9 @@
497 data = {
498 'job': job,
499 'show_cancel': job.status <= TestJob.RUNNING and job.can_cancel(request.user),
500+ 'show_failure': job.status > TestJob.COMPLETE and job.can_annotate(request.user),
501 'bread_crumb_trail': BreadCrumbTrail.leading_to(job_detail, pk=pk),
502- 'show_reload_page' : job.status <= TestJob.RUNNING,
503+ 'show_reload_page': job.status <= TestJob.RUNNING,
504 }
505
506 log_file = job.log_file
507@@ -656,6 +728,34 @@
508 "you cannot cancel this job", content_type="text/plain")
509
510
511+class FailureForm(forms.ModelForm):
512+ class Meta:
513+ model = TestJob
514+ fields = ('failure_tags', 'failure_comment')
515+
516+
517+def job_annotate_failure(request, pk):
518+ job = get_restricted_job(request.user, pk)
519+ if not job.can_annotate(request.user):
520+ raise PermissionDenied()
521+
522+ if request.method == 'POST':
523+ form = FailureForm(request.POST, instance=job)
524+ if form.is_valid():
525+ form.save()
526+ return redirect(job)
527+ else:
528+ form = FailureForm(instance=job)
529+
530+ return render_to_response(
531+ "lava_scheduler_app/job_annotate_failure.html",
532+ {
533+ 'form': form,
534+ 'job': job,
535+ },
536+ RequestContext(request))
537+
538+
539 def job_json(request, pk):
540 job = get_restricted_job(request.user, pk)
541 json_text = simplejson.dumps({

Subscribers

People subscribed via source and target branches