
SLA (Service-level agreement) is set of details for determining compliance with contracted values such as maximum error rate or minimum response time. Add two criteria: maximum time per iteration maximum error rate Change-Id: I1212bd684831461a7d7e33636d45a6d346e3b574 Blueprint: task-success-criteria
379 lines
15 KiB
Python
379 lines
15 KiB
Python
# Copyright 2013: Mirantis Inc.
|
|
# All Rights Reserved.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License. You may obtain
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
|
|
""" Rally command: task """
|
|
|
|
from __future__ import print_function
|
|
import json
|
|
import os
|
|
import pprint
|
|
import webbrowser
|
|
|
|
from oslo.config import cfg
|
|
import yaml
|
|
|
|
from rally.benchmark.processing import plot
|
|
from rally.benchmark.processing import utils
|
|
from rally.benchmark.sla import base as base_sla
|
|
from rally.cmd import cliutils
|
|
from rally.cmd.commands import use
|
|
from rally.cmd import envutils
|
|
from rally import db
|
|
from rally import exceptions
|
|
from rally.openstack.common import cliutils as common_cliutils
|
|
from rally.openstack.common.gettextutils import _
|
|
from rally.orchestrator import api
|
|
from rally import utils as rutils
|
|
|
|
|
|
class TaskCommands(object):
|
|
|
|
@cliutils.args('--deploy-id', type=str, dest='deploy_id', required=False,
|
|
help='UUID of the deployment')
|
|
@cliutils.args('--task', '--filename',
|
|
help='Path to the file with full configuration of task')
|
|
@cliutils.args('--tag',
|
|
help='Tag for this task')
|
|
@cliutils.args('--no-use', action='store_false', dest='do_use',
|
|
help='Don\'t set new task as default for future operations')
|
|
@envutils.with_default_deploy_id
|
|
def start(self, task, deploy_id=None, tag=None, do_use=False):
|
|
"""Run a benchmark task.
|
|
|
|
:param task: a file with yaml/json configration
|
|
:param deploy_id: a UUID of a deployment
|
|
:param tag: optional tag for this task
|
|
"""
|
|
with open(task, 'rb') as task_file:
|
|
config_dict = yaml.safe_load(task_file.read())
|
|
try:
|
|
task = api.create_task(deploy_id, tag)
|
|
print("=" * 80)
|
|
print(_("Task %(tag)s %(uuid)s is started")
|
|
% {"uuid": task["uuid"], "tag": task["tag"]})
|
|
print("-" * 80)
|
|
api.start_task(deploy_id, config_dict, task=task)
|
|
self.detailed(task_id=task['uuid'])
|
|
if do_use:
|
|
use.UseCommands().task(task['uuid'])
|
|
except exceptions.InvalidConfigException:
|
|
return(1)
|
|
|
|
@cliutils.args('--uuid', type=str, dest='task_id', help='UUID of task')
|
|
@envutils.with_default_task_id
|
|
def abort(self, task_id=None):
|
|
"""Force abort task
|
|
|
|
:param task_id: Task uuid
|
|
"""
|
|
api.abort_task(task_id)
|
|
|
|
@cliutils.args('--uuid', type=str, dest='task_id', help='UUID of task')
|
|
@envutils.with_default_task_id
|
|
def status(self, task_id=None):
|
|
"""Get status of task
|
|
|
|
:param task_id: Task uuid
|
|
Returns current status of task
|
|
"""
|
|
task = db.task_get(task_id)
|
|
print(_("Task %(task_id)s is %(status)s.")
|
|
% {'task_id': task_id, 'status': task['status']})
|
|
|
|
@cliutils.args(
|
|
'--uuid', type=str, dest='task_id',
|
|
help=('uuid of task, if --uuid is "last" results of most '
|
|
'recently created task will be displayed.'))
|
|
@cliutils.args('--iterations-data', dest='iterations_data',
|
|
action='store_true',
|
|
help='print detailed results for each iteration')
|
|
@envutils.with_default_task_id
|
|
def detailed(self, task_id=None, iterations_data=False):
|
|
"""Get detailed information about task
|
|
|
|
:param task_id: Task uuid
|
|
:param iterations_data: print detailed results for each iteration
|
|
Prints detailed information of task.
|
|
"""
|
|
def _print_iterations_data(raw):
|
|
headers = ['iteration', "full duration"]
|
|
float_cols = ['full duration']
|
|
for i in range(0, len(raw)):
|
|
if raw[i]['atomic_actions']:
|
|
for (c, a) in enumerate(raw[i]['atomic_actions'], 1):
|
|
action = str(c) + "-" + a['action']
|
|
headers.append(action)
|
|
float_cols.append(action)
|
|
break
|
|
table_rows = []
|
|
formatters = dict(zip(float_cols,
|
|
[cliutils.pretty_float_formatter(col, 3)
|
|
for col in float_cols]))
|
|
for (c, r) in enumerate(raw, 1):
|
|
dlist = [c]
|
|
d = []
|
|
if r['atomic_actions']:
|
|
for l in r['atomic_actions']:
|
|
d.append(l['duration'])
|
|
dlist.append(sum(d))
|
|
dlist = dlist + d
|
|
table_rows.append(rutils.Struct(**dict(zip(headers,
|
|
dlist))))
|
|
else:
|
|
data = dlist + ["N/A" for i in range(1, len(headers))]
|
|
table_rows.append(rutils.Struct(**dict(zip(headers,
|
|
data))))
|
|
common_cliutils.print_list(table_rows,
|
|
fields=headers,
|
|
formatters=formatters)
|
|
print()
|
|
|
|
def _get_atomic_action_durations(raw):
|
|
atomic_actions_names = []
|
|
for r in raw:
|
|
if 'atomic_actions' in r:
|
|
for a in r['atomic_actions']:
|
|
atomic_actions_names.append(a["action"])
|
|
break
|
|
result = {}
|
|
for atomic_action in atomic_actions_names:
|
|
result[atomic_action] = utils.get_durations(
|
|
raw,
|
|
lambda r: next(a["duration"] for a in r["atomic_actions"]
|
|
if a["action"] == atomic_action),
|
|
lambda r: any((a["action"] == atomic_action)
|
|
for a in r["atomic_actions"]))
|
|
return result
|
|
|
|
if task_id == "last":
|
|
task = db.task_get_detailed_last()
|
|
task_id = task.uuid
|
|
else:
|
|
task = db.task_get_detailed(task_id)
|
|
|
|
if task is None:
|
|
print("The task %s can not be found" % task_id)
|
|
return(1)
|
|
|
|
print()
|
|
print("=" * 80)
|
|
print(_("Task %(task_id)s is %(status)s.")
|
|
% {"task_id": task_id, "status": task["status"]})
|
|
|
|
if task["failed"]:
|
|
print("-" * 80)
|
|
verification = yaml.safe_load(task["verification_log"])
|
|
|
|
if not cfg.CONF.debug:
|
|
print(verification[0])
|
|
print(verification[1])
|
|
print()
|
|
print(_("For more details run:\nrally -vd task detailed %s")
|
|
% task["uuid"])
|
|
else:
|
|
print(yaml.safe_load(verification[2]))
|
|
return
|
|
|
|
for result in task["results"]:
|
|
key = result["key"]
|
|
print("-" * 80)
|
|
print()
|
|
print("test scenario %s" % key["name"])
|
|
print("args position %s" % key["pos"])
|
|
print("args values:")
|
|
pprint.pprint(key["kw"])
|
|
|
|
raw = result["data"]["raw"]
|
|
table_cols = ["action", "min (sec)", "avg (sec)", "max (sec)",
|
|
"90 percentile", "95 percentile", "success",
|
|
"count"]
|
|
float_cols = ["min (sec)", "avg (sec)", "max (sec)",
|
|
"90 percentile", "95 percentile"]
|
|
formatters = dict(zip(float_cols,
|
|
[cliutils.pretty_float_formatter(col, 3)
|
|
for col in float_cols]))
|
|
table_rows = []
|
|
|
|
action_durations = _get_atomic_action_durations(raw)
|
|
actions_list = action_durations.keys()
|
|
action_durations["total"] = utils.get_durations(
|
|
raw, lambda x: x["duration"], lambda r: not r["error"])
|
|
actions_list.append("total")
|
|
for action in actions_list:
|
|
durations = action_durations[action]
|
|
if durations:
|
|
data = [action,
|
|
min(durations),
|
|
utils.mean(durations),
|
|
max(durations),
|
|
utils.percentile(durations, 0.90),
|
|
utils.percentile(durations, 0.95),
|
|
"%.1f%%" % (len(durations) * 100.0 / len(raw)),
|
|
len(raw)]
|
|
else:
|
|
data = [action, None, None, None, None, None, 0, len(raw)]
|
|
table_rows.append(rutils.Struct(**dict(zip(table_cols, data))))
|
|
|
|
common_cliutils.print_list(table_rows, fields=table_cols,
|
|
formatters=formatters)
|
|
|
|
if iterations_data:
|
|
_print_iterations_data(raw)
|
|
|
|
# NOTE(hughsaunders): ssrs=scenario specific results
|
|
ssrs = []
|
|
for result in raw:
|
|
try:
|
|
ssrs.append(result['scenario_output']['data'])
|
|
except (KeyError, TypeError):
|
|
# No SSRs in this result
|
|
pass
|
|
if ssrs:
|
|
keys = set()
|
|
for ssr in ssrs:
|
|
keys.update(ssr.keys())
|
|
headers = ["key", "max", "avg", "min",
|
|
"90 pecentile", "95 pecentile"]
|
|
float_cols = ["max", "avg", "min",
|
|
"90 pecentile", "95 pecentile"]
|
|
formatters = dict(zip(float_cols,
|
|
[cliutils.pretty_float_formatter(col, 3)
|
|
for col in float_cols]))
|
|
table_rows = []
|
|
for key in keys:
|
|
values = [float(ssr[key]) for ssr in ssrs if key in ssr]
|
|
|
|
if values:
|
|
row = [str(key),
|
|
max(values),
|
|
utils.mean(values),
|
|
min(values),
|
|
utils.percentile(values, 0.90),
|
|
utils.percentile(values, 0.95)]
|
|
else:
|
|
row = [str(key)] + ['n/a'] * 5
|
|
table_rows.append(rutils.Struct(**dict(zip(headers, row))))
|
|
print("\nScenario Specific Results\n")
|
|
common_cliutils.print_list(table_rows,
|
|
fields=headers,
|
|
formatters=formatters)
|
|
|
|
for result in raw:
|
|
if result['scenario_output']['errors']:
|
|
print(result['scenario_output']['errors'])
|
|
|
|
print()
|
|
print("HINTS:")
|
|
print(_("* To plot HTML graphics with this data, run:"))
|
|
print("\trally task plot2html %s --out output.html" % task["uuid"])
|
|
print()
|
|
print(_("* To get raw JSON output of task results, run:"))
|
|
print("\trally task results %s\n" % task["uuid"])
|
|
|
|
@cliutils.args('--uuid', type=str, dest='task_id', help='uuid of task')
|
|
@cliutils.args('--pretty', type=str, help=('pretty print (pprint) '
|
|
'or json print (json)'))
|
|
@envutils.with_default_task_id
|
|
def results(self, task_id=None, pretty=False):
|
|
"""Print raw results of task.
|
|
|
|
:param task_id: Task uuid
|
|
:param pretty: Pretty print (pprint) or not (json)
|
|
"""
|
|
results = map(lambda x: {"key": x["key"], 'result': x['data']['raw']},
|
|
db.task_result_get_all_by_uuid(task_id))
|
|
|
|
if results:
|
|
if not pretty or pretty == 'json':
|
|
print(json.dumps(results))
|
|
elif pretty == 'pprint':
|
|
print()
|
|
pprint.pprint(results)
|
|
print()
|
|
else:
|
|
print(_("Wrong value for --pretty=%s") % pretty)
|
|
return(1)
|
|
else:
|
|
print(_("The task %s can not be found") % task_id)
|
|
return(1)
|
|
|
|
def list(self, task_list=None):
|
|
"""Print a list of all tasks."""
|
|
headers = ['uuid', 'created_at', 'status', 'failed', 'tag']
|
|
task_list = task_list or db.task_list()
|
|
if task_list:
|
|
common_cliutils.print_list(task_list, headers)
|
|
else:
|
|
print(_("There are no tasks. To run a new task, use:"
|
|
"\nrally task start"))
|
|
|
|
@cliutils.args('--uuid', type=str, dest='task_id', help='uuid of task')
|
|
@cliutils.args('--out', type=str, dest='out', required=False,
|
|
help='Path to output file.')
|
|
@cliutils.args('--open', dest='open_it', action='store_true',
|
|
help='Open it in browser.')
|
|
@envutils.with_default_task_id
|
|
def plot2html(self, task_id=None, out=None, open_it=False):
|
|
results = map(lambda x: {"key": x["key"], 'result': x['data']['raw']},
|
|
db.task_result_get_all_by_uuid(task_id))
|
|
|
|
output_file = out or ("%s.html" % task_id)
|
|
with open(output_file, "w+") as f:
|
|
f.write(plot.plot(results))
|
|
|
|
if open_it:
|
|
webbrowser.open_new_tab("file://" + os.path.realpath(output_file))
|
|
|
|
@cliutils.args('--force', action='store_true', help='force delete')
|
|
@cliutils.args('--uuid', type=str, dest='task_id', nargs="*",
|
|
metavar="TASK_ID",
|
|
help='uuid of task or a list of task uuids')
|
|
@envutils.with_default_task_id
|
|
def delete(self, task_id=None, force=False):
|
|
"""Delete a specific task and related results.
|
|
|
|
:param task_id: Task uuid or a list of task uuids
|
|
:param force: Force delete or not
|
|
"""
|
|
if isinstance(task_id, list):
|
|
for tid in task_id:
|
|
api.delete_task(tid, force=force)
|
|
else:
|
|
api.delete_task(task_id, force=force)
|
|
|
|
@cliutils.args("--uuid", type=str, dest="task_id", help="uuid of task")
|
|
@cliutils.args("--json", dest="tojson",
|
|
action="store_true",
|
|
help="output in json format")
|
|
@envutils.with_default_task_id
|
|
def sla_check(self, task_id=None, tojson=False):
|
|
"""Check if task was succeded according to SLA.
|
|
|
|
:param task_id: Task uuid.
|
|
:returns: Number of failed criteria.
|
|
"""
|
|
task = db.task_get_detailed(task_id)
|
|
failed_criteria = 0
|
|
rows = []
|
|
for row in base_sla.SLA.check_all(task):
|
|
failed_criteria += 0 if row['success'] else 1
|
|
rows.append(row if tojson else rutils.Struct(**row))
|
|
if tojson:
|
|
print(json.dumps(rows))
|
|
else:
|
|
common_cliutils.print_list(rows, ('benchmark', 'pos',
|
|
'criterion', 'success'))
|
|
return failed_criteria
|