Port the CLI from the argparse/oslo wrapper to typer
Replace the custom oslo.config/argparse CLI wrapper with a typer application. typer (over its vendored click) now owns the whole CLI surface (help, validation, strict rejection of unknown arguments); oslo.config and oslo.log are used only as libraries for config-file loading and logging setup. - Global options are generated at runtime from the live oslo Opt objects and appended to the root callback signature, then translated back into an argv slice for rally.api.API, so they stay in sync with oslo.log. - The six command groups become plain module-level functions on per-group sub-apps; the API instance is injected through a ContextVar instead of a parameter, keeping it out of the generated help and completion. - Each command's primary identifier is an ArgumentOrKeyword: it is accepted both positionally (rally task status <uuid>) and via its historical flag (--uuid), which is registered on the parser and hidden from help. - task start/validate accept "-" to read the input task from stdin. - The bash completion script is generated from the command tree (rally/cli/bashcomplete.py) and guarded by tests/unit/test_resources. - The cli_reference sphinx extension and the CHANGELOG are updated to match. Backward-incompatible: documented usage is preserved, but help and error messages are now rendered by typer/click (different wording and layout), and bash completion no longer suggests the primary-id flags (--uuid, --id, --env, --deployment, --task) -- they still work, and the identifiers can now also be passed positionally. Change-Id: I99341a53d8f79c2afa382f977bbddd9202eb6d09 Signed-off-by: Andriy Kurilin <andr.kurilin@gmail.com>
This commit is contained in:
@@ -26,10 +26,29 @@ Added
|
||||
~~~~~
|
||||
|
||||
* CI jobs for checking compatibility with python 3.13
|
||||
* ``rally task start`` and ``rally task validate`` can now read the task from
|
||||
stdin -- pass ``-`` in place of the file name, for example
|
||||
``cat task.yaml | rally task start -``.
|
||||
|
||||
Changed
|
||||
~~~~~~~
|
||||
|
||||
* The command-line interface has been rebuilt on `typer
|
||||
<https://typer.tiangolo.com>`_, replacing the custom argparse/oslo.config
|
||||
wrapper it grew up on. The change is backward compatible for documented
|
||||
usage: every command, option and primary identifier continues to work, and
|
||||
each primary identifier may now also be supplied positionally, for example
|
||||
``rally task status <uuid>`` in addition to ``rally task status --uuid
|
||||
<uuid>``. The only user-visible differences are cosmetic: help and error
|
||||
messages are now rendered by typer, and bash completion no longer lists the
|
||||
primary-id flags (``--uuid``, ``--id``, ``--env``, ``--deployment``,
|
||||
``--task``), which still work when typed explicitly.
|
||||
|
||||
* We no longer suppress PyMySQL's connect-time warnings. That workaround only
|
||||
existed to hide the old ``@@tx_isolation`` deprecation warning (SQLAlchemy
|
||||
#4120 / PyMySQL #614), which modern SQLAlchemy and PyMySQL have since fixed,
|
||||
so the ``mysql`` extra now pins ``PyMySQL>=0.9.0``.
|
||||
|
||||
* The ``pep8`` gate now runs ruff alongside flake8: ruff owns the pycodestyle,
|
||||
pyflakes and import-convention checks, while flake8 keeps the remaining
|
||||
hacking and Rally-specific rules.
|
||||
|
||||
+107
-124
@@ -13,64 +13,44 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import copy
|
||||
import inspect
|
||||
import typing as t
|
||||
|
||||
from docutils.parsers import rst
|
||||
import typer
|
||||
|
||||
from rally.cli import cliutils
|
||||
from rally.cli import main
|
||||
|
||||
from . import utils
|
||||
|
||||
|
||||
class Parser(object):
|
||||
"""A simplified interface of argparse.ArgumentParser"""
|
||||
def __init__(self):
|
||||
self.parsers = {}
|
||||
self.subparser = None
|
||||
self.defaults = {}
|
||||
self.arguments = []
|
||||
|
||||
def add_parser(self, name, help=None, description=None,
|
||||
formatter_class=None):
|
||||
parser = Parser()
|
||||
self.parsers[name] = {"description": description,
|
||||
"help": help,
|
||||
"fclass": formatter_class,
|
||||
"parser": parser}
|
||||
return parser
|
||||
|
||||
def set_defaults(self, command_object=None, action_fn=None,
|
||||
action_kwargs=None):
|
||||
if command_object:
|
||||
self.defaults["command_object"] = command_object
|
||||
if action_fn:
|
||||
self.defaults["action_fn"] = action_fn
|
||||
if action_kwargs:
|
||||
self.defaults["action_kwargs"] = action_kwargs
|
||||
|
||||
def add_subparsers(self, dest):
|
||||
# NOTE(andreykurilin): there is only one expected call
|
||||
if self.subparser:
|
||||
raise ValueError("Can't add one more subparser.")
|
||||
self.subparser = Parser()
|
||||
return self.subparser
|
||||
|
||||
def add_argument(self, *args, **kwargs):
|
||||
if "action_args" in args:
|
||||
return
|
||||
self.arguments.append((args, kwargs))
|
||||
if t.TYPE_CHECKING:
|
||||
from docutils import nodes
|
||||
|
||||
|
||||
DEFAULT_UUIDS_CMD = {
|
||||
"deployment": ["rally deployment create"],
|
||||
"task": ["rally task start"],
|
||||
"verification": ["rally verify start", "rally verify import_results"]
|
||||
"verification": ["rally verify start", "rally verify import"]
|
||||
}
|
||||
|
||||
# Maps the "use"-command hint for each default-from-environment id.
|
||||
USE_CMD = {
|
||||
"deployment": "rally deployment use",
|
||||
"task": "rally task use",
|
||||
"verification": "rally verify use",
|
||||
}
|
||||
|
||||
# Maps a parameter's env var to a default-uuid id.
|
||||
_ENVVAR_DEST = {
|
||||
"RALLY_ENV": "deployment",
|
||||
"RALLY_DEPLOYMENT": "deployment",
|
||||
"RALLY_TASK": "task",
|
||||
"RALLY_VERIFICATION": "verification",
|
||||
}
|
||||
|
||||
|
||||
def compose_note_about_default_uuids(argument, dest):
|
||||
def compose_note_about_default_uuids(argument, dest) -> "nodes.note":
|
||||
# TODO(andreykurilin): add references to commands
|
||||
return utils.note(
|
||||
"The default value for the ``%(arg)s`` argument is taken from "
|
||||
@@ -81,96 +61,97 @@ def compose_note_about_default_uuids(argument, dest):
|
||||
"cmd": "``, ``".join(DEFAULT_UUIDS_CMD[dest])})
|
||||
|
||||
|
||||
def compose_use_cmd_hint_msg(cmd):
|
||||
def compose_use_cmd_hint_msg(cmd: str) -> "nodes.hint":
|
||||
return utils.hint(
|
||||
"You can set the default value by executing ``%(cmd)s <uuid>``"
|
||||
" (ref__).\n\n __ #%(ref)s" % {"cmd": cmd,
|
||||
"ref": cmd.replace(" ", "-")})
|
||||
f"You can set the default value by executing ``{cmd} <uuid>``"
|
||||
f" (ref__).\n\n __ #{cmd.replace(' ', '-')}"
|
||||
)
|
||||
|
||||
|
||||
def make_arguments_section(category_name, cmd_name, arguments, defaults):
|
||||
def _first(value):
|
||||
if isinstance(value, (list, tuple)):
|
||||
return value[0] if value else None
|
||||
return value
|
||||
|
||||
|
||||
def _note_dest(cmd_name, param):
|
||||
"""Return the default-uuid note key for a parameter, or None.
|
||||
|
||||
A parameter earns the note when it reads one of the ``RALLY_*`` env vars
|
||||
that ``rally <thing> use`` populates. The ``use`` command sets these
|
||||
defaults itself, so it is excluded.
|
||||
"""
|
||||
if cmd_name == "use":
|
||||
return None
|
||||
return _ENVVAR_DEST.get(_first(getattr(param, "envvar", None)))
|
||||
|
||||
|
||||
def _display_names(param):
|
||||
"""Return the flag(s)/name shown for a parameter."""
|
||||
opts = list(getattr(param, "opts", None) or [])
|
||||
if opts and opts[0].startswith("-"):
|
||||
return opts
|
||||
# A positional argument: show its metavar (e.g. ``UUID``) rather than the
|
||||
# internal destination name.
|
||||
metavar = getattr(param, "metavar", None)
|
||||
return [metavar] if metavar else (opts or [param.name])
|
||||
|
||||
|
||||
def _iter_params(command):
|
||||
"""Yield the documentable parameters of a command (skip ``--help``)."""
|
||||
for param in command.params:
|
||||
opts = getattr(param, "opts", [])
|
||||
if opts and opts[0] in ("--help", "-h", "--version"):
|
||||
continue
|
||||
if getattr(param, "name", None) == "help":
|
||||
continue
|
||||
yield param
|
||||
|
||||
|
||||
def make_arguments_section(category_name, cmd_name, command):
|
||||
elements = [utils.paragraph("**Command arguments**:")]
|
||||
for args, kwargs in arguments:
|
||||
# for future changes...
|
||||
# :param args: a single command argument which can represented by
|
||||
# several names(for example, --uuid and --task-id) in cli.
|
||||
# :type args: tuple
|
||||
# :param kwargs: description of argument. Have next format:
|
||||
# {"dest": "action_kwarg_<name of keyword argument in code>",
|
||||
# "help": "just a description of argument"
|
||||
# "metavar": "[optional] metavar of argument. Example:"
|
||||
# "Example: argument '--file'; metavar 'path' ",
|
||||
# "type": "[optional] class object of argument's type",
|
||||
# "required": "[optional] boolean value"}
|
||||
# :type kwargs: dict
|
||||
dest = kwargs.get("dest").replace("action_kwarg_", "")
|
||||
for param in _iter_params(command):
|
||||
names = _display_names(param)
|
||||
flag = names[0]
|
||||
|
||||
description = []
|
||||
if cmd_name != "use":
|
||||
# lets add notes about specific default values and hint about
|
||||
# "use" command with reference
|
||||
if dest in ("deployment", "task"):
|
||||
description.append(compose_note_about_default_uuids(
|
||||
args[0], dest))
|
||||
description.append(
|
||||
compose_use_cmd_hint_msg("rally %s use" % dest))
|
||||
elif dest == "verification":
|
||||
description.append(compose_note_about_default_uuids(
|
||||
args[0], dest))
|
||||
description.append(
|
||||
compose_use_cmd_hint_msg("rally verify use"))
|
||||
note_dest = _note_dest(cmd_name, param)
|
||||
if note_dest is not None:
|
||||
description.append(
|
||||
compose_note_about_default_uuids(flag, note_dest))
|
||||
description.append(
|
||||
compose_use_cmd_hint_msg(USE_CMD[note_dest]))
|
||||
|
||||
description.append(kwargs.get("help"))
|
||||
description.append(getattr(param, "help", None))
|
||||
|
||||
action = kwargs.get("action")
|
||||
if not action:
|
||||
arg_type = kwargs.get("type")
|
||||
if arg_type:
|
||||
description.append("**Type**: %s" % arg_type.__name__)
|
||||
if not getattr(param, "is_flag", False):
|
||||
type_name = getattr(getattr(param, "type", None), "name", None)
|
||||
if type_name:
|
||||
description.append("**Type**: %s" % type_name)
|
||||
|
||||
skip_default = dest in ("deployment",
|
||||
"task_id",
|
||||
"verification")
|
||||
if not skip_default and dest in defaults:
|
||||
description.append("**Default**: %s" % defaults[dest])
|
||||
metavar = kwargs.get("metavar")
|
||||
default = getattr(param, "default", None)
|
||||
if note_dest is None and default is not None:
|
||||
description.append("**Default**: %s" % default)
|
||||
|
||||
ref = "%s_%s_%s" % (category_name, cmd_name, args[0].replace("-", ""))
|
||||
|
||||
if metavar:
|
||||
args = ["%s %s" % (arg, metavar) for arg in args]
|
||||
|
||||
elements.extend(utils.make_definition(", ".join(args),
|
||||
ref = "%s_%s_%s" % (category_name, cmd_name,
|
||||
flag.replace("-", "").replace(" ", ""))
|
||||
elements.extend(utils.make_definition(", ".join(names),
|
||||
ref, description))
|
||||
return elements
|
||||
|
||||
|
||||
def get_defaults(func):
|
||||
"""Return a map of argument:default_value for specified function."""
|
||||
spec = inspect.getfullargspec(func)
|
||||
if spec.defaults:
|
||||
return dict(zip(spec.args[-len(spec.defaults):], spec.defaults))
|
||||
return {}
|
||||
|
||||
|
||||
def make_command_section(category_name, name, parser):
|
||||
def make_command_section(category_name, name, command):
|
||||
section = utils.subcategory("rally %s %s" % (category_name, name))
|
||||
section.extend(utils.parse_text(parser["description"]))
|
||||
if parser["parser"].arguments:
|
||||
defaults = get_defaults(parser["parser"].defaults["action_fn"])
|
||||
section.extend(make_arguments_section(
|
||||
category_name, name, parser["parser"].arguments, defaults))
|
||||
description = inspect.getdoc(command.callback) or command.help or ""
|
||||
section.extend(utils.parse_text(description))
|
||||
if any(True for _ in _iter_params(command)):
|
||||
section.extend(make_arguments_section(category_name, name, command))
|
||||
return section
|
||||
|
||||
|
||||
def make_category_section(name, parser):
|
||||
def make_category_section(name, group):
|
||||
category_obj = utils.category("Category: %s" % name)
|
||||
# NOTE(andreykurilin): we are re-using `_add_command_parsers` method from
|
||||
# `rally.cli.cliutils`, but, since it was designed to print help message,
|
||||
# generated description for categories contains specification for all
|
||||
# sub-commands. We don't need information about sub-commands at this point,
|
||||
# so let's skip "generated description" and take it directly from category
|
||||
# class.
|
||||
description = parser.defaults["command_object"].__doc__
|
||||
description = group.help or ""
|
||||
# TODO(andreykurilin): write a decorator which will mark cli-class as
|
||||
# deprecated without changing its docstring.
|
||||
if description.startswith("[Deprecated"):
|
||||
@@ -180,9 +161,10 @@ def make_category_section(name, parser):
|
||||
category_obj.append(utils.warning(msg))
|
||||
category_obj.extend(utils.parse_text(description))
|
||||
|
||||
for command in sorted(parser.subparser.parsers.keys()):
|
||||
subparser = parser.subparser.parsers[command]
|
||||
category_obj.append(make_command_section(name, command, subparser))
|
||||
commands = getattr(group, "commands", {})
|
||||
for command in sorted(commands):
|
||||
category_obj.append(
|
||||
make_command_section(name, command, commands[command]))
|
||||
return category_obj
|
||||
|
||||
|
||||
@@ -191,17 +173,18 @@ class CLIReferenceDirective(rst.Directive):
|
||||
option_spec = {"group": str}
|
||||
|
||||
def run(self):
|
||||
parser = Parser()
|
||||
categories = copy.copy(main.categories)
|
||||
cli = typer.main.get_command(main.app)
|
||||
groups = getattr(cli, "commands", {})
|
||||
# only command groups (skip top-level leaf commands like ``version``)
|
||||
categories = [c for c, g in groups.items()
|
||||
if getattr(g, "commands", None)]
|
||||
if "group" in self.options:
|
||||
categories = {k: v for k, v in categories.items()
|
||||
if k == self.options["group"]}
|
||||
cliutils._add_command_parsers(categories, parser)
|
||||
categories = [c for c in categories
|
||||
if c == self.options["group"]]
|
||||
|
||||
content = []
|
||||
for cg in sorted(categories.keys()):
|
||||
content.append(make_category_section(
|
||||
cg, parser.parsers[cg]["parser"]))
|
||||
for cg in sorted(categories):
|
||||
content.append(make_category_section(cg, groups[cg]))
|
||||
return content
|
||||
|
||||
|
||||
|
||||
+47
-48
@@ -24,61 +24,60 @@ _rally()
|
||||
OPTS["db_revision"]=""
|
||||
OPTS["db_show"]="--creds"
|
||||
OPTS["db_upgrade"]=""
|
||||
OPTS["deployment_check"]="--deployment"
|
||||
OPTS["deployment_config"]="--deployment"
|
||||
OPTS["deployment_check"]=""
|
||||
OPTS["deployment_config"]=""
|
||||
OPTS["deployment_create"]="--name --fromenv --filename --no-use"
|
||||
OPTS["deployment_destroy"]="--deployment"
|
||||
OPTS["deployment_destroy"]=""
|
||||
OPTS["deployment_list"]=""
|
||||
OPTS["deployment_recreate"]="--filename --deployment"
|
||||
OPTS["deployment_show"]="--deployment"
|
||||
OPTS["deployment_use"]="--deployment"
|
||||
OPTS["env_check"]="--env --json --detailed"
|
||||
OPTS["env_cleanup"]="--json --env"
|
||||
OPTS["env_create"]="--name --description --extras --from-sysenv --spec --json --no-use"
|
||||
OPTS["env_delete"]="--env --force"
|
||||
OPTS["env_destroy"]="--env --skip-cleanup --json --detailed"
|
||||
OPTS["env_info"]="--env --json"
|
||||
OPTS["deployment_recreate"]="--filename"
|
||||
OPTS["deployment_show"]=""
|
||||
OPTS["deployment_use"]=""
|
||||
OPTS["env_check"]="--json --detailed"
|
||||
OPTS["env_cleanup"]="--json"
|
||||
OPTS["env_create"]="--name --description --extras --spec --from-sysenv --json --no-use"
|
||||
OPTS["env_delete"]="--force"
|
||||
OPTS["env_destroy"]="--skip-cleanup --json --detailed"
|
||||
OPTS["env_info"]="--json"
|
||||
OPTS["env_list"]="--json"
|
||||
OPTS["env_show"]="--env --json --only-spec"
|
||||
OPTS["env_use"]="--env --json"
|
||||
OPTS["plugin_list"]="--name --platform --plugin-base"
|
||||
OPTS["plugin_show"]="--name --platform"
|
||||
OPTS["task_abort"]="--uuid --soft"
|
||||
OPTS["task_delete"]="--force --uuid"
|
||||
OPTS["task_detailed"]="--uuid --iterations-data --filter-by"
|
||||
OPTS["task_export"]="--uuid --type --to --deployment"
|
||||
OPTS["env_show"]="--json --only-spec"
|
||||
OPTS["env_use"]="--json"
|
||||
OPTS["plugin_list"]="--platform --plugin-base"
|
||||
OPTS["plugin_show"]="--platform"
|
||||
OPTS["task_abort"]="--soft"
|
||||
OPTS["task_delete"]="--force"
|
||||
OPTS["task_detailed"]="--iterations-data --filter-by"
|
||||
OPTS["task_export"]="--type --to --deployment"
|
||||
OPTS["task_import"]="--file --deployment --tag"
|
||||
OPTS["task_list"]="--deployment --all-deployments --status --tag --uuids-only"
|
||||
OPTS["task_report"]="--out --open --html --html-static --json --uuid --deployment"
|
||||
OPTS["task_restart"]="--deployment --uuid --scenario --tag --no-use --abort-on-sla-failure"
|
||||
OPTS["task_results"]="--uuid"
|
||||
OPTS["task_sla-check"]="--uuid --json"
|
||||
OPTS["task_start"]="--deployment --task --task-args --task-args-file --tag --no-use --abort-on-sla-failure"
|
||||
OPTS["task_status"]="--uuid"
|
||||
OPTS["task_trends"]="--out --open --tasks --html-static"
|
||||
OPTS["task_use"]="--uuid"
|
||||
OPTS["task_validate"]="--deployment --task --task-args --task-args-file"
|
||||
OPTS["verify_add-verifier-ext"]="--id --source --version --extra-settings"
|
||||
OPTS["verify_configure-verifier"]="--id --deployment-id --reconfigure --extend --override --show"
|
||||
OPTS["verify_create-verifier"]="--name --type --platform --source --version --system-wide --extra-settings --no-use"
|
||||
OPTS["verify_delete"]="--uuid"
|
||||
OPTS["verify_delete-verifier"]="--id --deployment-id --force"
|
||||
OPTS["verify_delete-verifier-ext"]="--id --name"
|
||||
OPTS["verify_import"]="--id --deployment-id --file --run-args --no-use"
|
||||
OPTS["task_report"]="--out --open --html --html-static --json --deployment"
|
||||
OPTS["task_restart"]="--deployment --scenario --tag --no-use --abort-on-sla-failure"
|
||||
OPTS["task_results"]=""
|
||||
OPTS["task_sla-check"]="--json"
|
||||
OPTS["task_start"]="--deployment --task-args --task-args-file --tag --no-use --abort-on-sla-failure"
|
||||
OPTS["task_status"]=""
|
||||
OPTS["task_trends"]="--out --open --html-static"
|
||||
OPTS["task_use"]=""
|
||||
OPTS["task_validate"]="--deployment --task-args --task-args-file"
|
||||
OPTS["verify_add-verifier-ext"]="--source --extra-settings"
|
||||
OPTS["verify_configure-verifier"]="--deployment-id --reconfigure --extend --override --show"
|
||||
OPTS["verify_create-verifier"]="--name --type --platform --source --system-wide --extra-settings --no-use"
|
||||
OPTS["verify_delete"]=""
|
||||
OPTS["verify_delete-verifier"]="--deployment-id --force"
|
||||
OPTS["verify_delete-verifier-ext"]="--name"
|
||||
OPTS["verify_import"]="--deployment-id --file --run-args --no-use"
|
||||
OPTS["verify_list"]="--id --deployment-id --tag --status"
|
||||
OPTS["verify_list-plugins"]="--platform"
|
||||
OPTS["verify_list-verifier-exts"]="--id"
|
||||
OPTS["verify_list-verifier-tests"]="--id --pattern"
|
||||
OPTS["verify_list-verifier-exts"]=""
|
||||
OPTS["verify_list-verifier-tests"]="--pattern"
|
||||
OPTS["verify_list-verifiers"]="--status"
|
||||
OPTS["verify_report"]="--uuid --type --to --open"
|
||||
OPTS["verify_rerun"]="--uuid --deployment-id --failed --tag --concurrency --detailed --no-use"
|
||||
OPTS["verify_show"]="--uuid --sort-by --detailed"
|
||||
OPTS["verify_show-verifier"]="--id"
|
||||
OPTS["verify_start"]="--id --deployment-id --tag --pattern --concurrency --load-list --skip-list --xfail-list --detailed --no-use"
|
||||
OPTS["verify_update-verifier"]="--id --update-venv --version --system-wide --no-system-wide"
|
||||
OPTS["verify_use"]="--uuid"
|
||||
OPTS["verify_use-verifier"]="--id"
|
||||
|
||||
OPTS["verify_report"]="--type --to --open"
|
||||
OPTS["verify_rerun"]="--deployment-id --failed --tag --concurrency --detailed --no-use"
|
||||
OPTS["verify_show"]="--sort-by --detailed"
|
||||
OPTS["verify_show-verifier"]=""
|
||||
OPTS["verify_start"]="--deployment-id --tag --pattern --concurrency --load-list --skip-list --xfail-list --detailed --no-use"
|
||||
OPTS["verify_update-verifier"]="--update-venv --system-wide --no-system-wide"
|
||||
OPTS["verify_use"]=""
|
||||
OPTS["verify_use-verifier"]=""
|
||||
for OPT in ${!OPTS[*]} ; do
|
||||
CMD=${OPT%%_*}
|
||||
CMDSUB=${OPT#*_}
|
||||
@@ -106,4 +105,4 @@ _rally()
|
||||
return 0
|
||||
}
|
||||
|
||||
complete -o filenames -F _rally rally
|
||||
complete -o filenames -F _rally rally
|
||||
|
||||
+8
-29
@@ -48,7 +48,7 @@ dependencies = {file = ["requirements.txt"]}
|
||||
|
||||
[project.optional-dependencies]
|
||||
mysql = [
|
||||
"PyMySQL>=0.7.6" # MIT
|
||||
"PyMySQL>=0.9.0" # MIT
|
||||
]
|
||||
postgres = [
|
||||
"psycopg2>=2.5" # LGPL/ZPL
|
||||
@@ -81,6 +81,11 @@ strict_equality = true
|
||||
# declare optional result
|
||||
no_warn_no_return = true
|
||||
|
||||
# oslo.log ships no type information (no py.typed marker).
|
||||
[[tool.mypy.overrides]]
|
||||
module = "oslo_log.*"
|
||||
ignore_missing_imports = true
|
||||
|
||||
# FIXME(andreykurilin): all the following should be fixed
|
||||
|
||||
[[tool.mypy.overrides]]
|
||||
@@ -89,40 +94,12 @@ disable_error_code = ["attr-defined", "no-untyped-def"]
|
||||
|
||||
[[tool.mypy.overrides]]
|
||||
module = "rally.cli.cliutils"
|
||||
disable_error_code = ["arg-type", "assignment", "import-untyped", "index", "no-untyped-def", "var-annotated"]
|
||||
|
||||
[[tool.mypy.overrides]]
|
||||
module = "rally.cli.commands.db"
|
||||
disable_error_code = ["no-untyped-def"]
|
||||
|
||||
[[tool.mypy.overrides]]
|
||||
module = "rally.cli.commands.deployment"
|
||||
disable_error_code = ["import-not-found", "no-untyped-def"]
|
||||
|
||||
[[tool.mypy.overrides]]
|
||||
module = "rally.cli.commands.env"
|
||||
disable_error_code = ["arg-type", "func-returns-value", "no-untyped-def"]
|
||||
|
||||
[[tool.mypy.overrides]]
|
||||
module = "rally.cli.commands.plugin"
|
||||
disable_error_code = ["no-untyped-def"]
|
||||
|
||||
[[tool.mypy.overrides]]
|
||||
module = "rally.cli.commands.task"
|
||||
disable_error_code = ["no-untyped-def", "var-annotated"]
|
||||
|
||||
[[tool.mypy.overrides]]
|
||||
module = "rally.cli.commands.verify"
|
||||
disable_error_code = ["assignment", "attr-defined", "method-assign", "no-untyped-def"]
|
||||
|
||||
[[tool.mypy.overrides]]
|
||||
module = "rally.cli.envutils"
|
||||
disable_error_code = ["no-untyped-def"]
|
||||
|
||||
[[tool.mypy.overrides]]
|
||||
module = "rally.cli.main"
|
||||
disable_error_code = ["no-untyped-def"]
|
||||
|
||||
[[tool.mypy.overrides]]
|
||||
module = "rally.cli.task_results_loader"
|
||||
disable_error_code = ["attr-defined", "no-untyped-def"]
|
||||
@@ -307,6 +284,7 @@ select = [
|
||||
"ICN003", # forbid `from <module> import ...` for modules imported whole
|
||||
"PGH003", # `type: ignore` must name the specific error code
|
||||
"PGH004", # `noqa` must name the specific rule code
|
||||
"PLC0414", # band redundant aliases
|
||||
"TID251", # forbid the APIs banned below
|
||||
]
|
||||
ignore = [
|
||||
@@ -341,6 +319,7 @@ docstring-quotes = "double"
|
||||
datetime = "dt"
|
||||
typing = "t"
|
||||
typing_extensions = "te"
|
||||
"rally.common.logging" = "logging"
|
||||
|
||||
[tool.ruff.lint.flake8-import-conventions]
|
||||
# Builtins are imported as modules: forbid `from <stdlib> import ...` so the
|
||||
|
||||
+7
-6
@@ -1195,19 +1195,20 @@ class API(object):
|
||||
CONFIG_SEARCH_PATHS = [sys.prefix + "/etc/rally", "~/.rally", "/etc/rally"]
|
||||
CONFIG_FILE_NAME = "rally.conf"
|
||||
|
||||
def __init__(self, config_file=None, config_args=None,
|
||||
plugin_paths=None, skip_db_check=False):
|
||||
def __init__(
|
||||
self,
|
||||
config_file: str | None = None,
|
||||
config_args: list[str] | None = None,
|
||||
plugin_paths: list[str] | None = None,
|
||||
skip_db_check: bool = False
|
||||
):
|
||||
"""Initialize Rally API instance
|
||||
|
||||
:param config_file: Path to rally configuration file. If None, default
|
||||
path will be selected
|
||||
:type config_file: str
|
||||
:param config_args: Arguments for initialization current configuration
|
||||
:type config_args: list
|
||||
:param plugin_paths: Additional custom plugin locations
|
||||
:type plugin_paths: list
|
||||
:param skip_db_check: Allows to skip db revision check
|
||||
:type skip_db_check: bool
|
||||
"""
|
||||
|
||||
try:
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
# 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.
|
||||
|
||||
"""Primary identifiers that accept both a positional and a ``--flag`` form.
|
||||
|
||||
Rally has always exposed a command's primary identifier both ways, e.g.
|
||||
``rally task abort <uuid>`` and ``rally task abort --uuid <uuid>``. typer lets
|
||||
a parameter be an Argument *or* an Option, not both, so
|
||||
:class:`ArgumentOrKeyword` declares the positional form and :func:`install`
|
||||
registers the ``--flag`` alias
|
||||
directly in the parser, pointing at the same destination. Requiredness,
|
||||
env-var defaults, unknown-option and extra-argument errors all stay native --
|
||||
there is no hidden duplicate parameter and no post-hoc value merging.
|
||||
"""
|
||||
|
||||
import inspect
|
||||
import typing as t
|
||||
|
||||
import typer
|
||||
import typer.core
|
||||
|
||||
from rally.cli import cliutils
|
||||
|
||||
|
||||
if t.TYPE_CHECKING:
|
||||
import typer._click.core
|
||||
|
||||
|
||||
class ArgumentOrKeyword(typer.models.ArgumentInfo):
|
||||
"""A required positional argument that is also accepted as ``--flag``.
|
||||
|
||||
Use it in place of :func:`typer.Argument` and pass the legacy flag
|
||||
name(s)::
|
||||
|
||||
task_id: t.Annotated[
|
||||
str,
|
||||
ArgumentOrKeyword("--uuid", envvar=envutils.ENV_TASK,
|
||||
help="UUID of task.")
|
||||
]
|
||||
|
||||
typer builds an ordinary required Argument from it (the ``--flag`` is wired
|
||||
up later by :func:`install`); the flag itself stays out of ``--help`` and
|
||||
bash completion, so the positional form is the documented one.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*kw_decls: str,
|
||||
help: str | None = None,
|
||||
envvar: str | list[str] | None = None,
|
||||
metavar: str | None = None
|
||||
) -> None:
|
||||
if metavar is None and kw_decls:
|
||||
# ``--uuid`` -> ``UUID``, nicer in usage than the derived name.
|
||||
metavar = kw_decls[0].lstrip("-").replace("-", "_").upper()
|
||||
super().__init__(default=..., help=help, envvar=envvar,
|
||||
metavar=metavar)
|
||||
self.kw_decls = kw_decls
|
||||
|
||||
|
||||
def _patch_param(
|
||||
param: "typer.core.TyperOption",
|
||||
kw_decls: t.Sequence[str]
|
||||
) -> None:
|
||||
"""Register ``kw_decls`` as an option feeding ``param``'s destination."""
|
||||
original_add_to_parser = param.add_to_parser
|
||||
# A list argument (``nargs == -1``) accepts repeated ``--flag`` values.
|
||||
action = "append" if param.nargs == -1 else "store"
|
||||
|
||||
def add_to_parser(
|
||||
parser: "typer._click.core._OptionParser",
|
||||
ctx: "typer._click.core.Context"
|
||||
) -> None:
|
||||
original_add_to_parser(parser, ctx)
|
||||
# Don't let the (now optional) positional overwrite a value that the
|
||||
# ``--flag`` option already stored under the shared destination.
|
||||
argument = parser._args[-1]
|
||||
original_process = argument.process
|
||||
|
||||
def process(value: t.Any, state: t.Any) -> None:
|
||||
if value in (None, ()) and param.name in state.opts:
|
||||
return
|
||||
original_process(value, state)
|
||||
argument.process = process # type: ignore[method-assign]
|
||||
|
||||
parser.add_option(param, list(kw_decls), param.name,
|
||||
action=action, nargs=1)
|
||||
|
||||
param.add_to_parser = add_to_parser # type: ignore[method-assign]
|
||||
|
||||
|
||||
def install(command: typer.core.TyperGroup | typer.core.TyperCommand) -> None:
|
||||
"""Wire up every :class:`ArgumentOrKeyword` in a built command tree."""
|
||||
for _path, leaf, params in cliutils.iter_commands(command):
|
||||
if leaf.callback is None:
|
||||
continue
|
||||
signature = inspect.signature(inspect.unwrap(leaf.callback))
|
||||
marks = {}
|
||||
for name, parameter in signature.parameters.items():
|
||||
for meta in getattr(parameter.annotation, "__metadata__", ()):
|
||||
if isinstance(meta, ArgumentOrKeyword):
|
||||
marks[name] = meta.kw_decls
|
||||
if not marks:
|
||||
continue
|
||||
for param in params:
|
||||
if param.name in marks:
|
||||
_patch_param(param, marks[param.name])
|
||||
@@ -0,0 +1,100 @@
|
||||
# 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.
|
||||
|
||||
import itertools
|
||||
|
||||
|
||||
_HEADER = r"""#!/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
|
||||
|
||||
"""
|
||||
|
||||
_FOOTER = r""" 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
|
||||
"""
|
||||
|
||||
def generate() -> str:
|
||||
"""Return the bash completion script for the current CLI."""
|
||||
import typer
|
||||
|
||||
from rally.cli import cliutils
|
||||
from rally.cli import main
|
||||
|
||||
# ``typer.main.get_command`` builds the resolved command tree; we read the
|
||||
# flags there rather than from ``registered_commands`` because typer stores
|
||||
# a declared flag on the raw ``typer.Option`` ambiguously -- an Annotated
|
||||
# positional flag lands in ``OptionInfo.default``, not ``param_decls`` --
|
||||
# so only the built command exposes the correct ``--flags``.
|
||||
command = typer.main.get_command(main.app)
|
||||
lines = []
|
||||
for path, _leaf, params in cliutils.iter_commands(command):
|
||||
if len(path) != 2:
|
||||
# top-level leaf command (e.g. ``version``) -- no OPTS entry
|
||||
continue
|
||||
category, name = path
|
||||
opts = " ".join(
|
||||
itertools.chain.from_iterable(
|
||||
(
|
||||
# only long ``--flags``; short aliases (``-n``) are valid
|
||||
# but kept out of completion
|
||||
name for name in p.opts
|
||||
if (name.startswith("--")
|
||||
and name not in ("--help", "--version"))
|
||||
)
|
||||
for p in params
|
||||
))
|
||||
|
||||
lines.append(f' OPTS["{category}_{name}"]="{opts}"\n')
|
||||
return _HEADER + "".join(sorted(lines)) + _FOOTER
|
||||
+56
-483
@@ -13,67 +13,72 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import argparse
|
||||
import inspect
|
||||
"""Shared helpers for the Rally CLI."""
|
||||
|
||||
import contextvars
|
||||
import functools
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import textwrap
|
||||
import typing as t
|
||||
import warnings
|
||||
|
||||
import jsonschema
|
||||
import prettytable
|
||||
import sqlalchemy.exc
|
||||
import typer.core
|
||||
|
||||
from rally import api
|
||||
from rally import exceptions
|
||||
from rally.common import cfg
|
||||
from rally.common import logging
|
||||
from rally.common.plugin import info
|
||||
from rally.utils import encodeutils
|
||||
|
||||
|
||||
CONF = cfg.CONF
|
||||
LOG = logging.getLogger(__name__)
|
||||
if t.TYPE_CHECKING:
|
||||
import typer._click.core
|
||||
|
||||
from rally.api import API
|
||||
|
||||
|
||||
# Some CLI-specific constants
|
||||
MARGIN = 3
|
||||
_api: "contextvars.ContextVar[API]" = contextvars.ContextVar("rally_api")
|
||||
|
||||
|
||||
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 set_api(api: "API") -> None:
|
||||
"""Stash the per-invocation Rally API handle for the commands to read."""
|
||||
_api.set(api)
|
||||
|
||||
|
||||
def validate_args(fn, *args, **kwargs):
|
||||
"""Check that the supplied args are sufficient for calling a function.
|
||||
def get_api() -> "API":
|
||||
"""Return the Rally API handle stashed by `set_api`."""
|
||||
return _api.get()
|
||||
|
||||
>>> 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
|
||||
def iter_commands(
|
||||
command: "typer._click.core.Command",
|
||||
) -> t.Iterator[
|
||||
tuple[
|
||||
tuple[str, ...],
|
||||
typer.core.TyperCommand,
|
||||
list[typer.core.TyperOption]
|
||||
]
|
||||
]:
|
||||
"""Walk a built typer command tree and yield every leaf command.
|
||||
|
||||
:param command: a resolved typer command, e.g. the result of
|
||||
`typer.main.get_command`. Groups are recursed into; for each leaf
|
||||
command the generator yields ``(path, leaf, params)`` where:
|
||||
|
||||
* ``path`` is the tuple of command names from the root down to the
|
||||
leaf (the root group itself contributes no name), so
|
||||
``rally task status`` yields
|
||||
``("task", "status")`` and a top-level ``rally version`` yields
|
||||
``("version",)``;
|
||||
* ``leaf`` is the `typer.core.TyperCommand` itself;
|
||||
* ``params`` is its option/argument list
|
||||
"""
|
||||
required_args = [
|
||||
p.name for p in inspect.signature(fn).parameters.values()
|
||||
if p.default == inspect.Parameter.empty
|
||||
and p.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD]
|
||||
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)
|
||||
if isinstance(command, typer.core.TyperGroup):
|
||||
for name, sub in command.commands.items():
|
||||
for path, leaf, params in iter_commands(sub):
|
||||
yield (name, *path), leaf, params
|
||||
else:
|
||||
leaf = t.cast(typer.core.TyperCommand, command)
|
||||
params = t.cast("list[typer.core.TyperOption]", leaf.params)
|
||||
yield (), leaf, params
|
||||
|
||||
|
||||
def print_list(objs, fields, formatters=None, sortby_index=0,
|
||||
@@ -300,67 +305,24 @@ def make_table_header(table_label, table_width,
|
||||
return "\n".join((border_line, label_line,))
|
||||
|
||||
|
||||
def make_header(text, size=80, symbol="-"):
|
||||
def make_header(text: str, size: int=80, symbol: str="-") -> str:
|
||||
"""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
|
||||
return f"{symbol * size}\n{text}\n{symbol * size}\n"
|
||||
|
||||
|
||||
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)
|
||||
"""Run the wrapped command with Python warnings silenced."""
|
||||
@functools.wraps(f)
|
||||
def wrapper(*args, **kwargs):
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore")
|
||||
return f(*args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
|
||||
def pretty_float_formatter(field, ndigits=None):
|
||||
@@ -379,392 +341,3 @@ def pretty_float_formatter(field, ndigits=None):
|
||||
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__)
|
||||
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, str):
|
||||
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:
|
||||
known_errors = (exceptions.InvalidTaskConfig, )
|
||||
if logging.is_debug() and not isinstance(e, known_errors):
|
||||
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))}
|
||||
|
||||
+76
-54
@@ -1,6 +1,3 @@
|
||||
# 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
|
||||
@@ -14,71 +11,96 @@
|
||||
# under the License.
|
||||
|
||||
import re
|
||||
import typing as t
|
||||
|
||||
import typer
|
||||
|
||||
from rally.cli import cliutils
|
||||
from rally.cli import envutils
|
||||
from rally.common import cfg
|
||||
from rally.common import db
|
||||
|
||||
|
||||
class DBCommands(object):
|
||||
"""CLI commands for DB management."""
|
||||
db_app = typer.Typer(
|
||||
name="db", no_args_is_help=False,
|
||||
help="Set of commands that allow you to manage Rally's database.")
|
||||
|
||||
def recreate(self, api):
|
||||
"""Drop and create Rally database.
|
||||
|
||||
This will delete all existing data.
|
||||
"""
|
||||
print("Recreating database: ", end="")
|
||||
self.show(api, True)
|
||||
db.schema.schema_cleanup()
|
||||
print("Database deleted successfully")
|
||||
def _print_connection(show_creds: bool) -> None:
|
||||
if show_creds:
|
||||
print(cfg.CONF.database.connection)
|
||||
else:
|
||||
print(re.sub("//[^@]*@", "//**:**@", cfg.CONF.database.connection))
|
||||
|
||||
|
||||
@db_app.command()
|
||||
def recreate() -> None:
|
||||
"""Drop and create Rally database.
|
||||
|
||||
This will delete all existing data.
|
||||
"""
|
||||
print("Recreating database: ", end="")
|
||||
_print_connection(True)
|
||||
db.schema.schema_cleanup()
|
||||
print("Database deleted successfully")
|
||||
db.schema.schema_create()
|
||||
print("Database created successfully")
|
||||
envutils.clear_env()
|
||||
|
||||
|
||||
@db_app.command()
|
||||
def create() -> None:
|
||||
"""Create Rally database."""
|
||||
print("Creating database: ", end="")
|
||||
_print_connection(True)
|
||||
db.schema.schema_create()
|
||||
print("Database created successfully")
|
||||
|
||||
|
||||
@db_app.command()
|
||||
def ensure() -> None:
|
||||
"""Create Rally database if it doesn't exist."""
|
||||
print("Ensuring database exists: ", end="")
|
||||
_print_connection(True)
|
||||
|
||||
if not db.schema.schema_revision():
|
||||
db.schema.schema_create()
|
||||
print("Database created successfully")
|
||||
envutils.clear_env()
|
||||
else:
|
||||
print("Database already exists, nothing to do")
|
||||
|
||||
def create(self, api):
|
||||
"""Create Rally database."""
|
||||
print("Creating database: ", end="")
|
||||
self.show(api, True)
|
||||
db.schema.schema_create()
|
||||
print("Database created successfully")
|
||||
|
||||
def ensure(self, api):
|
||||
"""Creates Rally database if it doesn't exists."""
|
||||
print("Ensuring database exists: ", end="")
|
||||
self.show(api, True)
|
||||
@db_app.command()
|
||||
def upgrade() -> None:
|
||||
"""Upgrade Rally database to the latest state."""
|
||||
print("Upgrading database: ", end="")
|
||||
_print_connection(True)
|
||||
|
||||
if not db.schema.schema_revision():
|
||||
db.schema.schema_create()
|
||||
print("Database created successfully")
|
||||
else:
|
||||
print("Database already exists, nothing to do")
|
||||
start_revision = db.schema.schema_revision()
|
||||
db.schema.schema_upgrade()
|
||||
current_revision = db.schema.schema_revision()
|
||||
if start_revision != current_revision:
|
||||
print("Database schema upgraded successfully "
|
||||
"from {start} to {end} revision."
|
||||
.format(start=start_revision, end=current_revision))
|
||||
else:
|
||||
print("Database is already up to date")
|
||||
|
||||
def upgrade(self, api):
|
||||
"""Upgrade Rally database to the latest state."""
|
||||
print("Upgrading database: ", end="")
|
||||
self.show(api, True)
|
||||
|
||||
start_revision = db.schema.schema_revision()
|
||||
db.schema.schema_upgrade()
|
||||
current_revision = db.schema.schema_revision()
|
||||
if start_revision != current_revision:
|
||||
print("Database schema upgraded successfully "
|
||||
"from {start} to {end} revision."
|
||||
.format(start=start_revision, end=current_revision))
|
||||
else:
|
||||
print("Database is already up to date")
|
||||
@db_app.command()
|
||||
def revision() -> None:
|
||||
"""Print current Rally database revision UUID."""
|
||||
print(db.schema.schema_revision())
|
||||
|
||||
def revision(self, api):
|
||||
"""Print current Rally database revision UUID."""
|
||||
print(db.schema.schema_revision())
|
||||
|
||||
@cliutils.args("--creds", action="store_true", dest="show_creds",
|
||||
help="Do not hide credentials from connection string")
|
||||
def show(self, api, show_creds=False):
|
||||
"""Show the connection string."""
|
||||
if not show_creds:
|
||||
print(re.sub("//[^@]*@", "//**:**@", cfg.CONF.database.connection))
|
||||
else:
|
||||
print(cfg.CONF.database.connection)
|
||||
@db_app.command()
|
||||
def show(
|
||||
creds: t.Annotated[
|
||||
bool,
|
||||
typer.Option(
|
||||
"--creds",
|
||||
help="Do not hide credentials from connection string"
|
||||
)
|
||||
] = False,
|
||||
) -> None:
|
||||
"""Show the connection string."""
|
||||
_print_connection(creds)
|
||||
|
||||
+359
-280
@@ -18,11 +18,15 @@
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import typing as t
|
||||
|
||||
import jsonschema
|
||||
import typer
|
||||
|
||||
from rally import exceptions
|
||||
from rally import plugins
|
||||
from rally.api import API
|
||||
from rally.cli import argutils
|
||||
from rally.cli import cliutils
|
||||
from rally.cli import envutils
|
||||
from rally.cli import yamlutils as yaml
|
||||
@@ -31,309 +35,384 @@ from rally.common import utils
|
||||
from rally.env import env_mgr
|
||||
|
||||
|
||||
class DeploymentCommands(object):
|
||||
"""Set of commands that allow you to manage deployments."""
|
||||
deployment_app = typer.Typer(
|
||||
name="deployment", no_args_is_help=False,
|
||||
help="Set of commands that allow you to manage deployments.")
|
||||
|
||||
@cliutils.args("--name", type=str, required=True,
|
||||
help="Name of the deployment.")
|
||||
@cliutils.args("--fromenv", action="store_true",
|
||||
help="Read environment variables instead of config file.")
|
||||
@cliutils.args("--filename", type=str, required=False, metavar="<path>",
|
||||
help="Path to the configuration file of the deployment.")
|
||||
@cliutils.args("--no-use", action="store_false", dest="do_use",
|
||||
help="Don't set new deployment as default for"
|
||||
" future operations.")
|
||||
@plugins.ensure_plugins_are_loaded
|
||||
def create(self, api, name, fromenv=False, filename=None, do_use=False):
|
||||
"""Create new deployment.
|
||||
|
||||
This command will create a new deployment record in rally
|
||||
database. In the case of ExistingCloud deployment engine, it
|
||||
will use the cloud represented in the configuration. If the
|
||||
cloud doesn't exist, Rally can deploy a new one for you with
|
||||
Devstack or Fuel. Different deployment engines exist for these
|
||||
cases (see `rally plugin list --plugin-base Engine` for
|
||||
more details).
|
||||
def _list_deployments(api: API,
|
||||
deployment_list: list | None = None) -> None:
|
||||
headers = ["uuid", "created_at", "name", "status", "active"]
|
||||
current_deployment = envutils.get_global("RALLY_DEPLOYMENT")
|
||||
deployment_list = deployment_list or api.deployment.list()
|
||||
|
||||
If you use the ExistingCloud deployment engine, you can pass
|
||||
the deployment config by environment variables with ``--fromenv``:
|
||||
table_rows = []
|
||||
if deployment_list:
|
||||
for dep in deployment_list:
|
||||
r = [str(dep[column]) for column in headers[:-1]]
|
||||
r.append("" if dep["uuid"] != current_deployment else "*")
|
||||
table_rows.append(utils.Struct(**dict(zip(headers, r))))
|
||||
cliutils.print_list(table_rows, headers,
|
||||
sortby_index=headers.index("created_at"))
|
||||
else:
|
||||
print("There are no deployments. To create a new deployment, use:"
|
||||
"\nrally deployment create")
|
||||
|
||||
OS_USERNAME
|
||||
OS_PASSWORD
|
||||
OS_AUTH_URL
|
||||
OS_TENANT_NAME or OS_PROJECT_NAME
|
||||
OS_ENDPOINT_TYPE or OS_INTERFACE
|
||||
OS_ENDPOINT
|
||||
OS_REGION_NAME
|
||||
OS_CACERT
|
||||
OS_INSECURE
|
||||
OS_IDENTITY_API_VERSION
|
||||
|
||||
All other deployment engines need more complex configuration
|
||||
data, so it should be stored in a configuration file.
|
||||
def _update_openrc_deployment_file(deployment: str, credential: dict) -> None:
|
||||
openrc_path = os.path.expanduser("~/.rally/openrc-%s" % deployment)
|
||||
with open(openrc_path, "w+") as env_file:
|
||||
env_file.write("export OS_AUTH_URL='%(auth_url)s'\n"
|
||||
"export OS_USERNAME='%(username)s'\n"
|
||||
"export OS_PASSWORD='%(password)s'\n"
|
||||
"export OS_TENANT_NAME='%(tenant_name)s'\n"
|
||||
"export OS_PROJECT_NAME='%(tenant_name)s'\n"
|
||||
% credential)
|
||||
if credential.get("region_name"):
|
||||
env_file.write("export OS_REGION_NAME='%s'\n" %
|
||||
credential["region_name"])
|
||||
if credential.get("endpoint_type"):
|
||||
env_file.write("export OS_ENDPOINT_TYPE='%sURL'\n" %
|
||||
credential["endpoint_type"])
|
||||
env_file.write("export OS_INTERFACE='%s'\n" %
|
||||
credential["endpoint_type"])
|
||||
if credential.get("endpoint"):
|
||||
env_file.write("export OS_ENDPOINT='%s'\n" %
|
||||
credential["endpoint"])
|
||||
if credential.get("https_cacert"):
|
||||
env_file.write("export OS_CACERT='%s'\n" %
|
||||
credential["https_cacert"])
|
||||
if credential.get("project_domain_name"):
|
||||
env_file.write("export OS_IDENTITY_API_VERSION=3\n"
|
||||
"export OS_USER_DOMAIN_NAME='%s'\n"
|
||||
"export OS_PROJECT_DOMAIN_NAME='%s'\n" %
|
||||
(credential["user_domain_name"],
|
||||
credential["project_domain_name"]))
|
||||
expanded_path = os.path.expanduser("~/.rally/openrc")
|
||||
if os.path.exists(expanded_path):
|
||||
os.remove(expanded_path)
|
||||
os.symlink(openrc_path, expanded_path)
|
||||
|
||||
You can use physical servers, LXC containers, KVM virtual
|
||||
machines or virtual machines in OpenStack for deploying the
|
||||
cloud. Except physical servers, Rally can create cluster nodes
|
||||
for you. Interaction with virtualization software, OpenStack
|
||||
cloud or physical servers is provided by server providers.
|
||||
"""
|
||||
|
||||
if fromenv:
|
||||
result = env_mgr.EnvManager.create_spec_from_sys_environ()
|
||||
config = result["spec"]
|
||||
if "existing@openstack" in config:
|
||||
# NOTE(andreykurilin): if we are here it means that
|
||||
# rally-openstack package is installed
|
||||
import rally_openstack
|
||||
if rally_openstack.__version_tuple__ <= (1, 4, 0):
|
||||
if ("https_key" in config["existing@openstack"]
|
||||
and config["existing@openstack"]["https_key"]):
|
||||
print("WARNING: OS_KEY is ignored due to old version "
|
||||
"of rally-openstack package.")
|
||||
# NOTE(andreykurilin): To support
|
||||
# rally-openstack<=1.4.0 we need to remove
|
||||
# https_key, since OpenStackCredentials object
|
||||
# doesn't support it.
|
||||
# Latest rally-openstack fixed this issue with
|
||||
# https://github.com/openstack/rally-openstack/commit/c7483386e6b59474c83e3ecd0c7ee0e77ff50c02
|
||||
config["existing@openstack"].pop("https_key")
|
||||
def _use(api: API, deployment: t.Any) -> int | None:
|
||||
# TODO(astudenov): make this method platform independent
|
||||
try:
|
||||
if not isinstance(deployment, dict):
|
||||
deployment = api.deployment.get(deployment=deployment)
|
||||
except exceptions.DBRecordNotFound:
|
||||
print("Deployment %s is not found." % deployment)
|
||||
return 1
|
||||
print("Using deployment: %s" % deployment["uuid"])
|
||||
|
||||
envutils.update_globals_file(envutils.ENV_DEPLOYMENT, deployment["uuid"])
|
||||
envutils.update_globals_file(envutils.ENV_ENV, deployment["uuid"])
|
||||
|
||||
if "openstack" in deployment["credentials"]:
|
||||
creds = deployment["credentials"]["openstack"][0]
|
||||
_update_openrc_deployment_file(
|
||||
deployment["uuid"], creds["admin"] or creds["users"][0])
|
||||
print("~/.rally/openrc was updated\n\nHINTS:\n"
|
||||
"\n* To use standard OpenStack clients, set up your env by "
|
||||
"running:\n\tsource ~/.rally/openrc\n"
|
||||
" OpenStack clients are now configured, e.g run:\n\t"
|
||||
"openstack image list")
|
||||
return None
|
||||
|
||||
|
||||
@deployment_app.command()
|
||||
@plugins.ensure_plugins_are_loaded
|
||||
def create(
|
||||
name: t.Annotated[
|
||||
str,
|
||||
typer.Option(
|
||||
help="Name of the deployment."
|
||||
)
|
||||
],
|
||||
fromenv: t.Annotated[
|
||||
bool,
|
||||
typer.Option(
|
||||
help="Read environment variables instead of config file."
|
||||
)
|
||||
] = False,
|
||||
filename: t.Annotated[
|
||||
str | None,
|
||||
typer.Option(
|
||||
help="Path to the configuration file of the deployment."
|
||||
)
|
||||
] = None,
|
||||
no_use: t.Annotated[
|
||||
bool,
|
||||
typer.Option(
|
||||
"--no-use",
|
||||
help="Don't set new deployment as default for future operations."
|
||||
)
|
||||
] = False,
|
||||
) -> None:
|
||||
"""Create new deployment.
|
||||
|
||||
This command will create a new deployment record in rally
|
||||
database. In the case of ExistingCloud deployment engine, it
|
||||
will use the cloud represented in the configuration. If the
|
||||
cloud doesn't exist, Rally can deploy a new one for you with
|
||||
Devstack or Fuel. Different deployment engines exist for these
|
||||
cases (see `rally plugin list --plugin-base Engine` for
|
||||
more details).
|
||||
|
||||
If you use the ExistingCloud deployment engine, you can pass
|
||||
the deployment config by environment variables with ``--fromenv``:
|
||||
|
||||
OS_USERNAME
|
||||
OS_PASSWORD
|
||||
OS_AUTH_URL
|
||||
OS_TENANT_NAME or OS_PROJECT_NAME
|
||||
OS_ENDPOINT_TYPE or OS_INTERFACE
|
||||
OS_ENDPOINT
|
||||
OS_REGION_NAME
|
||||
OS_CACERT
|
||||
OS_INSECURE
|
||||
OS_IDENTITY_API_VERSION
|
||||
|
||||
All other deployment engines need more complex configuration
|
||||
data, so it should be stored in a configuration file.
|
||||
|
||||
You can use physical servers, LXC containers, KVM virtual
|
||||
machines or virtual machines in OpenStack for deploying the
|
||||
cloud. Except physical servers, Rally can create cluster nodes
|
||||
for you. Interaction with virtualization software, OpenStack
|
||||
cloud or physical servers is provided by server providers.
|
||||
"""
|
||||
api = cliutils.get_api()
|
||||
if fromenv:
|
||||
result = env_mgr.EnvManager.create_spec_from_sys_environ()
|
||||
config = result["spec"]
|
||||
if "existing@openstack" in config:
|
||||
# NOTE(andreykurilin): if we are here it means that
|
||||
# rally-openstack package is installed
|
||||
import rally_openstack # type: ignore[import-not-found]
|
||||
if rally_openstack.__version_tuple__ <= (1, 4, 0):
|
||||
if ("https_key" in config["existing@openstack"]
|
||||
and config["existing@openstack"]["https_key"]):
|
||||
print("WARNING: OS_KEY is ignored due to old version "
|
||||
"of rally-openstack package.")
|
||||
config["existing@openstack"].pop("https_key")
|
||||
else:
|
||||
if not filename:
|
||||
config = {}
|
||||
else:
|
||||
if not filename:
|
||||
config = {}
|
||||
else:
|
||||
with open(os.path.expanduser(filename), "rb") as deploy_file:
|
||||
config = yaml.safe_load(deploy_file.read())
|
||||
|
||||
try:
|
||||
deployment = api.deployment.create(config=config, name=name)
|
||||
except jsonschema.ValidationError:
|
||||
print("Config schema validation error: %s." % sys.exc_info()[1])
|
||||
return 1
|
||||
except exceptions.DBRecordExists:
|
||||
print("Error: %s" % sys.exc_info()[1])
|
||||
return 1
|
||||
|
||||
self.list(api, deployment_list=[deployment])
|
||||
if do_use:
|
||||
self.use(api, deployment)
|
||||
|
||||
@cliutils.args("--filename", type=str, required=False, metavar="<path>",
|
||||
help="Path to the configuration file of the deployment.")
|
||||
@cliutils.args("--deployment", dest="deployment", type=str,
|
||||
metavar="<uuid>", required=False,
|
||||
help="UUID or name of the deployment.")
|
||||
@envutils.with_default_deployment()
|
||||
@plugins.ensure_plugins_are_loaded
|
||||
def recreate(self, api, deployment=None, filename=None):
|
||||
"""Destroy and create an existing deployment.
|
||||
|
||||
Unlike 'deployment destroy', the deployment database record
|
||||
will not be deleted, so the deployment UUID stays the same.
|
||||
"""
|
||||
config = None
|
||||
if filename:
|
||||
with open(filename, "rb") as deploy_file:
|
||||
with open(os.path.expanduser(filename), "rb") as deploy_file:
|
||||
config = yaml.safe_load(deploy_file.read())
|
||||
|
||||
api.deployment.recreate(deployment=deployment, config=config)
|
||||
try:
|
||||
deployment = api.deployment.create(config=config, name=name)
|
||||
except jsonschema.ValidationError:
|
||||
print("Config schema validation error: %s." % sys.exc_info()[1])
|
||||
raise typer.Exit(code=1)
|
||||
except exceptions.DBRecordExists:
|
||||
print("Error: %s" % sys.exc_info()[1])
|
||||
raise typer.Exit(code=1)
|
||||
|
||||
@cliutils.args("--deployment", dest="deployment", type=str,
|
||||
metavar="<uuid>", required=False,
|
||||
help="UUID or name of the deployment.")
|
||||
@envutils.with_default_deployment()
|
||||
@plugins.ensure_plugins_are_loaded
|
||||
def destroy(self, api, deployment=None):
|
||||
"""Destroy existing deployment.
|
||||
_list_deployments(api, deployment_list=[deployment])
|
||||
if not no_use:
|
||||
_use(api, deployment)
|
||||
|
||||
This will delete all containers, virtual machines, OpenStack
|
||||
instances or Fuel clusters created during Rally deployment
|
||||
creation. Also it will remove the deployment record from the
|
||||
Rally database.
|
||||
"""
|
||||
api.deployment.destroy(deployment=deployment)
|
||||
|
||||
def list(self, api, deployment_list=None):
|
||||
"""List existing deployments."""
|
||||
@deployment_app.command()
|
||||
@plugins.ensure_plugins_are_loaded
|
||||
def recreate(
|
||||
deployment: t.Annotated[
|
||||
str,
|
||||
argutils.ArgumentOrKeyword(
|
||||
"--deployment",
|
||||
help="UUID or name of the deployment.",
|
||||
envvar=envutils.ENV_ENV
|
||||
)
|
||||
],
|
||||
filename: t.Annotated[
|
||||
str | None,
|
||||
typer.Option(
|
||||
help="Path to the configuration file of the deployment."
|
||||
)
|
||||
] = None,
|
||||
) -> None:
|
||||
"""Destroy and create an existing deployment.
|
||||
|
||||
headers = ["uuid", "created_at", "name", "status", "active"]
|
||||
current_deployment = envutils.get_global("RALLY_DEPLOYMENT")
|
||||
deployment_list = deployment_list or api.deployment.list()
|
||||
Unlike 'deployment destroy', the deployment database record
|
||||
will not be deleted, so the deployment UUID stays the same.
|
||||
"""
|
||||
api = cliutils.get_api()
|
||||
config = None
|
||||
if filename:
|
||||
with open(filename, "rb") as deploy_file:
|
||||
config = yaml.safe_load(deploy_file.read())
|
||||
|
||||
table_rows = []
|
||||
if deployment_list:
|
||||
for t in deployment_list:
|
||||
r = [str(t[column]) for column in headers[:-1]]
|
||||
r.append("" if t["uuid"] != current_deployment else "*")
|
||||
table_rows.append(utils.Struct(**dict(zip(headers, r))))
|
||||
cliutils.print_list(table_rows, headers,
|
||||
sortby_index=headers.index("created_at"))
|
||||
api.deployment.recreate(deployment=deployment, config=config)
|
||||
|
||||
|
||||
@deployment_app.command()
|
||||
@plugins.ensure_plugins_are_loaded
|
||||
def destroy(
|
||||
deployment: t.Annotated[
|
||||
str,
|
||||
argutils.ArgumentOrKeyword(
|
||||
"--deployment",
|
||||
help="UUID or name of the deployment.",
|
||||
envvar=envutils.ENV_ENV
|
||||
)
|
||||
],
|
||||
) -> None:
|
||||
"""Destroy existing deployment.
|
||||
|
||||
This will delete all containers, virtual machines, OpenStack
|
||||
instances or Fuel clusters created during Rally deployment
|
||||
creation. Also it will remove the deployment record from the
|
||||
Rally database.
|
||||
"""
|
||||
api = cliutils.get_api()
|
||||
api.deployment.destroy(deployment=deployment)
|
||||
|
||||
|
||||
@deployment_app.command(name="list")
|
||||
@plugins.ensure_plugins_are_loaded
|
||||
def list_() -> None:
|
||||
"""List existing deployments."""
|
||||
_list_deployments(cliutils.get_api())
|
||||
|
||||
|
||||
@deployment_app.command()
|
||||
@cliutils.suppress_warnings
|
||||
def config(
|
||||
deployment: t.Annotated[
|
||||
str,
|
||||
argutils.ArgumentOrKeyword(
|
||||
"--deployment",
|
||||
help="UUID or name of the deployment.",
|
||||
envvar=envutils.ENV_ENV
|
||||
)
|
||||
],
|
||||
) -> None:
|
||||
"""Display configuration of the deployment.
|
||||
|
||||
Output is the configuration of the deployment in a
|
||||
pretty-printed JSON format.
|
||||
"""
|
||||
deploy = cliutils.get_api().deployment.get(deployment=deployment)
|
||||
result = deploy["config"]
|
||||
print(json.dumps(result, sort_keys=True, indent=4))
|
||||
|
||||
|
||||
@deployment_app.command()
|
||||
@plugins.ensure_plugins_are_loaded
|
||||
def show(
|
||||
deployment: t.Annotated[
|
||||
str,
|
||||
argutils.ArgumentOrKeyword(
|
||||
"--deployment",
|
||||
help="UUID or name of the deployment.",
|
||||
envvar=envutils.ENV_ENV
|
||||
)
|
||||
],
|
||||
) -> None:
|
||||
"""Show the credentials of the deployment."""
|
||||
# TODO(astudenov): make this method platform independent
|
||||
headers = ["auth_url", "username", "password", "tenant_name",
|
||||
"region_name", "endpoint_type"]
|
||||
table_rows = []
|
||||
|
||||
deployment = cliutils.get_api().deployment.get(deployment=deployment)
|
||||
|
||||
creds = deployment["credentials"]["openstack"][0]
|
||||
users = creds["users"]
|
||||
admin = creds["admin"]
|
||||
credentials = users + [admin] if admin else users
|
||||
for ep in credentials:
|
||||
data = ["***" if m == "password" else ep.get(m, "")
|
||||
for m in headers]
|
||||
table_rows.append(utils.Struct(**dict(zip(headers, data))))
|
||||
cliutils.print_list(table_rows, headers)
|
||||
|
||||
|
||||
@deployment_app.command()
|
||||
@plugins.ensure_plugins_are_loaded
|
||||
def check(
|
||||
deployment: t.Annotated[
|
||||
str,
|
||||
argutils.ArgumentOrKeyword(
|
||||
"--deployment",
|
||||
help="UUID or name of the deployment.",
|
||||
envvar=envutils.ENV_ENV
|
||||
)
|
||||
],
|
||||
) -> None:
|
||||
"""Check all credentials and list all available services."""
|
||||
|
||||
def is_field_there(lst: list, field: str) -> bool:
|
||||
return bool([item for item in lst if field in item])
|
||||
|
||||
def print_error(user_type: str, error: dict) -> None:
|
||||
print("Error while checking %s credentials:" % user_type)
|
||||
if logging.is_debug():
|
||||
print(error["trace"])
|
||||
else:
|
||||
print("There are no deployments. To create a new deployment, use:"
|
||||
"\nrally deployment create")
|
||||
print("\t%s: %s" % (error["etype"], error["msg"]))
|
||||
|
||||
@cliutils.args("--deployment", dest="deployment", type=str,
|
||||
metavar="<uuid>", required=False,
|
||||
help="UUID or name of the deployment.")
|
||||
@envutils.with_default_deployment()
|
||||
@cliutils.suppress_warnings
|
||||
def config(self, api, deployment=None):
|
||||
"""Display configuration of the deployment.
|
||||
exit_code = 0
|
||||
|
||||
Output is the configuration of the deployment in a
|
||||
pretty-printed JSON format.
|
||||
"""
|
||||
deploy = api.deployment.get(deployment=deployment)
|
||||
result = deploy["config"]
|
||||
print(json.dumps(result, sort_keys=True, indent=4))
|
||||
info = cliutils.get_api().deployment.check(deployment=deployment)
|
||||
for platform in info:
|
||||
for i, creds in enumerate(info[platform]):
|
||||
failed = False
|
||||
|
||||
@cliutils.args("--deployment", dest="deployment", type=str,
|
||||
metavar="<uuid>", required=False,
|
||||
help="UUID or name of the deployment.")
|
||||
@envutils.with_default_deployment()
|
||||
@plugins.ensure_plugins_are_loaded
|
||||
def show(self, api, deployment=None):
|
||||
"""Show the credentials of the deployment."""
|
||||
# TODO(astudenov): make this method platform independent
|
||||
n = "" if len(info[platform]) == 1 else " #%s" % (i + 1)
|
||||
header = "Platform %s%s:" % (platform, n)
|
||||
print(cliutils.make_header(header))
|
||||
if "admin_error" in creds:
|
||||
print_error("admin", creds["admin_error"])
|
||||
failed = True
|
||||
if "user_error" in creds:
|
||||
print_error("users", creds["user_error"])
|
||||
failed = True
|
||||
|
||||
headers = ["auth_url", "username", "password", "tenant_name",
|
||||
"region_name", "endpoint_type"]
|
||||
table_rows = []
|
||||
|
||||
deployment = api.deployment.get(deployment=deployment)
|
||||
|
||||
creds = deployment["credentials"]["openstack"][0]
|
||||
users = creds["users"]
|
||||
admin = creds["admin"]
|
||||
credentials = users + [admin] if admin else users
|
||||
for ep in credentials:
|
||||
data = ["***" if m == "password" else ep.get(m, "")
|
||||
for m in headers]
|
||||
table_rows.append(utils.Struct(**dict(zip(headers, data))))
|
||||
cliutils.print_list(table_rows, headers)
|
||||
|
||||
@cliutils.args("--deployment", dest="deployment", type=str,
|
||||
metavar="<uuid>", required=False,
|
||||
help="UUID or name of the deployment.")
|
||||
@envutils.with_default_deployment()
|
||||
@plugins.ensure_plugins_are_loaded
|
||||
def check(self, api, deployment=None):
|
||||
"""Check all credentials and list all available services."""
|
||||
|
||||
def is_field_there(lst, field):
|
||||
return bool([item for item in lst if field in item])
|
||||
|
||||
def print_error(user_type, error):
|
||||
print("Error while checking %s credentials:" % user_type)
|
||||
if logging.is_debug():
|
||||
print(error["trace"])
|
||||
else:
|
||||
print("\t%s: %s" % (error["etype"], error["msg"]))
|
||||
|
||||
exit_code = 0
|
||||
|
||||
info = api.deployment.check(deployment=deployment)
|
||||
for platform in info:
|
||||
for i, creds in enumerate(info[platform]):
|
||||
failed = False
|
||||
|
||||
n = "" if len(info[platform]) == 1 else " #%s" % (i + 1)
|
||||
header = "Platform %s%s:" % (platform, n)
|
||||
print(cliutils.make_header(header))
|
||||
if "admin_error" in creds:
|
||||
print_error("admin", creds["admin_error"])
|
||||
failed = True
|
||||
if "user_error" in creds:
|
||||
print_error("users", creds["user_error"])
|
||||
failed = True
|
||||
|
||||
if not failed:
|
||||
print("Available services:")
|
||||
formatters = {
|
||||
"Service": lambda x: x.get("name"),
|
||||
"Service Type": lambda x: x.get("type"),
|
||||
"Status": lambda x: x.get("status", "Available")}
|
||||
if (is_field_there(creds["services"], "type")
|
||||
and is_field_there(creds["services"], "name")):
|
||||
headers = ["Service", "Service Type", "Status"]
|
||||
else:
|
||||
headers = ["Service", "Status"]
|
||||
|
||||
if is_field_there(creds["services"], "version"):
|
||||
headers.append("Version")
|
||||
|
||||
if is_field_there(creds["services"], "description"):
|
||||
headers.append("Description")
|
||||
|
||||
cliutils.print_list(creds["services"], headers,
|
||||
normalize_field_names=True,
|
||||
formatters=formatters)
|
||||
if not failed:
|
||||
print("Available services:")
|
||||
formatters = {
|
||||
"Service": lambda x: x.get("name"),
|
||||
"Service Type": lambda x: x.get("type"),
|
||||
"Status": lambda x: x.get("status", "Available")}
|
||||
if (is_field_there(creds["services"], "type")
|
||||
and is_field_there(creds["services"], "name")):
|
||||
headers = ["Service", "Service Type", "Status"]
|
||||
else:
|
||||
exit_code = 1
|
||||
print("\n")
|
||||
headers = ["Service", "Status"]
|
||||
|
||||
return exit_code
|
||||
if is_field_there(creds["services"], "version"):
|
||||
headers.append("Version")
|
||||
|
||||
def _update_openrc_deployment_file(self, deployment, credential):
|
||||
openrc_path = os.path.expanduser("~/.rally/openrc-%s" % deployment)
|
||||
with open(openrc_path, "w+") as env_file:
|
||||
env_file.write("export OS_AUTH_URL='%(auth_url)s'\n"
|
||||
"export OS_USERNAME='%(username)s'\n"
|
||||
"export OS_PASSWORD='%(password)s'\n"
|
||||
"export OS_TENANT_NAME='%(tenant_name)s'\n"
|
||||
"export OS_PROJECT_NAME='%(tenant_name)s'\n"
|
||||
% credential)
|
||||
if credential.get("region_name"):
|
||||
env_file.write("export OS_REGION_NAME='%s'\n" %
|
||||
credential["region_name"])
|
||||
if credential.get("endpoint_type"):
|
||||
env_file.write("export OS_ENDPOINT_TYPE='%sURL'\n" %
|
||||
credential["endpoint_type"])
|
||||
env_file.write("export OS_INTERFACE='%s'\n" %
|
||||
credential["endpoint_type"])
|
||||
if credential.get("endpoint"):
|
||||
env_file.write("export OS_ENDPOINT='%s'\n" %
|
||||
credential["endpoint"])
|
||||
if credential.get("https_cacert"):
|
||||
env_file.write("export OS_CACERT='%s'\n" %
|
||||
credential["https_cacert"])
|
||||
if credential.get("project_domain_name"):
|
||||
env_file.write("export OS_IDENTITY_API_VERSION=3\n"
|
||||
"export OS_USER_DOMAIN_NAME='%s'\n"
|
||||
"export OS_PROJECT_DOMAIN_NAME='%s'\n" %
|
||||
(credential["user_domain_name"],
|
||||
credential["project_domain_name"]))
|
||||
expanded_path = os.path.expanduser("~/.rally/openrc")
|
||||
if os.path.exists(expanded_path):
|
||||
os.remove(expanded_path)
|
||||
os.symlink(openrc_path, expanded_path)
|
||||
if is_field_there(creds["services"], "description"):
|
||||
headers.append("Description")
|
||||
|
||||
@cliutils.args("--deployment", dest="deployment", type=str,
|
||||
metavar="<uuid>", required=False,
|
||||
help="UUID or name of a deployment.")
|
||||
@plugins.ensure_plugins_are_loaded
|
||||
def use(self, api, deployment):
|
||||
"""Set active deployment."""
|
||||
# TODO(astudenov): make this method platform independent
|
||||
try:
|
||||
if not isinstance(deployment, dict):
|
||||
deployment = api.deployment.get(deployment=deployment)
|
||||
except exceptions.DBRecordNotFound:
|
||||
print("Deployment %s is not found." % deployment)
|
||||
return 1
|
||||
print("Using deployment: %s" % deployment["uuid"])
|
||||
cliutils.print_list(creds["services"], headers,
|
||||
normalize_field_names=True,
|
||||
formatters=formatters)
|
||||
else:
|
||||
exit_code = 1
|
||||
print("\n")
|
||||
|
||||
envutils.update_globals_file(envutils.ENV_DEPLOYMENT,
|
||||
deployment["uuid"])
|
||||
envutils.update_globals_file(envutils.ENV_ENV,
|
||||
deployment["uuid"])
|
||||
if exit_code:
|
||||
raise typer.Exit(code=exit_code)
|
||||
|
||||
if "openstack" in deployment["credentials"]:
|
||||
creds = deployment["credentials"]["openstack"][0]
|
||||
self._update_openrc_deployment_file(
|
||||
deployment["uuid"], creds["admin"] or creds["users"][0])
|
||||
print("~/.rally/openrc was updated\n\nHINTS:\n"
|
||||
"\n* To use standard OpenStack clients, set up your env by "
|
||||
"running:\n\tsource ~/.rally/openrc\n"
|
||||
" OpenStack clients are now configured, e.g run:\n\t"
|
||||
"openstack image list")
|
||||
|
||||
@deployment_app.command()
|
||||
@plugins.ensure_plugins_are_loaded
|
||||
def use(
|
||||
deployment: t.Annotated[
|
||||
str,
|
||||
argutils.ArgumentOrKeyword(
|
||||
"--deployment",
|
||||
help="UUID or name of a deployment."
|
||||
)
|
||||
],
|
||||
) -> None:
|
||||
"""Set active deployment."""
|
||||
rc = _use(cliutils.get_api(), deployment)
|
||||
if rc:
|
||||
raise typer.Exit(code=rc)
|
||||
|
||||
+446
-291
@@ -1,5 +1,3 @@
|
||||
# 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
|
||||
@@ -15,323 +13,480 @@
|
||||
import json
|
||||
import os
|
||||
import traceback
|
||||
import typing as t
|
||||
|
||||
import prettytable
|
||||
import typer
|
||||
|
||||
from rally import exceptions
|
||||
from rally.cli import argutils
|
||||
from rally.cli import cliutils
|
||||
from rally.cli import envutils
|
||||
from rally.cli import yamlutils as yaml
|
||||
from rally.env import env_mgr
|
||||
|
||||
|
||||
env_app = typer.Typer(
|
||||
name="env", no_args_is_help=False,
|
||||
help="Set of commands that allow you to manage envs.")
|
||||
|
||||
YES = u":-)"
|
||||
NO = u":-("
|
||||
|
||||
MSG_NO_ENVS = ("There are no environments. To create a new environment, "
|
||||
"use command bellow to create one:\nrally env create")
|
||||
|
||||
def _print(msg, silent=False):
|
||||
|
||||
def _print(msg: object, silent: bool = False) -> None:
|
||||
if not silent:
|
||||
print(msg)
|
||||
|
||||
|
||||
# TODO(boris-42): Wrap all methods to catch EnvManager Exceptions
|
||||
class EnvCommands(object):
|
||||
"""Set of commands that allow you to manage envs."""
|
||||
|
||||
@cliutils.args("--name", "-n", type=str, required=True,
|
||||
help="Name of the env.")
|
||||
@cliutils.args("--description", "-d", type=str, required=False,
|
||||
help="Env description")
|
||||
@cliutils.args("--extras", "-e", type=str, required=False,
|
||||
help="JSON or YAML dict with custom non validate info.")
|
||||
@cliutils.args("--from-sysenv", action="store_true", dest="from_sysenv",
|
||||
help="Iterate over all available platforms and check system"
|
||||
" environment for credentials.")
|
||||
@cliutils.args("--spec", "-s", type=str, required=False,
|
||||
metavar="<path>", help="Path to env spec.")
|
||||
@cliutils.args("--json", action="store_true", dest="to_json",
|
||||
help="Format output as JSON.")
|
||||
@cliutils.args("--no-use", action="store_false", dest="do_use",
|
||||
help="Don't set new env as default for future operations.")
|
||||
def create(self, api, name, description=None, extras=None,
|
||||
spec=None, from_sysenv=False, to_json=False, do_use=True):
|
||||
"""Create new environment."""
|
||||
|
||||
if spec is not None and from_sysenv:
|
||||
print("Arguments '--spec' and '--from-sysenv' cannot be used "
|
||||
"together, use only one of them.")
|
||||
return 1
|
||||
spec = spec or {}
|
||||
if spec:
|
||||
with open(os.path.expanduser(spec), "rb") as f:
|
||||
spec = yaml.safe_load(f.read())
|
||||
if extras:
|
||||
extras = yaml.safe_load(extras)
|
||||
|
||||
if from_sysenv:
|
||||
result = env_mgr.EnvManager.create_spec_from_sys_environ()
|
||||
spec = result["spec"]
|
||||
_print("Your system environment includes specifications of "
|
||||
"%s platform(s)." % len(spec), to_json)
|
||||
_print("Discovery information:", to_json)
|
||||
for p_name, p_result in result["discovery_details"].items():
|
||||
_print("\t - %s : %s." % (p_name, p_result["message"]),
|
||||
to_json)
|
||||
|
||||
if "traceback" in p_result:
|
||||
_print("".join(p_result["traceback"]), to_json)
|
||||
try:
|
||||
env = env_mgr.EnvManager.create(
|
||||
name, spec, description=description, extras=extras)
|
||||
except exceptions.ManagerInvalidSpec as e:
|
||||
_print("Env spec has wrong format:", to_json)
|
||||
_print(json.dumps(e.kwargs["spec"], indent=2), to_json)
|
||||
for err in e.kwargs["errors"]:
|
||||
_print(err, to_json)
|
||||
return 1
|
||||
except Exception:
|
||||
_print("Something went wrong during env creation:", to_json)
|
||||
_print(traceback.print_exc(), to_json)
|
||||
return 1
|
||||
|
||||
if do_use:
|
||||
self._use(env.uuid, to_json)
|
||||
self._show(env.data, to_json=to_json, only_spec=False)
|
||||
return 0
|
||||
|
||||
@cliutils.args("--json", action="store_true", dest="to_json",
|
||||
help="Format output as JSON.")
|
||||
@cliutils.args("--env", dest="env", type=str,
|
||||
metavar="<uuid>", required=False,
|
||||
help="UUID or name of the env.")
|
||||
@envutils.with_default_env()
|
||||
def cleanup(self, api, env=None, to_json=False):
|
||||
"""Perform disaster cleanup for specified environment.
|
||||
|
||||
Cases when Rally can leave undeleted resources after performing
|
||||
workload:
|
||||
|
||||
- Rally execution was interrupted and cleanup was not performed
|
||||
- The environment or a particular platform became unreachable which
|
||||
fail Rally execution of cleanup
|
||||
"""
|
||||
env = env_mgr.EnvManager.get(env)
|
||||
_print("Cleaning up resources for %s" % env, to_json)
|
||||
result = env.cleanup()
|
||||
|
||||
if to_json:
|
||||
print(json.dumps(result, indent=2))
|
||||
return int(any([p["errors"] for p in result.values()]))
|
||||
|
||||
print("Cleaning is finished. See the results bellow.")
|
||||
|
||||
return_code = 0
|
||||
for platform in sorted(result):
|
||||
cleanup_info = result[platform]
|
||||
print("\nInformation for %s platform." % platform)
|
||||
print("=" * 80)
|
||||
print("Status: %s" % cleanup_info["message"])
|
||||
for key in ("discovered", "deleted", "failed"):
|
||||
print("Total %s: %s" % (key, cleanup_info[key]))
|
||||
if cleanup_info["errors"]:
|
||||
return_code = 1
|
||||
errors = "\t- ".join(e["message"]
|
||||
for e in cleanup_info["errors"])
|
||||
print("Errors:\n\t- %s" % errors)
|
||||
|
||||
return return_code
|
||||
|
||||
@cliutils.args("--env", dest="env", type=str,
|
||||
metavar="<uuid>", required=False,
|
||||
help="UUID or name of the env.")
|
||||
@cliutils.args("--skip-cleanup", action="store_true", dest="skip_cleanup",
|
||||
help="Do not perform platforms cleanup before destroy.")
|
||||
@cliutils.args("--json", action="store_true", dest="to_json",
|
||||
help="Format output as JSON.")
|
||||
@cliutils.args("--detailed", action="store_true", dest="detailed",
|
||||
help="Show detailed information.")
|
||||
@envutils.with_default_env()
|
||||
def destroy(self, api, env=None, skip_cleanup=False, to_json=False,
|
||||
detailed=False):
|
||||
"""Destroy existing environment."""
|
||||
env = env_mgr.EnvManager.get(env)
|
||||
_print("Destroying %s" % env, to_json)
|
||||
result = env.destroy(skip_cleanup)
|
||||
return_code = int(result["destroy_info"]["skipped"])
|
||||
|
||||
if result["destroy_info"]["skipped"]:
|
||||
_print("%s Failed to destroy env %s: %s"
|
||||
% (NO, env, result["destroy_info"]["message"]), to_json)
|
||||
else:
|
||||
_print("%s Successfully destroyed env %s" % (YES, env), to_json)
|
||||
|
||||
if detailed or to_json:
|
||||
print(json.dumps(result, indent=2))
|
||||
|
||||
return return_code
|
||||
|
||||
@cliutils.args("--env", dest="env", type=str,
|
||||
metavar="<uuid>", required=False,
|
||||
help="UUID or name of the env.")
|
||||
@cliutils.args("--force", action="store_true", dest="force",
|
||||
help="Delete DB records even if env is not destroyed.")
|
||||
@envutils.with_default_env()
|
||||
def delete(self, api, env=None, force=False):
|
||||
"""Deletes all records related to the environment from db."""
|
||||
env_mgr.EnvManager.get(env).delete(force=force)
|
||||
# TODO(boris-42): clear env variables if default one is deleted
|
||||
|
||||
MSG_NO_ENVS = ("There are no environments. To create a new environment, "
|
||||
"use command bellow to create one:\nrally env create")
|
||||
|
||||
@cliutils.args("--json", action="store_true", dest="to_json",
|
||||
help="Format output as JSON.")
|
||||
@cliutils.suppress_warnings
|
||||
def list(self, api, to_json=False):
|
||||
"""List existing environments."""
|
||||
envs = env_mgr.EnvManager.list()
|
||||
if to_json:
|
||||
print(json.dumps([env.cached_data for env in envs], indent=2))
|
||||
elif not envs:
|
||||
print(self.MSG_NO_ENVS)
|
||||
else:
|
||||
cur_env = envutils.get_global(envutils.ENV_ENV)
|
||||
table = prettytable.PrettyTable()
|
||||
fields = ["uuid", "name", "status", "created_at", "description"]
|
||||
table.field_names = fields + ["default"]
|
||||
for env in envs:
|
||||
row = [env.cached_data[f] for f in fields]
|
||||
row.append(cur_env == env.cached_data["uuid"] and "*" or "")
|
||||
table.add_row(row)
|
||||
table.sortby = "created_at"
|
||||
table.reversesort = True
|
||||
table.align = "l"
|
||||
print(table.get_string())
|
||||
|
||||
def _show(self, env_data, to_json, only_spec):
|
||||
if only_spec:
|
||||
print(json.dumps(env_data["spec"], indent=2))
|
||||
elif to_json:
|
||||
print(json.dumps(env_data, indent=2))
|
||||
else:
|
||||
table = prettytable.PrettyTable()
|
||||
table.header = False
|
||||
for k in ["uuid", "name", "status",
|
||||
"created_at", "updated_at", "description"]:
|
||||
table.add_row([k, env_data[k]])
|
||||
|
||||
table.add_row(["extras", json.dumps(env_data["extras"], indent=2)])
|
||||
for p, data in env_data["platforms"].items():
|
||||
table.add_row(["platform: %s" % p,
|
||||
json.dumps(data["platform_data"], indent=2)])
|
||||
table.align = "l"
|
||||
print(table.get_string())
|
||||
|
||||
@cliutils.args("--env", dest="env", type=str,
|
||||
metavar="<uuid>", required=False,
|
||||
help="UUID or name of the env.")
|
||||
@cliutils.args("--json", action="store_true", dest="to_json",
|
||||
help="Format output as JSON.")
|
||||
@cliutils.args("--only-spec", action="store_true", dest="only_spec",
|
||||
help="Print only a spec for the environment.")
|
||||
@cliutils.suppress_warnings
|
||||
@envutils.with_default_env()
|
||||
def show(self, api, env=None, to_json=False, only_spec=False):
|
||||
"""Show base information about the environment record."""
|
||||
env_data = env_mgr.EnvManager.get(env).data
|
||||
self._show(env_data, to_json=to_json, only_spec=only_spec)
|
||||
|
||||
@cliutils.args("--env", dest="env", type=str,
|
||||
metavar="<uuid>", required=False,
|
||||
help="UUID or name of the env.")
|
||||
@cliutils.args("--json", action="store_true", dest="to_json",
|
||||
help="Format output as JSON.")
|
||||
@envutils.with_default_env()
|
||||
def info(self, api, env=None, to_json=False):
|
||||
"""Retrieve and show environment information."""
|
||||
env = env_mgr.EnvManager.get(env)
|
||||
env_info = env.get_info()
|
||||
return_code = int(any(v.get("error") for v in env_info.values()))
|
||||
|
||||
if to_json:
|
||||
print(json.dumps(env_info, indent=2))
|
||||
return return_code
|
||||
|
||||
def _show(env_data: dict, to_json: bool, only_spec: bool) -> None:
|
||||
if only_spec:
|
||||
print(json.dumps(env_data["spec"], indent=2))
|
||||
elif to_json:
|
||||
print(json.dumps(env_data, indent=2))
|
||||
else:
|
||||
table = prettytable.PrettyTable()
|
||||
table.field_names = ["platform", "info", "error"]
|
||||
for platform, data in env_info.items():
|
||||
table.add_row([
|
||||
platform, json.dumps(data["info"], indent=2),
|
||||
data.get("error") or ""
|
||||
])
|
||||
table.header = False
|
||||
for k in ["uuid", "name", "status",
|
||||
"created_at", "updated_at", "description"]:
|
||||
table.add_row([k, env_data[k]])
|
||||
|
||||
table.add_row(["extras", json.dumps(env_data["extras"], indent=2)])
|
||||
for p, data in env_data["platforms"].items():
|
||||
table.add_row(["platform: %s" % p,
|
||||
json.dumps(data["platform_data"], indent=2)])
|
||||
table.align = "l"
|
||||
print(env)
|
||||
print(table.get_string())
|
||||
return return_code
|
||||
|
||||
@cliutils.args("--env", dest="env", type=str,
|
||||
metavar="<uuid>", required=False,
|
||||
help="UUID or name of the env.")
|
||||
@cliutils.args("--json", action="store_true", dest="to_json",
|
||||
help="Format output as JSON.")
|
||||
@cliutils.args("--detailed", action="store_true", dest="detailed",
|
||||
help="Show detailed information.")
|
||||
@envutils.with_default_env()
|
||||
def check(self, api, env=None, to_json=False, detailed=False):
|
||||
"""Check availability of all platforms in environment."""
|
||||
env = env_mgr.EnvManager.get(env)
|
||||
data = env.check_health()
|
||||
available = all(x["available"] for x in data.values())
|
||||
|
||||
if to_json:
|
||||
print(json.dumps(data, indent=2))
|
||||
return not available
|
||||
def _use(env_uuid: str, to_json: bool) -> None:
|
||||
_print("Using environment: %s" % env_uuid, to_json)
|
||||
envutils.update_globals_file(envutils.ENV_ENV, env_uuid)
|
||||
|
||||
def _format_raw(plugin_name, el):
|
||||
return [
|
||||
el["available"] and YES or NO,
|
||||
plugin_name.split("@")[1], el["message"], plugin_name
|
||||
]
|
||||
|
||||
@env_app.command()
|
||||
def create(
|
||||
name: t.Annotated[
|
||||
str,
|
||||
typer.Option(
|
||||
"--name", "-n",
|
||||
help="Name of the env."
|
||||
)
|
||||
],
|
||||
description: t.Annotated[
|
||||
str | None,
|
||||
typer.Option(
|
||||
"--description", "-d",
|
||||
help="Env description"
|
||||
)
|
||||
] = None,
|
||||
extras: t.Annotated[
|
||||
str | None,
|
||||
typer.Option(
|
||||
"--extras", "-e",
|
||||
help="JSON or YAML dict with custom non validate info."
|
||||
)
|
||||
] = None,
|
||||
spec: t.Annotated[
|
||||
str | None,
|
||||
typer.Option(
|
||||
"--spec", "-s",
|
||||
help="Path to env spec."
|
||||
)
|
||||
] = None,
|
||||
from_sysenv: t.Annotated[
|
||||
bool,
|
||||
typer.Option(
|
||||
"--from-sysenv",
|
||||
help="Iterate over all available platforms and check system "
|
||||
"environment for credentials."
|
||||
)
|
||||
] = False,
|
||||
to_json: t.Annotated[
|
||||
bool,
|
||||
typer.Option(
|
||||
"--json",
|
||||
help="Format output as JSON."
|
||||
)
|
||||
] = False,
|
||||
no_use: t.Annotated[
|
||||
bool,
|
||||
typer.Option(
|
||||
"--no-use",
|
||||
help="Don't set new env as default for future operations."
|
||||
)
|
||||
] = False,
|
||||
) -> None:
|
||||
"""Create new environment."""
|
||||
if spec is not None and from_sysenv:
|
||||
print("Arguments '--spec' and '--from-sysenv' cannot be used "
|
||||
"together, use only one of them.")
|
||||
raise typer.Exit(code=1)
|
||||
spec_obj: t.Any = spec or {}
|
||||
if spec:
|
||||
with open(os.path.expanduser(spec), "rb") as f:
|
||||
spec_obj = yaml.safe_load(f.read())
|
||||
extras_obj = yaml.safe_load(extras) if extras else None
|
||||
|
||||
if from_sysenv:
|
||||
result = env_mgr.EnvManager.create_spec_from_sys_environ()
|
||||
spec_obj = result["spec"]
|
||||
_print("Your system environment includes specifications of "
|
||||
"%s platform(s)." % len(spec_obj), to_json)
|
||||
_print("Discovery information:", to_json)
|
||||
for p_name, p_result in result["discovery_details"].items():
|
||||
_print("\t - %s : %s." % (p_name, p_result["message"]), to_json)
|
||||
|
||||
if "traceback" in p_result:
|
||||
_print("".join(p_result["traceback"]), to_json)
|
||||
try:
|
||||
env = env_mgr.EnvManager.create(
|
||||
name, spec_obj, description=description, extras=extras_obj)
|
||||
except exceptions.ManagerInvalidSpec as e:
|
||||
_print("Env spec has wrong format:", to_json)
|
||||
_print(json.dumps(e.kwargs["spec"], indent=2), to_json)
|
||||
for err in e.kwargs["errors"]:
|
||||
_print(err, to_json)
|
||||
raise typer.Exit(code=1)
|
||||
except Exception:
|
||||
_print("Something went wrong during env creation:", to_json)
|
||||
_print(traceback.format_exc(), to_json)
|
||||
raise typer.Exit(code=1)
|
||||
|
||||
if not no_use:
|
||||
_use(env.uuid, to_json)
|
||||
_show(env.data, to_json=to_json, only_spec=False)
|
||||
|
||||
|
||||
@env_app.command()
|
||||
def cleanup(
|
||||
env: t.Annotated[
|
||||
str,
|
||||
argutils.ArgumentOrKeyword(
|
||||
"--env",
|
||||
envvar=envutils.ENV_ENV,
|
||||
help="UUID or name of the env."
|
||||
)
|
||||
],
|
||||
to_json: t.Annotated[
|
||||
bool,
|
||||
typer.Option(
|
||||
"--json",
|
||||
help="Format output as JSON."
|
||||
)
|
||||
] = False,
|
||||
) -> None:
|
||||
"""Perform disaster cleanup for specified environment.
|
||||
|
||||
Cases when Rally can leave undeleted resources after performing
|
||||
workload:
|
||||
|
||||
- Rally execution was interrupted and cleanup was not performed
|
||||
- The environment or a particular platform became unreachable which
|
||||
fail Rally execution of cleanup
|
||||
"""
|
||||
env_obj = env_mgr.EnvManager.get(env)
|
||||
_print("Cleaning up resources for %s" % env_obj, to_json)
|
||||
result = env_obj.cleanup()
|
||||
|
||||
if to_json:
|
||||
print(json.dumps(result, indent=2))
|
||||
if any(p["errors"] for p in result.values()):
|
||||
raise typer.Exit(code=1)
|
||||
return
|
||||
|
||||
print("Cleaning is finished. See the results bellow.")
|
||||
|
||||
return_code = 0
|
||||
for platform in sorted(result):
|
||||
cleanup_info = result[platform]
|
||||
print("\nInformation for %s platform." % platform)
|
||||
print("=" * 80)
|
||||
print("Status: %s" % cleanup_info["message"])
|
||||
for key in ("discovered", "deleted", "failed"):
|
||||
print("Total %s: %s" % (key, cleanup_info[key]))
|
||||
if cleanup_info["errors"]:
|
||||
return_code = 1
|
||||
errors = "\t- ".join(e["message"]
|
||||
for e in cleanup_info["errors"])
|
||||
print("Errors:\n\t- %s" % errors)
|
||||
|
||||
if return_code:
|
||||
raise typer.Exit(code=1)
|
||||
|
||||
|
||||
@env_app.command()
|
||||
def destroy(
|
||||
env: t.Annotated[
|
||||
str,
|
||||
argutils.ArgumentOrKeyword(
|
||||
"--env",
|
||||
envvar=envutils.ENV_ENV,
|
||||
help="UUID or name of the env."
|
||||
)
|
||||
],
|
||||
skip_cleanup: t.Annotated[
|
||||
bool,
|
||||
typer.Option(
|
||||
"--skip-cleanup",
|
||||
help="Do not perform platforms cleanup before destroy."
|
||||
)
|
||||
] = False,
|
||||
to_json: t.Annotated[
|
||||
bool,
|
||||
typer.Option(
|
||||
"--json",
|
||||
help="Format output as JSON."
|
||||
)
|
||||
] = False,
|
||||
detailed: t.Annotated[
|
||||
bool,
|
||||
typer.Option(
|
||||
"--detailed",
|
||||
help="Show detailed information."
|
||||
)
|
||||
] = False,
|
||||
) -> None:
|
||||
"""Destroy existing environment."""
|
||||
env_obj = env_mgr.EnvManager.get(env)
|
||||
_print("Destroying %s" % env_obj, to_json)
|
||||
result = env_obj.destroy(skip_cleanup)
|
||||
return_code = int(result["destroy_info"]["skipped"])
|
||||
|
||||
if result["destroy_info"]["skipped"]:
|
||||
_print("%s Failed to destroy env %s: %s"
|
||||
% (NO, env_obj, result["destroy_info"]["message"]), to_json)
|
||||
else:
|
||||
_print("%s Successfully destroyed env %s" % (YES, env_obj), to_json)
|
||||
|
||||
if detailed or to_json:
|
||||
print(json.dumps(result, indent=2))
|
||||
|
||||
if return_code:
|
||||
raise typer.Exit(code=return_code)
|
||||
|
||||
|
||||
@env_app.command()
|
||||
def delete(
|
||||
env: t.Annotated[
|
||||
str,
|
||||
argutils.ArgumentOrKeyword(
|
||||
"--env",
|
||||
envvar=envutils.ENV_ENV,
|
||||
help="UUID or name of the env."
|
||||
)
|
||||
],
|
||||
force: t.Annotated[
|
||||
bool,
|
||||
typer.Option(
|
||||
"--force",
|
||||
help="Delete DB records even if env is not destroyed."
|
||||
)
|
||||
] = False,
|
||||
) -> None:
|
||||
"""Delete all records related to the environment from db."""
|
||||
env_mgr.EnvManager.get(env).delete(force=force)
|
||||
|
||||
|
||||
@env_app.command(name="list")
|
||||
@cliutils.suppress_warnings
|
||||
def list_(
|
||||
to_json: t.Annotated[
|
||||
bool,
|
||||
typer.Option(
|
||||
"--json",
|
||||
help="Format output as JSON."
|
||||
)
|
||||
] = False,
|
||||
) -> None:
|
||||
"""List existing environments."""
|
||||
envs = env_mgr.EnvManager.list()
|
||||
if to_json:
|
||||
print(json.dumps([env.cached_data for env in envs], indent=2))
|
||||
elif not envs:
|
||||
print(MSG_NO_ENVS)
|
||||
else:
|
||||
cur_env = envutils.get_global(envutils.ENV_ENV)
|
||||
table = prettytable.PrettyTable()
|
||||
if detailed:
|
||||
table.field_names = ["Available", "Platform", "Message", "Plugin"]
|
||||
for plugin_name, r in data.items():
|
||||
table.add_row(_format_raw(plugin_name, r))
|
||||
else:
|
||||
table.field_names = ["Available", "Platform", "Message"]
|
||||
for plugin_name, r in data.items():
|
||||
table.add_row(_format_raw(plugin_name, r)[:3])
|
||||
|
||||
fields = ["uuid", "name", "status", "created_at", "description"]
|
||||
table.field_names = fields + ["default"]
|
||||
for env in envs:
|
||||
row = [env.cached_data[f] for f in fields]
|
||||
row.append(cur_env == env.cached_data["uuid"] and "*" or "")
|
||||
table.add_row(row)
|
||||
table.sortby = "created_at"
|
||||
table.reversesort = True
|
||||
table.align = "l"
|
||||
table.align["available"] = "c"
|
||||
table.sortby = "Platform"
|
||||
print("%s %s" % (env, available and YES or NO))
|
||||
print(table.get_string())
|
||||
if not available and detailed:
|
||||
for name, p_data in data.items():
|
||||
if p_data["available"]:
|
||||
continue
|
||||
print("-" * 4)
|
||||
print("Plugin %s raised exception:" % name)
|
||||
print("".join(p_data["traceback"]))
|
||||
|
||||
return not available
|
||||
|
||||
@cliutils.args("--env", dest="env", type=str,
|
||||
metavar="<uuid>", required=False,
|
||||
help="UUID or name of a env.")
|
||||
@cliutils.args("--json", action="store_true", dest="to_json",
|
||||
help="Format output as JSON.")
|
||||
def use(self, api, env, to_json=False):
|
||||
"""Set default environment."""
|
||||
try:
|
||||
env = env_mgr.EnvManager.get(env)
|
||||
except exceptions.DBRecordNotFound:
|
||||
_print("Can't use non existing environment %s." % env, to_json)
|
||||
return 1
|
||||
self._use(env.uuid, to_json)
|
||||
@env_app.command()
|
||||
@cliutils.suppress_warnings
|
||||
def show(
|
||||
env: t.Annotated[
|
||||
str,
|
||||
argutils.ArgumentOrKeyword(
|
||||
"--env",
|
||||
envvar=envutils.ENV_ENV,
|
||||
help="UUID or name of the env."
|
||||
)
|
||||
],
|
||||
to_json: t.Annotated[
|
||||
bool,
|
||||
typer.Option(
|
||||
"--json",
|
||||
help="Format output as JSON."
|
||||
)
|
||||
] = False,
|
||||
only_spec: t.Annotated[
|
||||
bool,
|
||||
typer.Option(
|
||||
"--only-spec",
|
||||
help="Print only a spec for the environment."
|
||||
)
|
||||
] = False,
|
||||
) -> None:
|
||||
"""Show base information about the environment record."""
|
||||
env_data = env_mgr.EnvManager.get(env).data
|
||||
_show(env_data, to_json=to_json, only_spec=only_spec)
|
||||
|
||||
def _use(self, env_uuid, to_json):
|
||||
_print("Using environment: %s" % env_uuid, to_json)
|
||||
envutils.update_globals_file(envutils.ENV_ENV, env_uuid)
|
||||
|
||||
@env_app.command()
|
||||
def info(
|
||||
env: t.Annotated[
|
||||
str,
|
||||
argutils.ArgumentOrKeyword(
|
||||
"--env",
|
||||
envvar=envutils.ENV_ENV,
|
||||
help="UUID or name of the env."
|
||||
)
|
||||
],
|
||||
to_json: t.Annotated[
|
||||
bool,
|
||||
typer.Option(
|
||||
"--json",
|
||||
help="Format output as JSON."
|
||||
)
|
||||
] = False,
|
||||
) -> None:
|
||||
"""Retrieve and show environment information."""
|
||||
env_obj = env_mgr.EnvManager.get(env)
|
||||
env_info = env_obj.get_info()
|
||||
return_code = int(any(v.get("error") for v in env_info.values()))
|
||||
|
||||
if to_json:
|
||||
print(json.dumps(env_info, indent=2))
|
||||
if return_code:
|
||||
raise typer.Exit(code=return_code)
|
||||
return
|
||||
|
||||
table = prettytable.PrettyTable()
|
||||
table.field_names = ["platform", "info", "error"]
|
||||
for platform, data in env_info.items():
|
||||
table.add_row([
|
||||
platform, json.dumps(data["info"], indent=2),
|
||||
data.get("error") or ""
|
||||
])
|
||||
table.align = "l"
|
||||
print(env_obj)
|
||||
print(table.get_string())
|
||||
if return_code:
|
||||
raise typer.Exit(code=return_code)
|
||||
|
||||
|
||||
@env_app.command()
|
||||
def check(
|
||||
env: t.Annotated[
|
||||
str,
|
||||
argutils.ArgumentOrKeyword(
|
||||
"--env",
|
||||
envvar=envutils.ENV_ENV,
|
||||
help="UUID or name of the env."
|
||||
)
|
||||
],
|
||||
to_json: t.Annotated[
|
||||
bool,
|
||||
typer.Option(
|
||||
"--json",
|
||||
help="Format output as JSON."
|
||||
)
|
||||
] = False,
|
||||
detailed: t.Annotated[
|
||||
bool,
|
||||
typer.Option(
|
||||
"--detailed",
|
||||
help="Show detailed information."
|
||||
)
|
||||
] = False,
|
||||
) -> None:
|
||||
"""Check availability of all platforms in environment."""
|
||||
env_obj = env_mgr.EnvManager.get(env)
|
||||
data = env_obj.check_health()
|
||||
available = all(x["available"] for x in data.values())
|
||||
|
||||
if to_json:
|
||||
print(json.dumps(data, indent=2))
|
||||
if not available:
|
||||
raise typer.Exit(code=1)
|
||||
return
|
||||
|
||||
def _format_raw(plugin_name: str, el: dict) -> list:
|
||||
return [
|
||||
el["available"] and YES or NO,
|
||||
plugin_name.split("@")[1], el["message"], plugin_name
|
||||
]
|
||||
|
||||
table = prettytable.PrettyTable()
|
||||
if detailed:
|
||||
table.field_names = ["Available", "Platform", "Message", "Plugin"]
|
||||
for plugin_name, r in data.items():
|
||||
table.add_row(_format_raw(plugin_name, r))
|
||||
else:
|
||||
table.field_names = ["Available", "Platform", "Message"]
|
||||
for plugin_name, r in data.items():
|
||||
table.add_row(_format_raw(plugin_name, r)[:3])
|
||||
|
||||
table.align = "l"
|
||||
table.align["available"] = "c"
|
||||
table.sortby = "Platform"
|
||||
print("%s %s" % (env_obj, available and YES or NO))
|
||||
print(table.get_string())
|
||||
if not available and detailed:
|
||||
for name, p_data in data.items():
|
||||
if p_data["available"]:
|
||||
continue
|
||||
print("-" * 4)
|
||||
print("Plugin %s raised exception:" % name)
|
||||
print("".join(p_data["traceback"]))
|
||||
|
||||
if not available:
|
||||
raise typer.Exit(code=1)
|
||||
|
||||
|
||||
@env_app.command()
|
||||
def use(
|
||||
env: t.Annotated[
|
||||
str,
|
||||
argutils.ArgumentOrKeyword(
|
||||
"--env",
|
||||
help="UUID or name of the env."
|
||||
)
|
||||
],
|
||||
to_json: t.Annotated[
|
||||
bool,
|
||||
typer.Option(
|
||||
"--json",
|
||||
help="Format output as JSON."
|
||||
)
|
||||
] = False,
|
||||
) -> None:
|
||||
"""Set default environment."""
|
||||
try:
|
||||
env_obj = env_mgr.EnvManager.get(env)
|
||||
except exceptions.DBRecordNotFound:
|
||||
_print("Can't use non existing environment %s." % env, to_json)
|
||||
raise typer.Exit(code=1)
|
||||
_use(env_obj.uuid, to_json)
|
||||
|
||||
+109
-84
@@ -13,100 +13,125 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import typing as t
|
||||
|
||||
import typer
|
||||
|
||||
from rally import exceptions
|
||||
from rally import plugins
|
||||
from rally.cli import argutils
|
||||
from rally.cli import cliutils
|
||||
from rally.common import utils
|
||||
from rally.common.plugin import plugin
|
||||
|
||||
|
||||
class PluginCommands(object):
|
||||
"""Set of commands that allow you to manage Rally plugins."""
|
||||
plugin_app = typer.Typer(
|
||||
name="plugin", no_args_is_help=False,
|
||||
help="Set of commands that allow you to manage Rally plugins.")
|
||||
|
||||
@staticmethod
|
||||
def _print_plugins_list(plugin_list):
|
||||
formatters = {
|
||||
"Name": lambda p: p.get_name(),
|
||||
"Platform": lambda p: p.get_platform(),
|
||||
"Title": lambda p: p.get_info()["title"],
|
||||
"Plugin base": lambda p: p._get_base().__name__
|
||||
}
|
||||
|
||||
cliutils.print_list(plugin_list, formatters=formatters,
|
||||
normalize_field_names=True,
|
||||
fields=["Plugin base", "Name", "Platform",
|
||||
"Title"])
|
||||
def _print_plugins_list(plugin_list: list) -> None:
|
||||
formatters = {
|
||||
"Name": lambda p: p.get_name(),
|
||||
"Platform": lambda p: p.get_platform(),
|
||||
"Title": lambda p: p.get_info()["title"],
|
||||
"Plugin base": lambda p: p._get_base().__name__
|
||||
}
|
||||
|
||||
@cliutils.args("--name", dest="name", type=str,
|
||||
help="Plugin name.")
|
||||
@cliutils.args("--platform", dest="platform", type=str,
|
||||
help="Plugin platform.")
|
||||
@plugins.ensure_plugins_are_loaded
|
||||
def show(self, api, name, platform=None):
|
||||
"""Show detailed information about a Rally plugin."""
|
||||
cliutils.print_list(plugin_list, formatters=formatters,
|
||||
normalize_field_names=True,
|
||||
fields=["Plugin base", "Name", "Platform", "Title"])
|
||||
|
||||
|
||||
@plugin_app.command()
|
||||
@plugins.ensure_plugins_are_loaded
|
||||
def show(
|
||||
name: t.Annotated[
|
||||
str,
|
||||
argutils.ArgumentOrKeyword(
|
||||
"--name",
|
||||
help="Plugin name."
|
||||
)
|
||||
],
|
||||
platform: t.Annotated[
|
||||
str | None,
|
||||
typer.Option(
|
||||
help="Plugin platform."
|
||||
)
|
||||
] = None,
|
||||
) -> None:
|
||||
"""Show detailed information about a Rally plugin."""
|
||||
name_lw = name.lower()
|
||||
all_plugins = plugin.Plugin.get_all(platform=platform)
|
||||
found = [p for p in all_plugins if name_lw in p.get_name().lower()]
|
||||
exact_match = [p for p in found if name_lw == p.get_name().lower()]
|
||||
|
||||
if not found:
|
||||
if platform:
|
||||
print("Plugin %(name)s@%(platform)s not found"
|
||||
% {"name": name, "platform": platform})
|
||||
else:
|
||||
print("Plugin %s not found at any platform" % name)
|
||||
raise typer.Exit(code=exceptions.PluginNotFound.error_code)
|
||||
|
||||
elif len(found) == 1 or exact_match:
|
||||
plugin_ = found[0] if len(found) == 1 else exact_match[0]
|
||||
plugin_info = plugin_.get_info()
|
||||
print(cliutils.make_header(plugin_info["title"]))
|
||||
print("NAME\n\t%s" % plugin_info["name"])
|
||||
print("PLATFORM\n\t%s" % plugin_info["platform"])
|
||||
print("MODULE\n\t%s" % plugin_info["module"])
|
||||
if plugin_info["description"]:
|
||||
print("DESCRIPTION\n\t", end="")
|
||||
print("\n\t".join(plugin_info["description"].split("\n")))
|
||||
if plugin_info["parameters"]:
|
||||
print("PARAMETERS")
|
||||
rows = [utils.Struct(name=p["name"], description=p["doc"])
|
||||
for p in plugin_info["parameters"]]
|
||||
cliutils.print_list(rows, fields=["name", "description"],
|
||||
sortby_index=None)
|
||||
else:
|
||||
print("Multiple plugins found:")
|
||||
_print_plugins_list(found)
|
||||
raise typer.Exit(code=exceptions.MultiplePluginsFound.error_code)
|
||||
|
||||
|
||||
@plugin_app.command(name="list")
|
||||
@plugins.ensure_plugins_are_loaded
|
||||
def list_(
|
||||
name: t.Annotated[
|
||||
str | None,
|
||||
typer.Argument(
|
||||
help="List only plugins that match the given name."
|
||||
)
|
||||
] = None,
|
||||
platform: t.Annotated[
|
||||
str | None,
|
||||
typer.Option(
|
||||
help="List only plugins that are in the specified platform."
|
||||
)
|
||||
] = None,
|
||||
base_cls: t.Annotated[
|
||||
str | None,
|
||||
typer.Option(
|
||||
"--plugin-base",
|
||||
help="Plugin base class."
|
||||
)
|
||||
] = None,
|
||||
) -> None:
|
||||
"""List all Rally plugins that match name and platform."""
|
||||
all_plugins = plugin.Plugin.get_all(platform=platform)
|
||||
matched = all_plugins
|
||||
if name:
|
||||
name_lw = name.lower()
|
||||
all_plugins = plugin.Plugin.get_all(platform=platform)
|
||||
found = [p for p in all_plugins if name_lw in p.get_name().lower()]
|
||||
exact_match = [p for p in found if name_lw == p.get_name().lower()]
|
||||
matched = [p for p in all_plugins if name_lw in p.get_name().lower()]
|
||||
|
||||
if not found:
|
||||
if platform:
|
||||
print(
|
||||
"Plugin %(name)s@%(platform)s not found"
|
||||
% {"name": name, "platform": platform}
|
||||
)
|
||||
else:
|
||||
print("Plugin %s not found at any platform" % name)
|
||||
return exceptions.PluginNotFound.error_code
|
||||
if base_cls:
|
||||
matched = [p for p in matched if p._get_base().__name__ == base_cls]
|
||||
|
||||
elif len(found) == 1 or exact_match:
|
||||
plugin_ = found[0] if len(found) == 1 else exact_match[0]
|
||||
plugin_info = plugin_.get_info()
|
||||
print(cliutils.make_header(plugin_info["title"]))
|
||||
print("NAME\n\t%s" % plugin_info["name"])
|
||||
print("PLATFORM\n\t%s" % plugin_info["platform"])
|
||||
print("MODULE\n\t%s" % plugin_info["module"])
|
||||
if plugin_info["description"]:
|
||||
print("DESCRIPTION\n\t", end="")
|
||||
print("\n\t".join(plugin_info["description"].split("\n")))
|
||||
if plugin_info["parameters"]:
|
||||
print("PARAMETERS")
|
||||
rows = [utils.Struct(name=p["name"],
|
||||
description=p["doc"])
|
||||
for p in plugin_info["parameters"]]
|
||||
cliutils.print_list(rows, fields=["name", "description"],
|
||||
sortby_index=None)
|
||||
else:
|
||||
print("Multiple plugins found:")
|
||||
self._print_plugins_list(found)
|
||||
return exceptions.MultiplePluginsFound.error_code
|
||||
|
||||
@cliutils.args(
|
||||
"--name", dest="name", type=str,
|
||||
help="List only plugins that match the given name.")
|
||||
@cliutils.args(
|
||||
"--platform", dest="platform", type=str,
|
||||
help="List only plugins that are in the specified platform.")
|
||||
@cliutils.args(
|
||||
"--plugin-base", dest="base_cls", type=str,
|
||||
help="Plugin base class.")
|
||||
@plugins.ensure_plugins_are_loaded
|
||||
def list(self, api, name=None, platform=None, base_cls=None):
|
||||
"""List all Rally plugins that match name and platform."""
|
||||
all_plugins = plugin.Plugin.get_all(platform=platform)
|
||||
matched = all_plugins
|
||||
if name:
|
||||
name_lw = name.lower()
|
||||
matched = [p for p in all_plugins
|
||||
if name_lw in p.get_name().lower()]
|
||||
|
||||
if base_cls:
|
||||
matched = [p for p in matched
|
||||
if p._get_base().__name__ == base_cls]
|
||||
|
||||
if not all_plugins:
|
||||
print("Platform %s not found" % platform)
|
||||
elif not matched:
|
||||
print("Plugin %s not found" % name)
|
||||
else:
|
||||
self._print_plugins_list(matched)
|
||||
if not all_plugins:
|
||||
print("Platform %s not found" % platform)
|
||||
elif not matched:
|
||||
print("Plugin %s not found" % name)
|
||||
else:
|
||||
_print_plugins_list(matched)
|
||||
|
||||
+997
-721
File diff suppressed because it is too large
Load Diff
+1089
-766
File diff suppressed because it is too large
Load Diff
+15
-70
@@ -13,8 +13,6 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import functools
|
||||
import inspect
|
||||
import os
|
||||
|
||||
from rally import exceptions
|
||||
@@ -26,9 +24,7 @@ ENV_DEPLOYMENT = "RALLY_DEPLOYMENT"
|
||||
ENV_TASK = "RALLY_TASK"
|
||||
ENV_VERIFIER = "RALLY_VERIFIER"
|
||||
ENV_VERIFICATION = "RALLY_VERIFICATION"
|
||||
ENVVARS = [ENV_ENV, ENV_DEPLOYMENT, ENV_TASK, ENV_VERIFIER, ENV_VERIFICATION]
|
||||
|
||||
MSG_MISSING_ARG = "Missing argument: --%(arg_name)s"
|
||||
ENVVARS = (ENV_ENV, ENV_DEPLOYMENT, ENV_TASK, ENV_VERIFIER, ENV_VERIFICATION)
|
||||
|
||||
|
||||
def _read_env_file(path, except_env=None):
|
||||
@@ -45,8 +41,7 @@ def _read_env_file(path, except_env=None):
|
||||
with open(path, "r") as env_file:
|
||||
content = env_file.readlines()
|
||||
for line in content:
|
||||
if except_env is None or not line.startswith("%s=" %
|
||||
except_env):
|
||||
if except_env is None or not line.startswith(f"{except_env}="):
|
||||
output.append(line)
|
||||
return output
|
||||
|
||||
@@ -96,7 +91,7 @@ def update_globals_file(key, value):
|
||||
if not os.path.exists(dir):
|
||||
os.makedirs(dir)
|
||||
expanded_path = os.path.join(dir, "globals")
|
||||
_update_env_file(expanded_path, key, "%s\n" % value)
|
||||
_update_env_file(expanded_path, key, f"{value}\n")
|
||||
|
||||
|
||||
def clear_global(global_key):
|
||||
@@ -117,71 +112,21 @@ def get_global(global_key, do_raise=False):
|
||||
_load_env_file(os.path.expanduser(PATH_GLOBALS))
|
||||
value = os.environ.get(global_key)
|
||||
if not value and do_raise:
|
||||
raise exceptions.InvalidArgumentsException("%s env is missing"
|
||||
% global_key)
|
||||
raise exceptions.InvalidArgumentsException(
|
||||
f"{global_key} env is missing"
|
||||
)
|
||||
return value
|
||||
|
||||
|
||||
def default_from_global(arg_name, env_name,
|
||||
cli_arg_name,
|
||||
message=MSG_MISSING_ARG):
|
||||
def wrapper(func):
|
||||
def load_globals():
|
||||
"""Load persisted Rally globals (``~/.rally/globals``) into os.environ."""
|
||||
|
||||
@functools.wraps(func)
|
||||
def inner(*args, **kwargs):
|
||||
params = list(inspect.signature(func).parameters)
|
||||
id_arg_index = params.index(arg_name)
|
||||
for line in _read_env_file(os.path.expanduser(PATH_GLOBALS)):
|
||||
key, _, value = line.partition("=")
|
||||
os.environ.setdefault(key, value.rstrip())
|
||||
|
||||
args = list(args)
|
||||
if ((len(args) <= id_arg_index or args[id_arg_index] is None)
|
||||
and arg_name not in kwargs):
|
||||
kwargs[arg_name] = get_global(env_name)
|
||||
if not kwargs[arg_name]:
|
||||
print(message % {"arg_name": cli_arg_name})
|
||||
return 1
|
||||
return func(*args, **kwargs)
|
||||
return inner
|
||||
return wrapper
|
||||
|
||||
|
||||
def with_default_env():
|
||||
# NOTE(boris-42): This allows smooth transition from deployment to env
|
||||
# set ENV_ENV from ENV_DEPLOYMENT if ENV is not presented
|
||||
# This should be removed with rally env command
|
||||
if not get_global(ENV_ENV):
|
||||
deployment = get_global(ENV_DEPLOYMENT)
|
||||
if deployment:
|
||||
os.environ[ENV_ENV] = deployment
|
||||
|
||||
return default_from_global(
|
||||
"env", ENV_ENV, "env",
|
||||
message="There is no default env. To set it use command:\n"
|
||||
"\trally env use <env_uuid>|<env_name>\n"
|
||||
"or pass uuid or name to your command using --%(arg_name)s")
|
||||
|
||||
|
||||
def with_default_deployment(cli_arg_name="uuid"):
|
||||
# NOTE(boris-42): This allows smooth transition from deployment to env
|
||||
# set ENV_ENV from ENV_DEPLOYMENT and use ENV_ENV
|
||||
# This should be removed with rally env command
|
||||
if not get_global(ENV_ENV):
|
||||
deployment = get_global(ENV_DEPLOYMENT)
|
||||
if deployment:
|
||||
os.environ[ENV_ENV] = deployment
|
||||
|
||||
return default_from_global(
|
||||
"deployment", ENV_ENV, cli_arg_name,
|
||||
message="There is no default deployment.\n"
|
||||
"\tPlease use command:\n"
|
||||
"\trally deployment use <deployment_uuid>|<deployment_name>\n"
|
||||
"or pass uuid of deployment to the --%(arg_name)s "
|
||||
"argument of this command")
|
||||
|
||||
|
||||
def with_default_verifier_id(cli_arg_name="id"):
|
||||
return default_from_global("verifier_id", ENV_VERIFIER, cli_arg_name)
|
||||
|
||||
|
||||
with_default_task_id = default_from_global("task_id", ENV_TASK, "uuid")
|
||||
with_default_verification_uuid = default_from_global("verification_uuid",
|
||||
ENV_VERIFICATION, "uuid")
|
||||
# set ENV_ENV from ENV_DEPLOYMENT and use ENV_ENV
|
||||
# This should be removed with rally env command
|
||||
if not os.environ.get(ENV_ENV) and os.environ.get(ENV_DEPLOYMENT):
|
||||
os.environ[ENV_ENV] = os.environ[ENV_DEPLOYMENT]
|
||||
|
||||
+210
-18
@@ -15,30 +15,222 @@
|
||||
|
||||
"""CLI interface for Rally."""
|
||||
|
||||
import inspect
|
||||
import re
|
||||
import sys
|
||||
import typing as t
|
||||
|
||||
import jsonschema
|
||||
import sqlalchemy.exc
|
||||
import typer
|
||||
|
||||
from rally import api as rally_api
|
||||
from rally import exceptions
|
||||
from rally.cli import argutils
|
||||
from rally.cli import cliutils
|
||||
from rally.cli.commands import db
|
||||
from rally.cli.commands import deployment
|
||||
from rally.cli.commands import env
|
||||
from rally.cli.commands import plugin
|
||||
from rally.cli.commands import task
|
||||
from rally.cli.commands import verify
|
||||
from rally.cli import envutils
|
||||
from rally.cli.commands.db import db_app
|
||||
from rally.cli.commands.deployment import deployment_app
|
||||
from rally.cli.commands.env import env_app
|
||||
from rally.cli.commands.plugin import plugin_app
|
||||
from rally.cli.commands.task import task_app
|
||||
from rally.cli.commands.verify import verify_app
|
||||
from rally.common import cfg
|
||||
from rally.common import logging
|
||||
|
||||
|
||||
categories = {
|
||||
"db": db.DBCommands,
|
||||
"env": env.EnvCommands,
|
||||
"deployment": deployment.DeploymentCommands,
|
||||
"plugin": plugin.PluginCommands,
|
||||
"task": task.TaskCommands,
|
||||
"verify": verify.VerifyCommands
|
||||
}
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def main():
|
||||
return cliutils.run(sys.argv, categories)
|
||||
if t.TYPE_CHECKING:
|
||||
import typer.core
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
# ``no_args_is_help=False`` -> a bare ``rally`` / ``rally task`` errors with
|
||||
# "Missing command." (exit 2) instead of printing help.
|
||||
app = typer.Typer(name="rally", help="Rally command-line interface.",
|
||||
no_args_is_help=False, add_completion=True)
|
||||
|
||||
app.add_typer(db_app, name="db")
|
||||
app.add_typer(deployment_app, name="deployment")
|
||||
app.add_typer(env_app, name="env")
|
||||
app.add_typer(plugin_app, name="plugin")
|
||||
app.add_typer(task_app, name="task")
|
||||
app.add_typer(verify_app, name="verify")
|
||||
|
||||
|
||||
@app.command(name="version")
|
||||
def print_version() -> None:
|
||||
"""Print the Rally version."""
|
||||
from rally.common import version
|
||||
|
||||
lines = ["Rally version: %s" % version.__version__]
|
||||
packages = version.plugins_versions()
|
||||
if packages:
|
||||
lines.append("\nInstalled Plugins:")
|
||||
lines.extend("\t%s: %s" % p for p in sorted(packages.items()))
|
||||
print("\n".join(lines))
|
||||
|
||||
|
||||
def _version_callback(value: bool) -> None:
|
||||
if value:
|
||||
print_version()
|
||||
raise typer.Exit()
|
||||
|
||||
|
||||
def _expand_signature(
|
||||
build_params: t.Callable[[], list[inspect.Parameter]]
|
||||
) -> t.Callable[[t.Callable[..., t.Any]], t.Callable[..., t.Any]]:
|
||||
"""Replace a function's ``**kwargs`` with dynamically-generated parameters.
|
||||
|
||||
typer reads the callback signature via :func:`inspect.signature`, so the
|
||||
fixed options can be written as an ordinary signature while the variable
|
||||
``**kwargs`` tail is swapped for whatever ``build_params`` yields (here the
|
||||
``oslo.log`` CLI options generated from oslo's own ``Opt`` objects, so
|
||||
``--log-file`` etc. stay in sync). The generated options arrive as keyword
|
||||
arguments -- i.e. in the real ``**kwargs`` at call time.
|
||||
"""
|
||||
def decorator(func: t.Callable[..., t.Any]) -> t.Callable[..., t.Any]:
|
||||
signature = inspect.signature(func)
|
||||
fixed = [p for p in signature.parameters.values()
|
||||
if p.kind is not inspect.Parameter.VAR_KEYWORD]
|
||||
func.__signature__ = signature.replace( # type: ignore[attr-defined]
|
||||
parameters=fixed + list(build_params()))
|
||||
return func
|
||||
return decorator
|
||||
|
||||
|
||||
@app.callback()
|
||||
@_expand_signature(logging.build_cli_params)
|
||||
def bootstrap(
|
||||
ctx: typer.Context,
|
||||
config_file: t.Annotated[
|
||||
list[str] | None,
|
||||
typer.Option(
|
||||
help="Path to a config file. Repeatable; later files take "
|
||||
"precedence."
|
||||
)
|
||||
] = None,
|
||||
config_dir: t.Annotated[
|
||||
list[str] | None,
|
||||
typer.Option(
|
||||
help="Path to a config directory. Repeatable."
|
||||
)
|
||||
] = None,
|
||||
plugin_paths: t.Annotated[
|
||||
list[str] | None,
|
||||
typer.Option(
|
||||
envvar="RALLY_PLUGIN_PATHS",
|
||||
help="Additional custom plugin locations."
|
||||
)
|
||||
] = None,
|
||||
version: t.Annotated[
|
||||
bool,
|
||||
typer.Option(
|
||||
"--version",
|
||||
callback=_version_callback,
|
||||
is_eager=True,
|
||||
help="Print the Rally version and exit."
|
||||
)
|
||||
] = False,
|
||||
**kwargs: t.Any,
|
||||
) -> None:
|
||||
"""Build the API and expose it to the sub-commands."""
|
||||
if any(opt in sys.argv[1:] for opt in ctx.help_option_names):
|
||||
return
|
||||
envutils.load_globals()
|
||||
config_args: list[str] = []
|
||||
for path in config_file or []:
|
||||
config_args += ["--config-file", path]
|
||||
for path in config_dir or []:
|
||||
config_args += ["--config-dir", path]
|
||||
config_args += logging.to_oslo_argv(kwargs)
|
||||
|
||||
paths = None
|
||||
if plugin_paths:
|
||||
paths = [p for item in plugin_paths
|
||||
for p in item.split(",") if p]
|
||||
|
||||
# do not run database check on commands that does not need that
|
||||
skip_db_check = ctx.invoked_subcommand in ("db", "plugin", "version")
|
||||
try:
|
||||
api = rally_api.API(config_args=config_args, plugin_paths=paths,
|
||||
skip_db_check=skip_db_check)
|
||||
except exceptions.RallyException as e:
|
||||
print(e)
|
||||
raise typer.Exit(code=2)
|
||||
cliutils.set_api(api)
|
||||
|
||||
|
||||
def _eat_all(param: "typer.core.TyperOption") -> None:
|
||||
"""Make one multi-value option consume space-separated values.
|
||||
|
||||
typer options are repeated-flag by default (``--tag a --tag b``); Rally
|
||||
historically accepted the space-separated ``--tag a b c`` (argparse
|
||||
``nargs="+"``). There is no per-parameter hook for this, so we override
|
||||
the option's parser to grab the following tokens up to the next flag.
|
||||
"""
|
||||
original_add_to_parser = param.add_to_parser
|
||||
|
||||
def add_to_parser(parser: t.Any, ctx: t.Any) -> None:
|
||||
original_add_to_parser(parser, ctx)
|
||||
for opt in param.opts:
|
||||
handler = parser._long_opt.get(opt) or parser._short_opt.get(opt)
|
||||
if handler is None:
|
||||
continue
|
||||
previous_process = handler.process
|
||||
|
||||
def process(value: t.Any, state: t.Any,
|
||||
_prev: t.Any = previous_process) -> None:
|
||||
values = [value]
|
||||
while state.rargs and not state.rargs[0].startswith("-"):
|
||||
values.append(state.rargs.pop(0))
|
||||
for item in values:
|
||||
_prev(item, state)
|
||||
handler.process = process
|
||||
break
|
||||
|
||||
param.add_to_parser = add_to_parser # type: ignore[method-assign]
|
||||
|
||||
|
||||
def _install_multivalue(
|
||||
command: "typer.core.TyperGroup | typer.core.TyperCommand"
|
||||
) -> None:
|
||||
"""Apply :func:`_eat_all` to every multi-value option in the tree."""
|
||||
for _path, _leaf, params in cliutils.iter_commands(command):
|
||||
for param in params:
|
||||
if param.multiple and param.opts and param.opts[0].startswith("-"):
|
||||
_eat_all(param)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
cli: "typer.core.TyperGroup" = (
|
||||
typer.main.get_command(app) # type: ignore[assignment]
|
||||
)
|
||||
_install_multivalue(cli)
|
||||
argutils.install(cli)
|
||||
try:
|
||||
cli()
|
||||
except (OSError, TypeError, ValueError, exceptions.RallyException,
|
||||
jsonschema.ValidationError) as e:
|
||||
if (logging.is_debug()
|
||||
and not isinstance(e, exceptions.InvalidTaskConfig)):
|
||||
LOG.exception("Unexpected exception in CLI")
|
||||
else:
|
||||
print(e)
|
||||
sys.exit(getattr(e, "error_code", 1))
|
||||
except sqlalchemy.exc.OperationalError as e:
|
||||
if logging.is_debug():
|
||||
LOG.exception("Something went wrong with the database")
|
||||
print(e)
|
||||
print("Looks like Rally can't connect to its DB.")
|
||||
print("Make sure the connection string in rally.conf is proper:")
|
||||
print(re.sub("//[^@]*@", "//**:**@", cfg.CONF.database.connection))
|
||||
sys.exit(1)
|
||||
except Exception:
|
||||
print("Command failed, please check log for more info")
|
||||
raise
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
main()
|
||||
|
||||
+84
-2
@@ -13,10 +13,13 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import dataclasses
|
||||
import functools
|
||||
import traceback
|
||||
import typing as t
|
||||
import warnings
|
||||
|
||||
from oslo_log import _options as log_options
|
||||
from oslo_log import handlers
|
||||
from oslo_log import log as oslogging
|
||||
|
||||
@@ -173,7 +176,7 @@ class CatcherHandler(log.handlers.BufferingHandler):
|
||||
self.buffer.append(record)
|
||||
|
||||
|
||||
class LogCatcher(object):
|
||||
class LogCatcher:
|
||||
"""Context manager that catches log messages.
|
||||
|
||||
User can make an assertion on their content or fetch them all.
|
||||
@@ -340,5 +343,84 @@ def log_deprecated_module(target, new_module, release):
|
||||
)
|
||||
|
||||
|
||||
def is_debug():
|
||||
def is_debug() -> bool:
|
||||
return CONF.debug or CONF.rally_debug
|
||||
|
||||
|
||||
_CLI_OPTS = [
|
||||
*DEBUG_OPTS, # --rally-debug
|
||||
*log_options.common_cli_opts, # --debug/-d
|
||||
*log_options.logging_cli_opts, # --log-file, --log-dir, ...
|
||||
]
|
||||
|
||||
_CLI_TYPES: dict[type, t.Any] = {
|
||||
cfg.BoolOpt: bool,
|
||||
cfg.StrOpt: str | None,
|
||||
cfg.IntOpt: int | None,
|
||||
cfg.ListOpt: list | None,
|
||||
cfg.MultiStrOpt: list | None,
|
||||
}
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class _FieldInfo:
|
||||
oslo_name: str
|
||||
is_bool: bool
|
||||
is_list: bool
|
||||
|
||||
|
||||
_CLI_FIELDS: dict[str, _FieldInfo] = {}
|
||||
|
||||
|
||||
def build_cli_params() -> list:
|
||||
"""Return the oslo.log CLI options as ``inspect.Parameter`` objects.
|
||||
|
||||
The rally CLI callback forges these onto its ``__signature__`` before typer
|
||||
reads it, so ``--log-file``/``--log-dir``/... are generated from oslo's own
|
||||
``Opt`` objects (kept in sync with whatever oslo.log offers) instead of
|
||||
hand-declared. Also populates the metadata consumed by
|
||||
`to_oslo_argv`. ``typer`` is imported lazily so importing this
|
||||
module (which happens almost everywhere) stays cheap.
|
||||
"""
|
||||
import inspect
|
||||
|
||||
import typer
|
||||
|
||||
params = []
|
||||
for opt in _CLI_OPTS:
|
||||
typ = _CLI_TYPES.get(type(opt), str | None)
|
||||
# Only booleans carry a default; every other option defaults to
|
||||
# ``None`` and lets oslo apply its own default when it re-parses argv.
|
||||
default = opt.default if isinstance(opt, cfg.BoolOpt) else None
|
||||
help_text = (opt.help or "").strip().split("\n")[0]
|
||||
option = typer.Option("--%s" % opt.name, help=help_text)
|
||||
params.append(
|
||||
inspect.Parameter(
|
||||
opt.dest,
|
||||
inspect.Parameter.KEYWORD_ONLY,
|
||||
default=default,
|
||||
annotation=t.Annotated[typ, option]
|
||||
)
|
||||
)
|
||||
_CLI_FIELDS[opt.dest] = _FieldInfo(
|
||||
oslo_name=opt.name,
|
||||
is_bool=isinstance(opt, cfg.BoolOpt),
|
||||
is_list=isinstance(opt, (cfg.ListOpt, cfg.MultiStrOpt)))
|
||||
return params
|
||||
|
||||
|
||||
def to_oslo_argv(values: t.Mapping) -> list[str]:
|
||||
"""Rebuild the oslo.config ``argv`` slice from the parsed logging options.
|
||||
|
||||
``values`` maps each option's ``dest`` to the value typer parsed for it.
|
||||
"""
|
||||
argv: list[str] = []
|
||||
for name, info in _CLI_FIELDS.items():
|
||||
value = values.get(name)
|
||||
if info.is_bool:
|
||||
if value:
|
||||
argv.append("--%s" % info.oslo_name)
|
||||
elif value is not None:
|
||||
for item in (value if info.is_list else [value]):
|
||||
argv += ["--%s" % info.oslo_name, str(item)]
|
||||
return argv
|
||||
|
||||
@@ -18,4 +18,5 @@ PyYAML # MIT
|
||||
python-subunit # Apache-2.0 or BSD
|
||||
requests!=2.20.0,!=2.24.0 # Apache-2.0
|
||||
SQLAlchemy>=2 # MIT
|
||||
typer>=0.26.0 # MIT
|
||||
virtualenv!=16.3.0 # MIT
|
||||
|
||||
@@ -22,14 +22,14 @@ from rally.utils import encodeutils
|
||||
class CLITestCase(unittest.TestCase):
|
||||
|
||||
def test_rally_cli(self):
|
||||
# ``rally`` with no sub-command prints the top-level help.
|
||||
try:
|
||||
subprocess.check_output(["rally"], stderr=subprocess.STDOUT)
|
||||
output = encodeutils.safe_decode(
|
||||
subprocess.check_output(["rally"], stderr=subprocess.STDOUT))
|
||||
except subprocess.CalledProcessError as e:
|
||||
output = encodeutils.safe_decode(e.output)
|
||||
else:
|
||||
self.fail("It should ve non-zero exit code.")
|
||||
|
||||
self.assertIn("the following arguments are required: category", output)
|
||||
self.assertIn("Usage: rally", output)
|
||||
|
||||
def test_version_cli(self):
|
||||
output = encodeutils.safe_decode(
|
||||
|
||||
@@ -16,57 +16,57 @@
|
||||
from unittest import mock
|
||||
|
||||
from rally.cli.commands import db
|
||||
from tests.unit import fakes
|
||||
from tests.unit import test
|
||||
|
||||
|
||||
class DBCommandsTestCase(test.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(DBCommandsTestCase, self).setUp()
|
||||
self.db_commands = db.DBCommands()
|
||||
self.fake_api = fakes.FakeAPI()
|
||||
|
||||
@mock.patch("rally.cli.commands.db._print_connection")
|
||||
@mock.patch("rally.cli.commands.db.envutils")
|
||||
@mock.patch("rally.cli.commands.db.db.schema")
|
||||
def test_recreate(self, mock_db_schema, mock_envutils):
|
||||
self.db_commands.recreate(self.fake_api)
|
||||
def test_recreate(self, mock_db_schema, mock_envutils,
|
||||
mock__print_connection):
|
||||
db.recreate()
|
||||
db_calls = [mock.call.schema_cleanup(),
|
||||
mock.call.schema_create()]
|
||||
self.assertEqual(db_calls, mock_db_schema.mock_calls)
|
||||
envutils_calls = [mock.call.clear_env()]
|
||||
self.assertEqual(envutils_calls, mock_envutils.mock_calls)
|
||||
|
||||
@mock.patch("rally.cli.commands.db._print_connection")
|
||||
@mock.patch("rally.cli.commands.db.db.schema")
|
||||
def test_create(self, mock_db_schema):
|
||||
self.db_commands.create(self.fake_api)
|
||||
def test_create(self, mock_db_schema, mock__print_connection):
|
||||
db.create()
|
||||
calls = [mock.call.schema_create()]
|
||||
self.assertEqual(calls, mock_db_schema.mock_calls)
|
||||
|
||||
@mock.patch("rally.cli.commands.db._print_connection")
|
||||
@mock.patch("rally.cli.commands.db.db.schema")
|
||||
def test_ensure_create(self, mock_db_schema):
|
||||
def test_ensure_create(self, mock_db_schema, mock__print_connection):
|
||||
mock_db_schema.schema_revision.return_value = None
|
||||
self.db_commands.ensure(self.fake_api)
|
||||
db.ensure()
|
||||
calls = [mock.call.schema_revision(),
|
||||
mock.call.schema_create()]
|
||||
self.assertEqual(calls, mock_db_schema.mock_calls)
|
||||
|
||||
@mock.patch("rally.cli.commands.db._print_connection")
|
||||
@mock.patch("rally.cli.commands.db.db.schema")
|
||||
def test_ensure_exists(self, mock_db_schema):
|
||||
def test_ensure_exists(self, mock_db_schema, mock__print_connection):
|
||||
mock_db_schema.schema_revision.return_value = "revision"
|
||||
self.db_commands.ensure(self.fake_api)
|
||||
db.ensure()
|
||||
calls = [mock.call.schema_revision()]
|
||||
self.assertEqual(calls, mock_db_schema.mock_calls)
|
||||
|
||||
@mock.patch("rally.cli.commands.db._print_connection")
|
||||
@mock.patch("rally.cli.commands.db.db.schema")
|
||||
def test_upgrade(self, mock_db_schema):
|
||||
self.db_commands.upgrade(self.fake_api)
|
||||
def test_upgrade(self, mock_db_schema, mock__print_connection):
|
||||
db.upgrade()
|
||||
calls = [mock.call.schema_upgrade()]
|
||||
mock_db_schema.assert_has_calls(calls)
|
||||
|
||||
@mock.patch("rally.cli.commands.db.db.schema")
|
||||
def test_revision(self, mock_db_schema):
|
||||
self.db_commands.revision(self.fake_api)
|
||||
db.revision()
|
||||
calls = [mock.call.schema_revision()]
|
||||
mock_db_schema.assert_has_calls(calls)
|
||||
|
||||
@@ -74,8 +74,8 @@ class DBCommandsTestCase(test.TestCase):
|
||||
@mock.patch("rally.cli.commands.db.cfg.CONF.database")
|
||||
def test_show(self, mock_conf_database, mock_print):
|
||||
mock_conf_database.connection = "http://aaa:bbb@testing.com:888"
|
||||
self.db_commands.show(self.fake_api)
|
||||
db.show()
|
||||
mock_print.assert_called_once_with("http://**:**@testing.com:888")
|
||||
mock_print.reset_mock()
|
||||
self.db_commands.show(self.fake_api, show_creds=True)
|
||||
db.show(creds=True)
|
||||
mock_print.assert_called_once_with("http://aaa:bbb@testing.com:888")
|
||||
|
||||
@@ -30,31 +30,31 @@ from tests.unit import test
|
||||
class DeploymentCommandsTestCase(test.TestCase):
|
||||
def setUp(self):
|
||||
super(DeploymentCommandsTestCase, self).setUp()
|
||||
self.deployment = deployment.DeploymentCommands()
|
||||
self.fake_api = fakes.FakeAPI()
|
||||
cliutils.set_api(self.fake_api)
|
||||
|
||||
@mock.patch.dict(os.environ, {"RALLY_DEPLOYMENT": "my_deployment_id"})
|
||||
@mock.patch("rally.cli.commands.deployment.DeploymentCommands.list")
|
||||
@mock.patch("rally.cli.commands.deployment._list_deployments")
|
||||
@mock.patch("rally.cli.commands.deployment.open",
|
||||
side_effect=mock.mock_open(read_data="{\"some\": \"json\"}"),
|
||||
create=True)
|
||||
def test_create(self, mock_open, mock_deployment_commands_list):
|
||||
self.deployment.create(self.fake_api, "fake_deploy", False,
|
||||
"path_to_config.json")
|
||||
def test_create(self, mock_open, mock__list_deployments):
|
||||
deployment.create(name="fake_deploy",
|
||||
filename="path_to_config.json")
|
||||
self.fake_api.deployment.create.assert_called_once_with(
|
||||
config={"some": "json"}, name="fake_deploy")
|
||||
|
||||
@mock.patch.dict(os.environ, {"RALLY_DEPLOYMENT": "my_deployment_id"})
|
||||
@mock.patch("rally.cli.commands.deployment.DeploymentCommands.list")
|
||||
def test_create_empty(self, mock_deployment_commands_list):
|
||||
self.deployment.create(self.fake_api, "fake_deploy")
|
||||
@mock.patch("rally.cli.commands.deployment._list_deployments")
|
||||
def test_create_empty(self, mock__list_deployments):
|
||||
deployment.create(name="fake_deploy")
|
||||
self.fake_api.deployment.create.assert_called_once_with(
|
||||
config={}, name="fake_deploy")
|
||||
|
||||
@mock.patch("rally.env.env_mgr.EnvManager.create_spec_from_sys_environ",
|
||||
return_value={"spec": {"auth_url": "http://fake"}})
|
||||
def test_create_fromenv(self, mock_create_spec_from_sys_environ):
|
||||
self.deployment.create(self.fake_api, "from_env", True)
|
||||
deployment.create(name="from_env", fromenv=True)
|
||||
self.fake_api.deployment.create.assert_called_once_with(
|
||||
config={"auth_url": "http://fake"},
|
||||
name="from_env"
|
||||
@@ -76,7 +76,7 @@ class DeploymentCommandsTestCase(test.TestCase):
|
||||
|
||||
with mock.patch.dict("sys.modules",
|
||||
{"rally_openstack": mock_rally_os}):
|
||||
self.deployment.create(self.fake_api, "from_env", True)
|
||||
deployment.create(name="from_env", fromenv=True)
|
||||
self.fake_api.deployment.create.assert_called_once_with(
|
||||
config={"existing@openstack": {"another_key": "another"}},
|
||||
name="from_env"
|
||||
@@ -84,33 +84,33 @@ class DeploymentCommandsTestCase(test.TestCase):
|
||||
|
||||
self.fake_api.deployment.create.reset_mock()
|
||||
mock_rally_os.__version_tuple__ = (1, 5, 0)
|
||||
self.deployment.create(self.fake_api, "from_env", True)
|
||||
deployment.create(name="from_env", fromenv=True)
|
||||
self.fake_api.deployment.create.assert_called_once_with(
|
||||
config={"existing@openstack": {"another_key": "another",
|
||||
"https_key": "some key"}},
|
||||
name="from_env"
|
||||
)
|
||||
|
||||
@mock.patch("rally.cli.commands.deployment.DeploymentCommands.list")
|
||||
@mock.patch("rally.cli.commands.deployment.DeploymentCommands.use")
|
||||
@mock.patch("rally.cli.commands.deployment._list_deployments")
|
||||
@mock.patch("rally.cli.commands.deployment._use")
|
||||
@mock.patch("rally.cli.commands.deployment.open",
|
||||
side_effect=mock.mock_open(read_data="{\"uuid\": \"uuid\"}"),
|
||||
create=True)
|
||||
def test_create_and_use(self, mock_open, mock_deployment_commands_use,
|
||||
mock_deployment_commands_list):
|
||||
def test_create_and_use(self, mock_open, mock__use,
|
||||
mock__list_deployments):
|
||||
self.fake_api.deployment.create.return_value = dict(uuid="uuid")
|
||||
self.deployment.create(self.fake_api, "fake_deploy", False,
|
||||
"path_to_config.json", True)
|
||||
deployment.create(name="fake_deploy",
|
||||
filename="path_to_config.json")
|
||||
self.fake_api.deployment.create.assert_called_once_with(
|
||||
config={"uuid": "uuid"}, name="fake_deploy")
|
||||
mock_deployment_commands_list.assert_called_once_with(
|
||||
mock__list_deployments.assert_called_once_with(
|
||||
self.fake_api, deployment_list=[{"uuid": "uuid"}])
|
||||
mock_deployment_commands_use.assert_called_once_with(
|
||||
mock__use.assert_called_once_with(
|
||||
self.fake_api, self.fake_api.deployment.create.return_value)
|
||||
|
||||
def test_recreate(self):
|
||||
deployment_id = "43924f8b-9371-4152-af9f-4cf02b4eced4"
|
||||
self.deployment.recreate(self.fake_api, deployment_id)
|
||||
deployment.recreate(deployment=deployment_id)
|
||||
self.fake_api.deployment.recreate.assert_called_once_with(
|
||||
deployment=deployment_id, config=None)
|
||||
|
||||
@@ -119,29 +119,16 @@ class DeploymentCommandsTestCase(test.TestCase):
|
||||
create=True)
|
||||
def test_recreate_config(self, mock_open):
|
||||
deployment_id = "43924f8b-9371-4152-af9f-4cf02b4eced4"
|
||||
self.deployment.recreate(self.fake_api, deployment_id,
|
||||
filename="my.json")
|
||||
deployment.recreate(deployment=deployment_id, filename="my.json")
|
||||
self.fake_api.deployment.recreate.assert_called_once_with(
|
||||
deployment=deployment_id, config={"some": "json"})
|
||||
|
||||
@mock.patch("rally.cli.commands.deployment.envutils.get_global")
|
||||
def test_recreate_no_deployment_id(self, mock_get_global):
|
||||
mock_get_global.side_effect = exceptions.InvalidArgumentsException
|
||||
self.assertRaises(exceptions.InvalidArgumentsException,
|
||||
self.deployment.recreate, None)
|
||||
|
||||
def test_destroy(self):
|
||||
deployment_id = "53fd0273-60ce-42e5-a759-36f1a683103e"
|
||||
self.deployment.destroy(self.fake_api, deployment_id)
|
||||
deployment.destroy(deployment=deployment_id)
|
||||
self.fake_api.deployment.destroy.assert_called_once_with(
|
||||
deployment=deployment_id)
|
||||
|
||||
@mock.patch("rally.cli.commands.deployment.envutils.get_global")
|
||||
def test_destroy_no_deployment_id(self, mock_get_global):
|
||||
mock_get_global.side_effect = exceptions.InvalidArgumentsException
|
||||
self.assertRaises(exceptions.InvalidArgumentsException,
|
||||
self.deployment.destroy, self.fake_api, None)
|
||||
|
||||
@mock.patch("rally.cli.commands.deployment.cliutils.print_list")
|
||||
@mock.patch("rally.cli.commands.deployment.utils.Struct")
|
||||
@mock.patch("rally.cli.commands.deployment.envutils.get_global")
|
||||
@@ -157,7 +144,7 @@ class DeploymentCommandsTestCase(test.TestCase):
|
||||
"active": "False"}]
|
||||
|
||||
self.fake_api.deployment.list.return_value = fake_deployment_list
|
||||
self.deployment.list(self.fake_api)
|
||||
deployment.list_()
|
||||
|
||||
fake_deployment = fake_deployment_list[0]
|
||||
fake_deployment["active"] = ""
|
||||
@@ -181,7 +168,7 @@ class DeploymentCommandsTestCase(test.TestCase):
|
||||
"status": "deploy->finished",
|
||||
"active": "True"}]
|
||||
self.fake_api.deployment.list.return_value = fake_deployment_list
|
||||
self.deployment.list(self.fake_api)
|
||||
deployment.list_()
|
||||
|
||||
fake_deployment = fake_deployment_list[0]
|
||||
fake_deployment["active"] = "*"
|
||||
@@ -197,18 +184,12 @@ class DeploymentCommandsTestCase(test.TestCase):
|
||||
deployment_id = "fa4a423e-f15d-4d83-971a-89574f892999"
|
||||
value = {"config": "config"}
|
||||
self.fake_api.deployment.get.return_value = value
|
||||
self.deployment.config(self.fake_api, deployment_id)
|
||||
deployment.config(deployment=deployment_id)
|
||||
mock_json_dumps.assert_called_once_with(value["config"],
|
||||
sort_keys=True, indent=4)
|
||||
self.fake_api.deployment.get.assert_called_once_with(
|
||||
deployment=deployment_id)
|
||||
|
||||
@mock.patch("rally.cli.commands.deployment.envutils.get_global")
|
||||
def test_config_no_deployment_id(self, mock_get_global):
|
||||
mock_get_global.side_effect = exceptions.InvalidArgumentsException
|
||||
self.assertRaises(exceptions.InvalidArgumentsException,
|
||||
self.deployment.config, self.fake_api, None)
|
||||
|
||||
@mock.patch("rally.cli.commands.deployment.cliutils.print_list")
|
||||
@mock.patch("rally.cli.commands.deployment.utils.Struct")
|
||||
def test_show(self, mock_struct, mock_print_list):
|
||||
@@ -220,11 +201,11 @@ class DeploymentCommandsTestCase(test.TestCase):
|
||||
"region_name": "r",
|
||||
"endpoint_type": consts.EndpointType.INTERNAL},
|
||||
"users": []}
|
||||
deployment = self.fake_api.deployment.get
|
||||
deployment.return_value = {"credentials": {"openstack": [
|
||||
dep_get = self.fake_api.deployment.get
|
||||
dep_get.return_value = {"credentials": {"openstack": [
|
||||
{"admin": value["admin"],
|
||||
"users": []}]}}
|
||||
self.deployment.show(self.fake_api, deployment_id)
|
||||
deployment.show(deployment=deployment_id)
|
||||
self.fake_api.deployment.get.assert_called_once_with(
|
||||
deployment=deployment_id)
|
||||
|
||||
@@ -234,12 +215,6 @@ class DeploymentCommandsTestCase(test.TestCase):
|
||||
mock_struct.assert_called_once_with(**dict(zip(headers, fake_data)))
|
||||
mock_print_list.assert_called_once_with([mock_struct()], headers)
|
||||
|
||||
@mock.patch("rally.cli.commands.deployment.envutils.get_global")
|
||||
def test_deploy_no_deployment_id(self, mock_get_global):
|
||||
mock_get_global.side_effect = exceptions.InvalidArgumentsException
|
||||
self.assertRaises(exceptions.InvalidArgumentsException,
|
||||
self.deployment.show, None)
|
||||
|
||||
@mock.patch("os.remove")
|
||||
@mock.patch("os.symlink")
|
||||
@mock.patch("os.path.exists", return_value=True)
|
||||
@@ -260,7 +235,7 @@ class DeploymentCommandsTestCase(test.TestCase):
|
||||
|
||||
with mock.patch("rally.cli.commands.deployment.open", mock.mock_open(),
|
||||
create=True) as mock_file:
|
||||
self.deployment.use(self.fake_api, deployment_id)
|
||||
deployment.use(deployment_id)
|
||||
self.assertEqual(3, mock_path_exists.call_count)
|
||||
mock__update_env_file.assert_has_calls([
|
||||
mock.call(os.path.expanduser("~/.rally/globals"),
|
||||
@@ -306,7 +281,7 @@ class DeploymentCommandsTestCase(test.TestCase):
|
||||
|
||||
with mock.patch("rally.cli.commands.deployment.open", mock.mock_open(),
|
||||
create=True) as mock_file:
|
||||
self.deployment.use(self.fake_api, deployment_id)
|
||||
deployment.use(deployment_id)
|
||||
self.assertEqual(3, mock_path_exists.call_count)
|
||||
mock__update_env_file.assert_has_calls([
|
||||
mock.call(os.path.expanduser("~/.rally/globals"),
|
||||
@@ -332,7 +307,7 @@ class DeploymentCommandsTestCase(test.TestCase):
|
||||
mock_remove.assert_called_once_with(os.path.expanduser(
|
||||
"~/.rally/openrc"))
|
||||
|
||||
@mock.patch("rally.cli.commands.deployment.DeploymentCommands."
|
||||
@mock.patch("rally.cli.commands.deployment."
|
||||
"_update_openrc_deployment_file")
|
||||
@mock.patch("rally.cli.envutils.update_globals_file")
|
||||
def test_use_by_name(self, mock_update_globals_file,
|
||||
@@ -342,7 +317,7 @@ class DeploymentCommandsTestCase(test.TestCase):
|
||||
"credentials": {"openstack": [fake_credentials]}}
|
||||
self.fake_api.deployment.list.return_value = [fake_deployment]
|
||||
self.fake_api.deployment.get.return_value = fake_deployment
|
||||
status = self.deployment.use(self.fake_api, deployment="fake_name")
|
||||
status = deployment.use("fake_name")
|
||||
self.assertIsNone(status)
|
||||
self.fake_api.deployment.get.assert_called_once_with(
|
||||
deployment="fake_name")
|
||||
@@ -358,7 +333,8 @@ class DeploymentCommandsTestCase(test.TestCase):
|
||||
exc = exceptions.DBRecordNotFound(criteria="uuid: %s" % deployment_id,
|
||||
table="deployments")
|
||||
self.fake_api.deployment.get.side_effect = exc
|
||||
self.assertEqual(1, self.deployment.use(self.fake_api, deployment_id))
|
||||
with self.assertExitCode(1):
|
||||
deployment.use(deployment_id)
|
||||
|
||||
@mock.patch("rally.cli.commands.deployment.logging.is_debug",
|
||||
return_value=False)
|
||||
@@ -393,8 +369,8 @@ class DeploymentCommandsTestCase(test.TestCase):
|
||||
|
||||
with mock.patch.object(deployment.cliutils, "print_list",
|
||||
new=print_list):
|
||||
self.assertEqual(
|
||||
1, self.deployment.check(self.fake_api, deployment_uuid))
|
||||
with self.assertExitCode(1):
|
||||
deployment.check(deployment=deployment_uuid)
|
||||
|
||||
self.assertEqual(
|
||||
"-----------------------------------------------------------------"
|
||||
@@ -446,8 +422,8 @@ class DeploymentCommandsTestCase(test.TestCase):
|
||||
"KeystoneError: connection refused"}}]
|
||||
}
|
||||
|
||||
self.assertEqual(
|
||||
1, self.deployment.check(self.fake_api, deployment_uuid))
|
||||
with self.assertExitCode(1):
|
||||
deployment.check(deployment=deployment_uuid)
|
||||
|
||||
self.assertEqual(
|
||||
"-----------------------------------------------------------------"
|
||||
|
||||
@@ -12,33 +12,33 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import inspect
|
||||
|
||||
from rally.cli import main
|
||||
from rally.cli.commands import db
|
||||
from rally.cli.commands import deployment
|
||||
from rally.cli.commands import env
|
||||
from rally.cli.commands import plugin
|
||||
from rally.cli.commands import task
|
||||
from rally.cli.commands import verify
|
||||
from rally.common.plugin import info
|
||||
from tests.unit import test
|
||||
|
||||
|
||||
APPS = [db.db_app, deployment.deployment_app, env.env_app, plugin.plugin_app,
|
||||
task.task_app, verify.verify_app]
|
||||
|
||||
|
||||
class DocstringsTestCase(test.TestCase):
|
||||
|
||||
@staticmethod
|
||||
def _get_all_category_methods(category):
|
||||
all_methods = inspect.getmembers(
|
||||
category,
|
||||
predicate=lambda x: inspect.ismethod(x) or inspect.isfunction(x))
|
||||
return [m for m in all_methods if not m[0].startswith("_")]
|
||||
|
||||
def test_params(self):
|
||||
for category in main.categories.values():
|
||||
all_methods = self._get_all_category_methods(category)
|
||||
for name, method in all_methods:
|
||||
m_info = info.parse_docstring(method.__doc__)
|
||||
for app in APPS:
|
||||
for command in app.registered_commands:
|
||||
func = command.callback
|
||||
if func is None or func.__doc__ is None:
|
||||
continue
|
||||
m_info = info.parse_docstring(func.__doc__)
|
||||
if m_info["params"]:
|
||||
print(m_info)
|
||||
self.fail("The description of parameters for CLI methods "
|
||||
"should be transmitted as a 'help' argument of "
|
||||
"`rally.cli.cliutils.arg` decorator. You should "
|
||||
"remove descriptions from docstring of "
|
||||
"`%s.%s.%s`" % (category.__module__,
|
||||
category.__class__,
|
||||
name))
|
||||
self.fail("The description of parameters for CLI commands "
|
||||
"should be passed as the 'help' argument of a "
|
||||
"`typer.Option`/`typer.Argument`. You should "
|
||||
"remove the parameter descriptions from the "
|
||||
"docstring of `%s:%s`" % (func.__module__,
|
||||
func.__qualname__))
|
||||
|
||||
@@ -27,14 +27,6 @@ from tests.unit import test
|
||||
|
||||
class EnvCommandsTestCase(test.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(EnvCommandsTestCase, self).setUp()
|
||||
self.env = env.EnvCommands()
|
||||
# TODO(boris-42): api argument is not used by EvnCommands
|
||||
# it's going to be removed when we remove rally.api
|
||||
# from other commands
|
||||
self.api = None
|
||||
|
||||
@staticmethod
|
||||
def gen_env_data(uid=None, name=None, description=None,
|
||||
status=env_mgr.STATUS.INIT, spec=None, extras=None):
|
||||
@@ -56,30 +48,30 @@ class EnvCommandsTestCase(test.TestCase):
|
||||
env._print("Test42", silent=False)
|
||||
mock_print.assert_called_once_with("Test42")
|
||||
env._print("Test43")
|
||||
mock_print.assert_has_calls([mock.call("Test42"), mock.call("Test43")])
|
||||
mock_print.assert_has_calls([mock.call("Test42"),
|
||||
mock.call("Test43")])
|
||||
|
||||
@mock.patch("rally.env.env_mgr.EnvManager.create")
|
||||
@mock.patch("rally.cli.commands.env.EnvCommands._show")
|
||||
def test_create_emtpy_use(self, mock_env_commands__show,
|
||||
@mock.patch("rally.cli.commands.env._show")
|
||||
def test_create_emtpy_use(self, mock__show,
|
||||
mock_env_manager_create):
|
||||
self.assertEqual(
|
||||
0, self.env.create(self.api, "test_name", "test_description"))
|
||||
env.create(name="test_name", description="test_description")
|
||||
mock_env_manager_create.assert_called_once_with(
|
||||
"test_name", {}, description="test_description", extras=None)
|
||||
mock_env_commands__show.assert_called_once_with(
|
||||
mock__show.assert_called_once_with(
|
||||
mock_env_manager_create.return_value.data,
|
||||
to_json=False, only_spec=False)
|
||||
|
||||
@mock.patch("rally.env.env_mgr.EnvManager.create")
|
||||
@mock.patch("rally.cli.commands.env.open", create=True)
|
||||
@mock.patch("rally.cli.commands.env.print")
|
||||
def test_create_spec_and_extra_no_use_to_json(self, mock_print, mock_open,
|
||||
mock_env_manager_create):
|
||||
def test_create_spec_and_extra_no_use_to_json(
|
||||
self, mock_print, mock_open, mock_env_manager_create):
|
||||
mock_open.side_effect = mock.mock_open(read_data="{\"a\": 1}")
|
||||
mock_env_manager_create.return_value.data = {"test": "test"}
|
||||
self.assertEqual(
|
||||
0, self.env.create(self.api, "n", "d", extras="{\"extra\": 123}",
|
||||
spec="spec.yml", to_json=True, do_use=False))
|
||||
env.create(name="n", description="d",
|
||||
extras="{\"extra\": 123}", spec="spec.yml",
|
||||
to_json=True, no_use=True)
|
||||
|
||||
mock_env_manager_create.assert_called_once_with(
|
||||
"n", {"a": 1}, description="d", extras={"extra": 123})
|
||||
@@ -90,15 +82,15 @@ class EnvCommandsTestCase(test.TestCase):
|
||||
@mock.patch("rally.cli.commands.env.open", create=True)
|
||||
def test_create_invalid_spec(self, mock_open, mock_print):
|
||||
mock_open.side_effect = mock.mock_open(read_data="[]")
|
||||
self.assertEqual(
|
||||
1, self.env.create(self.api, "n", "d", spec="spec.yml"))
|
||||
with self.assertExitCode(1):
|
||||
env.create(name="n", description="d", spec="spec.yml")
|
||||
mock_print.assert_has_calls([
|
||||
mock.call("Env spec has wrong format:"),
|
||||
mock.call("[]"),
|
||||
mock.call(mock.ANY)
|
||||
])
|
||||
|
||||
@mock.patch("rally.cli.commands.env.EnvCommands._show")
|
||||
@mock.patch("rally.cli.commands.env._show")
|
||||
@mock.patch("rally.env.env_mgr.EnvManager.create_spec_from_sys_environ")
|
||||
@mock.patch("rally.env.env_mgr.EnvManager.create")
|
||||
@mock.patch("rally.cli.commands.env.open", create=True)
|
||||
@@ -106,7 +98,7 @@ class EnvCommandsTestCase(test.TestCase):
|
||||
def test_create_from_sys_env(
|
||||
self, mock_print, mock_open, mock_env_manager_create,
|
||||
mock_env_manager_create_spec_from_sys_environ,
|
||||
mock_env_commands__show):
|
||||
mock__show):
|
||||
result = {
|
||||
"spec": {"foo": mock.Mock()},
|
||||
"discovery_details": collections.OrderedDict([
|
||||
@@ -117,9 +109,8 @@ class EnvCommandsTestCase(test.TestCase):
|
||||
}
|
||||
mock_env_manager_create_spec_from_sys_environ.return_value = result
|
||||
|
||||
self.assertEqual(
|
||||
0, self.env.create(self.api, "n", "d", spec=None,
|
||||
from_sysenv=True, do_use=False))
|
||||
env.create(name="n", description="d", spec=None,
|
||||
from_sysenv=True, no_use=True)
|
||||
self.assertEqual(
|
||||
[
|
||||
# check that the number of listed platforms is right
|
||||
@@ -138,15 +129,16 @@ class EnvCommandsTestCase(test.TestCase):
|
||||
|
||||
@mock.patch("rally.cli.commands.env.print")
|
||||
def test_create_with_incompatible_arguments(self, mock_print):
|
||||
self.assertEqual(
|
||||
1, self.env.create(self.api, "n", "d", spec="asd",
|
||||
from_sysenv=True))
|
||||
with self.assertExitCode(1):
|
||||
env.create(name="n", description="d", spec="asd",
|
||||
from_sysenv=True)
|
||||
|
||||
@mock.patch("rally.env.env_mgr.EnvManager.create")
|
||||
@mock.patch("rally.cli.commands.env.print")
|
||||
def test_create_exception(self, mock_print, mock_env_manager_create):
|
||||
mock_env_manager_create.side_effect = Exception
|
||||
self.assertEqual(1, self.env.create(self.api, "n", "d"))
|
||||
with self.assertExitCode(1):
|
||||
env.create(name="n", description="d")
|
||||
mock_print.assert_has_calls([
|
||||
mock.call("Something went wrong during env creation:"),
|
||||
mock.call(mock.ANY)
|
||||
@@ -176,7 +168,8 @@ class EnvCommandsTestCase(test.TestCase):
|
||||
]
|
||||
}
|
||||
}
|
||||
self.assertEqual(1, self.env.cleanup(self.api, env_))
|
||||
with self.assertExitCode(1):
|
||||
env.cleanup(env=env_)
|
||||
mock_env_manager_get.assert_called_once_with(env_)
|
||||
env_inst.cleanup.assert_called_once_with()
|
||||
|
||||
@@ -228,7 +221,8 @@ class EnvCommandsTestCase(test.TestCase):
|
||||
]
|
||||
}
|
||||
}
|
||||
self.assertEqual(1, self.env.cleanup(self.api, env_, to_json=True))
|
||||
with self.assertExitCode(1):
|
||||
env.cleanup(env=env_, to_json=True)
|
||||
mock_print.assert_called_once_with(
|
||||
json.dumps(env_inst.cleanup.return_value, indent=2))
|
||||
|
||||
@@ -243,7 +237,8 @@ class EnvCommandsTestCase(test.TestCase):
|
||||
"message": "42"
|
||||
}
|
||||
}
|
||||
self.assertEqual(1, self.env.destroy(self.api, env_))
|
||||
with self.assertExitCode(1):
|
||||
env.destroy(env_)
|
||||
mock_env_manager_get.assert_called_once_with(env_)
|
||||
env_inst.destroy.assert_called_once_with(False)
|
||||
mock_print.assert_has_calls([
|
||||
@@ -266,9 +261,7 @@ class EnvCommandsTestCase(test.TestCase):
|
||||
"message": "42"
|
||||
}
|
||||
}
|
||||
self.assertEqual(
|
||||
0,
|
||||
self.env.destroy(self.api, env_, skip_cleanup=True, to_json=True))
|
||||
env.destroy(env_, skip_cleanup=True, to_json=True)
|
||||
env_inst.destroy.assert_called_once_with(True)
|
||||
mock_print.assert_called_once_with(
|
||||
json.dumps(env_inst.destroy.return_value, indent=2))
|
||||
@@ -276,7 +269,7 @@ class EnvCommandsTestCase(test.TestCase):
|
||||
@mock.patch("rally.env.env_mgr.EnvManager.get")
|
||||
def test_delete(self, mock_env_manager_get):
|
||||
env_ = mock.Mock()
|
||||
self.env.delete(self.api, env_)
|
||||
env.delete(env_)
|
||||
mock_env_manager_get.assert_called_once_with(env_)
|
||||
mock_env_manager_get.return_value.delete.assert_called_once_with(
|
||||
force=False)
|
||||
@@ -284,7 +277,7 @@ class EnvCommandsTestCase(test.TestCase):
|
||||
@mock.patch("rally.env.env_mgr.EnvManager.get")
|
||||
def test_delete_force(self, mock_env_manager_get):
|
||||
env_ = mock.Mock()
|
||||
self.env.delete(self.api, env_, force=True)
|
||||
env.delete(env_, force=True)
|
||||
mock_env_manager_get.assert_called_once_with(env_)
|
||||
mock_env_manager_get.return_value.delete.assert_called_once_with(
|
||||
force=True)
|
||||
@@ -293,11 +286,11 @@ class EnvCommandsTestCase(test.TestCase):
|
||||
@mock.patch("rally.cli.commands.env.print")
|
||||
def test_list_empty(self, mock_print, mock_env_manager_list):
|
||||
mock_env_manager_list.return_value = []
|
||||
self.env.list(self.api, to_json=True)
|
||||
env.list_(to_json=True)
|
||||
mock_print.assert_called_once_with("[]")
|
||||
mock_print.reset_mock()
|
||||
self.env.list(self.api, to_json=False)
|
||||
mock_print.assert_called_once_with(self.env.MSG_NO_ENVS)
|
||||
env.list_(to_json=False)
|
||||
mock_print.assert_called_once_with(env.MSG_NO_ENVS)
|
||||
|
||||
@mock.patch("rally.env.env_mgr.EnvManager.list")
|
||||
@mock.patch("rally.cli.commands.env.print")
|
||||
@@ -306,7 +299,7 @@ class EnvCommandsTestCase(test.TestCase):
|
||||
env_b = env_mgr.EnvManager(self.gen_env_data())
|
||||
mock_env_manager_list.return_value = [env_a, env_b]
|
||||
|
||||
self.env.list(self.api, to_json=True)
|
||||
env.list_(to_json=True)
|
||||
mock_env_manager_list.assert_called_once_with()
|
||||
mock_print.assert_called_once_with(
|
||||
json.dumps([env_a.cached_data, env_b.cached_data], indent=2))
|
||||
@@ -314,7 +307,7 @@ class EnvCommandsTestCase(test.TestCase):
|
||||
for m in [mock_env_manager_list, mock_print]:
|
||||
m.reset_mock()
|
||||
|
||||
self.env.list(self.api)
|
||||
env.list_()
|
||||
mock_env_manager_list.assert_called_once_with()
|
||||
mock_print.assert_called_once_with(mock.ANY)
|
||||
|
||||
@@ -325,7 +318,7 @@ class EnvCommandsTestCase(test.TestCase):
|
||||
name="my best env",
|
||||
description="description")
|
||||
env_data["platforms"] = {}
|
||||
self.env._show(env_data, False, False)
|
||||
env._show(env_data, False, False)
|
||||
mock_print.assert_called_once_with(
|
||||
"+-------------+--------------------------------------+\n"
|
||||
"| uuid | a77004a6-7fe5-4b75-a278-009c3c5f6b20 |\n"
|
||||
@@ -339,25 +332,25 @@ class EnvCommandsTestCase(test.TestCase):
|
||||
|
||||
@mock.patch("rally.cli.commands.env.print")
|
||||
def test__show_to_json(self, mock_print):
|
||||
self.env._show("data", to_json=True, only_spec=False)
|
||||
env._show("data", to_json=True, only_spec=False)
|
||||
mock_print.assert_called_once_with("\"data\"")
|
||||
|
||||
@mock.patch("rally.cli.commands.env.print")
|
||||
def test__show_only_spec(self, mock_print):
|
||||
self.env._show({"spec": "data"}, to_json=False, only_spec=True)
|
||||
env._show({"spec": "data"}, to_json=False, only_spec=True)
|
||||
mock_print.assert_called_once_with("\"data\"")
|
||||
|
||||
@mock.patch("rally.env.env_mgr.EnvManager.get")
|
||||
@mock.patch("rally.cli.commands.env.EnvCommands._show")
|
||||
@mock.patch("rally.cli.commands.env._show")
|
||||
def test_show(self, mock__show, mock_env_manager_get):
|
||||
env_ = mock.Mock()
|
||||
self.env.show(self.api, env_)
|
||||
env.show(env=env_)
|
||||
mock_env_manager_get.assert_called_once_with(env_)
|
||||
mock__show.assert_called_once_with(
|
||||
mock_env_manager_get.return_value.data, to_json=False,
|
||||
only_spec=False)
|
||||
mock__show.reset_mock()
|
||||
self.env.show(self.api, env_, to_json=True)
|
||||
env.show(env=env_, to_json=True)
|
||||
mock__show.assert_called_once_with(
|
||||
mock_env_manager_get.return_value.data, to_json=True,
|
||||
only_spec=False)
|
||||
@@ -368,7 +361,7 @@ class EnvCommandsTestCase(test.TestCase):
|
||||
mock_env_manager_get.return_value.get_info.return_value = {
|
||||
"p1": {"info": {"a": True}}}
|
||||
|
||||
self.assertEqual(0, self.env.info(self.api, "any", to_json=True))
|
||||
env.info(env="any", to_json=True)
|
||||
mock_env_manager_get.assert_called_once_with("any")
|
||||
mock_print.assert_called_once_with(
|
||||
json.dumps(mock_env_manager_get.return_value.get_info.return_value,
|
||||
@@ -378,7 +371,8 @@ class EnvCommandsTestCase(test.TestCase):
|
||||
"p1": {"info": {"a": False}},
|
||||
"p2": {"info": {}, "error": "some error"}
|
||||
}
|
||||
self.assertEqual(1, self.env.info(self.api, "any", to_json=True))
|
||||
with self.assertExitCode(1):
|
||||
env.info(env="any", to_json=True)
|
||||
|
||||
@mock.patch("rally.env.env_mgr.EnvManager.get")
|
||||
@mock.patch("rally.cli.commands.env.print")
|
||||
@@ -387,7 +381,8 @@ class EnvCommandsTestCase(test.TestCase):
|
||||
"p1@pl1": {"info": {"a": False}},
|
||||
"p2@pl2": {"info": {}, "error": "some error"}
|
||||
}
|
||||
self.assertEqual(1, self.env.info(self.api, "any"))
|
||||
with self.assertExitCode(1):
|
||||
env.info(env="any")
|
||||
mock_print.assert_has_calls([
|
||||
mock.call(mock_env_manager_get.return_value),
|
||||
mock.call(
|
||||
@@ -409,7 +404,8 @@ class EnvCommandsTestCase(test.TestCase):
|
||||
"p1@p1": {"available": True, "message": "OK!"},
|
||||
"p2@p2": {"available": False, "message": "BAD !"}
|
||||
}
|
||||
self.assertEqual(1, self.env.check(self.api, "env_42"))
|
||||
with self.assertExitCode(1):
|
||||
env.check(env="env_42")
|
||||
mock_env_manager_get.assert_called_once_with("env_42")
|
||||
|
||||
mock_print.assert_has_calls([
|
||||
@@ -432,7 +428,8 @@ class EnvCommandsTestCase(test.TestCase):
|
||||
"p2@p2": {"available": False, "message": "BAD !",
|
||||
"traceback": "Filaneme\n Codeline\nError"}
|
||||
}
|
||||
self.assertEqual(1, self.env.check(self.api, "env_42", detailed=True))
|
||||
with self.assertExitCode(1):
|
||||
env.check(env="env_42", detailed=True)
|
||||
mock_env_manager_get.assert_called_once_with("env_42")
|
||||
|
||||
print(mock_print.call_args_list)
|
||||
@@ -457,7 +454,7 @@ class EnvCommandsTestCase(test.TestCase):
|
||||
mock_env_manager_get.return_value.check_health.return_value = {
|
||||
"p1": {"available": True}}
|
||||
|
||||
self.assertEqual(0, self.env.check(self.api, "some_env", to_json=True))
|
||||
env.check(env="some_env", to_json=True)
|
||||
mock_env_manager_get.assert_called_once_with("some_env")
|
||||
mock_print.assert_called_once_with(
|
||||
json.dumps(
|
||||
@@ -467,7 +464,8 @@ class EnvCommandsTestCase(test.TestCase):
|
||||
|
||||
mock_env_manager_get.return_value.check_health.return_value = {
|
||||
"p1": {"available": False}}
|
||||
self.assertEqual(1, self.env.check(self.api, "some_env", to_json=True))
|
||||
with self.assertExitCode(1):
|
||||
env.check(env="some_env", to_json=True)
|
||||
|
||||
@mock.patch("rally.env.env_mgr.EnvManager.get")
|
||||
@mock.patch("rally.cli.commands.env._print")
|
||||
@@ -475,19 +473,20 @@ class EnvCommandsTestCase(test.TestCase):
|
||||
mock_env_manager_get.side_effect = exceptions.DBRecordNotFound(
|
||||
criteria="", table="")
|
||||
env_ = str(uuid.uuid4())
|
||||
self.assertEqual(1, self.env.use(self.api, env_))
|
||||
with self.assertExitCode(1):
|
||||
env.use(env_)
|
||||
mock_env_manager_get.assert_called_once_with(env_)
|
||||
mock__print.assert_called_once_with(
|
||||
"Can't use non existing environment %s." % env_, False)
|
||||
|
||||
@mock.patch("rally.env.env_mgr.EnvManager.get")
|
||||
@mock.patch("rally.cli.commands.env.EnvCommands._use")
|
||||
@mock.patch("rally.cli.commands.env._use")
|
||||
def test_use(self, mock__use, mock_env_manager_get):
|
||||
mock_env_manager_get.side_effect = [
|
||||
mock.Mock(uuid="aa"), mock.Mock(uuid="bb")
|
||||
]
|
||||
self.assertIsNone(self.env.use(self.api, "aa"))
|
||||
self.assertIsNone(self.env.use(self.api, "bb", to_json=True))
|
||||
self.assertIsNone(env.use("aa"))
|
||||
self.assertIsNone(env.use("bb", to_json=True))
|
||||
|
||||
mock_env_manager_get.assert_has_calls(
|
||||
[mock.call("aa"), mock.call("bb")])
|
||||
@@ -497,11 +496,11 @@ class EnvCommandsTestCase(test.TestCase):
|
||||
@mock.patch("rally.cli.commands.env.envutils.update_globals_file")
|
||||
@mock.patch("rally.cli.commands.env.print")
|
||||
def test__use(self, mock_print, mock_update_globals_file):
|
||||
self.env._use("aa", True)
|
||||
env._use("aa", True)
|
||||
self.assertFalse(mock_print.called)
|
||||
mock_update_globals_file.assert_called_once_with("RALLY_ENV", "aa")
|
||||
mock_update_globals_file.reset_mock()
|
||||
|
||||
self.env._use("bb", False)
|
||||
env._use("bb", False)
|
||||
mock_print.assert_called_once_with("Using environment: bb")
|
||||
mock_update_globals_file.assert_called_once_with("RALLY_ENV", "bb")
|
||||
|
||||
@@ -18,6 +18,7 @@ from unittest import mock
|
||||
|
||||
import ddt
|
||||
|
||||
from rally import exceptions
|
||||
from rally.cli import cliutils
|
||||
from rally.cli.commands import plugin as plugin_cmd
|
||||
from rally.common import utils
|
||||
@@ -30,7 +31,6 @@ class PluginCommandsTestCase(test.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(PluginCommandsTestCase, self).setUp()
|
||||
self.plugin_cmd = plugin_cmd.PluginCommands()
|
||||
|
||||
@plugin.configure("p1", "p1_ns")
|
||||
class Plugin1(plugin.Plugin):
|
||||
@@ -75,7 +75,7 @@ class PluginCommandsTestCase(test.TestCase):
|
||||
|
||||
with mock.patch.object(plugin_cmd.cliutils, "print_list",
|
||||
new=print_list):
|
||||
plugin_cmd.PluginCommands._print_plugins_list(
|
||||
plugin_cmd._print_plugins_list(
|
||||
[self.Plugin1, self.Plugin2])
|
||||
|
||||
self.assertEqual(
|
||||
@@ -88,7 +88,7 @@ class PluginCommandsTestCase(test.TestCase):
|
||||
|
||||
def test_show(self):
|
||||
with utils.StdOutCapture() as out:
|
||||
plugin_cmd.PluginCommands().show(None, "p1", "p1_ns")
|
||||
plugin_cmd.show("p1", platform="p1_ns")
|
||||
output = out.getvalue()
|
||||
|
||||
self.assertIn("NAME\n\tp1", output)
|
||||
@@ -112,20 +112,26 @@ class PluginCommandsTestCase(test.TestCase):
|
||||
@ddt.unpack
|
||||
def test_show_not_found(self, name, platform, text):
|
||||
with utils.StdOutCapture() as out:
|
||||
plugin_cmd.PluginCommands().show(None, name, platform)
|
||||
with self.assertExitCode(exceptions.PluginNotFound.error_code):
|
||||
plugin_cmd.show(name, platform=platform)
|
||||
self.assertEqual(out.getvalue(), text)
|
||||
|
||||
@mock.patch("rally.cli.commands.plugin.PluginCommands._print_plugins_list")
|
||||
def test_show_many(self, mock_plugin_commands__print_plugins_list):
|
||||
@mock.patch("rally.cli.commands.plugin._print_plugins_list")
|
||||
def test_show_many(self, mock__print_plugins_list):
|
||||
with utils.StdOutCapture() as out:
|
||||
with mock.patch("rally.cli.commands.plugin.plugin.Plugin."
|
||||
"get_all") as mock_plugin_get_all:
|
||||
mock_plugin_get_all.return_value = [self.Plugin2, self.Plugin3]
|
||||
plugin_cmd.PluginCommands().show(None, "p", "p2_ns")
|
||||
self.assertEqual("Multiple plugins found:\n", out.getvalue())
|
||||
mock_plugin_get_all.assert_called_once_with(platform="p2_ns")
|
||||
mock_plugin_get_all.return_value = [self.Plugin2,
|
||||
self.Plugin3]
|
||||
with self.assertExitCode(
|
||||
exceptions.MultiplePluginsFound.error_code):
|
||||
plugin_cmd.show("p", platform="p2_ns")
|
||||
self.assertEqual("Multiple plugins found:\n",
|
||||
out.getvalue())
|
||||
mock_plugin_get_all.assert_called_once_with(
|
||||
platform="p2_ns")
|
||||
|
||||
mock_plugin_commands__print_plugins_list.assert_called_once_with([
|
||||
mock__print_plugins_list.assert_called_once_with([
|
||||
self.Plugin2, self.Plugin3])
|
||||
|
||||
@ddt.data(
|
||||
@@ -144,17 +150,17 @@ class PluginCommandsTestCase(test.TestCase):
|
||||
def test_list_not_found(self, name, platform, text):
|
||||
|
||||
with utils.StdOutCapture() as out:
|
||||
plugin_cmd.PluginCommands().list(None, name, platform)
|
||||
plugin_cmd.list_(name, platform=platform)
|
||||
self.assertEqual(text, out.getvalue())
|
||||
|
||||
@mock.patch("rally.cli.commands.plugin.PluginCommands._print_plugins_list")
|
||||
def test_list(self, mock_plugin_commands__print_plugins_list):
|
||||
@mock.patch("rally.cli.commands.plugin._print_plugins_list")
|
||||
def test_list(self, mock__print_plugins_list):
|
||||
|
||||
plugin_cmd.PluginCommands().list(None, None, "p1_ns")
|
||||
plugin_cmd.PluginCommands().list(None, "p1", "p1_ns")
|
||||
plugin_cmd.PluginCommands().list(None, "p2", None)
|
||||
plugin_cmd.list_(None, platform="p1_ns")
|
||||
plugin_cmd.list_("p1", platform="p1_ns")
|
||||
plugin_cmd.list_("p2")
|
||||
|
||||
mock_plugin_commands__print_plugins_list.assert_has_calls([
|
||||
mock__print_plugins_list.assert_has_calls([
|
||||
mock.call([self.Plugin1]),
|
||||
mock.call([self.Plugin1]),
|
||||
mock.call([self.Plugin2])
|
||||
|
||||
@@ -38,8 +38,8 @@ class TaskCommandsTestCase(test.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TaskCommandsTestCase, self).setUp()
|
||||
self.task = task.TaskCommands()
|
||||
self.fake_api = fakes.FakeAPI()
|
||||
cliutils.set_api(self.fake_api)
|
||||
|
||||
with mock.patch("rally.api.API.check_db_revision"):
|
||||
self.real_api = api.API()
|
||||
@@ -53,14 +53,14 @@ class TaskCommandsTestCase(test.TestCase):
|
||||
mock.mock_open(read_data=input_task).return_value,
|
||||
mock.mock_open(read_data="{'test': 1}").return_value
|
||||
]
|
||||
task_conf = self.task._load_and_validate_task(
|
||||
task_conf = task._load_and_validate_task(
|
||||
self.real_api, "in_task", args_file="in_args_path")
|
||||
self.assertEqual({"ab": 1}, task_conf)
|
||||
|
||||
mock_open.side_effect = [
|
||||
mock.mock_open(read_data=input_task).return_value
|
||||
]
|
||||
task_conf = self.task._load_and_validate_task(
|
||||
task_conf = task._load_and_validate_task(
|
||||
self.real_api, "in_task", raw_args=input_args)
|
||||
self.assertEqual({"ab": 2}, task_conf)
|
||||
|
||||
@@ -68,7 +68,7 @@ class TaskCommandsTestCase(test.TestCase):
|
||||
mock.mock_open(read_data=input_task).return_value,
|
||||
mock.mock_open(read_data="{'test': 1}").return_value
|
||||
]
|
||||
task_conf = self.task._load_and_validate_task(
|
||||
task_conf = task._load_and_validate_task(
|
||||
self.real_api, "in_task", raw_args=input_args,
|
||||
args_file="any_file")
|
||||
self.assertEqual({"ab": 2}, task_conf)
|
||||
@@ -77,7 +77,7 @@ class TaskCommandsTestCase(test.TestCase):
|
||||
mock.mock_open(read_data=input_task).return_value,
|
||||
mock.mock_open(read_data="{'test': 1}").return_value
|
||||
]
|
||||
task_conf = self.task._load_and_validate_task(
|
||||
task_conf = task._load_and_validate_task(
|
||||
self.real_api, "in_task", raw_args="test=2",
|
||||
args_file="any_file")
|
||||
self.assertEqual({"ab": 2}, task_conf)
|
||||
@@ -96,7 +96,7 @@ class TaskCommandsTestCase(test.TestCase):
|
||||
mock_open.side_effect = open_return_value
|
||||
|
||||
e = self.assertRaises(task.FailedToLoadTask,
|
||||
self.task._load_and_validate_task,
|
||||
task._load_and_validate_task,
|
||||
self.fake_api, task_file="in_task",
|
||||
args_file="in_args_path")
|
||||
self.assertEqual("Invalid --task-args-file passed:\n\n\t Error "
|
||||
@@ -109,7 +109,7 @@ class TaskCommandsTestCase(test.TestCase):
|
||||
task_file = __file__
|
||||
|
||||
e = self.assertRaises(task.FailedToLoadTask,
|
||||
self.task._load_and_validate_task, self.real_api,
|
||||
task._load_and_validate_task, self.real_api,
|
||||
task_file, raw_args="{'test': {}")
|
||||
self.assertEqual("Invalid --task-args passed:\n\n\t Value has to be "
|
||||
"YAML or JSON. Details:\n\nfoo", e.format_message())
|
||||
@@ -118,7 +118,7 @@ class TaskCommandsTestCase(test.TestCase):
|
||||
# the case #2
|
||||
mock_safe_load.reset_mock()
|
||||
e = self.assertRaises(task.FailedToLoadTask,
|
||||
self.task._load_and_validate_task, self.real_api,
|
||||
task._load_and_validate_task, self.real_api,
|
||||
task_file, raw_args="[]")
|
||||
self.assertEqual("Invalid --task-args passed:\n\n\t Value has to be "
|
||||
"YAML or JSON. Details:\n\nfoo", e.format_message())
|
||||
@@ -127,7 +127,7 @@ class TaskCommandsTestCase(test.TestCase):
|
||||
# the case #3
|
||||
mock_safe_load.reset_mock()
|
||||
e = self.assertRaises(task.FailedToLoadTask,
|
||||
self.task._load_and_validate_task, self.real_api,
|
||||
task._load_and_validate_task, self.real_api,
|
||||
task_file, raw_args="foo")
|
||||
self.assertEqual("Invalid --task-args passed:\n\n\t Value has to be "
|
||||
"YAML or JSON. Details:\n\nfoo", e.format_message())
|
||||
@@ -139,11 +139,11 @@ class TaskCommandsTestCase(test.TestCase):
|
||||
mock.mock_open(read_data="{'test': {{t}}}").return_value
|
||||
]
|
||||
e = self.assertRaises(task.FailedToLoadTask,
|
||||
self.task._load_and_validate_task, self.real_api,
|
||||
task._load_and_validate_task, self.real_api,
|
||||
"in_task")
|
||||
self.assertEqual("Invalid --task passed:\n\n\t Failed to render task "
|
||||
"template.\n\nPlease specify template task argument: "
|
||||
"t", e.format_message())
|
||||
self.assertEqual("Invalid task file passed:\n\n\t Failed to render "
|
||||
"task template.\n\nPlease specify template task "
|
||||
"argument: t", e.format_message())
|
||||
|
||||
@mock.patch("rally.cli.commands.task.yaml")
|
||||
@mock.patch("rally.cli.commands.task.open", create=True)
|
||||
@@ -154,9 +154,9 @@ class TaskCommandsTestCase(test.TestCase):
|
||||
mock_yaml.safe_load.side_effect = Exception("ERROR!!!PANIC!!!")
|
||||
|
||||
e = self.assertRaises(task.FailedToLoadTask,
|
||||
self.task._load_and_validate_task, self.fake_api,
|
||||
task._load_and_validate_task, self.fake_api,
|
||||
"in_task")
|
||||
self.assertEqual("Invalid --task passed:\n\n\t Wrong format of "
|
||||
self.assertEqual("Invalid task file passed:\n\n\t Wrong format of "
|
||||
"rendered input task. It should be YAML or JSON. "
|
||||
"Details:\n\nERROR!!!PANIC!!!", e.format_message())
|
||||
|
||||
@@ -166,8 +166,8 @@ class TaskCommandsTestCase(test.TestCase):
|
||||
"samples/tasks/scenarios/dummy/dummy.json")
|
||||
input_task = "{%% include \"%s\" %%}" % os.path.basename(
|
||||
other_template_path)
|
||||
expect = self.task._load_and_validate_task(self.real_api,
|
||||
other_template_path)
|
||||
expect = task._load_and_validate_task(self.real_api,
|
||||
other_template_path)
|
||||
|
||||
with mock.patch("rally.cli.commands.task.open",
|
||||
create=True) as mock_open:
|
||||
@@ -176,8 +176,8 @@ class TaskCommandsTestCase(test.TestCase):
|
||||
]
|
||||
input_task_file = os.path.join(
|
||||
os.path.dirname(other_template_path), "input_task.json")
|
||||
actual = self.task._load_and_validate_task(self.real_api,
|
||||
input_task_file)
|
||||
actual = task._load_and_validate_task(self.real_api,
|
||||
input_task_file)
|
||||
self.assertEqual(expect, actual)
|
||||
|
||||
@mock.patch("rally.cli.commands.task.open", create=True)
|
||||
@@ -185,20 +185,20 @@ class TaskCommandsTestCase(test.TestCase):
|
||||
mock_open.side_effect = IOError
|
||||
|
||||
e = self.assertRaises(task.FailedToLoadTask,
|
||||
self.task._load_and_validate_task,
|
||||
task._load_and_validate_task,
|
||||
api=self.fake_api, task_file="some_task",
|
||||
raw_args="task_args", args_file="task_args_file")
|
||||
self.assertEqual(
|
||||
"Invalid --task passed:\n\n\t Error reading some_task: ",
|
||||
"Invalid task file passed:\n\n\t Error reading some_task: ",
|
||||
e.format_message())
|
||||
|
||||
@mock.patch("rally.cli.commands.task.version")
|
||||
@mock.patch("rally.cli.commands.task.TaskCommands.use")
|
||||
@mock.patch("rally.cli.commands.task.TaskCommands._detailed")
|
||||
@mock.patch("rally.cli.commands.task.TaskCommands._load_and_validate_task",
|
||||
@mock.patch("rally.cli.commands.task._use")
|
||||
@mock.patch("rally.cli.commands.task._detailed")
|
||||
@mock.patch("rally.cli.commands.task._load_and_validate_task",
|
||||
return_value={"some": "json"})
|
||||
def test_start(self, mock__load_and_validate_task, mock__detailed,
|
||||
mock_use, mock_version):
|
||||
mock__use, mock_version):
|
||||
deployment_id = "e0617de9-77d1-4875-9b49-9d5789e29f20"
|
||||
task_path = "path_to_config.json"
|
||||
fake_task = fakes.FakeTask(uuid="some_new_uuid", tags=["tag"])
|
||||
@@ -207,9 +207,8 @@ class TaskCommandsTestCase(test.TestCase):
|
||||
self.fake_api.task.validate.return_value = fakes.FakeTask(
|
||||
some="json", uuid="some_uuid", temporary=True)
|
||||
|
||||
val = self.task.start(self.fake_api, task_path,
|
||||
deployment_id, do_use=True)
|
||||
self.assertEqual(2, val)
|
||||
with self.assertExitCode(2):
|
||||
task.start(task_path, deployment=deployment_id)
|
||||
mock_version.version_string.assert_called_once_with()
|
||||
self.fake_api.task.create.assert_called_once_with(
|
||||
deployment=deployment_id, tags=None)
|
||||
@@ -220,16 +219,14 @@ class TaskCommandsTestCase(test.TestCase):
|
||||
abort_on_sla_failure=False)
|
||||
mock__load_and_validate_task.assert_called_once_with(
|
||||
self.fake_api, task_path, args_file=None, raw_args=None)
|
||||
mock_use.assert_called_once_with(self.fake_api, "some_new_uuid")
|
||||
mock__use.assert_called_once_with(self.fake_api, "some_new_uuid")
|
||||
mock__detailed.assert_called_once_with(self.fake_api,
|
||||
task_id=fake_task["uuid"])
|
||||
mock__detailed.return_value = 0
|
||||
val1 = self.task.start(self.fake_api, task_path,
|
||||
deployment_id, do_use=True)
|
||||
self.assertEqual(0, val1)
|
||||
task.start(task_path, deployment=deployment_id)
|
||||
|
||||
@mock.patch("rally.cli.commands.task.TaskCommands._detailed")
|
||||
@mock.patch("rally.cli.commands.task.TaskCommands._load_and_validate_task",
|
||||
@mock.patch("rally.cli.commands.task._detailed")
|
||||
@mock.patch("rally.cli.commands.task._load_and_validate_task",
|
||||
return_value="some_config")
|
||||
def test_start_on_unfinished_deployment(self, mock__load_and_validate_task,
|
||||
mock__detailed):
|
||||
@@ -244,13 +241,12 @@ class TaskCommandsTestCase(test.TestCase):
|
||||
uuid=deployment_id,
|
||||
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",
|
||||
tags=["some_tag"]))
|
||||
with self.assertExitCode(1):
|
||||
task.start(task_path, deployment="any", tags=["some_tag"])
|
||||
self.assertFalse(mock__detailed.called)
|
||||
|
||||
@mock.patch("rally.cli.commands.task.TaskCommands._detailed")
|
||||
@mock.patch("rally.cli.commands.task.TaskCommands._load_and_validate_task",
|
||||
@mock.patch("rally.cli.commands.task._detailed")
|
||||
@mock.patch("rally.cli.commands.task._load_and_validate_task",
|
||||
return_value="some_config")
|
||||
def test_start_with_task_args(self, mock__load_and_validate_task,
|
||||
mock__detailed):
|
||||
@@ -259,13 +255,14 @@ class TaskCommandsTestCase(test.TestCase):
|
||||
uuid="new_uuid", tags=["some_tag"])
|
||||
self.fake_api.task.validate.return_value = fakes.FakeTask(
|
||||
uuid="some_id")
|
||||
mock__detailed.return_value = 0
|
||||
|
||||
task_path = "path_to_config.json"
|
||||
task_args = "task_args"
|
||||
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,
|
||||
tags=["some_tag"])
|
||||
task.start(task_path, deployment="any",
|
||||
task_args=task_args, task_args_file=task_args_file,
|
||||
tags=["some_tag"])
|
||||
|
||||
mock__load_and_validate_task.assert_called_once_with(
|
||||
self.fake_api, task_path, raw_args=task_args,
|
||||
@@ -282,14 +279,8 @@ class TaskCommandsTestCase(test.TestCase):
|
||||
self.fake_api.task.create.assert_called_once_with(
|
||||
deployment="any", tags=["some_tag"])
|
||||
|
||||
@mock.patch("rally.cli.commands.task.envutils.get_global")
|
||||
def test_start_no_deployment_id(self, mock_get_global):
|
||||
mock_get_global.side_effect = exceptions.InvalidArgumentsException
|
||||
self.assertRaises(exceptions.InvalidArgumentsException,
|
||||
self.task.start, "path_to_config.json", None)
|
||||
|
||||
@mock.patch("rally.cli.commands.task.TaskCommands._detailed")
|
||||
@mock.patch("rally.cli.commands.task.TaskCommands._load_and_validate_task")
|
||||
@mock.patch("rally.cli.commands.task._detailed")
|
||||
@mock.patch("rally.cli.commands.task._load_and_validate_task")
|
||||
def test_start_invalid_task(self, mock__load_and_validate_task,
|
||||
mock__detailed):
|
||||
task_obj = fakes.FakeTask(temporary=False, tag="tag", uuid="uuid")
|
||||
@@ -299,8 +290,8 @@ class TaskCommandsTestCase(test.TestCase):
|
||||
mock__load_and_validate_task.side_effect = exc
|
||||
|
||||
self.assertRaises(exceptions.InvalidTaskException,
|
||||
self.task.start, self.fake_api, "task_path",
|
||||
"deployment", tags=["tag"])
|
||||
task.start, "task_path",
|
||||
deployment="deployment", tags=["tag"])
|
||||
|
||||
self.assertFalse(self.fake_api.task.create.called)
|
||||
self.assertFalse(self.fake_api.task.start.called)
|
||||
@@ -311,8 +302,8 @@ class TaskCommandsTestCase(test.TestCase):
|
||||
self.fake_api.task.start.side_effect = KeyError()
|
||||
|
||||
self.assertRaises(KeyError,
|
||||
self.task.start, self.fake_api, "task_path",
|
||||
"deployment", tags=["tag"])
|
||||
task.start, "task_path",
|
||||
deployment="deployment", tags=["tag"])
|
||||
|
||||
self.fake_api.task.create.assert_called_once_with(
|
||||
deployment="deployment", tags=["tag"])
|
||||
@@ -324,7 +315,7 @@ class TaskCommandsTestCase(test.TestCase):
|
||||
|
||||
self.assertFalse(mock__detailed.called)
|
||||
|
||||
@mock.patch("rally.cli.commands.task.TaskCommands._start_task")
|
||||
@mock.patch("rally.cli.commands.task._start_task")
|
||||
@ddt.data({"scenario": None},
|
||||
{"scenario": "scenario_name"},
|
||||
{"scenario": "none_name"})
|
||||
@@ -350,17 +341,13 @@ class TaskCommandsTestCase(test.TestCase):
|
||||
]
|
||||
}
|
||||
if scenario == "none_name":
|
||||
self.assertEqual(
|
||||
1,
|
||||
self.task.restart(self.fake_api, "deployment_uuid",
|
||||
"task_uuid", scenarios=scenario)
|
||||
)
|
||||
with self.assertExitCode(1):
|
||||
task.restart(deployment="deployment_uuid",
|
||||
task_id="task_uuid", scenarios=scenario)
|
||||
else:
|
||||
self.assertEqual(
|
||||
mock__start_task.return_value,
|
||||
self.task.restart(self.fake_api, "deployment_uuid",
|
||||
"task_uuid", scenarios=scenario)
|
||||
)
|
||||
mock__start_task.return_value = 0
|
||||
task.restart(deployment="deployment_uuid",
|
||||
task_id="task_uuid", scenarios=scenario)
|
||||
self.fake_api.task.get.assert_called_once_with(task_id="task_uuid",
|
||||
detailed=True)
|
||||
|
||||
@@ -378,38 +365,24 @@ class TaskCommandsTestCase(test.TestCase):
|
||||
"msg": ""
|
||||
}
|
||||
}
|
||||
self.assertEqual(
|
||||
1,
|
||||
self.task.restart(self.fake_api, "deployment_uuid", "task_uuid")
|
||||
)
|
||||
with self.assertExitCode(1):
|
||||
task.restart(deployment="deployment_uuid", task_id="task_uuid")
|
||||
self.fake_api.task.get.assert_called_once_with(task_id="task_uuid",
|
||||
detailed=True)
|
||||
|
||||
def test_abort(self):
|
||||
test_uuid = "17860c43-2274-498d-8669-448eff7b073f"
|
||||
self.task.abort(self.fake_api, test_uuid)
|
||||
task.abort(test_uuid)
|
||||
self.fake_api.task.abort.assert_called_once_with(
|
||||
task_uuid=test_uuid, soft=False, wait=True)
|
||||
|
||||
@mock.patch("rally.cli.commands.task.envutils.get_global")
|
||||
def test_abort_no_task_id(self, mock_get_global):
|
||||
mock_get_global.side_effect = exceptions.InvalidArgumentsException
|
||||
self.assertRaises(exceptions.InvalidArgumentsException,
|
||||
self.task.abort, self.fake_api, None)
|
||||
|
||||
def test_status(self):
|
||||
test_uuid = "a3e7cefb-bec2-4802-89f6-410cc31f71af"
|
||||
value = {"task_id": "task", "status": "status"}
|
||||
self.fake_api.task.get.return_value = value
|
||||
self.task.status(self.fake_api, test_uuid)
|
||||
task.status(task_id=test_uuid)
|
||||
self.fake_api.task.get.assert_called_once_with(task_id=test_uuid)
|
||||
|
||||
@mock.patch("rally.cli.commands.task.envutils.get_global")
|
||||
def test_status_no_task_id(self, mock_get_global):
|
||||
mock_get_global.side_effect = exceptions.InvalidArgumentsException
|
||||
self.assertRaises(exceptions.InvalidArgumentsException,
|
||||
self.task.status, None)
|
||||
|
||||
@ddt.data({"iterations_data": False, "has_output": True,
|
||||
"filters": None},
|
||||
{"iterations_data": True, "has_output": False,
|
||||
@@ -549,9 +522,8 @@ class TaskCommandsTestCase(test.TestCase):
|
||||
detailed_value["subtasks"][0]["workloads"][0]["output"] = {
|
||||
"additive": [], "complete": []}
|
||||
self.fake_api.task.get.return_value = detailed_value
|
||||
self.task.detailed(self.fake_api, test_uuid,
|
||||
iterations_data=iterations_data,
|
||||
filters=filters)
|
||||
task.detailed(task_id=test_uuid, iterations_data=iterations_data,
|
||||
filters=filters)
|
||||
self.fake_api.task.get.assert_called_once_with(
|
||||
task_id=test_uuid, detailed=True)
|
||||
|
||||
@@ -574,7 +546,7 @@ class TaskCommandsTestCase(test.TestCase):
|
||||
self.fake_api.task.get.return_value = value
|
||||
|
||||
mock_logging.is_debug.return_value = debug
|
||||
self.task.detailed(self.fake_api, test_uuid)
|
||||
task.detailed(task_id=test_uuid)
|
||||
if debug:
|
||||
expected_calls = [
|
||||
mock.call("Task test_task_id: crashed"),
|
||||
@@ -599,23 +571,17 @@ class TaskCommandsTestCase(test.TestCase):
|
||||
"results": []
|
||||
}
|
||||
self.fake_api.task.get.return_value = value
|
||||
self.task.detailed(self.fake_api, test_uuid)
|
||||
task.detailed(task_id=test_uuid)
|
||||
expected_calls = [mock.call("Task test_task_id: init"),
|
||||
mock.call("\nThe task test_task_id marked as "
|
||||
"'init'. Results available when it "
|
||||
"is 'finished'.")]
|
||||
mock_stdout.write.assert_has_calls(expected_calls, any_order=True)
|
||||
|
||||
@mock.patch("rally.cli.commands.task.envutils.get_global")
|
||||
def test_detailed_no_task_id(self, mock_get_global):
|
||||
mock_get_global.side_effect = exceptions.InvalidArgumentsException
|
||||
self.assertRaises(exceptions.InvalidArgumentsException,
|
||||
self.task.detailed, None)
|
||||
|
||||
def test_detailed_wrong_id(self):
|
||||
test_uuid = "eb290c30-38d8-4c8f-bbcc-fc8f74b004ae"
|
||||
self.fake_api.task.get.side_effect = None
|
||||
self.task.detailed(self.fake_api, test_uuid)
|
||||
task.detailed(task_id=test_uuid)
|
||||
self.fake_api.task.get.assert_called_once_with(
|
||||
task_id=test_uuid, detailed=True)
|
||||
|
||||
@@ -699,7 +665,7 @@ class TaskCommandsTestCase(test.TestCase):
|
||||
|
||||
self.fake_api.task.get.return_value = task_obj
|
||||
|
||||
self.assertIsNone(self.task.results(self.fake_api, task_id))
|
||||
self.assertIsNone(task.results(task_id=task_id))
|
||||
self.assertEqual(1, mock_json_dumps.call_count)
|
||||
self.assertEqual(1, len(mock_json_dumps.call_args[0]))
|
||||
self.assertEqual(list(result), mock_json_dumps.call_args[0][0])
|
||||
@@ -721,7 +687,8 @@ class TaskCommandsTestCase(test.TestCase):
|
||||
task_obj["uuid"] = task_id
|
||||
self.fake_api.task.get.return_value = task_obj
|
||||
|
||||
self.assertEqual(1, self.task.results(self.fake_api, task_id))
|
||||
with self.assertExitCode(1):
|
||||
task.results(task_id=task_id)
|
||||
|
||||
self.fake_api.task.get.assert_called_once_with(
|
||||
task_id=task_id, detailed=True)
|
||||
@@ -733,33 +700,46 @@ class TaskCommandsTestCase(test.TestCase):
|
||||
consts.TaskStatus.ABORTED)))
|
||||
mock_stdout.write.assert_has_calls([mock.call(expected_out)])
|
||||
|
||||
def test_trends(self):
|
||||
self.task.export = mock.MagicMock()
|
||||
self.task.trends(self.fake_api,
|
||||
tasks=["uuid"],
|
||||
out="output.html")
|
||||
self.task.export.assert_called_once_with(
|
||||
@mock.patch("rally.cli.commands.task._export")
|
||||
def test_trends(self, mock__export):
|
||||
task.trends(["uuid"], out="output.html")
|
||||
mock__export.assert_called_once_with(
|
||||
self.fake_api, tasks=["uuid"], output_type="trends-html",
|
||||
output_dest="output.html", open_it=False)
|
||||
|
||||
def test_trends_no_tasks_given(self):
|
||||
ret = self.task.trends(self.fake_api, tasks=[],
|
||||
out="output.html", out_format="html")
|
||||
self.assertEqual(1, ret)
|
||||
@mock.patch("rally.cli.commands.task._export")
|
||||
def test_trends_html_static(self, mock__export):
|
||||
task.trends(["uuid"], out="output.html", html_static=True)
|
||||
mock__export.assert_called_once_with(
|
||||
self.fake_api, tasks=["uuid"],
|
||||
output_type="trends-html-static",
|
||||
output_dest="output.html", open_it=False)
|
||||
|
||||
def test_report(self):
|
||||
self.task.export = mock.MagicMock()
|
||||
self.task.report(self.fake_api, tasks="uuid",
|
||||
out="out", open_it=False, out_format="junit-xml")
|
||||
self.task.export.assert_called_once_with(
|
||||
self.fake_api, tasks="uuid", output_type="junit-xml",
|
||||
output_dest="out", open_it=False, deployment=None
|
||||
)
|
||||
def test_trends_no_tasks_given(self):
|
||||
with self.assertExitCode(1):
|
||||
task.trends([], out="output.html")
|
||||
|
||||
@mock.patch("rally.cli.commands.task._export")
|
||||
def test_report(self, mock__export):
|
||||
task.report("uuid", out="out")
|
||||
mock__export.assert_called_once_with(
|
||||
self.fake_api, tasks="uuid", output_type="html",
|
||||
output_dest="out", open_it=False, deployment=None)
|
||||
|
||||
mock__export.reset_mock()
|
||||
task.report("uuid", out="out", to_json=True)
|
||||
mock__export.assert_called_once_with(
|
||||
self.fake_api, tasks="uuid", output_type="json",
|
||||
output_dest="out", open_it=False, deployment=None)
|
||||
|
||||
mock__export.reset_mock()
|
||||
task.report("uuid", out="out", html_static=True)
|
||||
mock__export.assert_called_once_with(
|
||||
self.fake_api, tasks="uuid", output_type="html-static",
|
||||
output_dest="out", open_it=False, deployment=None)
|
||||
|
||||
@mock.patch("rally.cli.commands.task.cliutils.print_list")
|
||||
@mock.patch("rally.cli.commands.task.envutils.get_global",
|
||||
return_value="123456789")
|
||||
def test_list(self, mock_get_global, mock_print_list):
|
||||
def test_list(self, mock_print_list):
|
||||
self.fake_api.task.list.return_value = [
|
||||
{"uuid": "a",
|
||||
"created_at": "2007-01-01T00:00:01",
|
||||
@@ -767,9 +747,9 @@ class TaskCommandsTestCase(test.TestCase):
|
||||
"status": consts.TaskStatus.RUNNING,
|
||||
"tags": ["d"],
|
||||
"deployment_name": "some_name"}]
|
||||
self.task.list(self.fake_api, status="running")
|
||||
task.list_(deployment="123456789", status="running")
|
||||
self.fake_api.task.list.assert_called_once_with(
|
||||
deployment=mock_get_global.return_value,
|
||||
deployment="123456789",
|
||||
status=consts.TaskStatus.RUNNING)
|
||||
|
||||
headers = ["UUID", "Deployment name", "Created at", "Load duration",
|
||||
@@ -781,9 +761,7 @@ class TaskCommandsTestCase(test.TestCase):
|
||||
sortby_index=headers.index("Created at"),
|
||||
formatters=mock.ANY)
|
||||
|
||||
@mock.patch("rally.cli.commands.task.envutils.get_global",
|
||||
return_value="123456789")
|
||||
def test_list_uuids_only(self, mock_get_global):
|
||||
def test_list_uuids_only(self):
|
||||
self.fake_api.task.list.return_value = [
|
||||
{"uuid": "a",
|
||||
"created_at": "2007-01-01T00:00:01",
|
||||
@@ -793,31 +771,31 @@ class TaskCommandsTestCase(test.TestCase):
|
||||
"deployment_name": "some_name"}]
|
||||
out = io.StringIO()
|
||||
with mock.patch.object(sys, "stdout", new=out):
|
||||
self.task.list(self.fake_api, status="running", uuids_only=True)
|
||||
task.list_(deployment="123456789", status="running",
|
||||
uuids_only=True)
|
||||
self.assertEqual("a\n", out.getvalue())
|
||||
self.fake_api.task.list.assert_called_once_with(
|
||||
deployment=mock_get_global.return_value,
|
||||
deployment="123456789",
|
||||
status=consts.TaskStatus.RUNNING)
|
||||
|
||||
def test_list_wrong_status(self):
|
||||
self.assertEqual(1, self.task.list(self.fake_api, deployment="fake",
|
||||
status="wrong non existing status"))
|
||||
with self.assertExitCode(1):
|
||||
task.list_(deployment="fake",
|
||||
status="wrong non existing status")
|
||||
|
||||
def test_list_no_results(self):
|
||||
self.fake_api.task.list.return_value = []
|
||||
self.assertIsNone(self.task.list(self.fake_api, deployment="fake",
|
||||
all_deployments=True))
|
||||
self.assertIsNone(task.list_(deployment="fake",
|
||||
all_deployments=True))
|
||||
self.fake_api.task.list.assert_called_once_with()
|
||||
self.fake_api.task.list.reset_mock()
|
||||
|
||||
self.assertIsNone(self.task.list(self.fake_api, deployment="d",
|
||||
status=consts.TaskStatus.RUNNING))
|
||||
self.assertIsNone(task.list_(deployment="d",
|
||||
status=consts.TaskStatus.RUNNING))
|
||||
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):
|
||||
def test_list_output(self):
|
||||
self.fake_api.task.list.return_value = [
|
||||
{"uuid": "UUID-1",
|
||||
"created_at": "2007-01-01T00:00:01",
|
||||
@@ -844,7 +822,7 @@ class TaskCommandsTestCase(test.TestCase):
|
||||
|
||||
with mock.patch.object(task.cliutils, "print_list",
|
||||
new=print_list):
|
||||
self.task.list(self.fake_api, status="running")
|
||||
task.list_(deployment="123456789", status="running")
|
||||
|
||||
self.assertEqual(1, len(print_list_calls))
|
||||
|
||||
@@ -866,7 +844,7 @@ class TaskCommandsTestCase(test.TestCase):
|
||||
def test_delete(self):
|
||||
task_uuid = "8dcb9c5e-d60b-4022-8975-b5987c7833f7"
|
||||
force = False
|
||||
self.task.delete(self.fake_api, task_uuid, force=force)
|
||||
task.delete(task_id=task_uuid, force=force)
|
||||
self.fake_api.task.delete.assert_called_once_with(
|
||||
task_uuid=task_uuid, force=force)
|
||||
|
||||
@@ -876,7 +854,7 @@ class TaskCommandsTestCase(test.TestCase):
|
||||
"6a3cb11c-ac75-41e7-8ae7-935732bfb48f",
|
||||
"018af931-0e5a-40d5-9d6f-b13f4a3a09fc"]
|
||||
force = False
|
||||
self.task.delete(self.fake_api, task_uuids, force=force)
|
||||
task.delete(task_id=task_uuids, force=force)
|
||||
self.assertTrue(
|
||||
self.fake_api.task.delete.call_count == len(task_uuids))
|
||||
expected_calls = [mock.call(task_uuid=task_uuid,
|
||||
@@ -892,17 +870,15 @@ class TaskCommandsTestCase(test.TestCase):
|
||||
"pos": 0, "success": False, "detail": "Max foo, actually bar"}]
|
||||
|
||||
self.fake_api.task.get.return_value = task_obj
|
||||
result = self.task.sla_check(self.fake_api, task_id="fake_task_id")
|
||||
self.assertEqual(1, result)
|
||||
with self.assertExitCode(1):
|
||||
task.sla_check(task_id="fake_task_id")
|
||||
self.fake_api.task.get.assert_called_with(
|
||||
task_id="fake_task_id", detailed=True)
|
||||
|
||||
task_obj["subtasks"][0]["workloads"][0]["sla_results"]["sla"][0][
|
||||
"success"] = True
|
||||
|
||||
result = self.task.sla_check(self.fake_api, task_id="fake_task_id",
|
||||
tojson=True)
|
||||
self.assertEqual(0, result)
|
||||
task.sla_check(task_id="fake_task_id", tojson=True)
|
||||
|
||||
@mock.patch("rally.cli.commands.task.os.path.isfile", return_value=True)
|
||||
@mock.patch("rally.cli.commands.task.open",
|
||||
@@ -911,12 +887,12 @@ class TaskCommandsTestCase(test.TestCase):
|
||||
def test_validate(self, mock_open, mock_os_path_isfile):
|
||||
self.fake_api.task.render_template = self.real_api.task.render_template
|
||||
|
||||
self.task.validate(self.fake_api, "path_to_config.json", "fake_id")
|
||||
task.validate("path_to_config.json", deployment="fake_id")
|
||||
|
||||
self.fake_api.task.validate.assert_called_once_with(
|
||||
deployment="fake_id", config={"some": "json"})
|
||||
|
||||
@mock.patch("rally.cli.commands.task.TaskCommands._load_and_validate_task",
|
||||
@mock.patch("rally.cli.commands.task._load_and_validate_task",
|
||||
side_effect=task.FailedToLoadTask)
|
||||
def test_validate_failed_to_load_task(self, mock__load_and_validate_task):
|
||||
args = "args"
|
||||
@@ -924,21 +900,21 @@ class TaskCommandsTestCase(test.TestCase):
|
||||
|
||||
mock__load_and_validate_task.side_effect = KeyError("foo")
|
||||
|
||||
self.assertRaises(KeyError, self.task.validate, self.real_api,
|
||||
"path_to_task", "fake_deployment_id",
|
||||
self.assertRaises(KeyError, task.validate, "path_to_task",
|
||||
deployment="fake_deployment_id",
|
||||
task_args=args, task_args_file=args_file)
|
||||
self.assertFalse(self.fake_api.task.validate.called)
|
||||
|
||||
mock__load_and_validate_task.assert_called_once_with(
|
||||
self.real_api, "path_to_task", raw_args=args, args_file=args_file)
|
||||
self.fake_api, "path_to_task", raw_args=args, args_file=args_file)
|
||||
|
||||
@mock.patch("rally.cli.commands.task.TaskCommands._load_and_validate_task")
|
||||
@mock.patch("rally.cli.commands.task._load_and_validate_task")
|
||||
def test_validate_invalid(self, mock__load_and_validate_task):
|
||||
exc = exceptions.InvalidTaskException("foo")
|
||||
self.fake_api.task.validate.side_effect = exc
|
||||
self.assertRaises(exceptions.InvalidTaskException,
|
||||
self.task.validate, self.fake_api, "path_to_task",
|
||||
"deployment")
|
||||
task.validate, "path_to_task",
|
||||
deployment="deployment")
|
||||
self.fake_api.task.validate.assert_called_once_with(
|
||||
deployment="deployment",
|
||||
config=mock__load_and_validate_task.return_value)
|
||||
@@ -946,7 +922,7 @@ class TaskCommandsTestCase(test.TestCase):
|
||||
@mock.patch("rally.cli.envutils._rewrite_env_file")
|
||||
def test_use(self, mock__rewrite_env_file):
|
||||
task_id = "80422553-5774-44bd-98ac-38bd8c7a0feb"
|
||||
self.task.use(self.fake_api, task_id)
|
||||
task.use(task_id)
|
||||
mock__rewrite_env_file.assert_called_once_with(
|
||||
os.path.expanduser("~/.rally/globals"),
|
||||
["RALLY_TASK=%s\n" % task_id])
|
||||
@@ -956,8 +932,8 @@ class TaskCommandsTestCase(test.TestCase):
|
||||
exc = exceptions.DBRecordNotFound(criteria="uuid: %s" % task_id,
|
||||
table="tasks")
|
||||
self.fake_api.task.get.side_effect = exc
|
||||
self.assertRaises(exceptions.DBRecordNotFound, self.task.use,
|
||||
self.fake_api, task_id)
|
||||
self.assertRaises(exceptions.DBRecordNotFound, task.use,
|
||||
task_id)
|
||||
|
||||
@mock.patch("rally.cli.task_results_loader.load")
|
||||
@mock.patch("rally.cli.commands.task.os.path")
|
||||
@@ -977,9 +953,9 @@ class TaskCommandsTestCase(test.TestCase):
|
||||
mock_open.side_effect = mock_fd
|
||||
mock_load.return_value = [{"task": "task_1"}, {"task": "task2"}]
|
||||
|
||||
self.task.export(self.fake_api, tasks=["uuid", "file"],
|
||||
output_type="json", output_dest="output_dest",
|
||||
open_it=True)
|
||||
task._export(self.fake_api, tasks=["uuid", "file"],
|
||||
output_type="json", output_dest="output_dest",
|
||||
open_it=True)
|
||||
|
||||
self.fake_api.task.export.assert_called_once_with(
|
||||
tasks=["uuid"] + mock_load.return_value,
|
||||
@@ -992,7 +968,7 @@ class TaskCommandsTestCase(test.TestCase):
|
||||
# print
|
||||
self.fake_api.task.export.reset_mock()
|
||||
self.fake_api.task.export.return_value = {"print": "content"}
|
||||
self.task.export(self.fake_api, tasks="uuid", output_type="json")
|
||||
task._export(self.fake_api, tasks="uuid", output_type="json")
|
||||
self.fake_api.task.export.assert_called_once_with(
|
||||
tasks=["uuid"],
|
||||
output_type="json", output_dest=None
|
||||
@@ -1090,7 +1066,7 @@ class TaskCommandsTestCase(test.TestCase):
|
||||
"validation_result": json.dumps([error_type, error_message,
|
||||
error_traceback])
|
||||
}
|
||||
self.task.detailed(self.fake_api, test_uuid)
|
||||
task.detailed(task_id=test_uuid)
|
||||
self.fake_api.task.get.assert_called_once_with(
|
||||
task_id=test_uuid, detailed=True)
|
||||
mock_stdout.write.assert_has_calls([
|
||||
@@ -1104,9 +1080,8 @@ class TaskCommandsTestCase(test.TestCase):
|
||||
mock_os_path.expanduser = lambda path: path
|
||||
mock_load.return_value = ["results"]
|
||||
|
||||
self.task.import_results(self.fake_api,
|
||||
"deployment_uuid",
|
||||
"task_file", tags=["tag"])
|
||||
task.import_results("task_file", deployment="deployment_uuid",
|
||||
tags=["tag"])
|
||||
|
||||
mock_load.assert_called_once_with("task_file")
|
||||
self.fake_api.task.import_results.assert_called_once_with(
|
||||
@@ -1115,9 +1090,6 @@ class TaskCommandsTestCase(test.TestCase):
|
||||
|
||||
# not exist
|
||||
mock_os_path.exists.return_value = False
|
||||
self.assertEqual(
|
||||
1,
|
||||
self.task.import_results(self.fake_api,
|
||||
"deployment_uuid",
|
||||
"task_file", ["tag"])
|
||||
)
|
||||
with self.assertExitCode(1):
|
||||
task.import_results("task_file", deployment="deployment_uuid",
|
||||
tags=["tag"])
|
||||
|
||||
@@ -33,8 +33,8 @@ class VerifyCommandsTestCase(test.TestCase):
|
||||
def setUp(self):
|
||||
super(VerifyCommandsTestCase, self).setUp()
|
||||
|
||||
self.verify = verify.VerifyCommands()
|
||||
self.fake_api = fakes.FakeAPI()
|
||||
cliutils.set_api(self.fake_api)
|
||||
|
||||
self.deployment_name = "Some Deploy"
|
||||
self.deployment_uuid = "some-deploy-uuid"
|
||||
@@ -125,7 +125,7 @@ class VerifyCommandsTestCase(test.TestCase):
|
||||
@mock.patch("rally.cli.commands.verify.logging.is_debug",
|
||||
return_value=True)
|
||||
def test_list_plugins(self, mock_is_debug, mock_print_list):
|
||||
self.verify.list_plugins(self.fake_api, platform="some")
|
||||
verify.list_plugins(platform="some")
|
||||
self.fake_api.verifier.list_plugins.assert_called_once_with(
|
||||
platform="some")
|
||||
|
||||
@@ -134,9 +134,9 @@ class VerifyCommandsTestCase(test.TestCase):
|
||||
self.fake_api.verifier.create.return_value = self.verifier_uuid
|
||||
self.fake_api.verifier.get.return_value = self.verifier_data
|
||||
|
||||
self.verify.create_verifier(self.fake_api, "a", vtype="b",
|
||||
platform="c", source="d", version="e",
|
||||
system_wide=True, extra={})
|
||||
verify.create_verifier(name="a", vtype="b", platform="c",
|
||||
source="d", version="e", system_wide=True,
|
||||
extra={})
|
||||
self.fake_api.verifier.create.assert_called_once_with(
|
||||
name="a", vtype="b", platform="c", source="d", version="e",
|
||||
system_wide=True, extra_settings={})
|
||||
@@ -149,7 +149,7 @@ class VerifyCommandsTestCase(test.TestCase):
|
||||
@mock.patch("rally.cli.commands.verify.envutils.update_globals_file")
|
||||
def test_use_verifier(self, mock_update_globals_file):
|
||||
self.fake_api.verifier.get.return_value = self.verifier_data
|
||||
self.verify.use_verifier(self.fake_api, self.verifier_uuid)
|
||||
verify.use_verifier(verifier_id=self.verifier_uuid)
|
||||
self.fake_api.verifier.get.assert_called_once_with(
|
||||
verifier_id=self.verifier_uuid)
|
||||
mock_update_globals_file.assert_called_once_with(
|
||||
@@ -158,10 +158,10 @@ class VerifyCommandsTestCase(test.TestCase):
|
||||
@mock.patch("rally.cli.commands.verify.cliutils.print_list")
|
||||
def test_list_verifiers_empty_verifiers(self, mock_print_list):
|
||||
self.fake_api.verifier.list.return_value = []
|
||||
self.verify.list_verifiers(self.fake_api)
|
||||
verify.list_verifiers()
|
||||
|
||||
self.verify.list_verifiers(self.fake_api, "foo")
|
||||
self.verify.list_verifiers(self.fake_api)
|
||||
verify.list_verifiers(status="foo")
|
||||
verify.list_verifiers()
|
||||
|
||||
self.fake_api.verifier.list.assert_has_calls(
|
||||
[mock.call(status=None), mock.call(status="foo")])
|
||||
@@ -175,7 +175,7 @@ class VerifyCommandsTestCase(test.TestCase):
|
||||
"Active"]
|
||||
additional_keys = ["normalize_field_names", "sortby_index",
|
||||
"formatters"]
|
||||
self.verify.list_verifiers(self.fake_api)
|
||||
verify.list_verifiers()
|
||||
# astarove: should be replaced on mock_print_list.assert_called_once()
|
||||
self.assertEqual(1, mock_print_list.call_count)
|
||||
self.assertEqual(([self.verifier_data], additional_fields),
|
||||
@@ -183,10 +183,11 @@ class VerifyCommandsTestCase(test.TestCase):
|
||||
self.assertEqual(additional_keys.sort(),
|
||||
list(mock_print_list.call_args[1].keys()).sort())
|
||||
|
||||
@mock.patch("rally.cli.commands.verify._base_dir",
|
||||
return_value="./verifiers/")
|
||||
@mock.patch("rally.cli.commands.verify.envutils.get_global")
|
||||
def test_show_verifier(self, mock_get_global):
|
||||
def test_show_verifier(self, mock_get_global, mock__base_dir):
|
||||
self.fake_api.verifier.get.return_value = self.verifier_data
|
||||
self.verify._base_dir = mock.Mock(return_value="./verifiers/")
|
||||
|
||||
# It is a hard task to mock default value of function argument, so we
|
||||
# need to apply this workaround
|
||||
@@ -200,7 +201,7 @@ class VerifyCommandsTestCase(test.TestCase):
|
||||
|
||||
with mock.patch.object(verify.cliutils, "print_dict",
|
||||
new=print_dict):
|
||||
self.verify.show_verifier(self.fake_api, self.verifier_uuid)
|
||||
verify.show_verifier(self.verifier_uuid)
|
||||
|
||||
self.assertEqual(1, len(print_dict_calls))
|
||||
|
||||
@@ -230,54 +231,54 @@ class VerifyCommandsTestCase(test.TestCase):
|
||||
verifier_id=self.verifier_uuid)
|
||||
|
||||
def test_delete_verifier(self):
|
||||
self.verify.delete_verifier(self.fake_api, "v_id", "d_id", force=True)
|
||||
verify.delete_verifier(verifier_id="v_id", deployment="d_id",
|
||||
force=True)
|
||||
self.fake_api.verifier.delete.assert_called_once_with(
|
||||
verifier_id="v_id", deployment_id="d_id", force=True)
|
||||
|
||||
def test_update_verifier(self):
|
||||
self.verify.update_verifier(self.fake_api, self.verifier_uuid)
|
||||
with self.assertExitCode(1):
|
||||
verify.update_verifier(verifier_id=self.verifier_uuid)
|
||||
self.assertFalse(self.fake_api.verifier.update.called)
|
||||
|
||||
self.verify.update_verifier(self.fake_api, self.verification_uuid,
|
||||
update_venv=True,
|
||||
system_wide=True)
|
||||
with self.assertExitCode(1):
|
||||
verify.update_verifier(verifier_id=self.verification_uuid,
|
||||
update_venv=True, system_wide=True)
|
||||
self.assertFalse(self.fake_api.verifier.update.called)
|
||||
|
||||
self.verify.update_verifier(self.fake_api, self.verification_uuid,
|
||||
system_wide=True,
|
||||
no_system_wide=True)
|
||||
with self.assertExitCode(1):
|
||||
verify.update_verifier(verifier_id=self.verification_uuid,
|
||||
system_wide=True, no_system_wide=True)
|
||||
self.assertFalse(self.fake_api.verifier.update.called)
|
||||
|
||||
self.verify.update_verifier(self.fake_api, self.verification_uuid,
|
||||
version="a",
|
||||
system_wide=True)
|
||||
verify.update_verifier(verifier_id=self.verification_uuid,
|
||||
version="a", system_wide=True)
|
||||
self.fake_api.verifier.update.assert_called_once_with(
|
||||
verifier_id=self.verification_uuid, system_wide=True,
|
||||
version="a", update_venv=None)
|
||||
version="a", update_venv=False)
|
||||
|
||||
@mock.patch("rally.cli.commands.verify.open", create=True)
|
||||
@mock.patch("rally.cli.commands.verify.os.path.exists")
|
||||
def test_configure_verifier(self, mock_exists, mock_open):
|
||||
self.verify.configure_verifier(self.fake_api, self.verifier_uuid,
|
||||
self.deployment_uuid,
|
||||
new_configuration="/p/a/t/h",
|
||||
reconfigure=True,
|
||||
show=True)
|
||||
with self.assertExitCode(1):
|
||||
verify.configure_verifier(verifier_id=self.verifier_uuid,
|
||||
deployment=self.deployment_uuid,
|
||||
new_configuration="/p/a/t/h",
|
||||
reconfigure=True, show=True)
|
||||
self.assertFalse(self.fake_api.verifier.configure.called)
|
||||
|
||||
mock_exists.return_value = False
|
||||
self.verify.configure_verifier(self.fake_api, self.verifier_uuid,
|
||||
self.deployment_uuid,
|
||||
new_configuration="/p/a/t/h",
|
||||
show=True)
|
||||
with self.assertExitCode(1):
|
||||
verify.configure_verifier(verifier_id=self.verifier_uuid,
|
||||
deployment=self.deployment_uuid,
|
||||
new_configuration="/p/a/t/h", show=True)
|
||||
self.assertFalse(self.fake_api.verifier.override_configuration.called)
|
||||
|
||||
mock_exists.return_value = True
|
||||
mock_open.return_value = mock.mock_open(read_data="data").return_value
|
||||
self.verify.configure_verifier(self.fake_api, self.verifier_uuid,
|
||||
self.deployment_uuid,
|
||||
new_configuration="/p/a/t/h",
|
||||
show=True)
|
||||
verify.configure_verifier(verifier_id=self.verifier_uuid,
|
||||
deployment=self.deployment_uuid,
|
||||
new_configuration="/p/a/t/h", show=True)
|
||||
mock_open.assert_called_once_with("/p/a/t/h")
|
||||
self.fake_api.verifier.override_configuration(self.verifier_uuid,
|
||||
self.deployment_uuid,
|
||||
@@ -286,37 +287,39 @@ class VerifyCommandsTestCase(test.TestCase):
|
||||
tf = tempfile.NamedTemporaryFile()
|
||||
with open(tf.name, "w") as f:
|
||||
f.write("[DEFAULT]\nopt = val\n[foo]\nopt = val")
|
||||
self.verify.configure_verifier(self.fake_api, self.verifier_uuid,
|
||||
self.deployment_uuid,
|
||||
extra_options=tf.name)
|
||||
verify.configure_verifier(verifier_id=self.verifier_uuid,
|
||||
deployment=self.deployment_uuid,
|
||||
extra_options=tf.name)
|
||||
expected_options = {"foo": {"opt": "val"},
|
||||
"DEFAULT": {"opt": "val"}}
|
||||
self.fake_api.verifier.configure.assert_called_once_with(
|
||||
verifier=self.verifier_uuid, deployment_id=self.deployment_uuid,
|
||||
extra_options=expected_options, reconfigure=False)
|
||||
|
||||
self.verify.configure_verifier(self.fake_api, self.verifier_uuid,
|
||||
self.deployment_uuid,
|
||||
extra_options="{foo: {opt: val}, "
|
||||
"DEFAULT: {opt: val}}")
|
||||
verify.configure_verifier(verifier_id=self.verifier_uuid,
|
||||
deployment=self.deployment_uuid,
|
||||
extra_options="{foo: {opt: val}, "
|
||||
"DEFAULT: {opt: val}}")
|
||||
self.fake_api.verifier.configure.assert_called_with(
|
||||
verifier=self.verifier_uuid, deployment_id=self.deployment_uuid,
|
||||
extra_options=expected_options, reconfigure=False)
|
||||
|
||||
def test_list_verifier_tests(self):
|
||||
self.fake_api.verifier.list_tests.return_value = ["test_1", "test_2"]
|
||||
self.verify.list_verifier_tests(self.fake_api, self.verifier_uuid, "p")
|
||||
verify.list_verifier_tests(verifier_id=self.verifier_uuid,
|
||||
pattern="p")
|
||||
|
||||
self.fake_api.verifier.list_tests.return_value = []
|
||||
self.verify.list_verifier_tests(self.fake_api, self.verifier_uuid, "p")
|
||||
verify.list_verifier_tests(verifier_id=self.verifier_uuid,
|
||||
pattern="p")
|
||||
|
||||
self.fake_api.verifier.list_tests.assert_has_calls(
|
||||
[mock.call(verifier_id=self.verifier_uuid, pattern="p"),
|
||||
mock.call(verifier_id=self.verifier_uuid, pattern="p")])
|
||||
|
||||
def test_add_verifier_ext(self):
|
||||
self.verify.add_verifier_ext(self.fake_api, self.verifier_uuid,
|
||||
"a", "b", "c")
|
||||
verify.add_verifier_ext(verifier_id=self.verifier_uuid, source="a",
|
||||
version="b", extra="c")
|
||||
self.fake_api.verifier.add_extension.assert_called_once_with(
|
||||
verifier_id=self.verifier_uuid,
|
||||
source="a", version="b", extra_settings="c")
|
||||
@@ -327,9 +330,9 @@ class VerifyCommandsTestCase(test.TestCase):
|
||||
def test_list_verifier_exts_empty_list(self,
|
||||
mock_is_debug, mock_print_list):
|
||||
self.fake_api.verifier.list_extensions.return_value = []
|
||||
self.verify.list_verifier_exts(self.fake_api, self.verifier_uuid)
|
||||
verify.list_verifier_exts(verifier_id=self.verifier_uuid)
|
||||
|
||||
self.verify.list_verifier_exts(self.fake_api, self.verifier_uuid)
|
||||
verify.list_verifier_exts(verifier_id=self.verifier_uuid)
|
||||
|
||||
self.fake_api.verifier.list_extensions.assert_has_calls(
|
||||
[mock.call(verifier_id=self.verifier_uuid),
|
||||
@@ -342,9 +345,9 @@ class VerifyCommandsTestCase(test.TestCase):
|
||||
ver_exts = self.fake_api.verifier.list_extensions
|
||||
ver_exts.return_value = [mock.MagicMock()]
|
||||
fields = ["Name", "Entry point"]
|
||||
self.verify.list_verifier_exts(self.fake_api, self.verifier_uuid)
|
||||
verify.list_verifier_exts(verifier_id=self.verifier_uuid)
|
||||
|
||||
self.verify.list_verifier_exts(self.fake_api, self.verifier_uuid)
|
||||
verify.list_verifier_exts(verifier_id=self.verifier_uuid)
|
||||
|
||||
self.fake_api.verifier.list_extensions.assert_has_calls(
|
||||
[mock.call(verifier_id=self.verifier_uuid),
|
||||
@@ -362,9 +365,9 @@ class VerifyCommandsTestCase(test.TestCase):
|
||||
ver_exts = self.fake_api.verifier.list_extensions
|
||||
ver_exts.return_value = [mock.MagicMock()]
|
||||
fields = ["Name", "Entry point", "Location"]
|
||||
self.verify.list_verifier_exts(self.fake_api, self.verifier_uuid)
|
||||
verify.list_verifier_exts(verifier_id=self.verifier_uuid)
|
||||
|
||||
self.verify.list_verifier_exts(self.fake_api, self.verifier_uuid)
|
||||
verify.list_verifier_exts(verifier_id=self.verifier_uuid)
|
||||
|
||||
self.fake_api.verifier.list_extensions.assert_has_calls(
|
||||
[mock.call(verifier_id=self.verifier_uuid),
|
||||
@@ -375,17 +378,18 @@ class VerifyCommandsTestCase(test.TestCase):
|
||||
normalize_field_names=True)
|
||||
|
||||
def test_delete_verifier_ext(self):
|
||||
self.verify.delete_verifier_ext(self.fake_api, self.verifier_uuid,
|
||||
"ext_name")
|
||||
verify.delete_verifier_ext(verifier_id=self.verifier_uuid,
|
||||
name="ext_name")
|
||||
self.fake_api.verifier.delete_extension.assert_called_once_with(
|
||||
verifier_id=self.verifier_uuid, name="ext_name")
|
||||
|
||||
@mock.patch("rally.cli.commands.verify.envutils.update_globals_file")
|
||||
@mock.patch("rally.cli.commands.verify.os.path.exists")
|
||||
def test_start(self, mock_exists, mock_update_globals_file):
|
||||
self.verify.start(self.fake_api, self.verifier_uuid,
|
||||
self.deployment_uuid, pattern="pattern",
|
||||
load_list="load-list")
|
||||
with self.assertExitCode(1):
|
||||
verify.start(verifier_id=self.verifier_uuid,
|
||||
deployment=self.deployment_uuid, pattern="pattern",
|
||||
load_list="load-list")
|
||||
self.assertFalse(self.fake_api.verification.start.called)
|
||||
|
||||
verification = self.verification_data
|
||||
@@ -396,17 +400,20 @@ class VerifyCommandsTestCase(test.TestCase):
|
||||
self.fake_api.verification.get.return_value = verification
|
||||
|
||||
mock_exists.return_value = False
|
||||
self.verify.start(self.fake_api, self.verifier_uuid,
|
||||
self.deployment_uuid, load_list="/p/a/t/h")
|
||||
with self.assertExitCode(1):
|
||||
verify.start(verifier_id=self.verifier_uuid,
|
||||
deployment=self.deployment_uuid,
|
||||
load_list="/p/a/t/h")
|
||||
self.assertFalse(self.fake_api.verification.start.called)
|
||||
|
||||
mock_exists.return_value = True
|
||||
tf = tempfile.NamedTemporaryFile()
|
||||
with open(tf.name, "w") as f:
|
||||
f.write("test_1\ntest_2")
|
||||
self.verify.start(self.fake_api, self.verifier_uuid,
|
||||
self.deployment_uuid, tags=["foo"],
|
||||
load_list=tf.name)
|
||||
with self.assertExitCode(3):
|
||||
verify.start(verifier_id=self.verifier_uuid,
|
||||
deployment=self.deployment_uuid, tags=["foo"],
|
||||
load_list=tf.name)
|
||||
self.fake_api.verification.start.assert_called_once_with(
|
||||
verifier_id=self.verifier_uuid,
|
||||
deployment_id=self.deployment_uuid,
|
||||
@@ -415,16 +422,18 @@ class VerifyCommandsTestCase(test.TestCase):
|
||||
mock_exists.return_value = False
|
||||
self.fake_api.verification.start.reset_mock()
|
||||
|
||||
self.verify.start(self.fake_api, self.verifier_uuid,
|
||||
self.verifier_uuid, skip_list="/p/a/t/h")
|
||||
with self.assertExitCode(1):
|
||||
verify.start(verifier_id=self.verifier_uuid,
|
||||
deployment=self.verifier_uuid, skip_list="/p/a/t/h")
|
||||
self.assertFalse(self.fake_api.verification.start.called)
|
||||
|
||||
tf = tempfile.NamedTemporaryFile()
|
||||
with open(tf.name, "w") as f:
|
||||
f.write("test_1:\ntest_2: Reason\n")
|
||||
mock_exists.return_value = True
|
||||
self.verify.start(self.fake_api, self.verifier_uuid,
|
||||
self.deployment_uuid, skip_list=tf.name)
|
||||
with self.assertExitCode(3):
|
||||
verify.start(verifier_id=self.verifier_uuid,
|
||||
deployment=self.deployment_uuid, skip_list=tf.name)
|
||||
self.fake_api.verification.start.assert_called_once_with(
|
||||
verifier_id=self.verifier_uuid,
|
||||
deployment_id=self.deployment_uuid,
|
||||
@@ -433,16 +442,19 @@ class VerifyCommandsTestCase(test.TestCase):
|
||||
|
||||
mock_exists.return_value = False
|
||||
self.fake_api.verification.start.reset_mock()
|
||||
self.verify.start(self.fake_api, self.verifier_uuid,
|
||||
self.deployment_uuid, xfail_list="/p/a/t/h")
|
||||
with self.assertExitCode(1):
|
||||
verify.start(verifier_id=self.verifier_uuid,
|
||||
deployment=self.deployment_uuid,
|
||||
xfail_list="/p/a/t/h")
|
||||
self.assertFalse(self.fake_api.verification.start.called)
|
||||
|
||||
tf = tempfile.NamedTemporaryFile()
|
||||
with open(tf.name, "w") as f:
|
||||
f.write("test_1:\ntest_2: Reason\n")
|
||||
mock_exists.return_value = True
|
||||
self.verify.start(self.fake_api, self.verifier_uuid,
|
||||
self.deployment_uuid, xfail_list=tf.name)
|
||||
with self.assertExitCode(3):
|
||||
verify.start(verifier_id=self.verifier_uuid,
|
||||
deployment=self.deployment_uuid, xfail_list=tf.name)
|
||||
self.fake_api.verification.start.assert_called_once_with(
|
||||
verifier_id=self.verifier_uuid,
|
||||
deployment_id=self.deployment_uuid, tags=None,
|
||||
@@ -455,9 +467,10 @@ class VerifyCommandsTestCase(test.TestCase):
|
||||
|
||||
self.fake_api.verification.get.reset_mock()
|
||||
mock_update_globals_file.reset_mock()
|
||||
self.verify.start(self.fake_api, self.verifier_uuid,
|
||||
self.deployment_uuid, detailed=True,
|
||||
do_use=False)
|
||||
with self.assertExitCode(3):
|
||||
verify.start(verifier_id=self.verifier_uuid,
|
||||
deployment=self.deployment_uuid, detailed=True,
|
||||
no_use=True)
|
||||
self.assertFalse(self.fake_api.verification.get.called)
|
||||
self.assertFalse(mock_update_globals_file.called)
|
||||
|
||||
@@ -472,14 +485,14 @@ class VerifyCommandsTestCase(test.TestCase):
|
||||
uuid=deployment_id,
|
||||
status=consts.DeployStatus.DEPLOY_INIT)
|
||||
self.fake_api.verification.start.side_effect = exc
|
||||
self.assertEqual(
|
||||
1, self.verify.start(self.fake_api,
|
||||
self.deployment_uuid, deployment_id))
|
||||
with self.assertExitCode(1):
|
||||
verify.start(verifier_id=self.deployment_uuid,
|
||||
deployment=deployment_id)
|
||||
|
||||
@mock.patch("rally.cli.commands.verify.envutils.update_globals_file")
|
||||
def test_use(self, mock_update_globals_file):
|
||||
self.fake_api.verification.get.return_value = self.verification_data
|
||||
self.verify.use(self.fake_api, self.verification_uuid)
|
||||
verify.use(verification_uuid=self.verification_uuid)
|
||||
self.fake_api.verification.get.assert_called_once_with(
|
||||
verification_uuid=self.verification_uuid)
|
||||
mock_update_globals_file.assert_called_once_with(
|
||||
@@ -493,8 +506,8 @@ class VerifyCommandsTestCase(test.TestCase):
|
||||
"tests": self.results_data["tests"]}
|
||||
self.fake_api.verification.get.return_value = self.verification_data
|
||||
|
||||
self.verify.rerun(self.fake_api, self.verification_uuid,
|
||||
self.deployment_uuid, failed=True)
|
||||
verify.rerun(self.verification_uuid,
|
||||
deployment=self.deployment_uuid, failed=True)
|
||||
self.fake_api.verification.rerun.assert_called_once_with(
|
||||
verification_uuid=self.verification_uuid,
|
||||
concurrency=None,
|
||||
@@ -523,7 +536,7 @@ class VerifyCommandsTestCase(test.TestCase):
|
||||
|
||||
with mock.patch.object(verify.cliutils, "print_dict",
|
||||
new=print_dict):
|
||||
self.verify.show(self.fake_api, self.verifier_uuid, detailed=True)
|
||||
verify.show(self.verifier_uuid, detailed=True)
|
||||
|
||||
self.assertEqual(1, len(print_dict_calls))
|
||||
|
||||
@@ -580,7 +593,7 @@ class VerifyCommandsTestCase(test.TestCase):
|
||||
|
||||
with mock.patch.object(verify.cliutils, "print_dict",
|
||||
new=print_dict):
|
||||
self.verify.show(self.fake_api, self.verifier_uuid, detailed=False)
|
||||
verify.show(self.verifier_uuid, detailed=False)
|
||||
self.assertEqual(2, len(print_dict_calls))
|
||||
|
||||
self.assertEqual(
|
||||
@@ -639,12 +652,13 @@ class VerifyCommandsTestCase(test.TestCase):
|
||||
@mock.patch("rally.cli.commands.verify.cliutils.print_list")
|
||||
def test_list_empty_verifications(self, mock_print_list):
|
||||
self.fake_api.verification.list.return_value = []
|
||||
self.verify.list(self.fake_api, self.verifier_uuid,
|
||||
self.deployment_uuid)
|
||||
verify.list_(verifier_id=self.verifier_uuid,
|
||||
deployment=self.deployment_uuid)
|
||||
|
||||
self.verify.list(self.fake_api, self.verifier_uuid,
|
||||
self.deployment_uuid, "foo", "bar")
|
||||
self.verify.list(self.fake_api)
|
||||
verify.list_(verifier_id=self.verifier_uuid,
|
||||
deployment=self.deployment_uuid, tags="foo",
|
||||
status="bar")
|
||||
verify.list_()
|
||||
|
||||
self.fake_api.verification.list.assert_has_calls(
|
||||
[mock.call(verifier_id=self.verifier_uuid,
|
||||
@@ -659,8 +673,8 @@ class VerifyCommandsTestCase(test.TestCase):
|
||||
@mock.patch("rally.cli.commands.verify.cliutils.print_list")
|
||||
def test_list(self, mock_print_list):
|
||||
self.fake_api.verification.list.return_value = [self.verification_data]
|
||||
self.verify.list(self.fake_api, self.verifier_uuid,
|
||||
self.deployment_uuid)
|
||||
verify.list_(verifier_id=self.verifier_uuid,
|
||||
deployment=self.deployment_uuid)
|
||||
|
||||
additional_fields = ["UUID", "Tags", "Verifier name",
|
||||
"Deployment name", "Started at", "Finished at",
|
||||
@@ -675,11 +689,11 @@ class VerifyCommandsTestCase(test.TestCase):
|
||||
list(mock_print_list.call_args[1].keys()).sort())
|
||||
|
||||
def test_delete(self):
|
||||
self.verify.delete(self.fake_api, "v_uuid")
|
||||
verify.delete(verification_uuid=["v_uuid"])
|
||||
self.fake_api.verification.delete.assert_called_once_with(
|
||||
verification_uuid="v_uuid")
|
||||
|
||||
self.verify.delete(self.fake_api, ["v1_uuid", "v2_uuid"])
|
||||
verify.delete(verification_uuid=["v1_uuid", "v2_uuid"])
|
||||
self.fake_api.verification.delete.assert_has_calls(
|
||||
[mock.call(verification_uuid="v1_uuid"),
|
||||
mock.call(verification_uuid="v2_uuid")])
|
||||
@@ -695,10 +709,9 @@ class VerifyCommandsTestCase(test.TestCase):
|
||||
"files": {output_dest: content}, "open": output_dest}
|
||||
mock_os.path.exists.return_value = False
|
||||
|
||||
self.verify.report(self.fake_api,
|
||||
verification_uuid=self.verifier_uuid,
|
||||
output_type=output_type,
|
||||
output_dest=output_dest, open_it=True)
|
||||
verify.report(verification_uuid=[self.verifier_uuid],
|
||||
output_type=output_type,
|
||||
output_dest=output_dest, open_it=True)
|
||||
self.fake_api.verification.report.assert_called_once_with(
|
||||
uuids=[self.verifier_uuid], output_type=output_type,
|
||||
output_dest=output_dest)
|
||||
@@ -715,20 +728,22 @@ class VerifyCommandsTestCase(test.TestCase):
|
||||
self.fake_api.verification.report.return_value = {
|
||||
"files": {output_dest: content}, "print": "foo"}
|
||||
|
||||
self.verify.report(self.fake_api, self.verifier_uuid,
|
||||
output_type=output_type,
|
||||
output_dest=output_dest)
|
||||
verify.report(verification_uuid=self.verifier_uuid,
|
||||
output_type=output_type,
|
||||
output_dest=output_dest)
|
||||
|
||||
self.assertFalse(mock_open_new_tab.called)
|
||||
self.assertFalse(mock_os.makedirs.called)
|
||||
|
||||
@mock.patch("rally.cli.commands.verify.VerifyCommands.use")
|
||||
@mock.patch("rally.cli.commands.verify._use")
|
||||
@mock.patch("rally.cli.commands.verify.open", create=True)
|
||||
@mock.patch("rally.cli.commands.verify.os.path.exists")
|
||||
def test_import_results(self, mock_exists, mock_open, mock_use):
|
||||
def test_import_results(self, mock_exists, mock_open, mock__use):
|
||||
mock_exists.return_value = False
|
||||
self.verify.import_results(self.fake_api, self.verifier_uuid,
|
||||
self.deployment_uuid)
|
||||
with self.assertExitCode(1):
|
||||
verify.import_results(verifier_id=self.verifier_uuid,
|
||||
deployment=self.deployment_uuid,
|
||||
file_to_parse="/p/a/t/h")
|
||||
self.assertFalse(self.fake_api.verification.import_results.called)
|
||||
|
||||
verification = self.verification_data
|
||||
@@ -738,21 +753,21 @@ class VerifyCommandsTestCase(test.TestCase):
|
||||
|
||||
mock_exists.return_value = True
|
||||
mock_open.return_value = mock.mock_open(read_data="data").return_value
|
||||
self.verify.import_results(self.fake_api,
|
||||
verifier_id=self.verifier_uuid,
|
||||
deployment=self.deployment_uuid,
|
||||
file_to_parse="/p/a/t/h")
|
||||
verify.import_results(verifier_id=self.verifier_uuid,
|
||||
deployment=self.deployment_uuid,
|
||||
file_to_parse="/p/a/t/h")
|
||||
mock_open.assert_called_once_with("/p/a/t/h", "r")
|
||||
self.fake_api.verification.import_results.assert_called_once_with(
|
||||
verifier_id=self.verifier_uuid,
|
||||
deployment_id=self.deployment_uuid,
|
||||
data="data")
|
||||
|
||||
mock_use.assert_called_with(self.fake_api, self.verification_uuid)
|
||||
mock__use.assert_called_with(self.fake_api, self.verification_uuid)
|
||||
|
||||
mock_use.reset_mock()
|
||||
self.verify.import_results(self.fake_api, "v_id", "d_id", do_use=False)
|
||||
self.assertFalse(mock_use.called)
|
||||
mock__use.reset_mock()
|
||||
verify.import_results(verifier_id="v_id", deployment="d_id",
|
||||
file_to_parse="/p/a/t/h", no_use=True)
|
||||
self.assertFalse(mock__use.called)
|
||||
|
||||
@plugins.ensure_plugins_are_loaded
|
||||
def test_default_reporters(self):
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
# 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.
|
||||
|
||||
import contextlib
|
||||
import io
|
||||
import os
|
||||
import typing as t
|
||||
from unittest import mock
|
||||
|
||||
import ddt
|
||||
import typer
|
||||
|
||||
from rally.cli import argutils
|
||||
from tests.unit import test
|
||||
|
||||
|
||||
def _build(define: t.Callable) -> t.Any:
|
||||
"""Build a two-level app, wire the parser, return the click command."""
|
||||
app = typer.Typer(no_args_is_help=False)
|
||||
group = typer.Typer()
|
||||
app.add_typer(group, name="task")
|
||||
define(group)
|
||||
cli = typer.main.get_command(app)
|
||||
argutils.install(cli)
|
||||
return cli
|
||||
|
||||
|
||||
def _scalar_cli():
|
||||
def define(group):
|
||||
@group.command()
|
||||
def go(
|
||||
uuid: t.Annotated[
|
||||
str,
|
||||
argutils.ArgumentOrKeyword("--uuid", envvar="RALLY_TASK")
|
||||
]
|
||||
) -> None:
|
||||
print("UUID=%s" % uuid)
|
||||
return _build(define)
|
||||
|
||||
|
||||
def _list_cli():
|
||||
def define(group):
|
||||
@group.command()
|
||||
def go(
|
||||
uuids: t.Annotated[
|
||||
list[str],
|
||||
argutils.ArgumentOrKeyword("--uuid")
|
||||
]
|
||||
) -> None:
|
||||
print("UUIDS=%s" % ",".join(uuids))
|
||||
return _build(define)
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class ArgumentOrKeywordTestCase(test.TestCase):
|
||||
"""Exercise the actual click parser wiring, not the command functions."""
|
||||
|
||||
def _invoke(self, cli, args, env=None):
|
||||
out = io.StringIO()
|
||||
code = 0
|
||||
with mock.patch.dict(os.environ, env or {}), \
|
||||
contextlib.redirect_stdout(out), \
|
||||
contextlib.redirect_stderr(io.StringIO()):
|
||||
try:
|
||||
cli(args, prog_name="rally", standalone_mode=True)
|
||||
except SystemExit as e:
|
||||
code = e.code if isinstance(e.code, int) else 1
|
||||
return code, out.getvalue()
|
||||
|
||||
@ddt.data(
|
||||
# positional, --flag, env-var default, and --flag winning over the env
|
||||
{"args": ["task", "go", "POS"], "env": "", "expected": "POS"},
|
||||
{"args": ["task", "go", "--uuid", "FLG"], "env": "",
|
||||
"expected": "FLG"},
|
||||
{"args": ["task", "go"], "env": "ENV", "expected": "ENV"},
|
||||
{"args": ["task", "go", "--uuid", "FLG"], "env": "ENV",
|
||||
"expected": "FLG"},
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_scalar_resolves(self, args, env, expected):
|
||||
code, out = self._invoke(_scalar_cli(), args, env={"RALLY_TASK": env})
|
||||
self.assertEqual(0, code)
|
||||
self.assertIn("UUID=%s" % expected, out)
|
||||
|
||||
@ddt.data(
|
||||
["task", "go"], # required: nothing given, no env
|
||||
["task", "go", "--bogus"], # ``--uuid`` is real, so a typo is rejected
|
||||
["task", "go", "A", "B"], # extra positional argument
|
||||
)
|
||||
def test_scalar_rejects(self, args):
|
||||
code, _ = self._invoke(_scalar_cli(), args, env={"RALLY_TASK": ""})
|
||||
self.assertEqual(2, code)
|
||||
|
||||
@ddt.data(
|
||||
{"args": ["task", "go", "a", "b"], "expected": "a,b"},
|
||||
{"args": ["task", "go", "--uuid", "x", "--uuid", "y"],
|
||||
"expected": "x,y"},
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_list_resolves(self, args, expected):
|
||||
code, out = self._invoke(_list_cli(), args)
|
||||
self.assertEqual(0, code)
|
||||
self.assertIn("UUIDS=%s" % expected, out)
|
||||
+30
-615
@@ -17,37 +17,15 @@ import io
|
||||
from unittest import mock
|
||||
|
||||
import ddt
|
||||
import sqlalchemy.exc
|
||||
import typer
|
||||
|
||||
from rally import exceptions
|
||||
from rally.cli import cliutils
|
||||
from rally.cli.commands import deployment
|
||||
from rally.cli.commands import task
|
||||
from rally.cli.commands import verify
|
||||
from rally.common import cfg
|
||||
from tests.unit import test
|
||||
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
FAKE_TASK_UUID = "bb0f621c-29bd-495c-9d7a-d844335ed0fa"
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class CliUtilsTestCase(test.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(CliUtilsTestCase, self).setUp()
|
||||
self.categories = {
|
||||
"deployment": deployment.DeploymentCommands,
|
||||
"task": task.TaskCommands,
|
||||
"verify": verify.VerifyCommands
|
||||
}
|
||||
|
||||
def tearDown(self):
|
||||
self._unregister_opts()
|
||||
super(CliUtilsTestCase, self).tearDown()
|
||||
|
||||
def test_print_dict(self):
|
||||
out = io.StringIO()
|
||||
dict = {"key": "value"}
|
||||
@@ -202,112 +180,6 @@ class CliUtilsTestCase(test.TestCase):
|
||||
else:
|
||||
self.assertEqual(expected, formatter(obj))
|
||||
|
||||
def test__methods_of_with_class(self):
|
||||
class fake_class(object):
|
||||
def public(self):
|
||||
pass
|
||||
|
||||
def _private(self):
|
||||
pass
|
||||
result = cliutils._methods_of(fake_class)
|
||||
self.assertEqual(1, len(result))
|
||||
self.assertEqual("public", result[0][0])
|
||||
|
||||
def test__methods_of_with_object(self):
|
||||
class fake_class(object):
|
||||
def public(self):
|
||||
pass
|
||||
|
||||
def _private(self):
|
||||
pass
|
||||
mock_obj = fake_class()
|
||||
result = cliutils._methods_of(mock_obj)
|
||||
self.assertEqual(1, len(result))
|
||||
self.assertEqual("public", result[0][0])
|
||||
|
||||
def test__methods_of_empty_result(self):
|
||||
class fake_class(object):
|
||||
def _private(self):
|
||||
pass
|
||||
|
||||
def _private2(self):
|
||||
pass
|
||||
mock_obj = fake_class()
|
||||
result = cliutils._methods_of(mock_obj)
|
||||
self.assertEqual([], result)
|
||||
|
||||
def _unregister_opts(self):
|
||||
CONF.reset()
|
||||
category_opt = cfg.SubCommandOpt("category",
|
||||
title="Command categories",
|
||||
help="Available categories"
|
||||
)
|
||||
CONF.unregister_opt(category_opt)
|
||||
|
||||
@mock.patch("rally.api.API",
|
||||
side_effect=exceptions.RallyException("config_file"))
|
||||
def test_run_fails(self, mock_rally_api_api):
|
||||
ret = cliutils.run(["rally", "task list"], self.categories)
|
||||
self.assertEqual(2, ret)
|
||||
mock_rally_api_api.assert_called_once_with(
|
||||
config_args=["task list"], skip_db_check=True)
|
||||
|
||||
@mock.patch("rally.api.API.check_db_revision")
|
||||
def test_run_version(self, mock_api_check_db_revision):
|
||||
ret = cliutils.run(["rally", "version"], self.categories)
|
||||
self.assertEqual(0, ret)
|
||||
|
||||
@mock.patch("rally.api.API.check_db_revision")
|
||||
def test_run_bash_completion(self, mock_api_check_db_revision):
|
||||
ret = cliutils.run(["rally", "bash-completion"], self.categories)
|
||||
self.assertEqual(0, ret)
|
||||
|
||||
@mock.patch("rally.api.API.check_db_revision")
|
||||
@mock.patch("rally.common.db.api.task_get",
|
||||
side_effect=exceptions.DBRecordNotFound(
|
||||
criteria="uuid: %s" % FAKE_TASK_UUID, table="tasks"))
|
||||
def test_run_task_not_found(self, mock_task_get,
|
||||
mock_api_check_db_revision):
|
||||
ret = cliutils.run(["rally", "task", "status", "%s" % FAKE_TASK_UUID],
|
||||
self.categories)
|
||||
self.assertTrue(mock_task_get.called)
|
||||
self.assertEqual(203, ret)
|
||||
|
||||
@mock.patch("rally.api.API.check_db_revision")
|
||||
@mock.patch("rally.cli.cliutils.validate_args",
|
||||
side_effect=cliutils.MissingArgs("missing"))
|
||||
def test_run_task_failed(self, mock_validate_args,
|
||||
mock_api_check_db_revision):
|
||||
ret = cliutils.run(["rally", "task", "status", "%s" % FAKE_TASK_UUID],
|
||||
self.categories)
|
||||
self.assertTrue(mock_validate_args.called)
|
||||
self.assertEqual(1, ret)
|
||||
|
||||
@mock.patch("rally.api.API.check_db_revision")
|
||||
def test_run_failed_to_open_file(self, mock_api_check_db_revision):
|
||||
|
||||
class FailuresCommands(object):
|
||||
|
||||
def failed_to_open_file(self):
|
||||
raise IOError("No such file")
|
||||
|
||||
ret = cliutils.run(["rally", "failure", "failed-to-open-file"],
|
||||
{"failure": FailuresCommands})
|
||||
self.assertEqual(1, ret)
|
||||
|
||||
@mock.patch("rally.api.API.check_db_revision")
|
||||
def test_run_sqlalchmey_operational_failure(self,
|
||||
mock_api_check_db_revision):
|
||||
|
||||
class SQLAlchemyCommands(object):
|
||||
|
||||
def operational_failure(self):
|
||||
raise sqlalchemy.exc.OperationalError("Can't open DB file")
|
||||
|
||||
ret = cliutils.run(["rally", "failure", "operational-failure"],
|
||||
{"failure": SQLAlchemyCommands})
|
||||
self.assertEqual(1, ret)
|
||||
|
||||
class TestObj(object):
|
||||
x = 1
|
||||
y = 2
|
||||
@@ -577,500 +449,43 @@ class CliUtilsTestCase(test.TestCase):
|
||||
[self.TestObj()], ["x"],
|
||||
field_labels=["x", "y"], sortby_index=None, out=out)
|
||||
|
||||
def test_help_for_grouped_methods(self):
|
||||
class SomeCommand(object):
|
||||
@cliutils.help_group("1_manage")
|
||||
def install(self):
|
||||
pass
|
||||
|
||||
@cliutils.help_group("1_manage")
|
||||
def uninstall(self):
|
||||
pass
|
||||
class IterCommandsTestCase(test.TestCase):
|
||||
|
||||
@cliutils.help_group("1_manage")
|
||||
def reinstall(self):
|
||||
pass
|
||||
def _build(self):
|
||||
app = typer.Typer()
|
||||
group = typer.Typer()
|
||||
|
||||
@cliutils.help_group("2_launch")
|
||||
def run(self):
|
||||
pass
|
||||
|
||||
@cliutils.help_group("2_launch")
|
||||
def rerun(self):
|
||||
pass
|
||||
|
||||
@cliutils.help_group("3_results")
|
||||
def show(self):
|
||||
pass
|
||||
|
||||
@cliutils.help_group("3_results")
|
||||
def list(self):
|
||||
pass
|
||||
|
||||
def do_do_has_do_has_mesh(self):
|
||||
pass
|
||||
|
||||
self.assertEqual(
|
||||
"\n\nCommands:\n"
|
||||
" do-do-has-do-has-mesh \n"
|
||||
"\n"
|
||||
" install \n"
|
||||
" reinstall \n"
|
||||
" uninstall \n"
|
||||
"\n"
|
||||
" rerun \n"
|
||||
" run \n"
|
||||
"\n"
|
||||
" list \n"
|
||||
" show \n",
|
||||
cliutils._compose_category_description(SomeCommand))
|
||||
|
||||
|
||||
class ValidateArgsTest(test.TestCase):
|
||||
|
||||
def test_lambda_no_args(self):
|
||||
cliutils.validate_args(lambda: None)
|
||||
|
||||
def _test_lambda_with_args(self, *args, **kwargs):
|
||||
cliutils.validate_args(lambda x, y: None, *args, **kwargs)
|
||||
|
||||
def test_lambda_positional_args(self):
|
||||
self._test_lambda_with_args(1, 2)
|
||||
|
||||
def test_lambda_kwargs(self):
|
||||
self._test_lambda_with_args(x=1, y=2)
|
||||
|
||||
def test_lambda_mixed_kwargs(self):
|
||||
self._test_lambda_with_args(1, y=2)
|
||||
|
||||
def test_lambda_missing_args1(self):
|
||||
self.assertRaises(cliutils.MissingArgs,
|
||||
self._test_lambda_with_args)
|
||||
|
||||
def test_lambda_missing_args2(self):
|
||||
self.assertRaises(cliutils.MissingArgs,
|
||||
self._test_lambda_with_args, 1)
|
||||
|
||||
def test_lambda_missing_args3(self):
|
||||
self.assertRaises(cliutils.MissingArgs,
|
||||
self._test_lambda_with_args, y=2)
|
||||
|
||||
def test_lambda_missing_args4(self):
|
||||
self.assertRaises(cliutils.MissingArgs,
|
||||
self._test_lambda_with_args, 1, x=2)
|
||||
|
||||
def _test_lambda_with_default(self, *args, **kwargs):
|
||||
cliutils.validate_args(lambda x, y, z=3: None, *args, **kwargs)
|
||||
|
||||
def test_lambda_positional_args_with_default(self):
|
||||
self._test_lambda_with_default(1, 2)
|
||||
|
||||
def test_lambda_kwargs_with_default(self):
|
||||
self._test_lambda_with_default(x=1, y=2)
|
||||
|
||||
def test_lambda_mixed_kwargs_with_default(self):
|
||||
self._test_lambda_with_default(1, y=2)
|
||||
|
||||
def test_lambda_positional_args_all_with_default(self):
|
||||
self._test_lambda_with_default(1, 2, 3)
|
||||
|
||||
def test_lambda_kwargs_all_with_default(self):
|
||||
self._test_lambda_with_default(x=1, y=2, z=3)
|
||||
|
||||
def test_lambda_mixed_kwargs_all_with_default(self):
|
||||
self._test_lambda_with_default(1, y=2, z=3)
|
||||
|
||||
def test_lambda_with_default_missing_args1(self):
|
||||
self.assertRaises(cliutils.MissingArgs,
|
||||
self._test_lambda_with_default)
|
||||
|
||||
def test_lambda_with_default_missing_args2(self):
|
||||
self.assertRaises(cliutils.MissingArgs,
|
||||
self._test_lambda_with_default, 1)
|
||||
|
||||
def test_lambda_with_default_missing_args3(self):
|
||||
self.assertRaises(cliutils.MissingArgs,
|
||||
self._test_lambda_with_default, y=2)
|
||||
|
||||
def test_lambda_with_default_missing_args4(self):
|
||||
self.assertRaises(cliutils.MissingArgs,
|
||||
self._test_lambda_with_default, y=2, z=3)
|
||||
|
||||
def test_function_no_args(self):
|
||||
def func():
|
||||
pass
|
||||
cliutils.validate_args(func)
|
||||
|
||||
def _test_function_with_args(self, *args, **kwargs):
|
||||
def func(x, y):
|
||||
pass
|
||||
cliutils.validate_args(func, *args, **kwargs)
|
||||
|
||||
def test_function_positional_args(self):
|
||||
self._test_function_with_args(1, 2)
|
||||
|
||||
def test_function_kwargs(self):
|
||||
self._test_function_with_args(x=1, y=2)
|
||||
|
||||
def test_function_mixed_kwargs(self):
|
||||
self._test_function_with_args(1, y=2)
|
||||
|
||||
def test_function_missing_args1(self):
|
||||
self.assertRaises(cliutils.MissingArgs,
|
||||
self._test_function_with_args)
|
||||
|
||||
def test_function_missing_args2(self):
|
||||
self.assertRaises(cliutils.MissingArgs,
|
||||
self._test_function_with_args, 1)
|
||||
|
||||
def test_function_missing_args3(self):
|
||||
self.assertRaises(cliutils.MissingArgs,
|
||||
self._test_function_with_args, y=2)
|
||||
|
||||
def _test_function_with_default(self, *args, **kwargs):
|
||||
def func(x, y, z=3):
|
||||
pass
|
||||
cliutils.validate_args(func, *args, **kwargs)
|
||||
|
||||
def test_function_positional_args_with_default(self):
|
||||
self._test_function_with_default(1, 2)
|
||||
|
||||
def test_function_kwargs_with_default(self):
|
||||
self._test_function_with_default(x=1, y=2)
|
||||
|
||||
def test_function_mixed_kwargs_with_default(self):
|
||||
self._test_function_with_default(1, y=2)
|
||||
|
||||
def test_function_positional_args_all_with_default(self):
|
||||
self._test_function_with_default(1, 2, 3)
|
||||
|
||||
def test_function_kwargs_all_with_default(self):
|
||||
self._test_function_with_default(x=1, y=2, z=3)
|
||||
|
||||
def test_function_mixed_kwargs_all_with_default(self):
|
||||
self._test_function_with_default(1, y=2, z=3)
|
||||
|
||||
def test_function_with_default_missing_args1(self):
|
||||
self.assertRaises(cliutils.MissingArgs,
|
||||
self._test_function_with_default)
|
||||
|
||||
def test_function_with_default_missing_args2(self):
|
||||
self.assertRaises(cliutils.MissingArgs,
|
||||
self._test_function_with_default, 1)
|
||||
|
||||
def test_function_with_default_missing_args3(self):
|
||||
self.assertRaises(cliutils.MissingArgs,
|
||||
self._test_function_with_default, y=2)
|
||||
|
||||
def test_function_with_default_missing_args4(self):
|
||||
self.assertRaises(cliutils.MissingArgs,
|
||||
self._test_function_with_default, y=2, z=3)
|
||||
|
||||
def test_bound_method_no_args(self):
|
||||
class Foo(object):
|
||||
def bar(self):
|
||||
pass
|
||||
cliutils.validate_args(Foo().bar)
|
||||
|
||||
def _test_bound_method_with_args(self, *args, **kwargs):
|
||||
class Foo(object):
|
||||
def bar(self, x, y):
|
||||
pass
|
||||
cliutils.validate_args(Foo().bar, *args, **kwargs)
|
||||
|
||||
def test_bound_method_positional_args(self):
|
||||
self._test_bound_method_with_args(1, 2)
|
||||
|
||||
def test_bound_method_kwargs(self):
|
||||
self._test_bound_method_with_args(x=1, y=2)
|
||||
|
||||
def test_bound_method_mixed_kwargs(self):
|
||||
self._test_bound_method_with_args(1, y=2)
|
||||
|
||||
def test_bound_method_missing_args1(self):
|
||||
self.assertRaises(cliutils.MissingArgs,
|
||||
self._test_bound_method_with_args)
|
||||
|
||||
def test_bound_method_missing_args2(self):
|
||||
self.assertRaises(cliutils.MissingArgs,
|
||||
self._test_bound_method_with_args, 1)
|
||||
|
||||
def test_bound_method_missing_args3(self):
|
||||
self.assertRaises(cliutils.MissingArgs,
|
||||
self._test_bound_method_with_args, y=2)
|
||||
|
||||
def _test_bound_method_with_default(self, *args, **kwargs):
|
||||
class Foo(object):
|
||||
def bar(self, x, y, z=3):
|
||||
pass
|
||||
cliutils.validate_args(Foo().bar, *args, **kwargs)
|
||||
|
||||
def test_bound_method_positional_args_with_default(self):
|
||||
self._test_bound_method_with_default(1, 2)
|
||||
|
||||
def test_bound_method_kwargs_with_default(self):
|
||||
self._test_bound_method_with_default(x=1, y=2)
|
||||
|
||||
def test_bound_method_mixed_kwargs_with_default(self):
|
||||
self._test_bound_method_with_default(1, y=2)
|
||||
|
||||
def test_bound_method_positional_args_all_with_default(self):
|
||||
self._test_bound_method_with_default(1, 2, 3)
|
||||
|
||||
def test_bound_method_kwargs_all_with_default(self):
|
||||
self._test_bound_method_with_default(x=1, y=2, z=3)
|
||||
|
||||
def test_bound_method_mixed_kwargs_all_with_default(self):
|
||||
self._test_bound_method_with_default(1, y=2, z=3)
|
||||
|
||||
def test_bound_method_with_default_missing_args1(self):
|
||||
self.assertRaises(cliutils.MissingArgs,
|
||||
self._test_bound_method_with_default)
|
||||
|
||||
def test_bound_method_with_default_missing_args2(self):
|
||||
self.assertRaises(cliutils.MissingArgs,
|
||||
self._test_bound_method_with_default, 1)
|
||||
|
||||
def test_bound_method_with_default_missing_args3(self):
|
||||
self.assertRaises(cliutils.MissingArgs,
|
||||
self._test_bound_method_with_default, y=2)
|
||||
|
||||
def test_bound_method_with_default_missing_args4(self):
|
||||
self.assertRaises(cliutils.MissingArgs,
|
||||
self._test_bound_method_with_default, y=2, z=3)
|
||||
|
||||
def test_unbound_method_no_args(self):
|
||||
class Foo(object):
|
||||
def bar(self):
|
||||
pass
|
||||
cliutils.validate_args(Foo.bar, Foo())
|
||||
|
||||
def _test_unbound_method_with_args(self, *args, **kwargs):
|
||||
class Foo(object):
|
||||
def bar(self, x, y):
|
||||
pass
|
||||
cliutils.validate_args(Foo.bar, Foo(), *args, **kwargs)
|
||||
|
||||
def test_unbound_method_positional_args(self):
|
||||
self._test_unbound_method_with_args(1, 2)
|
||||
|
||||
def test_unbound_method_kwargs(self):
|
||||
self._test_unbound_method_with_args(x=1, y=2)
|
||||
|
||||
def test_unbound_method_mixed_kwargs(self):
|
||||
self._test_unbound_method_with_args(1, y=2)
|
||||
|
||||
def test_unbound_method_missing_args1(self):
|
||||
self.assertRaises(cliutils.MissingArgs,
|
||||
self._test_unbound_method_with_args)
|
||||
|
||||
def test_unbound_method_missing_args2(self):
|
||||
self.assertRaises(cliutils.MissingArgs,
|
||||
self._test_unbound_method_with_args, 1)
|
||||
|
||||
def test_unbound_method_missing_args3(self):
|
||||
self.assertRaises(cliutils.MissingArgs,
|
||||
self._test_unbound_method_with_args, y=2)
|
||||
|
||||
def _test_unbound_method_with_default(self, *args, **kwargs):
|
||||
class Foo(object):
|
||||
def bar(self, x, y, z=3):
|
||||
pass
|
||||
cliutils.validate_args(Foo.bar, Foo(), *args, **kwargs)
|
||||
|
||||
def test_unbound_method_positional_args_with_default(self):
|
||||
self._test_unbound_method_with_default(1, 2)
|
||||
|
||||
def test_unbound_method_kwargs_with_default(self):
|
||||
self._test_unbound_method_with_default(x=1, y=2)
|
||||
|
||||
def test_unbound_method_mixed_kwargs_with_default(self):
|
||||
self._test_unbound_method_with_default(1, y=2)
|
||||
|
||||
def test_unbound_method_with_default_missing_args1(self):
|
||||
self.assertRaises(cliutils.MissingArgs,
|
||||
self._test_unbound_method_with_default)
|
||||
|
||||
def test_unbound_method_with_default_missing_args2(self):
|
||||
self.assertRaises(cliutils.MissingArgs,
|
||||
self._test_unbound_method_with_default, 1)
|
||||
|
||||
def test_unbound_method_with_default_missing_args3(self):
|
||||
self.assertRaises(cliutils.MissingArgs,
|
||||
self._test_unbound_method_with_default, y=2)
|
||||
|
||||
def test_unbound_method_with_default_missing_args4(self):
|
||||
self.assertRaises(cliutils.MissingArgs,
|
||||
self._test_unbound_method_with_default, y=2, z=3)
|
||||
|
||||
def test_class_method_no_args(self):
|
||||
class Foo(object):
|
||||
@classmethod
|
||||
def bar(cls):
|
||||
pass
|
||||
cliutils.validate_args(Foo.bar)
|
||||
|
||||
def _test_class_method_with_args(self, *args, **kwargs):
|
||||
class Foo(object):
|
||||
@classmethod
|
||||
def bar(cls, x, y):
|
||||
pass
|
||||
cliutils.validate_args(Foo.bar, *args, **kwargs)
|
||||
|
||||
def test_class_method_positional_args(self):
|
||||
self._test_class_method_with_args(1, 2)
|
||||
|
||||
def test_class_method_kwargs(self):
|
||||
self._test_class_method_with_args(x=1, y=2)
|
||||
|
||||
def test_class_method_mixed_kwargs(self):
|
||||
self._test_class_method_with_args(1, y=2)
|
||||
|
||||
def test_class_method_missing_args1(self):
|
||||
self.assertRaises(cliutils.MissingArgs,
|
||||
self._test_class_method_with_args)
|
||||
|
||||
def test_class_method_missing_args2(self):
|
||||
self.assertRaises(cliutils.MissingArgs,
|
||||
self._test_class_method_with_args, 1)
|
||||
|
||||
def test_class_method_missing_args3(self):
|
||||
self.assertRaises(cliutils.MissingArgs,
|
||||
self._test_class_method_with_args, y=2)
|
||||
|
||||
def _test_class_method_with_default(self, *args, **kwargs):
|
||||
class Foo(object):
|
||||
@classmethod
|
||||
def bar(cls, x, y, z=3):
|
||||
pass
|
||||
cliutils.validate_args(Foo.bar, *args, **kwargs)
|
||||
|
||||
def test_class_method_positional_args_with_default(self):
|
||||
self._test_class_method_with_default(1, 2)
|
||||
|
||||
def test_class_method_kwargs_with_default(self):
|
||||
self._test_class_method_with_default(x=1, y=2)
|
||||
|
||||
def test_class_method_mixed_kwargs_with_default(self):
|
||||
self._test_class_method_with_default(1, y=2)
|
||||
|
||||
def test_class_method_with_default_missing_args1(self):
|
||||
self.assertRaises(cliutils.MissingArgs,
|
||||
self._test_class_method_with_default)
|
||||
|
||||
def test_class_method_with_default_missing_args2(self):
|
||||
self.assertRaises(cliutils.MissingArgs,
|
||||
self._test_class_method_with_default, 1)
|
||||
|
||||
def test_class_method_with_default_missing_args3(self):
|
||||
self.assertRaises(cliutils.MissingArgs,
|
||||
self._test_class_method_with_default, y=2)
|
||||
|
||||
def test_class_method_with_default_missing_args4(self):
|
||||
self.assertRaises(cliutils.MissingArgs,
|
||||
self._test_class_method_with_default, y=2, z=3)
|
||||
|
||||
def test_static_method_no_args(self):
|
||||
class Foo(object):
|
||||
@staticmethod
|
||||
def bar():
|
||||
pass
|
||||
cliutils.validate_args(Foo.bar)
|
||||
|
||||
def _test_static_method_with_args(self, *args, **kwargs):
|
||||
class Foo(object):
|
||||
@staticmethod
|
||||
def bar(x, y):
|
||||
pass
|
||||
cliutils.validate_args(Foo.bar, *args, **kwargs)
|
||||
|
||||
def test_static_method_positional_args(self):
|
||||
self._test_static_method_with_args(1, 2)
|
||||
|
||||
def test_static_method_kwargs(self):
|
||||
self._test_static_method_with_args(x=1, y=2)
|
||||
|
||||
def test_static_method_mixed_kwargs(self):
|
||||
self._test_static_method_with_args(1, y=2)
|
||||
|
||||
def test_static_method_missing_args1(self):
|
||||
self.assertRaises(cliutils.MissingArgs,
|
||||
self._test_static_method_with_args)
|
||||
|
||||
def test_static_method_missing_args2(self):
|
||||
self.assertRaises(cliutils.MissingArgs,
|
||||
self._test_static_method_with_args, 1)
|
||||
|
||||
def test_static_method_missing_args3(self):
|
||||
self.assertRaises(cliutils.MissingArgs,
|
||||
self._test_static_method_with_args, y=2)
|
||||
|
||||
def _test_static_method_with_default(self, *args, **kwargs):
|
||||
class Foo(object):
|
||||
@staticmethod
|
||||
def bar(x, y, z=3):
|
||||
pass
|
||||
cliutils.validate_args(Foo.bar, *args, **kwargs)
|
||||
|
||||
def test_static_method_positional_args_with_default(self):
|
||||
self._test_static_method_with_default(1, 2)
|
||||
|
||||
def test_static_method_kwargs_with_default(self):
|
||||
self._test_static_method_with_default(x=1, y=2)
|
||||
|
||||
def test_static_method_mixed_kwargs_with_default(self):
|
||||
self._test_static_method_with_default(1, y=2)
|
||||
|
||||
def test_static_method_with_default_missing_args1(self):
|
||||
self.assertRaises(cliutils.MissingArgs,
|
||||
self._test_static_method_with_default)
|
||||
|
||||
def test_static_method_with_default_missing_args2(self):
|
||||
self.assertRaises(cliutils.MissingArgs,
|
||||
self._test_static_method_with_default, 1)
|
||||
|
||||
def test_static_method_with_default_missing_args3(self):
|
||||
self.assertRaises(cliutils.MissingArgs,
|
||||
self._test_static_method_with_default, y=2)
|
||||
|
||||
def test_static_method_with_default_missing_args4(self):
|
||||
self.assertRaises(cliutils.MissingArgs,
|
||||
self._test_static_method_with_default, y=2, z=3)
|
||||
|
||||
def test_alias_decorator(self):
|
||||
alias_fn = mock.Mock(name="alias_fn")
|
||||
cmd_name = "test-command"
|
||||
wrapped = cliutils.alias(cmd_name)
|
||||
self.assertEqual(cmd_name, wrapped(alias_fn).alias)
|
||||
|
||||
def test_deprecated_args(self):
|
||||
def command():
|
||||
@app.command()
|
||||
def top() -> None:
|
||||
pass
|
||||
|
||||
def deprecated_args(func, *args, **kwargs):
|
||||
cliutils.deprecated_args(*args, **kwargs)(func)
|
||||
|
||||
e = self.assertRaises(ValueError, deprecated_args, command, | ||||