Deprecate rally task results command

This command produces task results in old format that missis a lot of
information. For backward compatibility, the new task results exporter
is introduced.

Change-Id: I28880642e370513c2430e8e0f67dd7a622023a92
This commit is contained in:
Andrey Kurilin 2020-03-22 15:06:53 +02:00
parent 30d5a8ed04
commit 8dbad440db
10 changed files with 437 additions and 131 deletions

View File

@ -42,6 +42,9 @@ Changed
Deprecated Deprecated
~~~~~~~~~~ ~~~~~~~~~~
* Command *rally task results* is deprecated. Use *rally task report --json*
instead.
* Module *rally.common.sshutils* is deprecated. Use *rally.utils.sshutils* * Module *rally.common.sshutils* is deprecated. Use *rally.utils.sshutils*
instead. instead.

View File

@ -16,7 +16,6 @@
"""Rally command: task""" """Rally command: task"""
from __future__ import print_function from __future__ import print_function
import collections
import datetime as dt import datetime as dt
import itertools import itertools
import json import json
@ -544,70 +543,16 @@ class TaskCommands(object):
@envutils.with_default_task_id @envutils.with_default_task_id
@cliutils.suppress_warnings @cliutils.suppress_warnings
def results(self, api, task_id=None): def results(self, api, task_id=None):
"""Display raw task results. """DEPRECATED since Rally 3.0.0."""
LOG.warning("CLI method `rally task results` is deprecated since "
This will produce a lot of output data about every iteration. "Rally 3.0.0 and will be removed soon. "
""" "Use `rally task report --json` instead.")
try:
task = api.task.get(task_id=task_id, detailed=True) self.export(api, tasks=[task_id], output_type="old-json-results")
finished_statuses = (consts.TaskStatus.FINISHED, except exceptions.RallyException as e:
consts.TaskStatus.ABORTED) print(e.format_message())
if task["status"] not in finished_statuses:
print("Task status is %s. Results available when it is one of %s."
% (task["status"], ", ".join(finished_statuses)))
return 1 return 1
# TODO(chenhb): Ensure `rally task results` puts out old format.
for workload in itertools.chain(
*[s["workloads"] for s in task["subtasks"]]):
for itr in workload["data"]:
itr["atomic_actions"] = collections.OrderedDict(
tutils.WrapperForAtomicActions(
itr["atomic_actions"]).items()
)
results = []
for w in itertools.chain(*[s["workloads"] for s in task["subtasks"]]):
w["runner"]["type"] = w["runner_type"]
def port_hook_cfg(h):
h["config"] = {
"name": h["config"]["action"][0],
"args": h["config"]["action"][1],
"description": h["config"].get("description", ""),
"trigger": {"name": h["config"]["trigger"][0],
"args": h["config"]["trigger"][1]}
}
return h
hooks = [port_hook_cfg(h) for h in w["hooks"]]
created_at = dt.datetime.strptime(w["created_at"],
"%Y-%m-%dT%H:%M:%S")
created_at = created_at.strftime("%Y-%d-%mT%H:%M:%S")
results.append({
"key": {
"name": w["name"],
"description": w["description"],
"pos": w["position"],
"kw": {
"args": w["args"],
"runner": w["runner"],
"context": w["contexts"],
"sla": w["sla"],
"hooks": [h["config"] for h in w["hooks"]],
}
},
"result": w["data"],
"sla": w["sla_results"].get("sla", []),
"hooks": hooks,
"load_duration": w["load_duration"],
"full_duration": w["full_duration"],
"created_at": created_at})
print(json.dumps(results, sort_keys=False, indent=4))
@cliutils.args("--deployment", dest="deployment", type=str, @cliutils.args("--deployment", dest="deployment", type=str,
metavar="<uuid>", required=False, metavar="<uuid>", required=False,
help="UUID or name of a deployment.") help="UUID or name of a deployment.")

View File

@ -0,0 +1,121 @@
# 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.
import collections
import datetime as dt
import itertools
import json
from rally import consts
from rally import exceptions
from rally.task import exporter
def _to_old_atomic_actions_format(atomic_actions):
"""Convert atomic actions to old format. """
old_style = collections.OrderedDict()
for action in atomic_actions:
duration = action["finished_at"] - action["started_at"]
if action["name"] in old_style:
name_template = action["name"] + " (%i)"
i = 2
while name_template % i in old_style:
i += 1
old_style[name_template % i] = duration
else:
old_style[action["name"]] = duration
return old_style
@exporter.configure("old-json-results")
class OldJSONExporter(exporter.TaskExporter):
"""Generates task report in JSON format as old `rally task results`."""
def __init__(self, *args, **kwargs):
super(OldJSONExporter, self).__init__(*args, **kwargs)
if len(self.tasks_results) != 1:
raise exceptions.RallyException(
f"'{self.get_fullname()}' task exporter can be used only for "
f"a one task.")
self.task = self.tasks_results[0]
def _get_report(self):
results = []
for w in itertools.chain(*[s["workloads"]
for s in self.task["subtasks"]]):
for itr in w["data"]:
itr["atomic_actions"] = _to_old_atomic_actions_format(
itr["atomic_actions"]
)
w["runner"]["type"] = w["runner_type"]
def port_hook_cfg(h):
h["config"] = {
"name": h["config"]["action"][0],
"args": h["config"]["action"][1],
"description": h["config"].get("description", ""),
"trigger": {"name": h["config"]["trigger"][0],
"args": h["config"]["trigger"][1]}
}
return h
hooks = [port_hook_cfg(h) for h in w["hooks"]]
created_at = dt.datetime.strptime(w["created_at"],
"%Y-%m-%dT%H:%M:%S")
created_at = created_at.strftime("%Y-%d-%mT%H:%M:%S")
results.append({
"key": {
"name": w["name"],
"description": w["description"],
"pos": w["position"],
"kw": {
"args": w["args"],
"runner": w["runner"],
"context": w["contexts"],
"sla": w["sla"],
"hooks": [h["config"] for h in w["hooks"]],
}
},
"result": w["data"],
"sla": w["sla_results"].get("sla", []),
"hooks": hooks,
"load_duration": w["load_duration"],
"full_duration": w["full_duration"],
"created_at": created_at})
return results
def generate(self):
if len(self.tasks_results) != 1:
raise exceptions.RallyException(
f"'{self.get_fullname()}' task exporter can be used only for "
f"a one task.")
finished_statuses = (consts.TaskStatus.FINISHED,
consts.TaskStatus.ABORTED)
if self.task["status"] not in finished_statuses:
raise exceptions.RallyException(
f"Task status is {self.task['status']}. Results available "
f"when it is one of {', '.join(finished_statuses)}."
)
results = json.dumps(self._get_report(), sort_keys=False, indent=4)
if self.output_destination:
return {"files": {self.output_destination: results},
"open": "file://" + self.output_destination}
else:
return {"print": results}

View File

@ -15,7 +15,7 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
ALLOWED_EXTRA_MISSING=400 ALLOWED_EXTRA_MISSING=4
show_diff () { show_diff () {
head -1 $1 head -1 $1

View File

@ -365,7 +365,8 @@ class TaskTestCase(testtools.TestCase):
rally("task start --task %s" % config.filename) rally("task start --task %s" % config.filename)
task_result_file = rally.gen_report_path(suffix="results") task_result_file = rally.gen_report_path(suffix="results")
self.addCleanup(os.remove, task_result_file) self.addCleanup(os.remove, task_result_file)
rally("task results", report_path=task_result_file, raw=True) rally("task results", report_path=task_result_file, raw=True,
getjson=True)
html_report = rally.gen_report_path(extension="html") html_report = rally.gen_report_path(extension="html")
rally("task report --html-static %s --out %s" rally("task report --html-static %s --out %s"

View File

@ -573,11 +573,24 @@ class TaskCommandsTestCase(test.TestCase):
"contexts": {"users": {}}, "contexts": {"users": {}},
"data": data or []}]}]} "data": data or []}]}]}
@mock.patch("rally.cli.commands.task.json.dumps") @mock.patch("rally.plugins.task.exporters.old_json_results.json.dumps")
def test_results(self, mock_json_dumps): def test_results(self, mock_json_dumps):
mock_json_dumps.return_value = ""
def fake_export(tasks, output_type, output_dest=None):
tasks = [self.fake_api.task.get(u, detailed=True) for u in tasks]
return self.real_api.task.export(tasks, output_type, output_dest)
self.fake_api.task.export.side_effect = fake_export
task_id = "foo_task_id" task_id = "foo_task_id"
task_obj = self._make_task(data=[{"atomic_actions": {"foo": 1.1}}]) task_obj = self._make_task(
data=[{"atomic_actions": [{"name": "foo",
"started_at": 0,
"finished_at": 1.1}]}]
)
task_obj["subtasks"][0]["workloads"][0]["hooks"] = [{ task_obj["subtasks"][0]["workloads"][0]["hooks"] = [{
"config": { "config": {
"action": ("foo", "arg"), "action": ("foo", "arg"),
@ -585,6 +598,7 @@ class TaskCommandsTestCase(test.TestCase):
}, },
"summary": {"success": 1}} "summary": {"success": 1}}
] ]
task_obj["uuid"] = task_id
def fix_r(workload): def fix_r(workload):
cfg = workload["runner"] cfg = workload["runner"]
@ -633,9 +647,16 @@ class TaskCommandsTestCase(test.TestCase):
@mock.patch("rally.cli.commands.task.sys.stdout") @mock.patch("rally.cli.commands.task.sys.stdout")
def test_results_no_data(self, mock_stdout): def test_results_no_data(self, mock_stdout):
def fake_export(tasks, output_type, output_dest=None):
tasks = [self.fake_api.task.get(u, detailed=True) for u in tasks]
return self.real_api.task.export(tasks, output_type, output_dest)
self.fake_api.task.export.side_effect = fake_export
task_id = "foo" task_id = "foo"
self.fake_api.task.get.return_value = self._make_task( task_obj = self._make_task(status=consts.TaskStatus.CRASHED)
status=consts.TaskStatus.CRASHED) task_obj["uuid"] = task_id
self.fake_api.task.get.return_value = task_obj
self.assertEqual(1, self.task.results(self.fake_api, task_id)) self.assertEqual(1, self.task.results(self.fake_api, task_id))

View File

@ -0,0 +1,120 @@
# 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.
def get_tasks_results():
task_id = "2fa4f5ff-7d23-4bb0-9b1f-8ee235f7f1c8"
workload = {"uuid": "uuid",
"name": "CinderVolumes.list_volumes",
"description": "List all volumes.",
"created_at": "2017-06-04T05:14:44",
"updated_at": "2017-06-04T05:15:14",
"task_uuid": task_id,
"position": 0,
"data": [
{
"duration": 0.2504892349243164,
"timestamp": 1584551892.7336202,
"idle_duration": 0.0,
"error": [],
"output": {
"additive": [],
"complete": []
},
"atomic_actions": [
{
"name": "foo",
"children": [
{
"name": "bar",
"children": [],
"started_at": 1584551892.733688,
"finished_at": 1584551892.984079
}
],
"started_at": 1584551892.7336745,
"finished_at": 1584551892.9840994
}
]
},
{
"duration": 0.25043749809265137,
"timestamp": 1584551892.7363858,
"idle_duration": 0.0,
"error": [],
"output": {
"additive": [],
"complete": []
},
"atomic_actions": [
{
"name": "foo",
"children": [
{
"name": "bar",
"children": [],
"started_at": 1584551892.7364488,
"finished_at": 1584551892.9867969
}
],
"started_at": 1584551892.736435,
"finished_at": 1584551892.9868152
}
]
}
],
"full_duration": 29.969523191452026,
"load_duration": 2.03029203414917,
"hooks": [],
"runner": {},
"runner_type": "runner_type",
"args": {},
"contexts": {},
"contexts_results": [],
"min_duration": 0.0,
"max_duration": 1.0,
"start_time": 0,
"statistics": {},
"failed_iteration_count": 0,
"total_iteration_count": 10,
"sla": {},
"sla_results": {"sla": []},
"pass_sla": True
}
task = {
"uuid": task_id,
"title": "task",
"description": "description",
"status": "finished",
"env_uuid": "env-uuid",
"env_name": "env-name",
"tags": [],
"created_at": "2017-06-04T05:14:44",
"updated_at": "2017-06-04T05:15:14",
"pass_sla": True,
"task_duration": 2.0,
"subtasks": [
{"uuid": "subtask_uuid",
"title": "subtask",
"description": "description",
"status": "finished",
"run_in_parallel": True,
"created_at": "2017-06-04T05:14:44",
"updated_at": "2017-06-04T05:15:14",
"sla": {},
"duration": 29.969523191452026,
"task_uuid": task_id,
"workloads": [workload]}
]}
return [task]

View File

@ -16,73 +16,18 @@ import os
from unittest import mock from unittest import mock
from rally.plugins.task.exporters import html from rally.plugins.task.exporters import html
from tests.unit.plugins.task.exporters import dummy_data
from tests.unit import test from tests.unit import test
PATH = "rally.plugins.task.exporters.html" PATH = "rally.plugins.task.exporters.html"
def get_tasks_results():
task_id = "2fa4f5ff-7d23-4bb0-9b1f-8ee235f7f1c8"
workload = {"uuid": "uuid",
"name": "CinderVolumes.list_volumes",
"description": "List all volumes.",
"created_at": "2017-06-04T05:14:44",
"updated_at": "2017-06-04T05:15:14",
"task_uuid": task_id,
"position": 0,
"data": {"raw": []},
"full_duration": 29.969523191452026,
"load_duration": 2.03029203414917,
"hooks": [],
"runner": {},
"runner_type": "runner_type",
"args": {},
"contexts": {},
"contexts_results": [],
"min_duration": 0.0,
"max_duration": 1.0,
"start_time": 0,
"statistics": {},
"failed_iteration_count": 0,
"total_iteration_count": 10,
"sla": {},
"sla_results": {"sla": []},
"pass_sla": True
}
task = {
"uuid": task_id,
"title": "task",
"description": "description",
"status": "finished",
"env_uuid": "env-uuid",
"env_name": "env-name",
"tags": [],
"created_at": "2017-06-04T05:14:44",
"updated_at": "2017-06-04T05:15:14",
"pass_sla": True,
"task_duration": 2.0,
"subtasks": [
{"uuid": "subtask_uuid",
"title": "subtask",
"description": "description",
"status": "finished",
"run_in_parallel": True,
"created_at": "2017-06-04T05:14:44",
"updated_at": "2017-06-04T05:15:14",
"sla": {},
"duration": 29.969523191452026,
"task_uuid": task_id,
"workloads": [workload]}
]}
return [task]
class HTMLExporterTestCase(test.TestCase): class HTMLExporterTestCase(test.TestCase):
@mock.patch("%s.plot.plot" % PATH, return_value="html") @mock.patch("%s.plot.plot" % PATH, return_value="html")
def test_generate(self, mock_plot): def test_generate(self, mock_plot):
tasks_results = get_tasks_results() tasks_results = dummy_data.get_tasks_results()
tasks_results.extend(get_tasks_results()) tasks_results.extend(dummy_data.get_tasks_results())
reporter = html.HTMLExporter(tasks_results, None) reporter = html.HTMLExporter(tasks_results, None)
reporter._generate_results = mock.MagicMock() reporter._generate_results = mock.MagicMock()

View File

@ -18,7 +18,7 @@ from unittest import mock
from rally.common import version as rally_version from rally.common import version as rally_version
from rally.plugins.task.exporters import json_exporter from rally.plugins.task.exporters import json_exporter
from tests.unit.plugins.task.exporters import test_html from tests.unit.plugins.task.exporters import dummy_data
from tests.unit import test from tests.unit import test
PATH = "rally.plugins.task.exporters.json_exporter" PATH = "rally.plugins.task.exporters.json_exporter"
@ -27,9 +27,11 @@ PATH = "rally.plugins.task.exporters.json_exporter"
class JSONExporterTestCase(test.TestCase): class JSONExporterTestCase(test.TestCase):
def test__generate_tasks(self): def test__generate_tasks(self):
tasks_results = test_html.get_tasks_results() tasks_results = dummy_data.get_tasks_results()
reporter = json_exporter.JSONExporter(tasks_results, None) reporter = json_exporter.JSONExporter(tasks_results, None)
w_data = tasks_results[0]["subtasks"][0]["workloads"][0]["data"]
self.assertEqual([ self.assertEqual([
collections.OrderedDict([ collections.OrderedDict([
("uuid", "2fa4f5ff-7d23-4bb0-9b1f-8ee235f7f1c8"), ("uuid", "2fa4f5ff-7d23-4bb0-9b1f-8ee235f7f1c8"),
@ -65,7 +67,7 @@ class JSONExporterTestCase(test.TestCase):
("load_duration", 2.03029203414917), ("load_duration", 2.03029203414917),
("full_duration", 29.969523191452026), ("full_duration", 29.969523191452026),
("statistics", {}), ("statistics", {}),
("data", {"raw": []}), ("data", w_data),
("failed_iteration_count", 0), ("failed_iteration_count", 0),
("total_iteration_count", 10), ("total_iteration_count", 10),
("created_at", "2017-06-04T05:14:44"), ("created_at", "2017-06-04T05:14:44"),
@ -86,7 +88,7 @@ class JSONExporterTestCase(test.TestCase):
@mock.patch("%s.dt" % PATH) @mock.patch("%s.dt" % PATH)
def test_generate(self, mock_dt, mock_json_dumps): def test_generate(self, mock_dt, mock_json_dumps):
mock_dt.datetime.utcnow.return_value = dt.datetime.utcnow() mock_dt.datetime.utcnow.return_value = dt.datetime.utcnow()
tasks_results = test_html.get_tasks_results() tasks_results = dummy_data.get_tasks_results()
# print # print
reporter = json_exporter.JSONExporter(tasks_results, None) reporter = json_exporter.JSONExporter(tasks_results, None)

View File

@ -0,0 +1,148 @@
# 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.
import collections
from unittest import mock
from rally import exceptions
from rally.plugins.task.exporters import old_json_results
from tests.unit.plugins.task.exporters import dummy_data
from tests.unit import test
PATH = "rally.plugins.task.exporters.old_json_results"
class OldJSONResultsTestCase(test.TestCase):
def test___init__(self):
old_json_results.OldJSONExporter([1], None)
self.assertRaises(
exceptions.RallyException,
old_json_results.OldJSONExporter, [1, 2], None
)
@mock.patch("%s.json.dumps" % PATH, return_value="json")
def test_generate(self, mock_json_dumps):
tasks_results = dummy_data.get_tasks_results()
# print
exporter = old_json_results.OldJSONExporter(tasks_results, None)
exporter._get_report = mock.MagicMock()
self.assertEqual({"print": "json"}, exporter.generate())
exporter._get_report.assert_called_once_with()
mock_json_dumps.assert_called_once_with(
exporter._get_report.return_value, sort_keys=False, indent=4)
# export to file
exporter = old_json_results.OldJSONExporter(
tasks_results, output_destination="path")
exporter._get_report = mock.MagicMock()
self.assertEqual({"files": {"path": "json"},
"open": "file://path"}, exporter.generate())
def test__get_report(self):
tasks_results = dummy_data.get_tasks_results()
exporter = old_json_results.OldJSONExporter(tasks_results, None)
self.assertEqual(
[
{
"created_at": "2017-04-06T05:14:44",
"full_duration": 29.969523191452026,
"hooks": [],
"key": {
"description": "List all volumes.",
"kw": {"args": {},
"context": {},
"hooks": [],
"runner": {"type": "runner_type"},
"sla": {}},
"name": "CinderVolumes.list_volumes",
"pos": 0
},
"load_duration": 2.03029203414917,
"result": [
{
"atomic_actions": collections.OrderedDict([
("foo", 0.250424861907959)]),
"duration": 0.2504892349243164,
"error": [],
"idle_duration": 0.0,
"output": {"additive": [], "complete": []},
"timestamp": 1584551892.7336202
},
{
"atomic_actions": collections.OrderedDict([
("foo", 0.250380277633667)]),
"duration": 0.25043749809265137,
"error": [],
"idle_duration": 0.0,
"output": {"additive": [], "complete": []},
"timestamp": 1584551892.7363858}],
"sla": []
}
], exporter._get_report())
def test__to_old_atomic_actions_format(self):
actions = [
{
"name": "foo",
"started_at": 0,
"finished_at": 1,
"children": []
},
{
"name": "foo",
"started_at": 1,
"finished_at": 2,
"children": []
},
{
"name": "foo",
"started_at": 2,
"finished_at": 3,
"children": []
},
{
"name": "bar",
"started_at": 3,
"finished_at": 5,
"children": [
{
"name": "xxx",
"started_at": 3,
"finished_at": 4,
"children": []
},
{
"name": "xxx",
"started_at": 4,
"finished_at": 5,
"children": []
}
]
}
]
actual = old_json_results._to_old_atomic_actions_format(actions)
self.assertIsInstance(actual, collections.OrderedDict)
# it is easier to compare list instead of constructing an ordered dict
actual = list(actual.items())
self.assertEqual(
[("foo", 1), ("foo (2)", 1), ("foo (3)", 1), ("bar", 2)],
actual
)