diff --git a/rally/cmd/cliutils.py b/rally/cmd/cliutils.py index 5ffd404c0a..80066fa8ad 100644 --- a/rally/cmd/cliutils.py +++ b/rally/cmd/cliutils.py @@ -15,6 +15,7 @@ from __future__ import print_function +import argparse import os import sys @@ -24,12 +25,52 @@ from rally.openstack.common.apiclient import exceptions from rally.openstack.common import cliutils from rally.openstack.common.gettextutils import _ from rally.openstack.common import log as logging +from rally import utils from rally import version + CONF = cfg.CONF LOG = logging.getLogger(__name__) +class CategoryParser(argparse.ArgumentParser): + + """Customized arguments parser + + We need this one to override hardcoded behavior. + So, we want to print item's help instead of 'error: too fiew arguments'. + Also, we want not to print positional arguments in help messge. + """ + + def format_help(self): + formatter = self._get_formatter() + + # usage + formatter.add_usage(self.usage, self._actions, + self._mutually_exclusive_groups) + + # description + formatter.add_text(self.description) + + # positionals, optionals and user-defined groups + # INFO(oanufriev) _action_groups[0] contains positional arguments. + for action_group in self._action_groups[1:]: + formatter.start_section(action_group.title) + formatter.add_text(action_group.description) + formatter.add_arguments(action_group._group_actions) + formatter.end_section() + + # epilog + formatter.add_text(self.epilog) + + # determine help from format above + return formatter.format_help() + + def error(self, message): + self.print_help(sys.stderr) + sys.exit(2) + + def pretty_float_formatter(field, ndigits=None): """Create a formatter function for the given float field. @@ -70,7 +111,49 @@ def _methods_of(obj): return result +def _compose_category_description(category): + + descr_pairs = _methods_of(category) + + description = "" + if category.__doc__: + description = category.__doc__.strip() + if descr_pairs: + description += "\n\nCommands:\n" + sublen = lambda item: len(item[0]) + first_column_len = max(map(sublen, descr_pairs)) + 3 + for item in descr_pairs: + name = item[0] + if item[1].__doc__: + doc = utils.parse_docstring( + item[1].__doc__)["short_description"] + else: + doc = "" + name += " " * (first_column_len - len(name)) + description += " %s%s\n" % (name, doc) + + return description + + +def _compose_action_description(action_fn): + description = "" + if action_fn.__doc__: + parsed_doc = utils.parse_docstring(action_fn.__doc__) + short = parsed_doc.get("short_description") + long = parsed_doc.get("long_description") + + description = "%s\n\n%s" % (short, long) if long else short + + return description + + def _add_command_parsers(categories, subparsers): + + # INFO(oanufriev) This monkey patching makes our custom parser class to be + # used instead of native. This affects all subparsers down from + # 'subparsers' parameter of this function (categories and actions). + subparsers._parser_class = CategoryParser + parser = subparsers.add_parser('version') parser = subparsers.add_parser('bash-completion') @@ -78,16 +161,20 @@ def _add_command_parsers(categories, subparsers): for category in categories: command_object = categories[category]() - - parser = subparsers.add_parser(category) + descr = _compose_category_description(categories[category]) + parser = subparsers.add_parser( + category, description=descr, + formatter_class=argparse.RawDescriptionHelpFormatter) parser.set_defaults(command_object=command_object) category_subparsers = parser.add_subparsers(dest='action') for action, action_fn in _methods_of(command_object): - kwargs = {"help": action_fn.__doc__, - "description": action_fn.__doc__} - parser = category_subparsers.add_parser(action, **kwargs) + descr = _compose_action_description(action_fn) + parser = category_subparsers.add_parser( + action, + formatter_class=argparse.RawDescriptionHelpFormatter, + description=descr, help=descr) action_kwargs = [] for args, kwargs in getattr(action_fn, 'args', []): @@ -104,7 +191,6 @@ def _add_command_parsers(categories, subparsers): parser.set_defaults(action_fn=action_fn) parser.set_defaults(action_kwargs=action_kwargs) - parser.add_argument('action_args', nargs='*') diff --git a/rally/cmd/commands/deployment.py b/rally/cmd/commands/deployment.py index 82dc5fed0e..ba88cadf7f 100644 --- a/rally/cmd/commands/deployment.py +++ b/rally/cmd/commands/deployment.py @@ -39,6 +39,7 @@ from rally import utils class DeploymentCommands(object): + """Set of commands that allow you to manage deployments.""" @cliutils.args('--name', type=str, required=True, help='A name of the deployment.') @@ -51,7 +52,30 @@ class DeploymentCommands(object): help='Don\'t set new deployment as default for' ' future operations') def create(self, name, fromenv=False, filename=None, do_use=False): - """Create a new deployment on the basis of configuration file. + """Create new deployment. + + This command will create new deployment record in rally database. + In case of ExistingCloud deployment engine it will use cloud, + represented in config. + In cases when cloud doesn't exists Rally will deploy new one + for you with Devstack or Fuel. For this purposes different deployment + engines are developed. + + If you use ExistionCloud deployment engine you can pass deployment + config by environment variables: + OS_USERNAME + OS_PASSWORD + OS_AUTH_URL + OS_TENANT_NAME + + All other deployment engines need more complex configuration data, so + it should be stored in configuration file. + + You can use physical servers, lxc containers, KVM virtual machines + or virtual machines in OpenStack for deploying the cloud in. + Except physical servers, Rally can create cluster nodes for you. + Interaction with virtualisation software, OpenStack + cloud or physical servers is provided by server providers. :param fromenv: boolean, read environment instead of config file :param filename: a path to the configuration file @@ -105,6 +129,9 @@ class DeploymentCommands(object): def recreate(self, deploy_id=None): """Destroy and create an existing deployment. + Unlike 'deployment destroy' command deployment database record will + not be deleted, so deployment's UUID stay same. + :param deploy_id: a UUID of the deployment """ api.recreate_deploy(deploy_id) @@ -113,17 +140,19 @@ class DeploymentCommands(object): help='UUID of a deployment.') @envutils.with_default_deploy_id def destroy(self, deploy_id=None): - """Destroy the deployment. + """Destroy existing deployment. - Release resources that are allocated for the deployment. The - Deployment, related tasks and their results are also deleted. + This will delete all containers, virtual machines, OpenStack instances + or Fuel clusters created during Rally deployment creation. Also it will + remove deployment record from Rally database. :param deploy_id: a UUID of the deployment """ api.destroy_deploy(deploy_id) def list(self, deployment_list=None): - """Print list of deployments.""" + """List existing deployments.""" + headers = ['uuid', 'created_at', 'name', 'status', 'active'] current_deploy_id = envutils.get_global('RALLY_DEPLOYMENT') deployment_list = deployment_list or db.deployment_list() @@ -150,9 +179,9 @@ class DeploymentCommands(object): help='Output in pretty print format') @envutils.with_default_deploy_id def config(self, deploy_id=None, output_json=None, output_pprint=None): - """Print on stdout a config of the deployment. + """Display configuration of the deployment. - Output can JSON or Pretty print format. + Output can JSON or Pretty print format. :param deploy_id: a UUID of the deployment :param output_json: Output in json format (Default) @@ -174,10 +203,11 @@ class DeploymentCommands(object): help='UUID of a deployment.') @envutils.with_default_deploy_id def endpoint(self, deploy_id=None): - """Print all endpoints of the deployment. + """Display all endpoints of the deployment. :param deploy_id: a UUID of the deployment """ + headers = ['auth_url', 'username', 'password', 'tenant_name', 'region_name', 'endpoint_type', 'admin_port'] table_rows = [] @@ -196,12 +226,11 @@ class DeploymentCommands(object): help='UUID of a deployment.') @envutils.with_default_deploy_id def check(self, deploy_id=None): - """Check the deployment. - - Check keystone authentication and list all available services. + """Check keystone authentication and list all available services. :param deploy_id: a UUID of the deployment """ + headers = ['services', 'type', 'status'] table_rows = [] try: diff --git a/rally/cmd/commands/info.py b/rally/cmd/commands/info.py index 43d6cd3859..a48916a57d 100644 --- a/rally/cmd/commands/info.py +++ b/rally/cmd/commands/info.py @@ -51,6 +51,11 @@ from rally import utils class InfoCommands(object): + """This command allows you to get quick doc of some rally entities. + + Available for scenario groups, scenarios, deployment engines and + server providers. + """ @cliutils.args("--query", dest="query", type=str, help="Search query.") def find(self, query): @@ -58,6 +63,7 @@ class InfoCommands(object): :param query: search query. """ + info = self._find_info(query) if info: diff --git a/rally/cmd/commands/show.py b/rally/cmd/commands/show.py index 06943a3171..b80f4ad53c 100644 --- a/rally/cmd/commands/show.py +++ b/rally/cmd/commands/show.py @@ -29,6 +29,11 @@ from rally import utils class ShowCommands(object): + """Show resources. + + Set of commands that allow you to view resourses, provided by OpenStack + cloud represented by deployment. + """ def _get_endpoints(self, deploy_id): deployment = db.deployment_get(deploy_id) @@ -41,10 +46,11 @@ class ShowCommands(object): help='the UUID of a deployment') @envutils.with_default_deploy_id def images(self, deploy_id=None): - """Show the images that are available in a deployment. + """Display available images. :param deploy_id: the UUID of a deployment """ + headers = ['UUID', 'Name', 'Size (B)'] mixed_case_fields = ['UUID', 'Name'] float_cols = ["Size (B)"] @@ -74,10 +80,11 @@ class ShowCommands(object): help='the UUID of a deployment') @envutils.with_default_deploy_id def flavors(self, deploy_id=None): - """Show the flavors that are available in a deployment. + """Display available flavors. :param deploy_id: the UUID of a deployment """ + headers = ['ID', 'Name', 'vCPUs', 'RAM (MB)', 'Swap (MB)', 'Disk (GB)'] mixed_case_fields = ['ID', 'Name', 'vCPUs'] float_cols = ['RAM (MB)', 'Swap (MB)', 'Disk (GB)'] @@ -107,6 +114,8 @@ class ShowCommands(object): help='the UUID of a deployment') @envutils.with_default_deploy_id def networks(self, deploy_id=None): + """Display configured networks.""" + headers = ['ID', 'Label', 'CIDR'] mixed_case_fields = ['ID', 'Label', 'CIDR'] table_rows = [] @@ -129,6 +138,8 @@ class ShowCommands(object): help='the UUID of a deployment') @envutils.with_default_deploy_id def secgroups(self, deploy_id=None): + """Display security groups.""" + headers = ['ID', 'Name', 'Description'] mixed_case_fields = ['ID', 'Name', 'Description'] table_rows = [] @@ -154,6 +165,8 @@ class ShowCommands(object): help='the UUID of a deployment') @envutils.with_default_deploy_id def keypairs(self, deploy_id=None): + """Display available ssh keypairs.""" + headers = ['Name', 'Fingerprint'] mixed_case_fields = ['Name', 'Fingerprint'] table_rows = [] diff --git a/rally/cmd/commands/task.py b/rally/cmd/commands/task.py index 1d99326f9b..1c09571fd6 100644 --- a/rally/cmd/commands/task.py +++ b/rally/cmd/commands/task.py @@ -38,6 +38,10 @@ from rally import utils as rutils class TaskCommands(object): + """Task management. + + Set of commands that allow you to manage benchmarking tasks and results. + """ @cliutils.args('--deploy-id', type=str, dest='deploy_id', required=False, help='UUID of the deployment') @@ -45,7 +49,10 @@ class TaskCommands(object): help='Path to the file with full configuration of task') @envutils.with_default_deploy_id def validate(self, task, deploy_id=None): - """Validate a task file. + """Validate a task configuration file. + + This will check that task configuration file has valid syntax and + all required options of scenarios, contexts, SLA and runners are set. :param task: a file with yaml/json configration :param deploy_id: a UUID of a deployment @@ -71,7 +78,7 @@ class TaskCommands(object): 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. + """Start benchmark task. :param task: a file with yaml/json configration :param deploy_id: a UUID of a deployment @@ -99,20 +106,22 @@ class TaskCommands(object): @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 + """Abort started benchmarking 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 + """Display current 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']}) @@ -126,12 +135,13 @@ class TaskCommands(object): 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 + """Display results table. :param task_id: Task uuid :param iterations_data: print detailed results for each iteration Prints detailed information of task. """ + def _print_iterations_data(raw_data): headers = ["iteration", "full duration"] float_cols = ["full duration"] @@ -300,12 +310,15 @@ class TaskCommands(object): help=('Output in json format(default)')) @envutils.with_default_task_id def results(self, task_id=None, output_pprint=None, output_json=None): - """Print raw results of task. + """Diplay raw task results. + + This will produce a lot of output data about every iteration. :param task_id: Task uuid :param output_pprint: Output in pretty print format :param output_json: Output in json format (Default) """ + results = map(lambda x: {"key": x["key"], 'result': x['data']['raw'], "sla": x["data"]["sla"]}, db.task_result_get_all_by_uuid(task_id)) @@ -325,7 +338,8 @@ class TaskCommands(object): return(1) def list(self, task_list=None): - """Print a list of all tasks.""" + """List all tasks, started and finished.""" + headers = ['uuid', 'created_at', 'status', 'failed', 'tag'] task_list = task_list or db.task_list() if task_list: @@ -380,11 +394,12 @@ class TaskCommands(object): 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. + """Delete task and its 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) @@ -397,7 +412,7 @@ class TaskCommands(object): 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. + """Display SLA check results table. :param task_id: Task uuid. :returns: Number of failed criteria. diff --git a/rally/cmd/commands/use.py b/rally/cmd/commands/use.py index c625c6e76a..aaa79bafd3 100644 --- a/rally/cmd/commands/use.py +++ b/rally/cmd/commands/use.py @@ -24,6 +24,11 @@ from rally import fileutils class UseCommands(object): + """Set of commands that allow you to set an active deployment and task. + + Active deployment and task allow you not to specify deployment UUID and + task UUID in the commands requiring this parameter. + """ def _update_openrc_deployment_file(self, deploy_id, endpoint): openrc_path = os.path.expanduser('~/.rally/openrc-%s' % deploy_id) @@ -55,10 +60,11 @@ class UseCommands(object): @cliutils.args('--name', type=str, dest='name', required=False, help='Name of the deployment') def deployment(self, deploy_id=None, name=None): - """Set the RALLY_DEPLOYMENT env var to be used by all CLI commands + """Set active deployment. :param deploy_id: a UUID of a deployment """ + if not (name or deploy_id): print('You should specify --name or --uuid of deployment') return 1 @@ -99,14 +105,9 @@ class UseCommands(object): @cliutils.args('--uuid', type=str, dest='task_id', required=False, help='UUID of the task') def task(self, task_id): - """Set the RALLY_TASK env var. + """Set active task. - Is used to allow the user not to specify a task UUID in the command - requiring this parameter. - If the task uuid specified in parameter by the user does not exist, - a TaskNotFound will be raised by task_get(). - - :param task_id: a UUID of a task + :param task_id: a UUID of task """ print('Using task: %s' % task_id) self._ensure_rally_configuration_dir_exists() diff --git a/rally/cmd/commands/verify.py b/rally/cmd/commands/verify.py index 2881120c09..86a2608192 100644 --- a/rally/cmd/commands/verify.py +++ b/rally/cmd/commands/verify.py @@ -34,6 +34,11 @@ from rally.verification.verifiers.tempest import json2html class VerifyCommands(object): + """Test cloud with Tempest + + Set of commands that allow you to perform Tempest tests of + OpenStack live cloud. + """ @cliutils.args("--deploy-id", dest="deploy_id", type=str, required=False, help="UUID of a deployment.") @@ -48,13 +53,14 @@ class VerifyCommands(object): @envutils.with_default_deploy_id def start(self, deploy_id=None, set_name="smoke", regex=None, tempest_config=None): - """Start running tempest tests against a live cloud cluster. + """Start set of tests. :param deploy_id: a UUID of a deployment :param set_name: Name of tempest test set :param regex: Regular expression of test :param tempest_config: User specified Tempest config file location """ + if regex: set_name = "full" if set_name not in consts.TEMPEST_TEST_SETS: @@ -65,7 +71,8 @@ class VerifyCommands(object): api.verify(deploy_id, set_name, regex, tempest_config) def list(self): - """Print a result list of verifications.""" + """Display all verifications table, started and finished.""" + fields = ['UUID', 'Deployment UUID', 'Set name', 'Tests', 'Failures', 'Created at', 'Status'] verifications = db.verification_list() @@ -89,7 +96,7 @@ class VerifyCommands(object): help='If specified, output will be saved to given file') def results(self, verification_uuid, output_file=None, output_html=None, output_json=None, output_pprint=None): - """Print raw results of verification. + """Get raw results of the verification. :param verification_uuid: Verification UUID :param output_file: If specified, output will be saved to given file @@ -99,6 +106,7 @@ class VerifyCommands(object): :param output_pprint: Save results in pprint format to the specified file """ + try: results = db.verification_result_get(verification_uuid)['data'] except exceptions.NotFoundException as e: @@ -132,6 +140,8 @@ class VerifyCommands(object): @cliutils.args('--detailed', dest='detailed', action='store_true', required=False, help='Prints traceback of failed tests') def show(self, verification_uuid, sort_by='name', detailed=False): + """Display results table of the verification.""" + try: sortby_index = ('name', 'duration').index(sort_by) except ValueError: @@ -182,4 +192,6 @@ class VerifyCommands(object): @cliutils.args('--sort-by', dest='sort_by', type=str, required=False, help='Tests can be sorted by "name" or "duration"') def detailed(self, verification_uuid, sort_by='name'): + """Display results table of verification with detailed errors.""" + self.show(verification_uuid, sort_by, True)