Merge lp:~qa-dashboard/qa-dashboard/idle-power into lp:qa-dashboard

Proposed by Chris Johnston
Status: Merged
Approved by: Chris Johnston
Approved revision: 341
Merged at revision: 341
Proposed branch: lp:~qa-dashboard/qa-dashboard/idle-power
Merge into: lp:qa-dashboard
Diff against target: 1509 lines (+1285/-4)
25 files modified
common/management/__init__.py (+32/-0)
common/management/commands/clear_items.py (+13/-0)
common/management/commands/list_items.py (+12/-0)
common/static/css/style.css (+40/-2)
common/utils.py (+3/-0)
idle_power/__init__.py (+14/-0)
idle_power/admin.py (+77/-0)
idle_power/management/__init__.py (+14/-0)
idle_power/management/commands/__init__.py (+14/-0)
idle_power/management/commands/jenkins_pull_idlepower.py (+208/-0)
idle_power/migrations/0001_initial.py (+201/-0)
idle_power/migrations/__init__.py (+14/-0)
idle_power/models.py (+85/-0)
idle_power/tables.py (+72/-0)
idle_power/templates/idle_power/machine_raw_data.html (+37/-0)
idle_power/templates/idle_power/machine_table.html (+30/-0)
idle_power/templates/idle_power/metrics.html (+28/-0)
idle_power/urls.py (+33/-0)
idle_power/utah_utils.py (+112/-0)
idle_power/views.py (+104/-0)
power/templates/power/power_layout.html (+4/-1)
qa_dashboard/settings.py (+1/-0)
qa_dashboard/urls.py (+1/-0)
scripts/fakeup_idle_power.py (+134/-0)
smoke/utah_utils.py (+2/-1)
To merge this branch: bzr merge lp:~qa-dashboard/qa-dashboard/idle-power
Reviewer Review Type Date Requested Status
Andy Doan (community) Approve
Review via email: mp+158831@code.launchpad.net

Commit message

Adds power idle to the dashboard

To post a comment you must log in.
336. By Chris Johnston

[r=Joe Talbott] Move power urls to power/urls.py, change /power/ and /api/power/ links to /power/software/ and /api/power/software/ from Chris Johnston

337. By Chris Johnston

[r=Andy Doan] Only run bzr pull, syncdb, migrate, collectstatic if there is a branch update. Also graceful apache from Chris Johnston

338. By Chris Johnston

Release version 2013.04.15

339. By Joe Talbott

[r=Andy Doan] Fix duplicate metrics due to recent utah log fix. from Joe Talbott

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

A couple of things:

 237 +from .models import (

We stopped using relative imports in lp:utah, might be good to not continue the pattern here.

496 + logging.info("Adding new log: {}".format(log))
logging.info("Adding new log: %s", log)

830 + path = models.CharField(max_length=512)
831 + name = models.CharField(max_length=100)

I'd make those arbitrarily large since postgres doesn't care

847 + command = models.CharField(max_length=4096)

We should probably name this "testcase" so that it stays inline with the YAML and our general terminology

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

872 + def job_bugs(self):
guessing that's a copy/paste thing, but I don't think we have a use for this at the time, so should probably remove it.

1066 === added file 'idle_power/tests.py'
that file is useless, i'd just kill it until there's something useful for it.

1202 + logging.error("{} {}".format(logfile_path, e))
1206 + logging.warn("Unable to parse {}".format(logfile_path))
bad logging form

def process_idle_power_log(....
This function is too large. You could probably break out the logic in the for loops that builds the detail object into its own function.

340. By Chris Johnston

[r=Joe Talbott] Fix issue where power url's show up as api/power/software from Chris Johnston

341. By Chris Johnston

resolve conflict

Revision history for this message
Andy Doan (doanac) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'common/management/__init__.py'
2--- common/management/__init__.py 2013-04-02 19:30:47 +0000
3+++ common/management/__init__.py 2013-04-16 20:51:23 +0000
4@@ -18,6 +18,7 @@
5 import logging
6 import re
7 import urllib2
8+import yaml
9
10 from django.core.management.base import BaseCommand
11 from optparse import make_option
12@@ -113,6 +114,13 @@
13 'MemoryResult',
14 'MemoryUpgrade',
15 ]
16+ elif arg == 'idlepower' or arg == 'idle_power':
17+ klass_list += [
18+ 'IdlePowerImage',
19+ 'IdlePowerLog',
20+ 'IdlePowerMachine',
21+ 'IdlePowerMetric',
22+ ]
23 else:
24 klass_list.append(arg)
25
26@@ -170,6 +178,30 @@
27 )
28
29
30+def get_kernel(build_url, artifact):
31+ """ Try to find a kernel in a utah log artifact. """
32+ retval = "unknown"
33+
34+ if artifact is None:
35+ return retval
36+
37+ log_path = "{}artifact/{}".format(
38+ build_url,
39+ artifact['relativePath'],
40+ )
41+
42+ data = jenkins_get(log_path, as_json=False)
43+ try:
44+ data = yaml.load(data)
45+ except yaml.YAMLError:
46+ logging.warn("failed to parse data: %s", data)
47+ raise
48+
49+ kernel = data['uname'][2]
50+
51+ return kernel
52+
53+
54 def profile(fn):
55 """ Decorator for time profiling function calls.
56
57
58=== modified file 'common/management/commands/clear_items.py'
59--- common/management/commands/clear_items.py 2013-03-01 22:09:19 +0000
60+++ common/management/commands/clear_items.py 2013-04-16 20:51:23 +0000
61@@ -51,6 +51,14 @@
62 PowerMetric,
63 )
64
65+from idle_power.models import (
66+ IdlePowerBuild,
67+ IdlePowerDetail,
68+ IdlePowerImage,
69+ IdlePowerLog,
70+ IdlePowerMachine,
71+)
72+
73 from memory.models import (
74 MemoryBuild,
75 MemoryDetail,
76@@ -103,6 +111,11 @@
77 MemoryDetail,
78 MemoryLog,
79 MemoryProcess,
80+ IdlePowerImage,
81+ IdlePowerLog,
82+ IdlePowerMachine,
83+ IdlePowerBuild,
84+ IdlePowerDetail,
85 ]
86
87 verbosity = int(options.get('verbosity'))
88
89=== modified file 'common/management/commands/list_items.py'
90--- common/management/commands/list_items.py 2013-03-01 22:09:19 +0000
91+++ common/management/commands/list_items.py 2013-04-16 20:51:23 +0000
92@@ -64,6 +64,13 @@
93 MemoryUpgrade,
94 )
95
96+from idle_power.models import (
97+ IdlePowerBuild,
98+ IdlePowerDetail,
99+ IdlePowerImage,
100+ IdlePowerLog,
101+ IdlePowerMachine,
102+)
103 from common.management import process_class_list
104
105
106@@ -103,6 +110,11 @@
107 MemoryResult,
108 MemoryUpgrade,
109 MemoryProcess,
110+ IdlePowerImage,
111+ IdlePowerLog,
112+ IdlePowerMachine,
113+ IdlePowerBuild,
114+ IdlePowerDetail,
115 ]
116
117 verbosity = int(options.get('verbosity'))
118
119=== modified file 'common/static/css/style.css'
120--- common/static/css/style.css 2013-04-11 15:40:42 +0000
121+++ common/static/css/style.css 2013-04-16 20:51:23 +0000
122@@ -400,6 +400,11 @@
123 margin-top: 1.2em; /** 18px (desired value) / 15px (computed font-size value) */
124 }
125
126+h5 {
127+ font-size : 1em;
128+ margin-bottom : 0.75em;
129+}
130+
131 p {
132 font-size: 1em;
133 line-height: 1.4;
134@@ -415,8 +420,12 @@
135 font-weight:bold;
136 }
137
138-table.basic thead a , table.basic tfoot a {
139- text-decoration:underline;
140+table.basic thead a, table.basic tfoot a {
141+ text-decoration: none;
142+}
143+
144+table.basic thead a:hover, table.basic tfoot a:hover {
145+ text-decoration: underline;
146 }
147
148 table.basic {
149@@ -724,3 +733,32 @@
150 width: 100%;
151 background-position:center;
152 }
153+
154+.box-padded {
155+ border-radius : 4px;
156+ background : #efefef;
157+ border : 0;
158+ margin-bottom : 20px;
159+ padding : 6px 5px 6px;
160+}
161+
162+.box-padded h3 {
163+ font-size : 1.219em;
164+ margin-bottom : 0.615em;
165+ margin-left : 5px;
166+ margin-top : 5px;
167+}
168+
169+.box-padded li h3 {
170+ font-size : 1.219em;
171+ margin-bottom : 0.615em;
172+ margin : 0;
173+}
174+
175+.box-padded div {
176+ border-radius : 4px;
177+ background : #fff;
178+ overflow : hidden;
179+ padding : 8px 8px 2px;
180+}
181+
182
183=== renamed file 'smoke/utah_parser.py' => 'common/utah_parser.py'
184=== modified file 'common/utils.py'
185--- common/utils.py 2013-03-21 18:59:36 +0000
186+++ common/utils.py 2013-04-16 20:51:23 +0000
187@@ -28,6 +28,9 @@
188 'power': re.compile(
189 ur"^power-(milestone|backfill|raring)-(desktop)-([^-]*)-(.*)"
190 ),
191+ 'idlepower': re.compile(
192+ ur"^poweridle-(milestone|backfill|raring)-(desktop)-([^-]*)-(.*)"
193+ ),
194 'upgrade': re.compile(
195 ur".*-upgrade-.*"
196 ),
197
198=== added directory 'idle_power'
199=== added file 'idle_power/__init__.py'
200--- idle_power/__init__.py 1970-01-01 00:00:00 +0000
201+++ idle_power/__init__.py 2013-04-16 20:51:23 +0000
202@@ -0,0 +1,14 @@
203+# QA Dashboard
204+# Copyright 2012-2013 Canonical Ltd.
205+
206+# This program is free software: you can redistribute it and/or modify it
207+# under the terms of the GNU Affero General Public License version 3, as
208+# published by the Free Software Foundation.
209+
210+# This program is distributed in the hope that it will be useful, but
211+# WITHOUT ANY WARRANTY; without even the implied warranties of
212+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
213+# PURPOSE. See the GNU Affero General Public License for more details.
214+
215+# You should have received a copy of the GNU Affero General Public License
216+# along with this program. If not, see <http://www.gnu.org/licenses/>.
217
218=== added file 'idle_power/admin.py'
219--- idle_power/admin.py 1970-01-01 00:00:00 +0000
220+++ idle_power/admin.py 2013-04-16 20:51:23 +0000
221@@ -0,0 +1,77 @@
222+# QA Dashboard
223+# Copyright 2013 Canonical Ltd.
224+
225+# This program is free software: you can redistribute it and/or modify it
226+# under the terms of the GNU Affero General Public License version 3, as
227+# published by the Free Software Foundation.
228+
229+# This program is distributed in the hope that it will be useful, but
230+# WITHOUT ANY WARRANTY; without even the implied warranties of
231+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
232+# PURPOSE. See the GNU Affero General Public License for more details.
233+
234+# You should have received a copy of the GNU Affero General Public License
235+# along with this program. If not, see <http://www.gnu.org/licenses/>.
236+
237+from .models import (
238+ IdlePowerImage,
239+ IdlePowerMachine,
240+ IdlePowerLog,
241+ IdlePowerDetail,
242+ IdlePowerBuild,
243+)
244+
245+from django.contrib import admin
246+
247+
248+class IdlePowerMachineAdmin(admin.ModelAdmin):
249+ list_display = ('name', 'mac_address')
250+ search_fields = ['name', 'mac_address']
251+
252+
253+class IdlePowerLogAdmin(admin.ModelAdmin):
254+ list_display = ('name', 'path')
255+ search_fields = ['name', 'path']
256+
257+
258+class IdlePowerImageAdmin(admin.ModelAdmin):
259+ list_display = (
260+ 'release',
261+ 'variant',
262+ 'arch',
263+ 'build_number',
264+ 'image_type',
265+ )
266+ list_filter = [
267+ 'release',
268+ 'variant',
269+ 'arch',
270+ 'build_number',
271+ ]
272+ search_fields = [
273+ 'release',
274+ 'variant',
275+ 'arch',
276+ 'build_number',
277+ ]
278+
279+
280+class IdlePowerDetailAdmin(admin.ModelAdmin):
281+ list_display = (
282+ 'image',
283+ 'testcase',
284+ )
285+
286+
287+class IdlePowerBuildAdmin(admin.ModelAdmin):
288+ list_display = (
289+ 'image',
290+ 'build',
291+ )
292+
293+
294+admin.site.register(IdlePowerMachine, IdlePowerMachineAdmin)
295+admin.site.register(IdlePowerLog, IdlePowerLogAdmin)
296+admin.site.register(IdlePowerImage, IdlePowerImageAdmin)
297+admin.site.register(IdlePowerDetail, IdlePowerDetailAdmin)
298+admin.site.register(IdlePowerBuild, IdlePowerBuildAdmin)
299
300=== added directory 'idle_power/management'
301=== added file 'idle_power/management/__init__.py'
302--- idle_power/management/__init__.py 1970-01-01 00:00:00 +0000
303+++ idle_power/management/__init__.py 2013-04-16 20:51:23 +0000
304@@ -0,0 +1,14 @@
305+# QA Dashboard
306+# Copyright 2012-2013 Canonical Ltd.
307+
308+# This program is free software: you can redistribute it and/or modify it
309+# under the terms of the GNU Affero General Public License version 3, as
310+# published by the Free Software Foundation.
311+
312+# This program is distributed in the hope that it will be useful, but
313+# WITHOUT ANY WARRANTY; without even the implied warranties of
314+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
315+# PURPOSE. See the GNU Affero General Public License for more details.
316+
317+# You should have received a copy of the GNU Affero General Public License
318+# along with this program. If not, see <http://www.gnu.org/licenses/>.
319
320=== added directory 'idle_power/management/commands'
321=== added file 'idle_power/management/commands/__init__.py'
322--- idle_power/management/commands/__init__.py 1970-01-01 00:00:00 +0000
323+++ idle_power/management/commands/__init__.py 2013-04-16 20:51:23 +0000
324@@ -0,0 +1,14 @@
325+# QA Dashboard
326+# Copyright 2012-2013 Canonical Ltd.
327+
328+# This program is free software: you can redistribute it and/or modify it
329+# under the terms of the GNU Affero General Public License version 3, as
330+# published by the Free Software Foundation.
331+
332+# This program is distributed in the hope that it will be useful, but
333+# WITHOUT ANY WARRANTY; without even the implied warranties of
334+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
335+# PURPOSE. See the GNU Affero General Public License for more details.
336+
337+# You should have received a copy of the GNU Affero General Public License
338+# along with this program. If not, see <http://www.gnu.org/licenses/>.
339
340=== added file 'idle_power/management/commands/jenkins_pull_idlepower.py'
341--- idle_power/management/commands/jenkins_pull_idlepower.py 1970-01-01 00:00:00 +0000
342+++ idle_power/management/commands/jenkins_pull_idlepower.py 2013-04-16 20:51:23 +0000
343@@ -0,0 +1,208 @@
344+# QA Dashboard
345+# Copyright 2013 Canonical Ltd.
346+
347+# This program is free software: you can redistribute it and/or modify
348+# it under the terms of the GNU Affero General Public License version
349+# 3, as published by the Free Software Foundation.
350+
351+# This program is distributed in the hope that it will be useful, but
352+# WITHOUT ANY WARRANTY; without even the implied warranties of
353+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
354+# PURPOSE. See the GNU Affero General Public License for more details.
355+
356+# You should have received a copy of the GNU Affero General Public
357+# License along with this program. If not, see
358+# <http://www.gnu.org/licenses/>.
359+
360+
361+import logging
362+import re
363+
364+from common.management import (
365+ JenkinsBaseCommand,
366+ get_kernel,
367+)
368+
369+from common.utils import regexes
370+
371+from performance.management import get_image_build_number
372+
373+from idle_power.models import (
374+ IdlePowerImage,
375+ IdlePowerLog,
376+ IdlePowerMachine,
377+ IdlePowerBuild,
378+)
379+
380+from idle_power.utah_utils import process_idle_power_log
381+
382+
383+def _get_power_logs(artifacts):
384+ """ Check for valid power results.
385+
386+ :returns: a list of power logs.
387+
388+ """
389+ logs = []
390+ utah_log_regex = re.compile(ur'^clientlogs\/utah.*.yaml')
391+
392+ for artifact in artifacts:
393+ logging.debug(artifact['relativePath'])
394+ path = artifact['relativePath']
395+ logging.debug("found %s", path)
396+ if utah_log_regex.match(path):
397+ logs.append(artifact)
398+
399+ return logs
400+
401+
402+def _process_utah_logs(
403+ logs,
404+ image,
405+ machine,
406+ jenkins_url=None,
407+ name=None,
408+):
409+
410+ details = []
411+
412+ for log in logs:
413+ logging.debug("log: %s", log)
414+ detail = process_idle_power_log(
415+ log,
416+ image,
417+ machine,
418+ jenkins_url=jenkins_url,
419+ name=name,
420+ )
421+
422+ if detail:
423+ details.append(detail)
424+
425+ return details
426+
427+
428+class Command(JenkinsBaseCommand):
429+ """ The command. """
430+
431+ job_regex = regexes['idlepower']
432+ job_types = ['milestone', 'backfill']
433+
434+ def extract_data(self, name):
435+ m = self.job_regex.match(name)
436+
437+ if m:
438+ self.release = m.group(1)
439+ self.variant = m.group(2)
440+ self.arch = m.group(3)
441+ self.machine_name = m.group(4)
442+
443+ self.install_data = dict(
444+ release=self.release,
445+ variant=self.variant,
446+ arch=self.arch,
447+ machine=self.machine_name,
448+ )
449+
450+ def process_job(self, job):
451+ self.machine, new_machine = IdlePowerMachine.objects.get_or_create(
452+ name=self.machine_name,
453+ )
454+
455+ if new_machine:
456+ logging.debug("Added new machine: %s", self.machine_name)
457+
458+ def _process_logs(self, build, build_date, install_data, func):
459+
460+ if not hasattr(func, '__call__'):
461+ raise Exception("invalid callable: %s", func)
462+
463+ logs = _get_power_logs(build['artifacts'])
464+ self.image_build_number = get_image_build_number(build)
465+ self.build_url = build['url']
466+ for log in logs:
467+ kernel = get_kernel(build['url'], log)
468+ log_path = "{}artifact/{}".format(
469+ build['url'],
470+ log['relativePath'],
471+ )
472+ dashboard_data = dict(
473+ build_date=build_date,
474+ build_number=self.image_build_number,
475+ log=log,
476+ log_path=log_path,
477+ release=install_data['release'],
478+ variant=install_data['variant'],
479+ arch=install_data['arch'],
480+ kernel=kernel,
481+ )
482+ if 'jenkins_build' in install_data:
483+ dashboard_data['jenkins_build'] = install_data['jenkins_build']
484+
485+ func(build, dashboard_data)
486+
487+ def add_result(self, build, dashboard_data):
488+ build_logs = []
489+ for l in dashboard_data['log']:
490+ log, new_log = IdlePowerLog.objects.get_or_create(
491+ path=dashboard_data['log_path'],
492+ name=dashboard_data['log']['relativePath'],
493+ )
494+
495+ if new_log:
496+ logging.info("Adding new log: %s", log)
497+
498+ build_logs.append(log)
499+
500+ self.image, new_image = IdlePowerImage.objects.get_or_create(
501+ release=dashboard_data['release'],
502+ variant=dashboard_data['variant'],
503+ arch=dashboard_data['arch'],
504+ build_number=dashboard_data['build_number'],
505+ # fake an md5 since we don't have the data
506+ md5="{}{}{}{}".format(
507+ self.arch,
508+ dashboard_data['release'],
509+ self.variant,
510+ dashboard_data['build_number'],
511+ )
512+ )
513+
514+ if new_image:
515+ logging.debug("Added new image: %s", self.image)
516+
517+ build, new_build = IdlePowerBuild.objects.get_or_create(
518+ image=self.image,
519+ machine=self.machine,
520+ build=dashboard_data['jenkins_build']
521+ )
522+
523+ if len(build_logs) > 0:
524+ _process_utah_logs(
525+ build_logs,
526+ image=self.image,
527+ machine=self.machine,
528+ jenkins_url=self.build_url,
529+ )
530+
531+ def remove_result(self, build, dashboard_data):
532+ """ Remove power data from the database """
533+ logging.info("Removing power result")
534+
535+ try:
536+ image = IdlePowerImage.objects.get(
537+ arch=self.arch,
538+ release=self.release,
539+ variant=self.variant,
540+ build_number=self.image_build_number,
541+ md5="{}{}{}{}".format(
542+ self.arch,
543+ self.release,
544+ self.variant,
545+ self.image_build_number,
546+ )
547+ )
548+
549+ image.delete()
550+ except IdlePowerImage.DoesNotExist:
551+ pass
552
553=== added directory 'idle_power/migrations'
554=== added file 'idle_power/migrations/0001_initial.py'
555--- idle_power/migrations/0001_initial.py 1970-01-01 00:00:00 +0000
556+++ idle_power/migrations/0001_initial.py 2013-04-16 20:51:23 +0000
557@@ -0,0 +1,201 @@
558+# -*- coding: utf-8 -*-
559+import datetime
560+from south.db import db
561+from south.v2 import SchemaMigration
562+from django.db import models
563+
564+
565+class Migration(SchemaMigration):
566+
567+ def forwards(self, orm):
568+ # Adding model 'IdlePowerImage'
569+ db.create_table('idle_power_images', (
570+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
571+ ('created_at', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
572+ ('updated_at', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, blank=True)),
573+ ('internal', self.gf('django.db.models.fields.BooleanField')(default=True)),
574+ ('publish', self.gf('django.db.models.fields.BooleanField')(default=True)),
575+ ('release', self.gf('django.db.models.fields.CharField')(max_length=200)),
576+ ('variant', self.gf('django.db.models.fields.CharField')(max_length=200)),
577+ ('arch', self.gf('django.db.models.fields.CharField')(max_length=200)),
578+ ('md5', self.gf('django.db.models.fields.CharField')(unique=True, max_length=200)),
579+ ('build_number', self.gf('django.db.models.fields.CharField')(max_length=50)),
580+ ('image_type', self.gf('django.db.models.fields.CharField')(default=u'daily', max_length=10)),
581+ ))
582+ db.send_create_signal('idle_power', ['IdlePowerImage'])
583+
584+ # Adding model 'IdlePowerMachine'
585+ db.create_table('idle_power_machines', (
586+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
587+ ('created_at', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
588+ ('updated_at', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, blank=True)),
589+ ('internal', self.gf('django.db.models.fields.BooleanField')(default=True)),
590+ ('publish', self.gf('django.db.models.fields.BooleanField')(default=True)),
591+ ('name', self.gf('django.db.models.fields.CharField')(unique=True, max_length=200)),
592+ ('mac_address', self.gf('django.db.models.fields.CharField')(max_length=200)),
593+ ))
594+ db.send_create_signal('idle_power', ['IdlePowerMachine'])
595+
596+ # Adding model 'IdlePowerLog'
597+ db.create_table('idle_power_logs', (
598+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
599+ ('created_at', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
600+ ('updated_at', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, blank=True)),
601+ ('internal', self.gf('django.db.models.fields.BooleanField')(default=True)),
602+ ('publish', self.gf('django.db.models.fields.BooleanField')(default=True)),
603+ ('path', self.gf('django.db.models.fields.CharField')(max_length=4096)),
604+ ('name', self.gf('django.db.models.fields.CharField')(max_length=4096)),
605+ ))
606+ db.send_create_signal('idle_power', ['IdlePowerLog'])
607+
608+ # Adding model 'IdlePowerDetail'
609+ db.create_table('idle_power_details', (
610+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
611+ ('created_at', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
612+ ('updated_at', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, blank=True)),
613+ ('internal', self.gf('django.db.models.fields.BooleanField')(default=True)),
614+ ('publish', self.gf('django.db.models.fields.BooleanField')(default=True)),
615+ ('image', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['idle_power.IdlePowerImage'], null=True)),
616+ ('machine', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['idle_power.IdlePowerMachine'])),
617+ ('testcase', self.gf('django.db.models.fields.CharField')(max_length=4096)),
618+ ('calculated_energy', self.gf('django.db.models.fields.PositiveIntegerField')()),
619+ ('charge_start', self.gf('django.db.models.fields.PositiveIntegerField')()),
620+ ('charge_used', self.gf('django.db.models.fields.PositiveIntegerField')()),
621+ ('energy_start', self.gf('django.db.models.fields.PositiveIntegerField')()),
622+ ('energy_used', self.gf('django.db.models.fields.PositiveIntegerField')()),
623+ ('ran_at', self.gf('django.db.models.fields.DateTimeField')()),
624+ ('jenkins_url', self.gf('django.db.models.fields.URLField')(max_length=200)),
625+ ))
626+ db.send_create_signal('idle_power', ['IdlePowerDetail'])
627+
628+ # Adding model 'IdlePowerBuild'
629+ db.create_table('idle_power_builds', (
630+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
631+ ('created_at', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
632+ ('updated_at', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, blank=True)),
633+ ('internal', self.gf('django.db.models.fields.BooleanField')(default=True)),
634+ ('publish', self.gf('django.db.models.fields.BooleanField')(default=True)),
635+ ('image', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['idle_power.IdlePowerImage'], null=True)),
636+ ('machine', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['idle_power.IdlePowerMachine'])),
637+ ('build', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['common.JenkinsBuild'])),
638+ ))
639+ db.send_create_signal('idle_power', ['IdlePowerBuild'])
640+
641+
642+ def backwards(self, orm):
643+ # Deleting model 'IdlePowerImage'
644+ db.delete_table('idle_power_images')
645+
646+ # Deleting model 'IdlePowerMachine'
647+ db.delete_table('idle_power_machines')
648+
649+ # Deleting model 'IdlePowerLog'
650+ db.delete_table('idle_power_logs')
651+
652+ # Deleting model 'IdlePowerDetail'
653+ db.delete_table('idle_power_details')
654+
655+ # Deleting model 'IdlePowerBuild'
656+ db.delete_table('idle_power_builds')
657+
658+
659+ models = {
660+ 'common.bug': {
661+ 'Meta': {'object_name': 'Bug', 'db_table': "'bugs'"},
662+ 'bug_no': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
663+ 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
664+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
665+ 'internal': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
666+ 'publish': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
667+ 'status': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
668+ 'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'})
669+ },
670+ 'common.jenkinsbuild': {
671+ 'Meta': {'unique_together': "(('job', 'build_number'),)", 'object_name': 'JenkinsBuild', 'db_table': "'jenkins_builds'"},
672+ 'bugs': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'build_bugs'", 'symmetrical': 'False', 'to': "orm['common.Bug']"}),
673+ 'build_number': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
674+ 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
675+ 'failed': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
676+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
677+ 'internal': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
678+ 'job': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['common.JenkinsJob']"}),
679+ 'publish': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
680+ 'ran_at': ('django.db.models.fields.DateTimeField', [], {}),
681+ 'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'})
682+ },
683+ 'common.jenkinsjob': {
684+ 'Meta': {'object_name': 'JenkinsJob', 'db_table': "'jenkins_jobs'"},
685+ 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
686+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
687+ 'internal': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
688+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '200'}),
689+ 'publish': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
690+ 'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
691+ 'url': ('django.db.models.fields.URLField', [], {'max_length': '200'})
692+ },
693+ 'idle_power.idlepowerbuild': {
694+ 'Meta': {'object_name': 'IdlePowerBuild', 'db_table': "'idle_power_builds'"},
695+ 'build': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['common.JenkinsBuild']"}),
696+ 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
697+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
698+ 'image': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['idle_power.IdlePowerImage']", 'null': 'True'}),
699+ 'internal': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
700+ 'machine': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['idle_power.IdlePowerMachine']"}),
701+ 'publish': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
702+ 'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'})
703+ },
704+ 'idle_power.idlepowerdetail': {
705+ 'Meta': {'object_name': 'IdlePowerDetail', 'db_table': "'idle_power_details'"},
706+ 'calculated_energy': ('django.db.models.fields.PositiveIntegerField', [], {}),
707+ 'charge_start': ('django.db.models.fields.PositiveIntegerField', [], {}),
708+ 'charge_used': ('django.db.models.fields.PositiveIntegerField', [], {}),
709+ 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
710+ 'energy_start': ('django.db.models.fields.PositiveIntegerField', [], {}),
711+ 'energy_used': ('django.db.models.fields.PositiveIntegerField', [], {}),
712+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
713+ 'image': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['idle_power.IdlePowerImage']", 'null': 'True'}),
714+ 'internal': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
715+ 'jenkins_url': ('django.db.models.fields.URLField', [], {'max_length': '200'}),
716+ 'machine': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['idle_power.IdlePowerMachine']"}),
717+ 'publish': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
718+ 'ran_at': ('django.db.models.fields.DateTimeField', [], {}),
719+ 'testcase': ('django.db.models.fields.CharField', [], {'max_length': '4096'}),
720+ 'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'})
721+ },
722+ 'idle_power.idlepowerimage': {
723+ 'Meta': {'object_name': 'IdlePowerImage', 'db_table': "'idle_power_images'"},
724+ 'arch': ('django.db.models.fields.CharField', [], {'max_length': '200'}),
725+ 'build_number': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
726+ 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
727+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
728+ 'image_type': ('django.db.models.fields.CharField', [], {'default': "u'daily'", 'max_length': '10'}),
729+ 'internal': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
730+ 'md5': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '200'}),
731+ 'publish': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
732+ 'release': ('django.db.models.fields.CharField', [], {'max_length': '200'}),
733+ 'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
734+ 'variant': ('django.db.models.fields.CharField', [], {'max_length': '200'})
735+ },
736+ 'idle_power.idlepowerlog': {
737+ 'Meta': {'object_name': 'IdlePowerLog', 'db_table': "'idle_power_logs'"},
738+ 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
739+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
740+ 'internal': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
741+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '4096'}),
742+ 'path': ('django.db.models.fields.CharField', [], {'max_length': '4096'}),
743+ 'publish': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
744+ 'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'})
745+ },
746+ 'idle_power.idlepowermachine': {
747+ 'Meta': {'object_name': 'IdlePowerMachine', 'db_table': "'idle_power_machines'"},
748+ 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
749+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
750+ 'internal': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
751+ 'mac_address': ('django.db.models.fields.CharField', [], {'max_length': '200'}),
752+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '200'}),
753+ 'publish': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
754+ 'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'})
755+ }
756+ }
757+
758+ complete_apps = ['idle_power']
759\ No newline at end of file
760
761=== added file 'idle_power/migrations/__init__.py'
762--- idle_power/migrations/__init__.py 1970-01-01 00:00:00 +0000
763+++ idle_power/migrations/__init__.py 2013-04-16 20:51:23 +0000
764@@ -0,0 +1,14 @@
765+# QA Dashboard
766+# Copyright 2012-2013 Canonical Ltd.
767+
768+# This program is free software: you can redistribute it and/or modify it
769+# under the terms of the GNU Affero General Public License version 3, as
770+# published by the Free Software Foundation.
771+
772+# This program is distributed in the hope that it will be useful, but
773+# WITHOUT ANY WARRANTY; without even the implied warranties of
774+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
775+# PURPOSE. See the GNU Affero General Public License for more details.
776+
777+# You should have received a copy of the GNU Affero General Public License
778+# along with this program. If not, see <http://www.gnu.org/licenses/>.
779
780=== added file 'idle_power/models.py'
781--- idle_power/models.py 1970-01-01 00:00:00 +0000
782+++ idle_power/models.py 2013-04-16 20:51:23 +0000
783@@ -0,0 +1,85 @@
784+# QA Dashboard
785+# Copyright 2012-2013 Canonical Ltd.
786+
787+# This program is free software: you can redistribute it and/or modify it
788+# under the terms of the GNU Affero General Public License version 3, as
789+# published by the Free Software Foundation.
790+
791+# This program is distributed in the hope that it will be useful, but
792+# WITHOUT ANY WARRANTY; without even the implied warranties of
793+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
794+# PURPOSE. See the GNU Affero General Public License for more details.
795+
796+# You should have received a copy of the GNU Affero General Public License
797+# along with this program. If not, see <http://www.gnu.org/licenses/>.
798+
799+from django.db import models
800+
801+from common.models import (
802+ DashboardBaseModel,
803+ JenkinsBuild,
804+)
805+
806+from performance.models import (
807+ ImageBase,
808+ MachineBase,
809+)
810+
811+
812+class IdlePowerImage(ImageBase):
813+ """ Idle Power Image model. """
814+
815+ class Meta:
816+ db_table = "idle_power_images"
817+
818+
819+class IdlePowerMachine(MachineBase):
820+ """ Idle Power Machine model. """
821+
822+ class Meta:
823+ db_table = "idle_power_machines"
824+
825+
826+class IdlePowerLog(DashboardBaseModel):
827+ """ Idle Power logs for a build. """
828+
829+ class Meta:
830+ db_table = "idle_power_logs"
831+
832+ path = models.CharField(max_length=4096)
833+ name = models.CharField(max_length=4096)
834+
835+ def __unicode__(self):
836+ return "{}".format(self.name)
837+
838+
839+class IdlePowerDetail(DashboardBaseModel):
840+ """ Idle Power raw data. """
841+
842+ class Meta:
843+ db_table = "idle_power_details"
844+
845+ image = models.ForeignKey(IdlePowerImage, null=True)
846+ machine = models.ForeignKey(IdlePowerMachine)
847+ testcase = models.CharField(max_length=4096)
848+ calculated_energy = models.PositiveIntegerField()
849+ charge_start = models.PositiveIntegerField()
850+ charge_used = models.PositiveIntegerField()
851+ energy_start = models.PositiveIntegerField()
852+ energy_used = models.PositiveIntegerField()
853+ ran_at = models.DateTimeField("date run")
854+ jenkins_url = models.URLField()
855+
856+ def __unicode__(self):
857+ return "{}".format(self.testcase)
858+
859+
860+class IdlePowerBuild(DashboardBaseModel):
861+ """ Jenkins Build image. """
862+
863+ class Meta:
864+ db_table = "idle_power_builds"
865+
866+ image = models.ForeignKey(IdlePowerImage, null=True)
867+ machine = models.ForeignKey(IdlePowerMachine)
868+ build = models.ForeignKey(JenkinsBuild)
869
870=== added file 'idle_power/tables.py'
871--- idle_power/tables.py 1970-01-01 00:00:00 +0000
872+++ idle_power/tables.py 2013-04-16 20:51:23 +0000
873@@ -0,0 +1,72 @@
874+# QA Dashboard
875+# Copyright 2012-2013 Canonical Ltd.
876+
877+# This program is free software: you can redistribute it and/or modify it
878+# under the terms of the GNU Affero General Public License version 3, as
879+# published by the Free Software Foundation.
880+
881+# This program is distributed in the hope that it will be useful, but
882+# WITHOUT ANY WARRANTY; without even the implied warranties of
883+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
884+# PURPOSE. See the GNU Affero General Public License for more details.
885+
886+# You should have received a copy of the GNU Affero General Public License
887+# along with this program. If not, see <http://www.gnu.org/licenses/>.
888+
889+import django_tables2 as tables
890+from django_tables2.utils import Accessor as A
891+
892+
893+class MachineTable(tables.Table):
894+ build_number = tables.LinkColumn(
895+ 'idle_machine_raw_data',
896+ accessor='image__build_number',
897+ kwargs={
898+ 'machine_id': A('machine__id'),
899+ 'arch': A('image__arch'),
900+ 'build_number': A('image__build_number'),
901+ },
902+ verbose_name="Build Number",
903+ )
904+ testcase = tables.Column(verbose_name="Test Case")
905+ energy_avg = tables.TemplateColumn(
906+ '{{ record.energy_avg|floatformat:2 }}',
907+ verbose_name="Average Energy used (mW)",
908+ attrs={'td': {'class': 'num'}},
909+ )
910+ energy_stddev = tables.TemplateColumn(
911+ '{{ record.energy_stddev|floatformat:2 }}',
912+ verbose_name="Standard Deviation",
913+ attrs={'td': {'class': 'num'}},
914+ )
915+
916+ class Meta:
917+ attrs = {'class': 'basic'}
918+
919+
920+class DataTable(tables.Table):
921+ testcase = tables.Column(verbose_name="Test Case")
922+ energy_start = tables.Column(
923+ verbose_name="Energy Start (mW)",
924+ attrs={'td': {'class': 'num'}},
925+ )
926+ energy_used = tables.Column(
927+ verbose_name="Energy used (mW)",
928+ attrs={'td': {'class': 'num'}},
929+ )
930+ calculated_energy = tables.Column(
931+ verbose_name="Calculated Energy (mW)",
932+ attrs={'td': {'class': 'num'}},
933+ )
934+ charge_start = tables.Column(
935+ verbose_name="Charge Start (mA)",
936+ attrs={'td': {'class': 'num'}},
937+ )
938+ charge_used = tables.Column(
939+ verbose_name="Charge Used (mA)",
940+ attrs={'td': {'class': 'num'}},
941+ )
942+
943+ class Meta:
944+ attrs = {'class': 'basic'}
945+ order_by = ('testcase',)
946
947=== added directory 'idle_power/templates'
948=== added directory 'idle_power/templates/idle_power'
949=== added file 'idle_power/templates/idle_power/machine_raw_data.html'
950--- idle_power/templates/idle_power/machine_raw_data.html 1970-01-01 00:00:00 +0000
951+++ idle_power/templates/idle_power/machine_raw_data.html 2013-04-16 20:51:23 +0000
952@@ -0,0 +1,37 @@
953+{% extends "power/power_layout.html" %}
954+{% load dashboard_extras %}
955+{% load render_table from django_tables2 %}
956+
957+{% block page_name %}{{ name }} - {{ arch }} - raw data{% endblock %}
958+{% block extra_headers %}
959+<style>
960+td {
961+ font-size: 0.8em;
962+}
963+th {
964+ font-size: 0.9em;
965+}
966+.box-padded {
967+ margin: 20px;
968+}
969+</style>
970+{% endblock extra_headers %}
971+
972+{% block content %}
973+<div class='grid_15'>
974+ <h2>{{ name }} raw data</h2>
975+ <p>
976+ <strong>Build #:</strong> <a href="{{ jenkins_url }}">{{ build_number }}</a><br />
977+ <strong>Ran at:</strong> {{ ran_at }}<br />
978+ <strong>Arch:</strong> {{ arch }}<br />
979+ <strong>Release:</strong> {{ release }}<br />
980+ <strong>Variant:</strong> {{ variant }}
981+ </p>
982+</div>
983+<div class='grid_8'>
984+{% render_table table %}
985+</div>
986+<div class='grid_7'>
987+ {% include "idle_power/metrics.html" %}
988+</div>
989+{% endblock %}
990
991=== added file 'idle_power/templates/idle_power/machine_table.html'
992--- idle_power/templates/idle_power/machine_table.html 1970-01-01 00:00:00 +0000
993+++ idle_power/templates/idle_power/machine_table.html 2013-04-16 20:51:23 +0000
994@@ -0,0 +1,30 @@
995+{% extends "power/power_layout.html" %}
996+{% load dashboard_extras %}
997+{% load render_table from django_tables2 %}
998+
999+{% block page_name %}{{ name }}{% endblock %}
1000+{% block extra_headers %}
1001+<style>
1002+td {
1003+ font-size: 0.8em;
1004+}
1005+th {
1006+ font-size: 0.9em;
1007+}
1008+.box-padded {
1009+ margin: 20px;
1010+}
1011+</style>
1012+{% endblock extra_headers %}
1013+
1014+{% block content %}
1015+<div class='grid_15'>
1016+ <h2>{{ name }} Data Overview</h2>
1017+ <p>
1018+ <strong>Arch:</strong> {{ arch }}<br />
1019+ </p>
1020+</div>
1021+<div class='grid_8'>
1022+{% render_table table %}
1023+</div>
1024+{% endblock %}
1025
1026=== added file 'idle_power/templates/idle_power/metrics.html'
1027--- idle_power/templates/idle_power/metrics.html 1970-01-01 00:00:00 +0000
1028+++ idle_power/templates/idle_power/metrics.html 2013-04-16 20:51:23 +0000
1029@@ -0,0 +1,28 @@
1030+<div class="box box-padded">
1031+<h3>Metrics Explained</h3>
1032+<div>
1033+The metrics in this chart come from the <a href="https://www.kernel.org/doc/Documentation/power/power_supply_class.txt">power supply</a> driver in the kernel.
1034+
1035+ <dl>
1036+ <dt>Energy Start</dt>
1037+ <dd>The amount of energy in mW the battery contained.</dd>
1038+ <dt>Energy Used</dt>
1039+ <dd>This is difference between the "energy_now" measurements at the
1040+ start and end of the test. It represents the actual energy
1041+ that was consumed.</dd>
1042+ <dt>Computed Energy</dt>
1043+ <dd>We can multiple the "charge used" by an estimate of the
1044+ voltage:
1045+ <pre>end_voltage + 0.5 * abs(start_voltage - end_voltage)</pre>
1046+ to make a sanity check on the "energy used" metric. If this
1047+ value has a >1% difference, then we mark it so you'll know
1048+ something is inconsistent.</dd>
1049+ <dt>Charge Start</dt>
1050+ <dd>The amount of charge (in mA) reported by the battery at the
1051+ beginning of the test.</dd>
1052+ <dt>Charge Used</dt>
1053+ <dd>The difference of the "charge_now" metric at the start and end
1054+ the test.</dd>
1055+ </dl>
1056+</div>
1057+</div>
1058
1059=== added file 'idle_power/urls.py'
1060--- idle_power/urls.py 1970-01-01 00:00:00 +0000
1061+++ idle_power/urls.py 2013-04-16 20:51:23 +0000
1062@@ -0,0 +1,33 @@
1063+# QA Dashboard
1064+# Copyright 2012-2013 Canonical Ltd.
1065+
1066+# This program is free software: you can redistribute it and/or modify it
1067+# under the terms of the GNU Affero General Public License version 3, as
1068+# published by the Free Software Foundation.
1069+
1070+# This program is distributed in the hope that it will be useful, but
1071+# WITHOUT ANY WARRANTY; without even the implied warranties of
1072+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
1073+# PURPOSE. See the GNU Affero General Public License for more details.
1074+
1075+# You should have received a copy of the GNU Affero General Public License
1076+# along with this program. If not, see <http://www.gnu.org/licenses/>.
1077+
1078+from django.conf.urls.defaults import (
1079+ patterns,
1080+ url,
1081+)
1082+
1083+urlpatterns = patterns(
1084+ 'idle_power.views',
1085+ url(
1086+ r'^machine/(?P<machine_id>\d+)/(?P<arch>\w+)/$',
1087+ 'machine_detail',
1088+ name='idle_machine_detail'
1089+ ),
1090+ url(
1091+ r'^machine/(?P<machine_id>\d+)/(?P<arch>\w+)/(?P<build_number>\d+)/$',
1092+ 'machine_raw_data',
1093+ name='idle_machine_raw_data'
1094+ ),
1095+)
1096
1097=== added file 'idle_power/utah_utils.py'
1098--- idle_power/utah_utils.py 1970-01-01 00:00:00 +0000
1099+++ idle_power/utah_utils.py 2013-04-16 20:51:23 +0000
1100@@ -0,0 +1,112 @@
1101+# QA Dashboard
1102+# Copyright 2012-2013 Canonical Ltd.
1103+
1104+# This program is free software: you can redistribute it and/or modify it
1105+# under the terms of the GNU Affero General Public License version 3, as
1106+# published by the Free Software Foundation.
1107+
1108+# This program is distributed in the hope that it will be useful, but
1109+# WITHOUT ANY WARRANTY; without even the implied warranties of
1110+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
1111+# PURPOSE. See the GNU Affero General Public License for more details.
1112+
1113+# You should have received a copy of the GNU Affero General Public License
1114+# along with this program. If not, see <http://www.gnu.org/licenses/>.
1115+
1116+"""
1117+UTAH result parsing code.
1118+"""
1119+
1120+import logging
1121+
1122+from common.utah_parser import UTAHParser, ParserError
1123+
1124+from .models import (
1125+ IdlePowerDetail,
1126+)
1127+
1128+FLAVOR = 'ubuntu'
1129+
1130+
1131+def process_idle_power_log(
1132+ logfile,
1133+ image,
1134+ machine,
1135+ jenkins_url=None,
1136+ name=None
1137+):
1138+ """
1139+ Parse a utah client log for smoke test results.
1140+ """
1141+ details = []
1142+
1143+ logfile_path = logfile
1144+ if jenkins_url is not None:
1145+ logfile_path = "{}/artifact/{}".format(
1146+ jenkins_url,
1147+ logfile,
1148+ )
1149+
1150+ if jenkins_url is None:
1151+ jenkins_url = "http://jenkins.qa.ubuntu.com/"
1152+
1153+ parser = UTAHParser()
1154+
1155+ try:
1156+ data = parser.parse(logfile_path)
1157+ except ParserError as e:
1158+ if not e.zero_entry:
1159+ logging.error("%s %s", logfile_path, e)
1160+ return details
1161+
1162+ if data is None:
1163+ logging.warn("Unable to parse %s", logfile_path)
1164+ return details
1165+
1166+ # Use the name from the logs if there is one, if there isn't one
1167+ # and one is passed in use that.
1168+ if 'name' not in data:
1169+ data['name'] = 'unnamed'
1170+
1171+ if name is None or data['name'] != 'unnamed':
1172+ name = data['name']
1173+ data['machine'] = machine
1174+ data['image'] = image
1175+ data['jenkins_url'] = jenkins_url
1176+
1177+ _add_details(details, data)
1178+
1179+
1180+def _add_details(details, data):
1181+ for command in data['commands']:
1182+ if command['cmd_type'] != 'testcase_test':
1183+ continue
1184+ testcase = command['testcase']
1185+ start_voltage = command['battery']['start']['voltage_now'] / 1000
1186+ end_voltage = command['battery']['end']['voltage_now'] / 1000
1187+ energy_start = command['battery']['start']['energy_now'] / 1000
1188+ energy_end = command['battery']['end']['energy_now'] / 1000
1189+ energy_used = energy_start - energy_end
1190+ charge_start = command['battery']['start']['charge_now'] / 1000
1191+ charge_end = command['battery']['end']['charge_now'] / 1000
1192+ charge_used = charge_start - charge_end
1193+ ran_at = command['start_time']
1194+ voltage_est = (
1195+ end_voltage + 0.5 * abs(start_voltage - end_voltage)
1196+ ) / 1000
1197+ calculated_energy = charge_used * voltage_est
1198+ detail, new_detail = IdlePowerDetail.objects.get_or_create(
1199+ image=data['image'],
1200+ machine=data['machine'],
1201+ jenkins_url=data['jenkins_url'],
1202+ ran_at=ran_at,
1203+ testcase=testcase,
1204+ calculated_energy=calculated_energy,
1205+ charge_start=charge_start,
1206+ charge_used=charge_used,
1207+ energy_start=energy_start,
1208+ energy_used=energy_used,
1209+ )
1210+ details.append(detail)
1211+
1212+ return details
1213
1214=== added file 'idle_power/views.py'
1215--- idle_power/views.py 1970-01-01 00:00:00 +0000
1216+++ idle_power/views.py 2013-04-16 20:51:23 +0000
1217@@ -0,0 +1,104 @@
1218+# QA Dashboard
1219+# Copyright 2012-2013 Canonical Ltd.
1220+
1221+# This program is free software: you can redistribute it and/or modify it
1222+# under the terms of the GNU Affero General Public License version 3, as
1223+# published by the Free Software Foundation.
1224+
1225+# This program is distributed in the hope that it will be useful, but
1226+# WITHOUT ANY WARRANTY; without even the implied warranties of
1227+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
1228+# PURPOSE. See the GNU Affero General Public License for more details.
1229+
1230+# You should have received a copy of the GNU Affero General Public License
1231+# along with this program. If not, see <http://www.gnu.org/licenses/>.
1232+
1233+from django.views.decorators.http import require_GET
1234+from django.template import RequestContext
1235+from django.db.models import Avg, StdDev
1236+
1237+from django.shortcuts import (
1238+ render_to_response,
1239+ get_object_or_404,
1240+)
1241+
1242+from django_tables2 import RequestConfig
1243+
1244+from idle_power.models import (
1245+ IdlePowerMachine,
1246+ IdlePowerDetail,
1247+)
1248+
1249+from idle_power.tables import DataTable, MachineTable
1250+
1251+
1252+@require_GET
1253+def machine_detail(request, machine_id, arch):
1254+ machine = get_object_or_404(IdlePowerMachine, id=machine_id)
1255+ qs = IdlePowerDetail.objects.filter(
1256+ machine__id=machine_id
1257+ ).values(
1258+ 'image__build_number',
1259+ 'image__arch',
1260+ 'testcase',
1261+ 'machine__id',
1262+ ).annotate(
1263+ energy_avg=Avg('energy_used'),
1264+ energy_stddev=StdDev('energy_used'),
1265+ ).order_by('-image__build_number')
1266+ table = MachineTable(qs)
1267+ RequestConfig(request, paginate={"per_page": 100}).configure(table)
1268+ data = {
1269+ 'name': machine.name,
1270+ 'arch': arch,
1271+ 'table': table,
1272+ }
1273+
1274+ return render_to_response(
1275+ 'idle_power/machine_table.html',
1276+ data,
1277+ RequestContext(request)
1278+ )
1279+
1280+
1281+@require_GET
1282+def machine_raw_data(request, machine_id, arch, build_number):
1283+ machine = get_object_or_404(IdlePowerMachine, id=machine_id)
1284+
1285+ qs = IdlePowerDetail.objects.filter(
1286+ machine__id=machine_id,
1287+ image__build_number=build_number,
1288+ image__arch=arch,
1289+ ).values(
1290+ 'image__build_number',
1291+ 'image__arch',
1292+ 'image__release',
1293+ 'image__variant',
1294+ 'ran_at',
1295+ 'testcase',
1296+ 'energy_start',
1297+ 'energy_used',
1298+ 'calculated_energy',
1299+ 'charge_start',
1300+ 'charge_used',
1301+ 'jenkins_url',
1302+ )
1303+ table = DataTable(qs)
1304+ RequestConfig(request, paginate={"per_page": 100}).configure(table)
1305+ details = qs[0]
1306+ data = {
1307+ 'name': machine.name,
1308+ 'id': machine.id,
1309+ 'arch': details['image__arch'],
1310+ 'table': table,
1311+ 'build_number': build_number,
1312+ 'release': details['image__release'],
1313+ 'ran_at': details['ran_at'],
1314+ 'variant': details['image__variant'],
1315+ 'jenkins_url': details['jenkins_url'],
1316+ }
1317+ return render_to_response(
1318+ "idle_power/machine_raw_data.html",
1319+ data,
1320+ RequestContext(request)
1321+ )
1322
1323=== modified file 'power/templates/power/power_layout.html'
1324--- power/templates/power/power_layout.html 2013-04-02 21:09:05 +0000
1325+++ power/templates/power/power_layout.html 2013-04-16 20:51:23 +0000
1326@@ -1,2 +1,5 @@
1327 {% extends "layout.html" %}
1328-{% block sub_nav %}{% endblock %}
1329+{% block sub_nav_links %}
1330+<li {% ifequal url.1 'software' %}class="active"{% endifequal %}><a class="sub-nav-item" href="{% url power_arch_overview 'amd64' %}">Software Testing</a></li>
1331+<li {% ifequal url.1 'idle' %}class="active"{% endifequal %}><a class="sub-nav-item" href="{% url idle_machine_detail 1 'armhf' %}">Idle Testing</a></li>
1332+{% endblock %}
1333
1334=== modified file 'qa_dashboard/settings.py'
1335--- qa_dashboard/settings.py 2013-04-05 00:37:07 +0000
1336+++ qa_dashboard/settings.py 2013-04-16 20:51:23 +0000
1337@@ -147,6 +147,7 @@
1338 'power',
1339 'smoke',
1340 'sru',
1341+ 'idle_power',
1342 )
1343
1344 INSTALLED_APPS = (
1345
1346=== modified file 'qa_dashboard/urls.py'
1347--- qa_dashboard/urls.py 2013-04-16 11:57:48 +0000
1348+++ qa_dashboard/urls.py 2013-04-16 20:51:23 +0000
1349@@ -28,6 +28,7 @@
1350 '',
1351 url(r'^power/software/', include('power.urls')),
1352 url(r'^api/power/software/', include('power.urls_api')),
1353+ url(r'^power/idle/', include('idle_power.urls')),
1354 )
1355
1356 urlpatterns += patterns(
1357
1358=== added file 'scripts/fakeup_idle_power.py'
1359--- scripts/fakeup_idle_power.py 1970-01-01 00:00:00 +0000
1360+++ scripts/fakeup_idle_power.py 2013-04-16 20:51:23 +0000
1361@@ -0,0 +1,134 @@
1362+import datetime
1363+import logging
1364+import os
1365+import random
1366+import time
1367+import sys
1368+
1369+sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
1370+os.environ['DJANGO_SETTINGS_MODULE'] = 'qa_dashboard.settings'
1371+
1372+logging.basicConfig(level=logging.INFO)
1373+
1374+from idle_power.models import (
1375+ IdlePowerDetail,
1376+ IdlePowerImage,
1377+ IdlePowerMachine,
1378+ IdlePowerResult,
1379+ IdlePowerLog,
1380+)
1381+
1382+
1383+def add_machines(count=3):
1384+
1385+ machines = []
1386+
1387+ for i in range(count):
1388+ machines.append(
1389+ IdlePowerMachine.objects.create(
1390+ name='idle-power-test-{}'.format(i),
1391+ )
1392+ )
1393+
1394+ return machines
1395+
1396+
1397+def add_log():
1398+ log = IdlePowerLog.objects.create(
1399+ path='idle-power-test-path',
1400+ name='idle-power-test-name',
1401+ )
1402+ return log
1403+
1404+
1405+def add_images(count=1):
1406+
1407+ images = []
1408+
1409+ for i in range(count):
1410+ images.append(
1411+ IdlePowerImage.objects.create(
1412+ arch='amd64',
1413+ variant='desktop',
1414+ release='raring',
1415+ build_number='{}'.format(i),
1416+ md5='fake{}'.format(i),
1417+ )
1418+ )
1419+ images.append(
1420+ IdlePowerImage.objects.create(
1421+ arch='i386',
1422+ variant='desktop',
1423+ release='raring',
1424+ build_number='{}'.format(i),
1425+ md5='fake1{}'.format(i),
1426+ )
1427+ )
1428+
1429+ return images
1430+
1431+
1432+def add_details(machine, log, image=None, upgrade=None, count=3):
1433+
1434+ details = []
1435+
1436+ for j in range(count):
1437+ date = datetime.datetime.now()
1438+ result = IdlePowerResult.objects.create(
1439+ image=image,
1440+ machine=machine,
1441+ value=0,
1442+ ran_at=date,
1443+ name=m,
1444+ log=log,
1445+ )
1446+ for i in range(count):
1447+ details.append(
1448+ IdlePowerDetail.objects.create(
1449+ image=image,
1450+ machine=machine,
1451+ command="idle",
1452+ voltage_avg=random.uniform(0.1, 1) * 100,
1453+ energy_used=random.uniform(0.1, 5) * 100,
1454+ energy_start=random.uniform(0.1, 1) * 100,
1455+ charge_start=random.uniform(0.1, 1) * 100,
1456+ charge_used=random.uniform(1.1, 2) * 100,
1457+ ran_at=date,
1458+ result=result,
1459+ )
1460+ )
1461+ time.sleep(2)
1462+
1463+
1464+def cleanup():
1465+
1466+ for klass in [
1467+ IdlePowerDetail,
1468+ IdlePowerResult,
1469+ IdlePowerMachine,
1470+ IdlePowerImage,
1471+ IdlePowerLog
1472+ ]:
1473+ logging.debug("klass: {}".format(klass))
1474+ klass.objects.all().delete()
1475+
1476+
1477+def list_stuff():
1478+
1479+ for klass in [
1480+ IdlePowerImage,
1481+ IdlePowerMachine,
1482+ IdlePowerDetail,
1483+ IdlePowerLog,
1484+ ]:
1485+ for item in klass.objects.all():
1486+ logging.debug(item)
1487+
1488+if __name__ == "__main__":
1489+ cleanup()
1490+ machines = add_machines(count=3)
1491+ images = add_images(count=10)
1492+ log = add_log()
1493+ for image in images:
1494+ for machine in machines:
1495+ add_details(machine, log, image=image)
1496
1497=== modified file 'smoke/utah_utils.py'
1498--- smoke/utah_utils.py 2013-03-22 20:05:56 +0000
1499+++ smoke/utah_utils.py 2013-04-16 20:51:23 +0000
1500@@ -19,7 +19,8 @@
1501
1502 import logging
1503
1504-from smoke.utah_parser import UTAHParser, ParserError
1505+from common.utah_parser import UTAHParser, ParserError
1506+
1507 from smoke.models import (
1508 Build,
1509 Run,

Subscribers

People subscribed via source and target branches

to all changes: