Merge lp:~doanac/lava-scheduler/failure-reporting into lp:lava-scheduler
- failure-reporting
- Merge into trunk
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Linaro Validation Team | Pending | ||
Review via email: mp+141679@code.launchpad.net |
Commit message
Description of the change
This change is an attempt to rid us of the google doc:
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://
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://
3) Reports view:
When you first look at this type of view, it might not have failures annotated. it would look like:
http://
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://
This page is then pretty simple:
http://
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://
Michael Hudson-Doyle (mwhudson) wrote : | # |
Andy Doan (doanac) wrote : | # |
All comments addressed in latest push.
On 01/02/2013 07:46 PM, Michael Hudson-Doyle wrote:
>> + dt = request.
>> >+ if dt:
>> >+ jobs = jobs.filter(
> 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_
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.
>> >+ if device:
>> >+ jobs = jobs.filter(
>> >+
>> >+ start = request.
>> >+ if start:
>> >+ now = datetime.
>> >+ start = now + datetime.
>> >+
>> >+ end = request.
>> >+ if end:
>> >+ end = now + datetime.
>> >+ jobs = jobs.filter(
> 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.
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
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({ |
Andy Doan <email address hidden> writes:
> Andy Doan has proposed merging lp:~doanac/lava-scheduler/failure-reporting into lp:lava-scheduler. /code.launchpad .net/~doanac/ lava-scheduler/ failure- reporting/ +merge/ 141679 /docs.google. com/a/linaro. org/spreadsheet /ccc?key= 0AnxpY5uv- BlNdG9zYTdDLWZW RVFGaWFxQzRLNWt aNmc people. linaro. org/~doanac/ failure- pics/admin_ panel.png people. linaro. org/~doanac/ failure- pics/reports- main.png people. linaro. org/~doanac/ failure- pics/failure- view-empty. png people. linaro. org/~doanac/ failure- pics/failure- action. png people. linaro. org/~doanac/ failure- pics/failure- annotate. png people. linaro. org/~doanac/ failure- pics/failure- view-data. png
>
> Requested reviews:
> Linaro Validation Team (linaro-validation)
>
> For more details, see:
> https:/
>
> This change is an attempt to rid us of the google doc:
>
> https:/
>
> 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://
>
> 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://
>
> 3) Reports view:
> When you first look at this type of view, it might not have failures annotated. it would look like:
>
> http://
>
> 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://
>
> This page is then pretty simple:
>
> http://
>
> 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://
This is a super awesome surprise! A few comments below, but mostly I'm
a massive +1.
> _app/templates/ lava_scheduler_ app/failure_ report. html' app/templates/ lava_scheduler_ app/failure_ report. html 1970-01-01 00:00:00 +0000 app/templates/ lava_scheduler_ app/failure_ report. html 2013-01-02 22:49:23 +0000 _app/_content. html" %}
> === added file 'lava_scheduler
> --- lava_scheduler_
> +++ lava_scheduler_
> @@ -0,0 +1,12 @@
> +{% extends "lava_scheduler
> +
> +{% load django_tables2 %}
> +
> +{% block content %}
> +<h2>Failure Report</h2>
> +
> +{% block content_columns %}
I don't think you need this.
> +{% render_table failed_job_table %} _app/templates/ lava_scheduler_ app/job_ annotate_ failure. html' app/templates/ lava_scheduler_ app/job_ annotate_ failure. html 1970-01-01 00:00:00 +0000 app/templates/ lava_scheduler_ app/job_ annotate_ failure. html 2013-01-02 22:49:23 +0000 _app/job_ sidebar. html" %} job.annotate_ failure job.pk %}" method="post"...
> +{% endblock %}
> +
> +{% endblock %}
>
> === added file 'lava_scheduler
> --- lava_scheduler_
> +++ lava_scheduler_
> @@ -0,0 +1,16 @@
> +{% extends "lava_scheduler
> +
> +{% 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.