Extend tags feature of tasks

* Add ability to setup multiple tags for a task.

  The database supports an unlimited number of tags per tasks. It would be
  nice to support that feature from user side too.

* The tags feature is a bit useless, if there is no way to filter by them.
  This patch adds this ability to `rally task list` command.

Change-Id: I959fa904a38f193f77f25b53ba3ee86760fff91c
This commit is contained in:
Andrey Kurilin 2017-06-07 17:02:36 +03:00
parent 91577f0f1f
commit 26cc4be584
9 changed files with 170 additions and 97 deletions

View File

@ -33,7 +33,7 @@ _rally()
OPTS["task_detailed"]="--uuid --iterations-data"
OPTS["task_export"]="--uuid --type --to"
OPTS["task_import"]="--file --deployment --tag"
OPTS["task_list"]="--deployment --all-deployments --status --uuids-only"
OPTS["task_list"]="--deployment --all-deployments --status --tag --uuids-only"
OPTS["task_report"]="--out --open --html --html-static --uuid"
OPTS["task_results"]="--uuid"
OPTS["task_sla-check"]="--uuid --json"

View File

@ -381,14 +381,14 @@ class _Task(APIGroup):
@api_wrapper(path=API_REQUEST_PREFIX + "/task/create",
method="POST")
def create(self, deployment, tag):
def create(self, deployment, tags=None):
"""Create a task without starting it.
Task is a list of benchmarks that will be called one by one, results of
execution will be stored in DB.
:param deployment: UUID or name of the deployment
:param tag: tag for this task
:param tags: a list of tags for this task
:returns: Task object
"""
deployment = objects.Deployment.get(deployment)
@ -399,7 +399,7 @@ class _Task(APIGroup):
status=deployment["status"])
return objects.Task(deployment_uuid=deployment["uuid"],
tag=tag).to_dict()
tags=tags).to_dict()
@api_wrapper(path=API_REQUEST_PREFIX + "/task/validate",
method="GET")
@ -548,7 +548,7 @@ class _Task(APIGroup):
@api_wrapper(path=API_REQUEST_PREFIX + "/task/import_results",
method="POST")
def import_results(self, deployment, task_results, tag=None):
def import_results(self, deployment, task_results, tags=None):
"""Import json results of a test into rally database"""
deployment = objects.Deployment.get(deployment)
if deployment["status"] != consts.DeployStatus.DEPLOY_FINISHED:
@ -557,7 +557,8 @@ class _Task(APIGroup):
uuid=deployment["uuid"],
status=deployment["status"])
task_inst = objects.Task(deployment_uuid=deployment["uuid"], tag=tag)
task_inst = objects.Task(deployment_uuid=deployment["uuid"],
tags=tags)
task_inst.update_status(consts.TaskStatus.RUNNING)
for result in task_results:
subtask_obj = task_inst.add_subtask(title=result["key"]["name"])

View File

@ -194,7 +194,8 @@ class TaskCommands(object):
help="Path to the file with input task args (dict in "
"JSON/YAML). These args are used "
"to render the Jinja2 template in the input task.")
@cliutils.args("--tag", help="Tag for this task")
@cliutils.args("--tag", nargs="+", dest="tags", type=str, required=False,
help="Mark the task with a tag or a few tags.")
@cliutils.args("--no-use", action="store_false", dest="do_use",
help="Don't set new task as default for future operations.")
@cliutils.args("--abort-on-sla-failure", action="store_true",
@ -204,7 +205,7 @@ class TaskCommands(object):
@envutils.with_default_deployment(cli_arg_name="deployment")
@plugins.ensure_plugins_are_loaded
def start(self, api, task_file, deployment=None, task_args=None,
task_args_file=None, tag=None, do_use=False,
task_args_file=None, tags=None, do_use=False,
abort_on_sla_failure=False):
"""Start benchmark task.
@ -221,26 +222,25 @@ class TaskCommands(object):
used to render the Jinja2 template in
the input task.
:param deployment: UUID or name of the deployment
:param tag: optional tag for this task
:param tags: optional tag for this task
:param do_use: if True, the new task will be stored as the default one
for future operations
:param abort_on_sla_failure: if True, the execution of a benchmark
scenario will stop when any SLA check
for it fails
"""
input_task = self._load_and_validate_task(api, task_file,
raw_args=task_args,
args_file=task_args_file)
print("Running Rally version", version.version_string())
try:
task_instance = api.task.create(deployment=deployment, tag=tag)
task_instance = api.task.create(deployment=deployment, tags=tags)
tags = "[tags: '%s']" % "', '".join(tags) if tags else ""
print(cliutils.make_header(
_("Task %(tag)s %(uuid)s: started")
% {"uuid": task_instance["uuid"],
"tag": task_instance["tag"]}))
_("Task %(tags)s %(uuid)s: started")
% {"uuid": task_instance["uuid"], "tags": tags}))
print("Benchmarking... This can take a while...\n")
print("To track task status use:\n")
print("\trally task status\n\tor\n\trally task detailed\n")
@ -498,11 +498,13 @@ class TaskCommands(object):
@cliutils.args("--status", type=str, dest="status",
help="List tasks with specified status."
" Available statuses: %s" % ", ".join(consts.TaskStatus))
@cliutils.args("--tag", nargs="+", dest="tags", type=str, required=False,
help="Tags to filter tasks by.")
@cliutils.args("--uuids-only", action="store_true",
dest="uuids_only", help="List task UUIDs only.")
@envutils.with_default_deployment(cli_arg_name="deployment")
def list(self, api, deployment=None, all_deployments=False, status=None,
uuids_only=False):
tags=None, uuids_only=False):
"""List tasks, started and finished.
Displayed tasks can be filtered by status or deployment. By
@ -518,10 +520,10 @@ class TaskCommands(object):
filters = {}
headers = ["uuid", "deployment_name", "created_at", "duration",
"status", "tag"]
"status", "tags"]
if status in consts.TaskStatus:
filters.setdefault("status", status)
filters["status"] = status
elif status:
print(_("Error: Invalid task status '%s'.\n"
"Available statuses: %s") % (
@ -530,7 +532,10 @@ class TaskCommands(object):
return(1)
if not all_deployments:
filters.setdefault("deployment", deployment)
filters["deployment"] = deployment
if tags:
filters["tags"] = tags
task_list = api.task.list(**filters)
@ -540,9 +545,16 @@ class TaskCommands(object):
print_header=False,
print_border=False)
elif task_list:
def tags_formatter(t):
if not t["tags"]:
return ""
return "'%s'" % "', '".join(t["tags"])
cliutils.print_list(
task_list,
headers, sortby_index=headers.index("created_at"))
headers,
sortby_index=headers.index("created_at"),
formatters={"tags": tags_formatter})
else:
if status:
print(_("There are no tasks in '%s' status. "
@ -885,26 +897,26 @@ class TaskCommands(object):
@cliutils.args("--deployment", dest="deployment", type=str,
metavar="<uuid>", required=False,
help="UUID or name of a deployment.")
@cliutils.args("--tag", help="Tag for this task")
@cliutils.args("--tag", nargs="+", dest="tags", type=str, required=False,
help="Mark the task with a tag or a few tags.")
@envutils.with_default_deployment(cli_arg_name="deployment")
@cliutils.alias("import")
@cliutils.suppress_warnings
def import_results(self, api, deployment=None, task_file=None, tag=None):
def import_results(self, api, deployment=None, task_file=None, tags=None):
"""Import json results of a test into rally database
:param task_file: list, pathes files with tasks results
:param deployment: UUID or name of the deployment
:param tag: optional tag for this task
:param tags: optional tag for this task
"""
if os.path.exists(os.path.expanduser(task_file)):
tasks_results = self._load_task_results_file(api, task_file)
task = api.task.import_results(deployment=deployment,
task_results=tasks_results,
tag=tag)
tags=tags)
print(_("Task UUID: %s.") % task["uuid"])
else:
print(_("ERROR: Invalid file name passed: %s"
) % task_file,
print(_("ERROR: Invalid file name passed: %s") % task_file,
file=sys.stderr)
return 1

View File

@ -197,7 +197,7 @@ def task_update_status(task_uuid, status, allowed_statuses):
status)
def task_list(status=None, deployment=None):
def task_list(status=None, deployment=None, tags=None):
"""Get a list of tasks.
:param status: Task status to filter the returned list on. If set to
@ -205,9 +205,12 @@ def task_list(status=None, deployment=None):
:param deployment: Deployment UUID to filter the returned list on.
If set to None, tasks from all deployments will be
returned.
:param tags: A list of tags to filter tasks by.
:returns: A list of dicts with data on the tasks.
"""
return get_impl().task_list(status=status, deployment=deployment)
return get_impl().task_list(status=status,
deployment=deployment,
tags=tags)
def task_delete(uuid, status=None):

View File

@ -205,8 +205,7 @@ class Connection(object):
return task
def _make_old_task(self, task):
tags = self._tags_get(task.uuid, consts.TagType.TASK)
tag = tags[0] if tags else ""
tags = sorted(self._tags_get(task.uuid, consts.TagType.TASK))
return {
"id": task.id,
@ -215,7 +214,7 @@ class Connection(object):
"status": task.status,
"created_at": task.created_at,
"updated_at": task.updated_at,
"tag": tag,
"tags": tags,
"verification_log": json.dumps(task.validation_result)
}
@ -302,9 +301,9 @@ class Connection(object):
task["results"] = self._task_result_get_all_by_uuid(task["uuid"])
return task
# @db_api.serialize
@db_api.serialize
def task_create(self, values):
new_tag = values.pop("tag", None)
tags = values.pop("tags", None)
# TODO(ikhudoshyn): currently 'input_task'
# does not come in 'values'
# After completely switching to the new
@ -317,33 +316,31 @@ class Connection(object):
task.update(values)
task.save()
if new_tag:
if tags:
for t in set(tags):
tag = models.Tag()
tag.update({
"uuid": task.uuid,
tag.update({"uuid": task.uuid,
"type": consts.TagType.TASK,
"tag": new_tag
})
"tag": t})
tag.save()
return self._make_old_task(task)
task.tags = sorted(self._tags_get(task.uuid, consts.TagType.TASK))
return task
# @db_api.serialize
def task_update(self, uuid, values):
session = get_session()
values.pop("uuid", None)
new_tag = values.pop("tag", None)
tags = values.pop("tags", None)
with session.begin():
task = self._task_get(uuid, session=session)
task.update(values)
if new_tag:
if tags:
for t in set(tags):
tag = models.Tag()
tag.update({
"uuid": uuid,
tag.update({"uuid": task.uuid,
"type": consts.TagType.TASK,
"tag": new_tag
})
"tag": t})
tag.save()
return self._make_old_task(task)
@ -365,7 +362,9 @@ class Connection(object):
return result
# @db_api.serialize
def task_list(self, status=None, deployment=None):
def task_list(self, status=None, deployment=None, tags=None):
session = get_session()
with session.begin():
query = self.model_query(models.Task)
filters = {}
@ -374,10 +373,14 @@ class Connection(object):
if deployment is not None:
filters["deployment_uuid"] = self.deployment_get(
deployment)["uuid"]
if filters:
query = query.filter_by(**filters)
if tags:
uuids = self._uuids_by_tags_get(
consts.TagType.TASK, tags)
query = query.filter(models.Task.uuid.in_(uuids))
return [self._make_old_task(task) for task in query.all()]
def task_delete(self, uuid, status=None):

View File

@ -406,8 +406,9 @@ class Task(object):
return db.task_get_status(uuid)
@staticmethod
def list(status=None, deployment=None):
return [Task(db_task) for db_task in db.task_list(status, deployment)]
def list(status=None, deployment=None, tags=None):
return [Task(db_task) for db_task in db.task_list(
status, deployment=deployment, tags=tags)]
@staticmethod
def delete_by_uuid(uuid, status=None):

View File

@ -19,9 +19,11 @@ import os.path
import ddt
import mock
import six
import rally
from rally import api
from rally.cli import cliutils
from rally.cli.commands import task
from rally.common import yamlutils as yaml
from rally import consts
@ -201,7 +203,7 @@ class TaskCommandsTestCase(test.TestCase):
mock_version):
deployment_id = "e0617de9-77d1-4875-9b49-9d5789e29f20"
task_path = "path_to_config.json"
fake_task = fakes.FakeTask(uuid="some_new_uuid", tag="tag")
fake_task = fakes.FakeTask(uuid="some_new_uuid", tags=["tag"])
self.fake_api.task.create.return_value = fake_task
self.fake_api.task.validate.return_value = fakes.FakeTask(
some="json", uuid="some_uuid", temporary=True)
@ -209,7 +211,7 @@ class TaskCommandsTestCase(test.TestCase):
self.task.start(self.fake_api, task_path, deployment_id, do_use=True)
mock_version.version_string.assert_called_once_with()
self.fake_api.task.create.assert_called_once_with(
deployment=deployment_id, tag=None)
deployment=deployment_id, tags=None)
self.fake_api.task.start.assert_called_once_with(
deployment=deployment_id,
config=mock__load_and_validate_task.return_value,
@ -238,7 +240,8 @@ class TaskCommandsTestCase(test.TestCase):
status=consts.DeployStatus.DEPLOY_INIT)
self.fake_api.task.create.side_effect = exc
self.assertEqual(1, self.task.start(self.fake_api, task_path,
deployment="any", tag="some_tag"))
deployment="any",
tags=["some_tag"]))
self.assertFalse(mock_detailed.called)
@mock.patch("rally.cli.commands.task.TaskCommands.detailed")
@ -246,9 +249,9 @@ class TaskCommandsTestCase(test.TestCase):
return_value="some_config")
def test_start_with_task_args(self, mock__load_and_validate_task,
mock_detailed):
fake_task = fakes.FakeTask(uuid="new_uuid", tag="some_tag")
fake_task = fakes.FakeTask(uuid="new_uuid", tags=["some_tag"])
self.fake_api.task.create.return_value = fakes.FakeTask(
uuid="new_uuid", tag="some_tag")
uuid="new_uuid", tags=["some_tag"])
self.fake_api.task.validate.return_value = fakes.FakeTask(
uuid="some_id")
@ -257,7 +260,7 @@ class TaskCommandsTestCase(test.TestCase):
task_args_file = "task_args_file"
self.task.start(self.fake_api, task_path, deployment="any",
task_args=task_args, task_args_file=task_args_file,
tag="some_tag")
tags=["some_tag"])
mock__load_and_validate_task.assert_called_once_with(
self.fake_api, task_path, raw_args=task_args,
@ -272,7 +275,7 @@ class TaskCommandsTestCase(test.TestCase):
self.fake_api,
task_id=fake_task["uuid"])
self.fake_api.task.create.assert_called_once_with(
deployment="any", tag="some_tag")
deployment="any", tags=["some_tag"])
@mock.patch("rally.cli.commands.task.envutils.get_global")
def test_start_no_deployment_id(self, mock_get_global):
@ -292,7 +295,7 @@ class TaskCommandsTestCase(test.TestCase):
self.assertRaises(exceptions.InvalidTaskException,
self.task.start, self.fake_api, "task_path",
"deployment", tag="tag")
"deployment", tags=["tag"])
self.assertFalse(self.fake_api.task.create.called)
self.assertFalse(self.fake_api.task.start.called)
@ -304,10 +307,10 @@ class TaskCommandsTestCase(test.TestCase):
self.assertRaises(KeyError,
self.task.start, self.fake_api, "task_path",
"deployment", tag="tag")
"deployment", tags=["tag"])
self.fake_api.task.create.assert_called_once_with(
deployment="deployment", tag="tag")
deployment="deployment", tags=["tag"])
self.fake_api.task.start.assert_called_once_with(
deployment="deployment", config=task_cfg,
@ -835,7 +838,7 @@ class TaskCommandsTestCase(test.TestCase):
created_at=dt.datetime.now(),
updated_at=dt.datetime.now(),
status="c",
tag="d",
tags=["d"],
deployment_name="some_name")]
self.task.list(self.fake_api, status="running")
self.fake_api.task.list.assert_called_once_with(
@ -843,11 +846,12 @@ class TaskCommandsTestCase(test.TestCase):
status=consts.TaskStatus.RUNNING)
headers = ["uuid", "deployment_name", "created_at", "duration",
"status", "tag"]
"status", "tags"]
mock_print_list.assert_called_once_with(
self.fake_api.task.list.return_value, headers,
sortby_index=headers.index("created_at"))
sortby_index=headers.index("created_at"),
formatters=mock.ANY)
@mock.patch("rally.cli.commands.task.cliutils.print_list")
@mock.patch("rally.cli.commands.task.envutils.get_global",
@ -884,6 +888,54 @@ class TaskCommandsTestCase(test.TestCase):
self.fake_api.task.list.assert_called_once_with(
deployment="d", status=consts.TaskStatus.RUNNING)
@mock.patch("rally.cli.commands.task.envutils.get_global",
return_value="123456789")
def test_list_output(self, mock_get_global):
self.fake_api.task.list.return_value = [
fakes.FakeTask(uuid="UUID-1",
created_at="2007-01-01T00:00:01",
duration="0:00:00.000009",
status="init",
tags=[],
deployment_name="some_name"),
fakes.FakeTask(uuid="UUID-2",
created_at="2007-02-01T00:00:01",
duration="0:00:00.000010",
status="finished",
tags=["tag-1", "tag-2"],
deployment_name="some_name")]
# It is a hard task to mock default value of function argument, so we
# need to apply this workaround
original_print_list = cliutils.print_list
print_list_calls = []
def print_list(*args, **kwargs):
print_list_calls.append(six.StringIO())
kwargs["out"] = print_list_calls[-1]
original_print_list(*args, **kwargs)
with mock.patch.object(task.cliutils, "print_list",
new=print_list):
self.task.list(self.fake_api, status="running")
self.assertEqual(1, len(print_list_calls))
self.assertEqual(
"+--------+-----------------+---------------------"
"+----------------+----------+------------------+\n"
"| uuid | deployment_name | created_at "
"| duration | status | tags |\n"
"+--------+-----------------+---------------------"
"+----------------+----------+------------------+\n"
"| UUID-1 | some_name | 2007-01-01T00:00:01 "
"| 0:00:00.000009 | init | |\n"
"| UUID-2 | some_name | 2007-02-01T00:00:01 "
"| 0:00:00.000010 | finished | 'tag-1', 'tag-2' |\n"
"+--------+-----------------+---------------------"
"+----------------+----------+------------------+\n",
print_list_calls[0].getvalue())
def test_delete(self):
task_uuid = "8dcb9c5e-d60b-4022-8975-b5987c7833f7"
force = False
@ -1117,13 +1169,14 @@ class TaskCommandsTestCase(test.TestCase):
self.task.import_results(self.fake_api,
"deployment_uuid",
"task_file", "tag")
"task_file", tags=["tag"])
self.task._load_task_results_file.assert_called_once_with(
self.fake_api, "task_file"
)
self.fake_api.task.import_results.assert_called_once_with(
deployment="deployment_uuid", task_results=["results"], tag="tag")
deployment="deployment_uuid", task_results=["results"],
tags=["tag"])
# not exist
mock_os_path.exists.return_value = False
@ -1131,5 +1184,5 @@ class TaskCommandsTestCase(test.TestCase):
1,
self.task.import_results(self.fake_api,
"deployment_uuid",
"task_file", "tag")
"task_file", ["tag"])
)

View File

@ -122,12 +122,12 @@ class TasksTestCase(test.DBTestCase):
self.assertEqual(db_task["status"], consts.TaskStatus.INIT)
def test_task_create_with_tag(self):
task = self._create_task(values={"tag": "test_tag"})
task = self._create_task(values={"tags": ["test_tag"]})
db_task = self._get_task(task["uuid"])
self.assertIsNotNone(db_task["uuid"])
self.assertIsNotNone(db_task["id"])
self.assertEqual(db_task["status"], consts.TaskStatus.INIT)
self.assertEqual(db_task["tag"], "test_tag")
self.assertEqual(db_task["tags"], ["test_tag"])
def test_task_create_without_uuid(self):
_uuid = "19be8589-48b0-4af1-a369-9bebaaa563ab"
@ -145,11 +145,11 @@ class TasksTestCase(test.DBTestCase):
task = self._create_task({})
db.task_update(task["uuid"], {
"status": consts.TaskStatus.CRASHED,
"tag": "test_tag"
"tags": ["test_tag"]
})
db_task = self._get_task(task["uuid"])
self.assertEqual(db_task["status"], consts.TaskStatus.CRASHED)
self.assertEqual(db_task["tag"], "test_tag")
self.assertEqual(db_task["tags"], ["test_tag"])
def test_task_update_not_found(self):
self.assertRaises(exceptions.TaskNotFound,
@ -375,7 +375,7 @@ class TasksTestCase(test.DBTestCase):
"trace": "foo t/b",
}
task1 = self._create_task({"validation_result": validation_result,
"tag": "bar"})
"tags": ["bar"]})
key = {
"name": "atata",
"description": "tatata",
@ -408,7 +408,7 @@ class TasksTestCase(test.DBTestCase):
task1_full = db.task_get_detailed(task1["uuid"])
self.assertEqual(validation_result,
json.loads(task1_full["verification_log"]))
self.assertEqual("bar", task1_full["tag"])
self.assertEqual(["bar"], task1_full["tags"])
results = task1_full["results"]
self.assertEqual(1, len(results))
self.assertEqual(key, results[0]["key"])

View File

@ -224,17 +224,18 @@ class TaskAPITestCase(test.TestCase):
self.assertEqual("2", self.task_inst.render_template(
task_template=template))
@mock.patch("rally.common.objects.Deployment.get",
return_value={
"uuid": "b0d9cd6c-2c94-4417-a238-35c7019d0257",
"status": consts.DeployStatus.DEPLOY_FINISHED})
@mock.patch("rally.common.objects.Deployment.get")
@mock.patch("rally.common.objects.Task")
def test_create(self, mock_task, mock_deployment_get):
tag = "a"
mock_deployment_get.return_value = {
"uuid": "b0d9cd6c-2c94-4417-a238-35c7019d0257",
"status": consts.DeployStatus.DEPLOY_FINISHED}
tags = ["a"]
self.task_inst.create(
deployment=mock_deployment_get.return_value["uuid"], tag=tag)
deployment=mock_deployment_get.return_value["uuid"], tags=tags)
mock_task.assert_called_once_with(
deployment_uuid=mock_deployment_get.return_value["uuid"], tag=tag)
deployment_uuid=mock_deployment_get.return_value["uuid"],
tags=tags)
@mock.patch("rally.common.objects.Deployment.get",
return_value={
@ -243,10 +244,9 @@ class TaskAPITestCase(test.TestCase):
"status": consts.DeployStatus.DEPLOY_INIT})
def test_create_on_unfinished_deployment(self, mock_deployment_get):
deployment_id = mock_deployment_get.return_value["uuid"]
tag = "a"
self.assertRaises(exceptions.DeploymentNotFinishedStatus,
self.task_inst.create, deployment=deployment_id,
tag=tag)
tags=["a"])
@mock.patch("rally.api.objects.Task")
@mock.patch("rally.api.objects.Deployment.get")
@ -489,7 +489,7 @@ class TaskAPITestCase(test.TestCase):
)
mock_task.assert_called_once_with(deployment_uuid="deployment_uuid",
tag=None)
tags=None)
mock_task.return_value.update_status.assert_has_calls(
[mock.call(consts.TaskStatus.RUNNING),
mock.call(consts.SubtaskStatus.FINISHED)]
@ -536,7 +536,7 @@ class TaskAPITestCase(test.TestCase):
)
mock_task.assert_called_once_with(deployment_uuid="deployment_uuid",
tag=None)
tags=None)
mock_task.return_value.update_status.assert_has_calls(
[mock.call(consts.TaskStatus.RUNNING),
mock.call(consts.SubtaskStatus.FINISHED)]
@ -575,7 +575,7 @@ class TaskAPITestCase(test.TestCase):
self.task_inst.import_results,
deployment="deployment_uuid",
task_results=[],
tag="tag")
tags=["tag"])
class BaseDeploymentTestCase(test.TestCase):