766 lines
26 KiB
Python
766 lines
26 KiB
Python
# Copyright 2013: Mirantis Inc.
|
|
# 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.
|
|
|
|
from __future__ import print_function
|
|
|
|
import argparse
|
|
import inspect
|
|
import json
|
|
import os
|
|
import sys
|
|
import textwrap
|
|
import warnings
|
|
|
|
import jsonschema
|
|
import prettytable
|
|
import six
|
|
import sqlalchemy.exc
|
|
|
|
from rally import api
|
|
from rally.common import cfg
|
|
from rally.common import logging
|
|
from rally.common.plugin import info
|
|
from rally import exceptions
|
|
from rally.utils import encodeutils
|
|
|
|
|
|
CONF = cfg.CONF
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
|
|
# Some CLI-specific constants
|
|
MARGIN = 3
|
|
|
|
|
|
class MissingArgs(Exception):
|
|
"""Supplied arguments are not sufficient for calling a function."""
|
|
def __init__(self, missing):
|
|
self.missing = missing
|
|
msg = "Missing arguments: %s" % ", ".join(missing)
|
|
super(MissingArgs, self).__init__(msg)
|
|
|
|
|
|
def validate_args(fn, *args, **kwargs):
|
|
"""Check that the supplied args are sufficient for calling a function.
|
|
|
|
>>> validate_args(lambda a: None)
|
|
Traceback (most recent call last):
|
|
...
|
|
MissingArgs: Missing argument(s): a
|
|
>>> validate_args(lambda a, b, c, d: None, 0, c=1)
|
|
Traceback (most recent call last):
|
|
...
|
|
MissingArgs: Missing argument(s): b, d
|
|
|
|
:param fn: the function to check
|
|
:param args: the positional arguments supplied
|
|
:param kwargs: the keyword arguments supplied
|
|
"""
|
|
argspec = inspect.getargspec(fn)
|
|
|
|
num_defaults = len(argspec.defaults or [])
|
|
required_args = argspec.args[:len(argspec.args) - num_defaults]
|
|
|
|
if getattr(fn, "__self__", None):
|
|
required_args.pop(0)
|
|
|
|
missing_required_args = required_args[len(args):]
|
|
missing = [arg for arg in missing_required_args if arg not in kwargs]
|
|
if missing:
|
|
raise MissingArgs(missing)
|
|
|
|
|
|
def print_list(objs, fields, formatters=None, sortby_index=0,
|
|
mixed_case_fields=None, field_labels=None,
|
|
normalize_field_names=False,
|
|
table_label=None, print_header=True, print_border=True,
|
|
out=sys.stdout):
|
|
"""Print a list or objects as a table, one row per object.
|
|
|
|
:param objs: iterable of :class:`Resource`
|
|
:param fields: attributes that correspond to columns, in order
|
|
:param formatters: `dict` of callables for field formatting
|
|
:param sortby_index: index of the field for sorting table rows
|
|
:param mixed_case_fields: fields corresponding to object attributes that
|
|
have mixed case names (e.g., 'serverId')
|
|
:param field_labels: Labels to use in the heading of the table, default to
|
|
fields.
|
|
:param normalize_field_names: If True, field names will be transformed,
|
|
e.g. "Field Name" -> "field_name", otherwise they will be used
|
|
unchanged.
|
|
:param table_label: Label to use as header for the whole table.
|
|
:param print_header: print table header.
|
|
:param print_border: print table border.
|
|
:param out: stream to write output to.
|
|
|
|
"""
|
|
formatters = formatters or {}
|
|
mixed_case_fields = mixed_case_fields or []
|
|
field_labels = field_labels or fields
|
|
if len(field_labels) != len(fields):
|
|
raise ValueError("Field labels list %(labels)s has different number of"
|
|
" elements than fields list %(fields)s"
|
|
% {"labels": field_labels, "fields": fields})
|
|
|
|
if sortby_index is None:
|
|
kwargs = {}
|
|
else:
|
|
kwargs = {"sortby": field_labels[sortby_index]}
|
|
pt = prettytable.PrettyTable(field_labels)
|
|
pt.align = "l"
|
|
|
|
for o in objs:
|
|
row = []
|
|
for field in fields:
|
|
if field in formatters:
|
|
row.append(formatters[field](o))
|
|
else:
|
|
field_name = field
|
|
|
|
if normalize_field_names:
|
|
if field_name not in mixed_case_fields:
|
|
field_name = field_name.lower()
|
|
field_name = field_name.replace(" ", "_").replace("-", "_")
|
|
|
|
if isinstance(o, dict):
|
|
data = o.get(field_name, "")
|
|
else:
|
|
data = getattr(o, field_name, "")
|
|
row.append(data)
|
|
pt.add_row(row)
|
|
|
|
if not print_border or not print_header:
|
|
pt.set_style(prettytable.PLAIN_COLUMNS)
|
|
pt.left_padding_width = 0
|
|
pt.right_padding_width = 1
|
|
|
|
table_body = pt.get_string(header=print_header,
|
|
border=print_border,
|
|
**kwargs) + "\n"
|
|
|
|
table_header = ""
|
|
|
|
if table_label:
|
|
table_width = table_body.index("\n")
|
|
table_header = make_table_header(table_label, table_width)
|
|
table_header += "\n"
|
|
|
|
if six.PY3:
|
|
if table_header:
|
|
out.write(encodeutils.safe_encode(table_header).decode())
|
|
out.write(encodeutils.safe_encode(table_body).decode())
|
|
else:
|
|
if table_header:
|
|
out.write(encodeutils.safe_encode(table_header))
|
|
out.write(encodeutils.safe_encode(table_body))
|
|
|
|
|
|
def print_dict(obj, fields=None, formatters=None, mixed_case_fields=False,
|
|
normalize_field_names=False, property_label="Property",
|
|
value_label="Value", table_label=None, print_header=True,
|
|
print_border=True, wrap=0, out=sys.stdout):
|
|
"""Print dict as a table.
|
|
|
|
:param obj: dict to print
|
|
:param fields: `dict` of keys to print from d. Defaults to all keys
|
|
:param formatters: `dict` of callables for field formatting
|
|
:param mixed_case_fields: fields corresponding to object attributes that
|
|
have mixed case names (e.g., 'serverId')
|
|
:param normalize_field_names: If True, field names will be transformed,
|
|
e.g. "Field Name" -> "field_name", otherwise they will be used
|
|
unchanged.
|
|
:param property_label: label of "property" column
|
|
:param value_label: label of "value" column
|
|
:param table_label: Label to use as header for the whole table.
|
|
:param print_header: print table header.
|
|
:param print_border: print table border.
|
|
:param out: stream to write output to.
|
|
"""
|
|
formatters = formatters or {}
|
|
mixed_case_fields = mixed_case_fields or []
|
|
if not fields:
|
|
if isinstance(obj, dict):
|
|
fields = sorted(obj.keys())
|
|
else:
|
|
fields = [name for name in dir(obj)
|
|
if (not name.startswith("_")
|
|
and not callable(getattr(obj, name)))]
|
|
|
|
pt = prettytable.PrettyTable([property_label, value_label], caching=False)
|
|
pt.align = "l"
|
|
for field_name in fields:
|
|
if field_name in formatters:
|
|
data = formatters[field_name](obj)
|
|
else:
|
|
field = field_name
|
|
if normalize_field_names:
|
|
if field not in mixed_case_fields:
|
|
field = field_name.lower()
|
|
field = field.replace(" ", "_").replace("-", "_")
|
|
|
|
if isinstance(obj, dict):
|
|
data = obj.get(field, "")
|
|
else:
|
|
data = getattr(obj, field, "")
|
|
|
|
# convert dict to str to check length
|
|
if isinstance(data, (dict, list)):
|
|
data = json.dumps(data)
|
|
if wrap > 0:
|
|
data = textwrap.fill(six.text_type(data), wrap)
|
|
# if value has a newline, add in multiple rows
|
|
# e.g. fault with stacktrace
|
|
if (data and isinstance(data, six.string_types)
|
|
and (r"\n" in data or "\r" in data)):
|
|
# "\r" would break the table, so remove it.
|
|
if "\r" in data:
|
|
data = data.replace("\r", "")
|
|
lines = data.strip().split(r"\n")
|
|
col1 = field_name
|
|
for line in lines:
|
|
pt.add_row([col1, line])
|
|
col1 = ""
|
|
else:
|
|
if data is None:
|
|
data = "-"
|
|
pt.add_row([field_name, data])
|
|
|
|
table_body = pt.get_string(header=print_header,
|
|
border=print_border) + "\n"
|
|
|
|
table_header = ""
|
|
|
|
if table_label:
|
|
table_width = table_body.index("\n")
|
|
table_header = make_table_header(table_label, table_width)
|
|
table_header += "\n"
|
|
|
|
if six.PY3:
|
|
if table_header:
|
|
out.write(encodeutils.safe_encode(table_header).decode())
|
|
out.write(encodeutils.safe_encode(table_body).decode())
|
|
else:
|
|
if table_header:
|
|
out.write(encodeutils.safe_encode(table_header))
|
|
out.write(encodeutils.safe_encode(table_body))
|
|
|
|
|
|
def make_table_header(table_label, table_width,
|
|
junction_char="+", horizontal_char="-",
|
|
vertical_char="|"):
|
|
"""Generalized way make a table header string.
|
|
|
|
:param table_label: label to print on header
|
|
:param table_width: total width of table
|
|
:param junction_char: character used where vertical and
|
|
horizontal lines meet.
|
|
:param horizontal_char: character used for horizontal lines.
|
|
:param vertical_char: character used for vertical lines.
|
|
|
|
:returns: string
|
|
"""
|
|
|
|
if len(table_label) >= (table_width - 2):
|
|
raise ValueError(
|
|
"Table header %s is longer than total width of the table.")
|
|
|
|
label_and_space_width = table_width - len(table_label) - 2
|
|
padding = 0 if label_and_space_width % 2 == 0 else 1
|
|
|
|
half_table_width = label_and_space_width // 2
|
|
left_spacing = (" " * half_table_width)
|
|
right_spacing = (" " * (half_table_width + padding))
|
|
|
|
border_line = "".join((junction_char,
|
|
(horizontal_char * (table_width - 2)),
|
|
junction_char,))
|
|
|
|
label_line = "".join((vertical_char,
|
|
left_spacing,
|
|
table_label,
|
|
right_spacing,
|
|
vertical_char,))
|
|
|
|
return "\n".join((border_line, label_line,))
|
|
|
|
|
|
def make_header(text, size=80, symbol="-"):
|
|
"""Unified way to make header message to CLI.
|
|
|
|
:param text: what text to write
|
|
:param size: Length of header decorative line
|
|
:param symbol: What symbol to use to create header
|
|
"""
|
|
header = symbol * size + "\n"
|
|
header += "%s\n" % text
|
|
header += symbol * size + "\n"
|
|
return header
|
|
|
|
|
|
def suppress_warnings(f):
|
|
f._suppress_warnings = True
|
|
return f
|
|
|
|
|
|
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 few arguments'.
|
|
Also, we want not to print positional arguments in help message.
|
|
"""
|
|
|
|
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)
|
|
if message.startswith("argument") and message.endswith("is required"):
|
|
# NOTE(pirsriva) Argparse will currently raise an error
|
|
# message for only 1 missing argument at a time i.e. in the
|
|
# error message it WILL NOT LIST ALL the missing arguments
|
|
# at once INSTEAD only 1 missing argument at a time
|
|
missing_arg = message.split()[1]
|
|
print("Missing argument:\n%s" % missing_arg)
|
|
sys.exit(2)
|
|
|
|
|
|
def pretty_float_formatter(field, ndigits=None):
|
|
"""Create a float value formatter function for the given field.
|
|
|
|
:param field: str name of an object, which value should be formatted
|
|
:param ndigits: int number of digits after decimal point to round
|
|
default is None - this disables rounding
|
|
:returns: field formatter function
|
|
"""
|
|
def _formatter(obj):
|
|
value = obj[field] if isinstance(obj, dict) else getattr(obj, field)
|
|
if type(value) in (int, float):
|
|
if ndigits:
|
|
return round(value, ndigits)
|
|
return value
|
|
return "n/a"
|
|
return _formatter
|
|
|
|
|
|
def args(*args, **kwargs):
|
|
def _decorator(func):
|
|
func.__dict__.setdefault("args", []).insert(0, (args, kwargs))
|
|
if "metavar" not in kwargs and "action" not in kwargs:
|
|
# NOTE(andreykurilin): argparse constructs awful metavars...
|
|
kwargs["metavar"] = "<%s>" % args[0].replace(
|
|
"--", "").replace("-", "_")
|
|
return func
|
|
return _decorator
|
|
|
|
|
|
def alias(command_name):
|
|
"""Allow cli to use alias command name instead of function name.
|
|
|
|
:param command_name: desired command name
|
|
"""
|
|
def decorator(func):
|
|
func.alias = command_name
|
|
return func
|
|
return decorator
|
|
|
|
|
|
def deprecated_args(*args, **kwargs):
|
|
def _decorator(func):
|
|
if "release" not in kwargs:
|
|
raise ValueError("'release' is required keyword argument of "
|
|
"'deprecated_args' decorator.")
|
|
release = kwargs.pop("release")
|
|
alternative = kwargs.pop("alternative", None)
|
|
|
|
help_msg = "[Deprecated since Rally %s] " % release
|
|
if alternative:
|
|
help_msg += "Use '%s' instead. " % alternative
|
|
if "help" in kwargs:
|
|
help_msg += kwargs["help"]
|
|
kwargs["help"] = help_msg
|
|
|
|
func.__dict__.setdefault("args", []).insert(0, (args, kwargs))
|
|
func.__dict__.setdefault("deprecated_args", {})
|
|
func.deprecated_args[args[0]] = (release, alternative)
|
|
return func
|
|
return _decorator
|
|
|
|
|
|
def help_group(uuid):
|
|
"""Label cli method with specific group.
|
|
|
|
Joining methods by groups allows to compose more user-friendly help
|
|
messages in CLI.
|
|
|
|
:param uuid: Name of group to find common methods. It will be used for
|
|
sorting groups in help message, so you can start uuid with
|
|
some number (i.e "1_launcher", "2_management") to put groups in proper
|
|
order. Note: default group had "0" uuid.
|
|
"""
|
|
|
|
def wrapper(func):
|
|
func.help_group = uuid
|
|
return func
|
|
return wrapper
|
|
|
|
|
|
def _methods_of(cls):
|
|
"""Get all callable methods of a class that don't start with underscore.
|
|
|
|
:returns: a list of tuples of the form (method_name, method)
|
|
"""
|
|
# The idea of unbound methods exists in Python 2 and was removed in
|
|
# Python 3, so "inspect.ismethod" is used here for Python 2 and
|
|
# "inspect.isfunction" for Python 3.
|
|
all_methods = inspect.getmembers(
|
|
cls, predicate=lambda x: inspect.ismethod(x) or inspect.isfunction(x))
|
|
methods = [m for m in all_methods if not m[0].startswith("_")]
|
|
|
|
help_groups = {}
|
|
for m in methods:
|
|
group = getattr(m[1], "help_group", "0")
|
|
help_groups.setdefault(group, []).append(m)
|
|
|
|
if len(help_groups) > 1:
|
|
# we should sort methods by groups
|
|
methods = []
|
|
for group in sorted(help_groups.items(), key=lambda x: x[0]):
|
|
if methods:
|
|
# None -> empty line between groups
|
|
methods.append((None, None))
|
|
methods.extend(group[1])
|
|
return methods
|
|
|
|
|
|
def _compose_category_description(category):
|
|
|
|
descr_pairs = _methods_of(category)
|
|
|
|
description = ""
|
|
doc = category.__doc__
|
|
if doc:
|
|
description = doc.strip()
|
|
if descr_pairs:
|
|
description += "\n\nCommands:\n"
|
|
sublen = lambda item: len(item[0]) if item[0] else 0
|
|
first_column_len = max(map(sublen, descr_pairs)) + MARGIN
|
|
for item in descr_pairs:
|
|
if item[0] is None:
|
|
description += "\n"
|
|
continue
|
|
name = getattr(item[1], "alias", item[0].replace("_", "-"))
|
|
if item[1].__doc__:
|
|
doc = info.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 = info.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 _print_version():
|
|
from rally.common import version
|
|
|
|
print("Rally version: %s" % version.version_string())
|
|
packages = version.plugins_versions()
|
|
if packages:
|
|
print("\nInstalled Plugins:")
|
|
print("\n".join("\t%s: %s" % p for p in sorted(packages.items())))
|
|
|
|
|
|
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("bash-completion")
|
|
parser.add_argument("query_category", nargs="?")
|
|
|
|
for category in categories:
|
|
command_object = categories[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 method_name, method in _methods_of(command_object):
|
|
if method is None:
|
|
continue
|
|
method_name = method_name.replace("_", "-")
|
|
descr = _compose_action_description(method)
|
|
parser = category_subparsers.add_parser(
|
|
getattr(method, "alias", method_name),
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
description=descr, help=descr)
|
|
|
|
action_kwargs = []
|
|
for args, kwargs in getattr(method, "args", []):
|
|
# FIXME(markmc): hack to assume dest is the arg name without
|
|
# the leading hyphens if no dest is supplied
|
|
kwargs.setdefault("dest", args[0][2:])
|
|
action_kwargs.append(kwargs["dest"])
|
|
kwargs["dest"] = "action_kwarg_" + kwargs["dest"]
|
|
parser.add_argument(*args, **kwargs)
|
|
|
|
parser.set_defaults(action_fn=method)
|
|
parser.set_defaults(action_kwargs=action_kwargs)
|
|
parser.add_argument("action_args", nargs="*")
|
|
|
|
|
|
def validate_deprecated_args(argv, fn):
|
|
if (len(argv) > 3
|
|
and (argv[2] == fn.__name__)
|
|
and getattr(fn, "deprecated_args", None)):
|
|
for item, details in fn.deprecated_args.items():
|
|
if item in argv[3:]:
|
|
msg = ("The argument `%s` is deprecated since Rally %s." %
|
|
(item, details[0]))
|
|
if details[1]:
|
|
msg += " Use `%s` instead." % details[1]
|
|
LOG.warning(msg)
|
|
|
|
|
|
def run(argv, categories):
|
|
if len(argv) > 1 and argv[1] in ["version", "--version"]:
|
|
_print_version()
|
|
return 0
|
|
|
|
parser = lambda subparsers: _add_command_parsers(categories, subparsers)
|
|
category_opt = cfg.SubCommandOpt("category",
|
|
title="Command categories",
|
|
help="Available categories",
|
|
handler=parser)
|
|
|
|
CONF.register_cli_opt(category_opt)
|
|
help_msg = ("Additional custom plugin locations. Multiple files or "
|
|
"directories may be specified. All plugins in the specified"
|
|
" directories and subdirectories will be imported. Plugins in"
|
|
" /opt/rally/plugins and ~/.rally/plugins will always be "
|
|
"imported.")
|
|
|
|
CONF.register_cli_opt(cfg.ListOpt("plugin-paths",
|
|
default=os.environ.get(
|
|
"RALLY_PLUGIN_PATHS"),
|
|
help=help_msg))
|
|
|
|
# NOTE(andreykurilin): this dirty hack is done to unblock the gates.
|
|
# Currently, we are using oslo.config for CLI purpose (don't do this!)
|
|
# and it makes the things too complicated.
|
|
# To discover which CLI method can be affected by warnings and which not
|
|
# (based on suppress_warnings decorator) we need to obtain a desired
|
|
# CLI method. It can be done only after initialization of oslo_config
|
|
# which is located in rally.api.API init method.
|
|
# Initialization of rally.api.API can produce a warning (for example,
|
|
# from pymysql), so suppressing of warnings later will not work in such
|
|
# case (it is what actually had happened now in our CI with the latest
|
|
# release of PyMySQL).
|
|
#
|
|
# https://bitbucket.org/zzzeek/sqlalchemy/issues/4120/mysql-5720-warns-on-tx_isolation
|
|
try:
|
|
import pymysql
|
|
warnings.filterwarnings("ignore", category=pymysql.Warning)
|
|
except ImportError:
|
|
pass
|
|
|
|
try:
|
|
rapi = api.API(config_args=argv[1:], skip_db_check=True)
|
|
except exceptions.RallyException as e:
|
|
print(e)
|
|
return(2)
|
|
|
|
if CONF.category.name == "bash-completion":
|
|
print(_generate_bash_completion_script())
|
|
return(0)
|
|
|
|
fn = CONF.category.action_fn
|
|
fn_args = [encodeutils.safe_decode(arg)
|
|
for arg in CONF.category.action_args]
|
|
# api instance always is the first argument
|
|
fn_args.insert(0, rapi)
|
|
fn_kwargs = {}
|
|
for k in CONF.category.action_kwargs:
|
|
v = getattr(CONF.category, "action_kwarg_" + k)
|
|
if v is None:
|
|
continue
|
|
if isinstance(v, six.string_types):
|
|
v = encodeutils.safe_decode(v)
|
|
fn_kwargs[k] = v
|
|
|
|
# call the action with the remaining arguments
|
|
# check arguments
|
|
try:
|
|
validate_args(fn, *fn_args, **fn_kwargs)
|
|
except MissingArgs as e:
|
|
# NOTE(mikal): this isn't the most helpful error message ever. It is
|
|
# long, and tells you a lot of things you probably don't want to know
|
|
# if you just got a single arg wrong.
|
|
print(fn.__doc__)
|
|
CONF.print_help()
|
|
print("Missing arguments:")
|
|
for missing in e.missing:
|
|
for arg in fn.args:
|
|
if arg[1].get("dest", "").endswith(missing):
|
|
print(" " + arg[0][0])
|
|
break
|
|
return(1)
|
|
|
|
try:
|
|
validate_deprecated_args(argv, fn)
|
|
|
|
# skip db check for db and plugin commands
|
|
if CONF.category.name not in ("db", "plugin"):
|
|
rapi.check_db_revision()
|
|
|
|
if getattr(fn, "_suppress_warnings", False):
|
|
with warnings.catch_warnings():
|
|
warnings.simplefilter("ignore")
|
|
ret = fn(*fn_args, **fn_kwargs)
|
|
else:
|
|
ret = fn(*fn_args, **fn_kwargs)
|
|
return ret
|
|
|
|
except (IOError, TypeError, ValueError,
|
|
exceptions.RallyException, jsonschema.ValidationError) as e:
|
|
if logging.is_debug():
|
|
LOG.exception("Unexpected exception in CLI")
|
|
else:
|
|
print(e)
|
|
return getattr(e, "error_code", 1)
|
|
except sqlalchemy.exc.OperationalError as e:
|
|
if logging.is_debug():
|
|
LOG.exception("Something went wrong with database")
|
|
print(e)
|
|
print("Looks like Rally can't connect to its DB.")
|
|
print("Make sure that connection string in rally.conf is proper:")
|
|
print(CONF.database.connection)
|
|
return 1
|
|
except Exception:
|
|
print("Command failed, please check log for more info")
|
|
raise
|
|
|
|
|
|
def _generate_bash_completion_script():
|
|
from rally.cli import main
|
|
bash_data = """#!/bin/bash
|
|
|
|
# Standalone _filedir() alternative.
|
|
# This exempts from dependence of bash completion routines
|
|
function _rally_filedir()
|
|
{
|
|
test "${1}" \\
|
|
&& COMPREPLY=( \\
|
|
$(compgen -f -- "${cur}" | grep -E "${1}") \\
|
|
$(compgen -o plusdirs -- "${cur}") ) \\
|
|
|| COMPREPLY=( \\
|
|
$(compgen -o plusdirs -f -- "${cur}") \\
|
|
$(compgen -d -- "${cur}") )
|
|
}
|
|
|
|
_rally()
|
|
{
|
|
declare -A SUBCOMMANDS
|
|
declare -A OPTS
|
|
|
|
%(data)s
|
|
for OPT in ${!OPTS[*]} ; do
|
|
CMD=${OPT%%%%_*}
|
|
CMDSUB=${OPT#*_}
|
|
SUBCOMMANDS[${CMD}]+="${CMDSUB} "
|
|
done
|
|
|
|
COMMANDS="${!SUBCOMMANDS[*]}"
|
|
COMPREPLY=()
|
|
|
|
local cur="${COMP_WORDS[COMP_CWORD]}"
|
|
local prev="${COMP_WORDS[COMP_CWORD-1]}"
|
|
|
|
if [[ $cur =~ ^(\\.|\\~|\\/) ]] || [[ $prev =~ ^--out(|put-file)$ ]] ; then
|
|
_rally_filedir
|
|
elif [[ $prev =~ ^--(task|filename)$ ]] ; then
|
|
_rally_filedir "\\.json|\\.yaml|\\.yml"
|
|
elif [ $COMP_CWORD == "1" ] ; then
|
|
COMPREPLY=($(compgen -W "$COMMANDS" -- ${cur}))
|
|
elif [ $COMP_CWORD == "2" ] ; then
|
|
COMPREPLY=($(compgen -W "${SUBCOMMANDS[${prev}]}" -- ${cur}))
|
|
else
|
|
COMMAND="${COMP_WORDS[1]}_${COMP_WORDS[2]}"
|
|
COMPREPLY=($(compgen -W "${OPTS[$COMMAND]}" -- ${cur}))
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
complete -o filenames -F _rally rally
|
|
"""
|
|
completion = []
|
|
for category, cmds in main.categories.items():
|
|
for name, command in _methods_of(cmds):
|
|
if name is None:
|
|
continue
|
|
command_name = getattr(command, "alias", name.replace("_", "-"))
|
|
args_list = []
|
|
for arg in getattr(command, "args", []):
|
|
if getattr(command, "deprecated_args", []):
|
|
if arg[0][0] not in command.deprecated_args:
|
|
args_list.append(arg[0][0])
|
|
else:
|
|
args_list.append(arg[0][0])
|
|
args = " ".join(args_list)
|
|
|
|
completion.append(""" OPTS["{cat}_{cmd}"]="{args}"\n""".format(
|
|
cat=category, cmd=command_name, args=args))
|
|
return bash_data % {"data": "".join(sorted(completion))}
|