add new command line handler 11/811/4
authorJo¶rgen Karlsson <jorgen.w.karlsson@ericsson.com>
Fri, 12 Jun 2015 09:19:57 +0000 (11:19 +0200)
committerJo¶rgen Karlsson <jorgen.w.karlsson@ericsson.com>
Thu, 18 Jun 2015 13:37:30 +0000 (15:37 +0200)
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 <jorgen.w.karlsson@ericsson.com>
Change-Id: If0672594efa4c94c94ebb73f0bc97ecfe3e00c62

yardstick/cmd/__init__.py [new file with mode: 0644]
yardstick/cmd/cli.py [new file with mode: 0644]
yardstick/cmd/commands/__init__.py [new file with mode: 0644]
yardstick/cmd/commands/task.py [new file with mode: 0644]
yardstick/cmdparser.py [deleted file]
yardstick/common/utils.py
yardstick/main.py

diff --git a/yardstick/cmd/__init__.py b/yardstick/cmd/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/yardstick/cmd/cli.py b/yardstick/cmd/cli.py
new file mode 100644 (file)
index 0000000..78e4e4c
--- /dev/null
@@ -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 (file)
index 0000000..e69de29
diff --git a/yardstick/cmd/commands/task.py b/yardstick/cmd/commands/task.py
new file mode 100644 (file)
index 0000000..d562256
--- /dev/null
@@ -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 (file)
index e8f7703..0000000
+++ /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
index 4de0fcf..c482b4d 100644 (file)
@@ -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."""
 
index e8f6b54..c16a42e 100755 (executable)
     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()