2
# Copyright 2013 Canonical Ltd.
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License version 3, as
6
# published by the Free Software Foundation.
8
# This program is distributed in the hope that it will be useful, but
9
# WITHOUT ANY WARRANTY; without even the implied warranties of
10
# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
11
# PURPOSE. See the GNU Affero General Public License for more details.
13
# You should have received a copy of the GNU Affero General Public
14
# License along with this program. If not, see
15
# <http://www.gnu.org/licenses/>.
22
from django.db import models
24
from common.management import (
28
from eventstat.models import (
37
from performance.management import (
38
get_image_build_number,
45
def wrapped(*args, **kwargs):
46
logging.warn("fn: %s", fn.__name__)
48
return fn(*args, **kwargs)
53
class Command(JenkinsBaseCommand):
55
job_regex = re.compile(
56
ur"eventstat-([^-]+)-([^-]+)-([^-]+)-(install|upgrade)-([^-]+)-(.*)",
61
def _get_eventstat_logs(self, artifacts):
62
""" Check for valid eventstat results.
64
:returns: a list of eventstat logs.
69
eventstat_log_regex = re.compile(
70
ur'^(results\/|)(\d+\/|)eventstat.csv',
72
log_regex = re.compile(ur'^(results\/|)(\d+\/|)eventstat.(csv,log)')
73
utah_log_regex = re.compile(ur'^(clientlogs\/|)utah.*.yaml')
75
for artifact in artifacts:
76
logging.info("artifact: %s", artifact)
77
path = artifact['relativePath']
78
num = get_run_number(path, prefix="results\/")
80
if utah_log_regex.match(path):
81
logging.debug("found %s", path)
82
logs['utah'] = artifact
93
logging.debug("artifact: %s", path)
94
if eventstat_log_regex.match(path):
95
logging.debug("found %s", path)
96
logs[num]['data_file'] = artifact
97
if log_regex.match(path):
98
logging.debug("found eventstat log %s", path)
99
logs[num]['logs'].append(artifact)
103
def _process_logs(self, build, build_date, install_data, func):
104
""" Process eventstat logs calling func() for each log. """
106
if not hasattr(func, '__call__'):
107
raise Exception("invalid callable: {}".format(func))
109
unzip_path = self.get_artifacts(build)
110
install_data['unzip_path'] = unzip_path
112
logs = self._get_eventstat_logs(build['artifacts'])
113
if 'data_file' in logs:
114
logging.info("logs: %s", logs)
116
image_build_number = get_image_build_number(build)
118
utah_log = logs.pop('utah', None)
120
kernel = self.get_kernel(build['url'], utah_log)
122
unzip_path = '{}artifact'.format(build['url'])
123
online_path = unzip_path
125
if 'unzip_path' in install_data:
126
unzip_path = "{}/archive".format(install_data['unzip_path'])
129
# skip incomplete logs
130
if logs[num]['data_file'] is None:
133
logs[num]['data_file'],
137
log_name = logs[num]['data_file']['relativePath']
138
log_path = "{}/{}".format(
142
log_path_url = "{}/{}".format(
146
logging.info("log_path_url: %s", log_path_url)
148
install_type = install_data['method']
149
dashboard_data = dict(
150
release=install_data['release'],
151
variant=install_data['variant'],
152
arch=install_data['arch'],
153
build_date=build_date,
156
if 'jenkins_build' in install_data:
157
dashboard_data['jenkins_build'] = install_data['jenkins_build']
159
if install_type == 'install':
160
dashboard_data['build_number'] = image_build_number
162
dashboard_data['date'] = build_date
164
# XXX: if we start having builds that upgrade across
165
# releases we will need to update this
166
dashboard_data['from_release'] = install_data['release']
168
# Assume we need to retrieve the logs if there's no 'unzip_path'
169
if 'unzip_path' not in install_data:
170
logging.warn("Getting logs from jenkins directly...")
171
dashboard_data['output'] = jenkins_get(
176
dashboard_data['output'] = self.get_local_file(log_path)
177
dashboard_data['csv'] = log_path
179
dashboard_data['log_path'] = log_path
180
dashboard_data['log_path_url'] = log_path_url
181
dashboard_data['log_name'] = log_name
185
def _parse_task(self, task):
188
return task.split()[0]
190
return os.path.basename(task.split()[0])
192
def _task_type(self, task):
195
return TASK_TYPE_CHOICES[0][1]
197
return TASK_TYPE_CHOICES[1][1]
199
def _parse_log(self, filename):
200
""" Parse an eventstat.csv file. """
204
'Init Function': 'init_function',
205
'Callback': 'callback',
210
'Std.Dev.': 'stddev',
214
with open(filename, 'rb') as f:
216
reader = csv.reader(f)
219
row_key = row[0].rstrip(':')
220
logging.info("row_key type: %s", type(row_key))
223
interval = int(row_key)
227
if row_key not in mapping and interval is None:
228
logging.warn("unkown row: %s", row_key)
231
field = mapping.get(row_key, row_key)
235
for i in range(len(row)):
237
data.insert(i, dict())
241
# Add interval values to a sub-array.
242
if interval is not None:
243
if 'intervals' not in data[i]:
244
data[i]['intervals'] = []
246
data[i]['intervals'].append(
255
data[i]['command'] = value
256
value = self._parse_task(value)
257
logging.info("value: %s", value)
258
data[i]['task_type'] = self._task_type(value)
260
data[i][field] = value
264
def _valid_log_format(self, filename):
265
""" Determine if an eventstat log is valid.
266
Right now this means it has a 'Max:' row.
271
with open(filename, 'rb') as f:
273
reader = csv.reader(f)
276
if row[0].rstrip(':') == 'Max':
282
def add_result(self, dashboard_data):
283
""" Add eventstat data to the database. """
285
if not self._valid_log_format(dashboard_data['csv']):
287
"Invalid log format: %s",
288
dashboard_data['log_path_url'],
292
logging.info("Adding eventstat result.")
294
"Found good log: %s",
295
dashboard_data['log_path_url'],
298
items = self._parse_log(dashboard_data['csv'])
299
logging.info("items: %s", items)
301
if 'build_number' in dashboard_data:
302
image, new_image = EventstatImage.objects.get_or_create(
303
release=dashboard_data['release'],
304
variant=dashboard_data['variant'],
305
arch=dashboard_data['arch'],
306
build_number=dashboard_data['build_number'],
307
# fake an md5 since we don't have the data
308
md5="{}{}{}{}".format(
312
dashboard_data['build_number'],
318
upg, new_upgr = EventstatUpgrade.objects.get_or_create(
319
release=dashboard_data['release'],
320
variant=dashboard_data['variant'],
321
arch=dashboard_data['arch'],
322
date=dashboard_data['date'],
323
from_release=dashboard_data['from_release'],
328
log, new_log = EventstatLog.objects.get_or_create(
329
path=dashboard_data['log_path_url'],
330
name=dashboard_data['log_name'],
334
if 'jenkins_build' in dashboard_data:
335
jenkins_url = dashboard_data['jenkins_build'].url
338
task_types = [x[1] for x in TASK_TYPE_CHOICES]
339
task_types.append('total')
340
for task_type in task_types:
341
metric, new_metric = EventstatMetric.objects.get_or_create(
343
upgrade=self.upgrade,
344
machine=self.machine,
346
kernel=dashboard_data['kernel'],
348
ran_at=dashboard_data['build_date'],
353
metrics[task_type] = metric
356
logging.info("item: %s", item)
357
task_type = item['task_type']
359
for interval in item['intervals']:
360
value = int(float(interval['value']))
361
interval = int(float(interval['interval']))
363
result, new_result = EventstatResult.objects.get_or_create(
365
upgrade=self.upgrade,
366
machine=self.machine,
367
ran_at=dashboard_data['build_date'],
368
kernel=dashboard_data['kernel'],
369
metric=metrics[task_type],
372
name=item['task'][:200],
374
init_function=item['init_function'],
375
callback=item['callback'],
378
jenkins_url=jenkins_url,
381
for task_type in task_types:
388
results = EventstatResult.objects.filter(
391
upgrade=self.upgrade,
392
machine=self.machine,
395
if task_type != "total":
396
results = results.filter(
399
values.append('task_type')
401
results = results.values(*values).annotate(
405
models.StdDev('value'),
408
agg_len = len(results)
410
logging.warn("Strange aggregation result count: %s", agg_len)
411
logging.warn(" machine.id: %s", self.machine.id)
412
if self.image is not None:
413
logging.warn(" image.id: %s", self.image.id)
414
if self.upgrade is not None:
415
logging.warn(" upgrade.id: %s", self.upgrade.id)
416
logging.warn(" task_type: %s", task_type)
421
metrics[task_type].average = result['value__avg']
422
metrics[task_type].minimum = result['value__min']
423
metrics[task_type].maximum = result['value__max']
424
metrics[task_type].stddev = result['value__stddev']
425
metrics[task_type].publish = True
426
metrics[task_type].save()
428
def remove_result(self, dashboard_data):
429
""" Remove eventstat data from the database. """
431
logging.info("Removing eventstat result.")
433
def extract_data(self, name):
434
m = self.job_regex.match(name)
437
self.release = m.group(1)
438
self.variant = m.group(2)
439
self.arch = m.group(3)
440
self.method = m.group(4)
441
self.workload = m.group(5)
442
self.machine_name = m.group(6)
444
logging.info("name: %s", name)
445
logging.info("method: %s", self.method)
447
self.install_data = dict(
448
release=self.release,
449
variant=self.variant,
454
def process_job(self, job):
455
logging.info("job.name: %s", job['name'])
456
self.machine, new_machine = EventstatMachine.objects.get_or_create(
457
name=self.machine_name,