From 40f49a42dd8476ad2332ecf2e0a57e6bf511aa63 Mon Sep 17 00:00:00 2001 From: Jo¶rgen Karlsson Date: Fri, 12 Jun 2015 11:19:57 +0200 Subject: add new command line handler MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New command line handler with pluggable classes. Task subcommand added. To run a scenario: yardstick -d task start samples/ping.yaml $ yardstick -h usage: yardstick [-h] [-V] [-d] [-v] {task} ... Command-line interface to yardstick optional arguments: -h, --help show this help message and exit -V, --version display version -d, --debug increase output verbosity to debug -v, --verbose increase output verbosity to info subcommands: {task} $ yardstick task -h usage: yardstick task [-h] {start} ... Task commands. Set of commands to manage benchmark tasks. optional arguments: -h, --help show this help message and exit subcommands: {start} $ yardstick task start -h usage: yardstick task start [-h] [--keep-deploy] [--parse-only] [--output-file OUTPUT_FILE] taskfile Start a benchmark scenario. positional arguments: taskfile path to taskfile optional arguments: -h, --help show this help message and exit --keep-deploy keep context deployed in cloud --parse-only parse the benchmark config file and exit --output-file OUTPUT_FILE file where output is stored, default /tmp/yardstick.out JIRA :- Signed-off-by: Jo¶rgen Karlsson Change-Id: If0672594efa4c94c94ebb73f0bc97ecfe3e00c62 --- yardstick/cmd/__init__.py | 0 yardstick/cmd/cli.py | 128 ++++++++++++++++++++++++++ yardstick/cmd/commands/__init__.py | 0 yardstick/cmd/commands/task.py | 179 +++++++++++++++++++++++++++++++++++++ yardstick/cmdparser.py | 71 --------------- yardstick/common/utils.py | 8 ++ yardstick/main.py | 149 +----------------------------- 7 files changed, 317 insertions(+), 218 deletions(-) create mode 100644 yardstick/cmd/__init__.py create mode 100644 yardstick/cmd/cli.py create mode 100644 yardstick/cmd/commands/__init__.py create mode 100644 yardstick/cmd/commands/task.py delete mode 100644 yardstick/cmdparser.py (limited to 'yardstick') diff --git a/yardstick/cmd/__init__.py b/yardstick/cmd/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/yardstick/cmd/cli.py b/yardstick/cmd/cli.py new file mode 100644 index 000000000..78e4e4c20 --- /dev/null +++ b/yardstick/cmd/cli.py @@ -0,0 +1,128 @@ +############################################################################## +# Copyright (c) 2015 Ericsson AB and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## + +''' +Command-line interface to yardstick +''' + +import argparse +import logging + +from pkg_resources import get_distribution +from argparse import RawDescriptionHelpFormatter + +from yardstick.cmd.commands import task + + +class YardstickCLI(): + '''Command-line interface to yardstick''' + + # Command categories + categories = { + 'task': task.TaskCommands + } + + def __init__(self): + self._version = 'yardstick version %s ' % \ + get_distribution('yardstick').version + + def _get_base_parser(self): + '''get base (top level) parser''' + + parser = argparse.ArgumentParser( + prog='yardstick', + formatter_class=RawDescriptionHelpFormatter, + description=YardstickCLI.__doc__ or '' + ) + + # Global options + + parser.add_argument( + "-V", "--version", + help="display version", + version=self._version, + action="version" + ) + + parser.add_argument( + "-d", "--debug", + help="increase output verbosity to debug", + action="store_true" + ) + + parser.add_argument( + "-v", "--verbose", + help="increase output verbosity to info", + action="store_true" + ) + + return parser + + def _find_actions(self, subparsers, actions_module): + '''find action methods''' + # Find action methods inside actions_module and + # add them to the command parser. + # The 'actions_module' argument may be a class + # or module. Action methods start with 'do_' + for attr in (a for a in dir(actions_module) if a.startswith('do_')): + command = attr[3:].replace('_', '-') + callback = getattr(actions_module, attr) + desc = callback.__doc__ or '' + arguments = getattr(callback, 'arguments', []) + subparser = subparsers.add_parser( + command, + description=desc + ) + for (args, kwargs) in arguments: + subparser.add_argument(*args, **kwargs) + subparser.set_defaults(func=callback) + + def _get_parser(self): + '''get a command-line parser''' + parser = self._get_base_parser() + + subparsers = parser.add_subparsers( + title='subcommands', + ) + + # add subcommands + for category in YardstickCLI.categories: + command_object = YardstickCLI.categories[category]() + desc = command_object.__doc__ or '' + subparser = subparsers.add_parser( + category, description=desc, + formatter_class=RawDescriptionHelpFormatter + ) + subparser.set_defaults(command_object=command_object) + cmd_subparsers = subparser.add_subparsers(title='subcommands') + self._find_actions(cmd_subparsers, command_object) + + return parser + + def main(self, argv): + '''run the command line interface''' + + # get argument parser + parser = self._get_parser() + + # parse command-line + args = parser.parse_args(argv) + + # handle global opts + logger = logging.getLogger('yardstick') + logger.setLevel(logging.WARNING) + + if args.verbose: + logger.getLogger('yardstick').setLevel(logging.INFO) + + if args.debug: + logger.setLevel(logging.DEBUG) + + # dispatch to category parser + args.func(args) diff --git a/yardstick/cmd/commands/__init__.py b/yardstick/cmd/commands/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/yardstick/cmd/commands/task.py b/yardstick/cmd/commands/task.py new file mode 100644 index 000000000..d562256ba --- /dev/null +++ b/yardstick/cmd/commands/task.py @@ -0,0 +1,179 @@ +############################################################################## +# Copyright (c) 2015 Ericsson AB and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## + +""" Handler for yardstick command 'task' """ + +import sys +import yaml +import atexit +import pkg_resources +import ipaddress + +from yardstick.benchmark.context.model import Context +from yardstick.benchmark.runners import base as base_runner + +from yardstick.common.utils import cliargs + +output_file_default = "/tmp/yardstick.out" + + +class TaskCommands(object): + '''Task commands. + + Set of commands to manage benchmark tasks. + ''' + + @cliargs("taskfile", type=str, help="path to taskfile", nargs=1) + @cliargs("--keep-deploy", help="keep context deployed in cloud", + action="store_true") + @cliargs("--parse-only", help="parse the benchmark config file and exit", + action="store_true") + @cliargs("--output-file", help="file where output is stored, default %s" % + output_file_default, default=output_file_default) + def do_start(self, args): + '''Start a benchmark scenario.''' + + atexit.register(atexit_handler) + + parser = TaskParser(args.taskfile[0]) + scenarios, run_in_parallel = parser.parse() + + if args.parse_only: + sys.exit(0) + + for context in Context.list: + context.deploy() + + runners = [] + if run_in_parallel: + for scenario in scenarios: + runner = run_one_scenario(scenario, args.output_file) + runners.append(runner) + + # Wait for runners to finish + for runner in runners: + runner_join(runner) + print "Runner ended, output in", args.output_file + else: + # run serially + for scenario in scenarios: + runner = run_one_scenario(scenario, args.output_file) + runner_join(runner) + print "Runner ended, output in", args.output_file + + if args.keep_deploy: + # keep deployment, forget about stack (hide it for exit handler) + Context.list = [] + else: + for context in Context.list: + context.undeploy() + + print "Done, exiting" + + +# TODO: Move stuff below into TaskCommands class !? + +class TaskParser(object): + '''Parser for task config files in yaml format''' + def __init__(self, path): + self.path = path + + def parse(self): + '''parses the task file and return an context and scenario instances''' + print "Parsing task config:", self.path + try: + with open(self.path) as stream: + cfg = yaml.load(stream) + except IOError as ioerror: + sys.exit(ioerror) + + if cfg["schema"] != "yardstick:task:0.1": + sys.exit("error: file %s has unknown schema %s" % (self.path, + cfg["schema"])) + + # TODO: support one or many contexts? Many would simpler and precise + if "context" in cfg: + context_cfgs = [cfg["context"]] + else: + context_cfgs = cfg["contexts"] + + for cfg_attrs in context_cfgs: + context = Context() + context.init(cfg_attrs) + + run_in_parallel = cfg.get("run_in_parallel", False) + + # TODO we need something better here, a class that represent the file + return cfg["scenarios"], run_in_parallel + + +def atexit_handler(): + '''handler for process termination''' + base_runner.Runner.terminate_all() + + if len(Context.list) > 0: + print "Undeploying all contexts" + for context in Context.list: + context.undeploy() + + +def is_ip_addr(addr): + '''check if string addr is an IP address''' + try: + ipaddress.ip_address(unicode(addr)) + return True + except ValueError: + return False + + +def run_one_scenario(scenario_cfg, output_file): + '''run one scenario using context''' + key_filename = pkg_resources.resource_filename( + 'yardstick.resources', 'files/yardstick_key') + + host = Context.get_server(scenario_cfg["host"]) + + runner_cfg = scenario_cfg["runner"] + runner_cfg['host'] = host.public_ip + runner_cfg['user'] = host.context.user + runner_cfg['key_filename'] = key_filename + runner_cfg['output_filename'] = output_file + + if "target" in scenario_cfg: + if is_ip_addr(scenario_cfg["target"]): + scenario_cfg["ipaddr"] = scenario_cfg["target"] + else: + target = Context.get_server(scenario_cfg["target"]) + + # get public IP for target server, some scenarios require it + if target.public_ip: + runner_cfg['target'] = target.public_ip + + # TODO scenario_cfg["ipaddr"] is bad naming + if host.context != target.context: + # target is in another context, get its public IP + scenario_cfg["ipaddr"] = target.public_ip + else: + # target is in the same context, get its private IP + scenario_cfg["ipaddr"] = target.private_ip + + runner = base_runner.Runner.get(runner_cfg) + + print "Starting runner of type '%s'" % runner_cfg["type"] + runner.run(scenario_cfg["type"], scenario_cfg) + + return runner + + +def runner_join(runner): + '''join (wait for) a runner, exit process at runner failure''' + status = runner.join() + base_runner.Runner.release(runner) + if status != 0: + sys.exit("Runner failed") diff --git a/yardstick/cmdparser.py b/yardstick/cmdparser.py deleted file mode 100644 index e8f770386..000000000 --- a/yardstick/cmdparser.py +++ /dev/null @@ -1,71 +0,0 @@ -############################################################################## -# Copyright (c) 2015 Ericsson AB and others. -# -# All rights reserved. This program and the accompanying materials -# are made available under the terms of the Apache License, Version 2.0 -# which accompanies this distribution, and is available at -# http://www.apache.org/licenses/LICENSE-2.0 -############################################################################## - -""" Argument parser for yardstick command line tool - -""" - -import argparse -import logging - -from pkg_resources import get_distribution - - -class CmdParser(argparse.ArgumentParser): - def __init__(self): - argparse.ArgumentParser.__init__(self) - - self.output_file_default = "/tmp/yardstick.out" - self._version = "yardstick version %s " % \ - get_distribution('yardstick').version - - self.__add_arguments() - - def __add_arguments(self): - self.add_argument("-d", "--debug", - help="increase output verbosity to debug", - action="store_true") - - self.add_argument("-v", "--verbose", - help="increase output verbosity to info", - action="store_true") - - self.add_argument("-V", "--version", - help="display version", - version=self._version, - action="version") - - self.add_argument("--keep-deploy", - help="keep context deployed in cloud", - action="store_true") - - self.add_argument("--parse-only", - help="parse the benchmark config file and exit", - action="store_true") - - self.add_argument("--output-file", - help="file where output is stored, default %s" % - self.output_file_default, - default=self.output_file_default) - - self.add_argument("taskfile", type=str, - help="path to taskfile", nargs=1) - - def parse_args(self): - args = argparse.ArgumentParser.parse_args(self) - - logger = logging.getLogger('yardstick') - - logger.setLevel(logging.WARNING) - if args.verbose: - logger.setLevel(logging.INFO) - if args.debug: - logger.setLevel(logging.DEBUG) - - return args diff --git a/yardstick/common/utils.py b/yardstick/common/utils.py index 4de0fcfd5..c482b4da4 100644 --- a/yardstick/common/utils.py +++ b/yardstick/common/utils.py @@ -22,6 +22,14 @@ from oslo_utils import importutils import yardstick +# Decorator for cli-args +def cliargs(*args, **kwargs): + def _decorator(func): + func.__dict__.setdefault('arguments', []).insert(0, (args, kwargs)) + return func + return _decorator + + def itersubclasses(cls, _seen=None): """Generator over all subclasses of a given class in depth first order.""" diff --git a/yardstick/main.py b/yardstick/main.py index e8f6b54aa..c16a42e91 100755 --- a/yardstick/main.py +++ b/yardstick/main.py @@ -38,159 +38,14 @@ NFV TST """ - import sys -import yaml -import atexit -import pkg_resources -import ipaddress - -from yardstick.benchmark.context.model import Context -from yardstick.benchmark.runners import base as base_runner -from yardstick.cmdparser import CmdParser - - -class TaskParser(object): - '''Parser for task config files in yaml format''' - def __init__(self, path): - self.path = path - - def parse(self): - '''parses the task file and return an context and scenario instances''' - print "Parsing task config:", self.path - try: - with open(self.path) as stream: - cfg = yaml.load(stream) - except IOError as ioerror: - sys.exit(ioerror) - - if cfg["schema"] != "yardstick:task:0.1": - sys.exit("error: file %s has unknown schema %s" % (self.path, - cfg["schema"])) - - # TODO: support one or many contexts? Many would simpler and precise - if "context" in cfg: - context_cfgs = [cfg["context"]] - else: - context_cfgs = cfg["contexts"] - - for cfg_attrs in context_cfgs: - context = Context() - context.init(cfg_attrs) - - run_in_parallel = cfg.get("run_in_parallel", False) - - # TODO we need something better here, a class that represent the file - return cfg["scenarios"], run_in_parallel - - -def atexit_handler(): - '''handler for process termination''' - base_runner.Runner.terminate_all() - - if len(Context.list) > 0: - print "Undeploying all contexts" - for context in Context.list: - context.undeploy() - - -def is_ip_addr(addr): - '''check if string addr is an IP address''' - try: - ipaddress.ip_address(unicode(addr)) - return True - except ValueError: - return False - - -def run_one_scenario(scenario_cfg, output_file): - '''run one scenario using context''' - key_filename = pkg_resources.resource_filename( - 'yardstick.resources', 'files/yardstick_key') - - host = Context.get_server(scenario_cfg["host"]) - runner_cfg = scenario_cfg["runner"] - runner_cfg['host'] = host.public_ip - runner_cfg['user'] = host.context.user - runner_cfg['key_filename'] = key_filename - runner_cfg['output_filename'] = output_file - - if "target" in scenario_cfg: - if is_ip_addr(scenario_cfg["target"]): - scenario_cfg["ipaddr"] = scenario_cfg["target"] - else: - target = Context.get_server(scenario_cfg["target"]) - - # get public IP for target server, some scenarios require it - if target.public_ip: - runner_cfg['target'] = target.public_ip - - # TODO scenario_cfg["ipaddr"] is bad naming - if host.context != target.context: - # target is in another context, get its public IP - scenario_cfg["ipaddr"] = target.public_ip - else: - # target is in the same context, get its private IP - scenario_cfg["ipaddr"] = target.private_ip - - runner = base_runner.Runner.get(runner_cfg) - - print "Starting runner of type '%s'" % runner_cfg["type"] - runner.run(scenario_cfg["type"], scenario_cfg) - - return runner - - -def runner_join(runner): - '''join (wait for) a runner, exit process at runner failure''' - status = runner.join() - base_runner.Runner.release(runner) - if status != 0: - sys.exit("Runner failed") +from yardstick.cmd.cli import YardstickCLI def main(): '''yardstick main''' - - atexit.register(atexit_handler) - - prog_args = CmdParser().parse_args() - - parser = TaskParser(prog_args.taskfile[0]) - scenarios, run_in_parallel = parser.parse() - - if prog_args.parse_only: - sys.exit(0) - - for context in Context.list: - context.deploy() - - runners = [] - if run_in_parallel: - for scenario in scenarios: - runner = run_one_scenario(scenario, prog_args.output_file) - runners.append(runner) - - # Wait for runners to finish - for runner in runners: - runner_join(runner) - print "Runner ended, output in", prog_args.output_file - else: - # run serially - for scenario in scenarios: - runner = run_one_scenario(scenario, prog_args.output_file) - runner_join(runner) - print "Runner ended, output in", prog_args.output_file - - if prog_args.keep_deploy: - # keep deployment, forget about stack (hide it for exit handler) - Context.list = [] - else: - for context in Context.list: - context.undeploy() - - print "Done, exiting" + YardstickCLI().main(sys.argv[1:]) if __name__ == '__main__': main() -- cgit 1.2.3-korg