Revert "Remove senlin CLI commands"
This reverts commit 3bda3b997bf3701746c7a2e4760f9528a18a2cbd. We cannot rely on openstackclient as it is today. There are OSC parameters preventing senlin client from normal operation and we see no easy resolution when both OSC and senlinclient has been released. Change-Id: I6b09699afcd45d36caea3e9f6ee0d132cc57354f
This commit is contained in:
parent
02f5a76d0a
commit
ece213914b
210
senlinclient/cliargs.py
Normal file
210
senlinclient/cliargs.py
Normal file
@ -0,0 +1,210 @@
|
||||
# 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 argparse
|
||||
|
||||
from senlinclient.common.i18n import _
|
||||
from senlinclient.common import sdk
|
||||
from senlinclient.common import utils
|
||||
|
||||
|
||||
def add_global_identity_args(parser):
|
||||
parser.add_argument(
|
||||
'--os-auth-plugin', dest='auth_plugin', metavar='AUTH_PLUGIN',
|
||||
default=utils.env('OS_AUTH_PLUGIN', default=None),
|
||||
help=_('Authentication plugin, default to env[OS_AUTH_PLUGIN]'))
|
||||
|
||||
parser.add_argument(
|
||||
'--os-auth-url', dest='auth_url', metavar='AUTH_URL',
|
||||
default=utils.env('OS_AUTH_URL'),
|
||||
help=_('Defaults to env[OS_AUTH_URL]'))
|
||||
|
||||
parser.add_argument(
|
||||
'--os-project-id', dest='project_id', metavar='PROJECT_ID',
|
||||
default=utils.env('OS_PROJECT_ID'),
|
||||
help=_('Defaults to env[OS_PROJECT_ID].'))
|
||||
|
||||
parser.add_argument(
|
||||
'--os-project-name', dest='project_name', metavar='PROJECT_NAME',
|
||||
default=utils.env('OS_PROJECT_NAME'),
|
||||
help=_('Defaults to env[OS_PROJECT_NAME].'))
|
||||
|
||||
parser.add_argument(
|
||||
'--os-tenant-id', dest='tenant_id', metavar='TENANT_ID',
|
||||
default=utils.env('OS_TENANT_ID'),
|
||||
help=_('Defaults to env[OS_TENANT_ID].'))
|
||||
|
||||
parser.add_argument(
|
||||
'--os-tenant-name', dest='tenant_name', metavar='TENANT_NAME',
|
||||
default=utils.env('OS_TENANT_NAME'),
|
||||
help=_('Defaults to env[OS_TENANT_NAME].'))
|
||||
|
||||
parser.add_argument(
|
||||
'--os-domain-id', dest='domain_id', metavar='DOMAIN_ID',
|
||||
default=utils.env('OS_DOMAIN_ID'),
|
||||
help=_('Domain ID for scope of authorization, defaults to '
|
||||
'env[OS_DOMAIN_ID].'))
|
||||
|
||||
parser.add_argument(
|
||||
'--os-domain-name', dest='domain_name', metavar='DOMAIN_NAME',
|
||||
default=utils.env('OS_DOMAIN_NAME'),
|
||||
help=_('Domain name for scope of authorization, defaults to '
|
||||
'env[OS_DOMAIN_NAME].'))
|
||||
|
||||
parser.add_argument(
|
||||
'--os-project-domain-id', dest='project_domain_id',
|
||||
metavar='PROJECT_DOMAIN_ID',
|
||||
default=utils.env('OS_PROJECT_DOMAIN_ID'),
|
||||
help=_('Project domain ID for scope of authorization, defaults to '
|
||||
'env[OS_PROJECT_DOMAIN_ID].'))
|
||||
|
||||
parser.add_argument(
|
||||
'--os-project-domain-name', dest='project_domain_name',
|
||||
metavar='PROJECT_DOMAIN_NAME',
|
||||
default=utils.env('OS_PROJECT_DOMAIN_NAME'),
|
||||
help=_('Project domain name for scope of authorization, defaults to '
|
||||
'env[OS_PROJECT_DOMAIN_NAME].'))
|
||||
|
||||
parser.add_argument(
|
||||
'--os-user-domain-id', dest='user_domain_id',
|
||||
metavar='USER_DOMAIN_ID',
|
||||
default=utils.env('OS_USER_DOMAIN_ID'),
|
||||
help=_('User domain ID for scope of authorization, defaults to '
|
||||
'env[OS_USER_DOMAIN_ID].'))
|
||||
|
||||
parser.add_argument(
|
||||
'--os-user-domain-name', dest='user_domain_name',
|
||||
metavar='USER_DOMAIN_NAME',
|
||||
default=utils.env('OS_USER_DOMAIN_NAME'),
|
||||
help=_('User domain name for scope of authorization, defaults to '
|
||||
'env[OS_USER_DOMAIN_NAME].'))
|
||||
|
||||
parser.add_argument(
|
||||
'--os-username', dest='username', metavar='USERNAME',
|
||||
default=utils.env('OS_USERNAME'),
|
||||
help=_('Defaults to env[OS_USERNAME].'))
|
||||
|
||||
parser.add_argument(
|
||||
'--os-user-id', dest='user_id', metavar='USER_ID',
|
||||
default=utils.env('OS_USER_ID'),
|
||||
help=_('Defaults to env[OS_USER_ID].'))
|
||||
|
||||
parser.add_argument(
|
||||
'--os-password', dest='password', metavar='PASSWORD',
|
||||
default=utils.env('OS_PASSWORD'),
|
||||
help=_('Defaults to env[OS_PASSWORD]'))
|
||||
|
||||
parser.add_argument(
|
||||
'--os-trust-id', dest='trust_id', metavar='TRUST_ID',
|
||||
default=utils.env('OS_TRUST_ID'),
|
||||
help=_('Defaults to env[OS_TRUST_ID]'))
|
||||
|
||||
verify_group = parser.add_mutually_exclusive_group()
|
||||
|
||||
verify_group.add_argument(
|
||||
'--os-cacert', dest='verify', metavar='CA_BUNDLE_FILE',
|
||||
default=utils.env('OS_CACERT', default=True),
|
||||
help=_('Path of CA TLS certificate(s) used to verify the remote '
|
||||
'server\'s certificate. Without this option senlin looks '
|
||||
'for the default system CA certificates.'))
|
||||
|
||||
verify_group.add_argument(
|
||||
'--verify',
|
||||
action='store_true',
|
||||
help=_('Verify server certificate (default)'))
|
||||
|
||||
verify_group.add_argument(
|
||||
'--insecure', dest='verify', action='store_false',
|
||||
help=_('Explicitly allow senlinclient to perform "insecure SSL" '
|
||||
'(HTTPS) requests. The server\'s certificate will not be '
|
||||
'verified against any certificate authorities. This '
|
||||
'option should be used with caution.'))
|
||||
|
||||
parser.add_argument(
|
||||
'--os-token', dest='token', metavar='TOKEN',
|
||||
default=utils.env('OS_TOKEN', default=None),
|
||||
help=_('A string token to bootstrap the Keystone database, defaults '
|
||||
'to env[OS_TOKEN]'))
|
||||
|
||||
parser.add_argument(
|
||||
'--os-access-info', dest='access_info', metavar='ACCESS_INFO',
|
||||
default=utils.env('OS_ACCESS_INFO'),
|
||||
help=_('Access info, defaults to env[OS_ACCESS_INFO]'))
|
||||
|
||||
parser.add_argument(
|
||||
'--os-api-name', dest='user_preferences',
|
||||
metavar='<service>=<name>',
|
||||
action=sdk.ProfileAction,
|
||||
default=sdk.ProfileAction.env('OS_API_NAME'),
|
||||
help=_('Desired API names, defaults to env[OS_API_NAME]'))
|
||||
|
||||
parser.add_argument(
|
||||
'--os-api-region', dest='user_preferences',
|
||||
metavar='<service>=<region>',
|
||||
action=sdk.ProfileAction,
|
||||
default=sdk.ProfileAction.env('OS_API_REGION', 'OS_REGION_NAME'),
|
||||
help=_('Desired API region, defaults to env[OS_API_REGION]'))
|
||||
|
||||
parser.add_argument(
|
||||
'--os-api-version', dest='user_preferences',
|
||||
metavar='<service>=<version>',
|
||||
action=sdk.ProfileAction,
|
||||
default=sdk.ProfileAction.env('OS_API_VERSION'),
|
||||
help=_('Desired API versions, defaults to env[OS_API_VERSION]'))
|
||||
|
||||
parser.add_argument(
|
||||
'--os-api-interface', dest='user_preferences',
|
||||
metavar='<service>=<interface>',
|
||||
action=sdk.ProfileAction,
|
||||
default=sdk.ProfileAction.env('OS_INTERFACE'),
|
||||
help=_('Desired API interface, defaults to env[OS_INTERFACE]'))
|
||||
|
||||
|
||||
# parser.add_argument(
|
||||
# '--os-cert',
|
||||
# help=_('Path of certificate file to use in SSL connection. This '
|
||||
# 'file can optionally be prepended with the private key.'))
|
||||
#
|
||||
# parser.add_argument(
|
||||
# '--os-key',
|
||||
# help=_('Path of client key to use in SSL connection. This option is '
|
||||
# 'not necessary if your key is prepended to your cert file.'))
|
||||
|
||||
|
||||
def add_global_args(parser, version):
|
||||
# GLOBAL ARGUMENTS
|
||||
parser.add_argument(
|
||||
'-h', '--help', action='store_true',
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
parser.add_argument(
|
||||
'--version', action='version', version=version,
|
||||
help=_("Shows the client version and exits."))
|
||||
|
||||
parser.add_argument(
|
||||
'-d', '--debug', action='store_true',
|
||||
default=bool(utils.env('SENLINCLIENT_DEBUG')),
|
||||
help=_('Defaults to env[SENLINCLIENT_DEBUG].'))
|
||||
|
||||
parser.add_argument(
|
||||
'-v', '--verbose', action="store_true", default=False,
|
||||
help=_("Print more verbose output."))
|
||||
|
||||
parser.add_argument(
|
||||
'--api-timeout',
|
||||
help=_('Number of seconds to wait for an API response, '
|
||||
'defaults to system socket timeout'))
|
||||
|
||||
parser.add_argument(
|
||||
'--senlin-api-version',
|
||||
default=utils.env('SENLIN_API_VERSION', default='1'),
|
||||
help=_('Version number for Senlin API to use, Default to "1".'))
|
321
senlinclient/shell.py
Normal file
321
senlinclient/shell.py
Normal file
@ -0,0 +1,321 @@
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Command-line interface to the Senlin clustering API.
|
||||
"""
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import sys
|
||||
|
||||
from oslo_utils import encodeutils
|
||||
from oslo_utils import importutils
|
||||
import six
|
||||
|
||||
import senlinclient
|
||||
from senlinclient import cliargs
|
||||
from senlinclient import client as senlin_client
|
||||
from senlinclient.common import exc
|
||||
from senlinclient.common.i18n import _
|
||||
from senlinclient.common import utils
|
||||
|
||||
osprofiler_profiler = importutils.try_import("osprofiler.profiler")
|
||||
USER_AGENT = 'python-senlinclient'
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class HelpFormatter(argparse.HelpFormatter):
|
||||
def start_section(self, heading):
|
||||
# Title-case the headings
|
||||
heading = '%s%s' % (heading[0].upper(), heading[1:])
|
||||
super(HelpFormatter, self).start_section(heading)
|
||||
|
||||
|
||||
class SenlinShell(object):
|
||||
def _setup_logging(self, debug):
|
||||
log_lvl = logging.DEBUG if debug else logging.WARNING
|
||||
logging.basicConfig(format="%(levelname)s (%(module)s) %(message)s",
|
||||
level=log_lvl)
|
||||
logging.getLogger('iso8601').setLevel(logging.WARNING)
|
||||
logging.getLogger('urllib3.connectionpool').setLevel(logging.WARNING)
|
||||
|
||||
def _setup_verbose(self, verbose):
|
||||
if verbose:
|
||||
exc.verbose = 1
|
||||
|
||||
def _find_actions(self, subparsers, actions_module):
|
||||
for attr in (a for a in dir(actions_module) if a.startswith('do_')):
|
||||
command = attr[3:].replace('_', '-')
|
||||
callback = getattr(actions_module, attr)
|
||||
|
||||
# get callback documentation string
|
||||
desc = callback.__doc__ or ''
|
||||
help = desc.strip().split('\n')[0]
|
||||
arguments = getattr(callback, 'arguments', [])
|
||||
|
||||
subparser = subparsers.add_parser(command,
|
||||
help=help,
|
||||
description=desc,
|
||||
add_help=False,
|
||||
formatter_class=HelpFormatter)
|
||||
|
||||
subparser.add_argument('-h', '--help',
|
||||
action='help',
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
for (args, kwargs) in arguments:
|
||||
subparser.add_argument(*args, **kwargs)
|
||||
subparser.set_defaults(func=callback)
|
||||
|
||||
self.subcommands[command] = subparser
|
||||
|
||||
def do_bash_completion(self, args):
|
||||
"""Prints all of the commands and options to stdout.
|
||||
|
||||
The senlin.bash_completion script doesn't have to hard code them.
|
||||
"""
|
||||
commands = set()
|
||||
options = set()
|
||||
for sc_str, sc in self.subcommands.items():
|
||||
if sc_str == 'bash_completion' or sc_str == 'bash-completion':
|
||||
continue
|
||||
|
||||
commands.add(sc_str)
|
||||
for option in list(sc._optionals._option_string_actions):
|
||||
options.add(option)
|
||||
|
||||
print(' '.join(commands | options))
|
||||
|
||||
def add_profiler_args(self, parser):
|
||||
if osprofiler_profiler:
|
||||
parser.add_argument(
|
||||
'--profile', metavar='HMAC_KEY',
|
||||
help=_('HMAC key to use for encrypting context data for '
|
||||
'performance profiling of operation. This key should '
|
||||
'be the value of HMAC key configured in osprofiler '
|
||||
'middleware in senlin, it is specified in the paste '
|
||||
'deploy configuration (/etc/senlin/api-paste.ini). '
|
||||
'Without the key, profiling will not be triggered '
|
||||
'even if osprofiler is enabled on server side.'))
|
||||
|
||||
def _add_bash_completion_subparser(self, subparsers):
|
||||
subparser = subparsers.add_parser('bash_completion',
|
||||
add_help=False,
|
||||
formatter_class=HelpFormatter)
|
||||
|
||||
subparser.set_defaults(func=self.do_bash_completion)
|
||||
self.subcommands['bash_completion'] = subparser
|
||||
|
||||
def get_subcommand_parser(self, base_parser, version):
|
||||
parser = base_parser
|
||||
|
||||
self.subcommands = {}
|
||||
subparsers = parser.add_subparsers(metavar='<subcommand>')
|
||||
submodule = utils.import_versioned_module(version, 'shell')
|
||||
self._find_actions(subparsers, submodule)
|
||||
self._find_actions(subparsers, self)
|
||||
self._add_bash_completion_subparser(subparsers)
|
||||
|
||||
return parser
|
||||
|
||||
@utils.arg('command', metavar='<subcommand>', nargs='?',
|
||||
help=_('Display help for <subcommand>.'))
|
||||
def do_help(self, args):
|
||||
"""Display help about this program or one of its subcommands."""
|
||||
if getattr(args, 'command', None):
|
||||
if args.command in self.subcommands:
|
||||
self.subcommands[args.command].print_help()
|
||||
else:
|
||||
raise exc.CommandError("'%s' is not a valid subcommand" %
|
||||
args.command)
|
||||
else:
|
||||
self.parser.print_help()
|
||||
|
||||
def _check_identity_arguments(self, args):
|
||||
# TODO(Qiming): validate the token authentication path and the trust
|
||||
# authentication path
|
||||
|
||||
if not args.auth_url:
|
||||
msg = _('You must provide an auth url via --os-auth-url (or '
|
||||
' env[OS_AUTH_URL])')
|
||||
raise exc.CommandError(msg)
|
||||
|
||||
# username or user_id or token must be specified
|
||||
if not (args.username or args.user_id or args.token):
|
||||
msg = _('You must provide a user name, a user_id or a '
|
||||
'token for authentication')
|
||||
raise exc.CommandError(msg)
|
||||
|
||||
# if both username and user_id are specified, user_id takes precedence
|
||||
if (args.username and args.user_id):
|
||||
msg = _('Both user name and user ID are specified, Senlin will '
|
||||
'use user ID for authentication')
|
||||
print(_('WARNING: %s') % msg)
|
||||
|
||||
if 'v3' in args.auth_url:
|
||||
if (args.username and not args.user_id):
|
||||
if not (args.user_domain_id or args.user_domain_name):
|
||||
msg = _('Either user domain ID (--user-domain-id / '
|
||||
'env[OS_USER_DOMAIN_ID]) or user domain name '
|
||||
'(--user-domain-name / env[OS_USER_DOMAIN_NAME]) '
|
||||
'must be specified, because user name may not be '
|
||||
'unique.')
|
||||
raise exc.CommandError(msg)
|
||||
|
||||
# password is needed if username or user_id is present
|
||||
if (args.username or args.user_id) and not (args.password):
|
||||
msg = _('You must provide a password for user %s') % (
|
||||
args.username or args.user_id)
|
||||
raise exc.CommandError(msg)
|
||||
|
||||
# project name or ID is needed, or else sdk may find the wrong project
|
||||
if (not (args.project_id or args.project_name or args.tenant_id
|
||||
or args.tenant_name)):
|
||||
if not (args.user_id):
|
||||
msg = _('Either project/tenant ID or project/tenant name '
|
||||
'must be specified, or else Senlin cannot know '
|
||||
'which project to use.')
|
||||
raise exc.CommandError(msg)
|
||||
else:
|
||||
msg = _('Neither project ID nor project name is specified. '
|
||||
'Senlin will use user\'s default project which may '
|
||||
'result in authentication error.')
|
||||
print(_('WARNING: %s') % msg)
|
||||
|
||||
# both project name and ID are specified, ID takes precedence
|
||||
if ((args.project_id or args.tenant_id) and
|
||||
(args.project_name or args.tenant_name)):
|
||||
msg = _('Both project/tenant name and project/tenant ID are '
|
||||
'specified, Senlin will use project ID for '
|
||||
'authentication')
|
||||
print(_('WARNING: %s') % msg)
|
||||
|
||||
# project name may not be unique
|
||||
if 'v3' in args.auth_url:
|
||||
if (not (args.project_id or args.tenant_id) and
|
||||
(args.project_name or args.tenant_name) and
|
||||
not (args.project_domain_id or args.project_domain_name)):
|
||||
msg = _('Either project domain ID (--project-domain-id / '
|
||||
'env[OS_PROJECT_DOMAIN_ID]) orr project domain name '
|
||||
'(--project-domain-name / env[OS_PROJECT_DOMAIN_NAME '
|
||||
'must be specified, because project/tenant name may '
|
||||
'not be unique.')
|
||||
raise exc.CommandError(msg)
|
||||
|
||||
def _setup_senlin_client(self, api_ver, args):
|
||||
"""Create senlin client using given args."""
|
||||
kwargs = {
|
||||
'auth_plugin': args.auth_plugin or 'password',
|
||||
'auth_url': args.auth_url,
|
||||
'project_name': args.project_name or args.tenant_name,
|
||||
'project_id': args.project_id or args.tenant_id,
|
||||
'domain_name': args.domain_name,
|
||||
'domain_id': args.domain_id,
|
||||
'project_domain_name': args.project_domain_name,
|
||||
'project_domain_id': args.project_domain_id,
|
||||
'user_domain_name': args.user_domain_name,
|
||||
'user_domain_id': args.user_domain_id,
|
||||
'username': args.username,
|
||||
'user_id': args.user_id,
|
||||
'password': args.password,
|
||||
'verify': args.verify,
|
||||
'token': args.token,
|
||||
'trust_id': args.trust_id,
|
||||
}
|
||||
|
||||
return senlin_client.Client('1', args.user_preferences, USER_AGENT,
|
||||
**kwargs)
|
||||
|
||||
def main(self, argv):
|
||||
# Parse args once to find version
|
||||
parser = argparse.ArgumentParser(
|
||||
prog='senlin',
|
||||
description=__doc__.strip(),
|
||||
epilog=_('Type "senlin help <COMMAND>" for help on a specific '
|
||||
'command.'),
|
||||
add_help=False,
|
||||
formatter_class=HelpFormatter,
|
||||
)
|
||||
|
||||
cliargs.add_global_args(parser, version=senlinclient.__version__)
|
||||
cliargs.add_global_identity_args(parser)
|
||||
self.add_profiler_args(parser)
|
||||
base_parser = parser
|
||||
|
||||
(options, args) = base_parser.parse_known_args(argv)
|
||||
|
||||
self._setup_logging(options.debug)
|
||||
self._setup_verbose(options.verbose)
|
||||
|
||||
# build available subcommands based on version
|
||||
api_ver = options.senlin_api_version
|
||||
LOG.info(api_ver)
|
||||
subcommand_parser = self.get_subcommand_parser(base_parser, api_ver)
|
||||
self.parser = subcommand_parser
|
||||
|
||||
# Handle top-level --help/-h before attempting to parse
|
||||
# a command off the command line
|
||||
if not args and options.help or not argv:
|
||||
self.do_help(options)
|
||||
return 0
|
||||
|
||||
# Parse args again and call whatever callback was selected
|
||||
args = subcommand_parser.parse_args(argv)
|
||||
|
||||
# Short-circuit and deal with help command right away.
|
||||
if args.func == self.do_help:
|
||||
self.do_help(args)
|
||||
return 0
|
||||
elif args.func == self.do_bash_completion:
|
||||
self.do_bash_completion(args)
|
||||
return 0
|
||||
|
||||
# Check if identity information are sufficient
|
||||
self._check_identity_arguments(args)
|
||||
|
||||
# Setup Senlin client connection
|
||||
sc = self._setup_senlin_client(api_ver, args)
|
||||
|
||||
profile = osprofiler_profiler and options.profile
|
||||
if profile:
|
||||
osprofiler_profiler.init(options.profile)
|
||||
|
||||
args.func(sc.service, args)
|
||||
|
||||
if profile:
|
||||
trace_id = osprofiler_profiler.get().get_base_id()
|
||||
print(_("Trace ID: %s") % trace_id)
|
||||
print(_("To display trace use next command:\n"
|
||||
"osprofiler trace show --html %s ") % trace_id)
|
||||
|
||||
|
||||
def main(args=None):
|
||||
try:
|
||||
if args is None:
|
||||
args = sys.argv[1:]
|
||||
|
||||
SenlinShell().main(args)
|
||||
except KeyboardInterrupt:
|
||||
print(_("... terminating senlin client"), file=sys.stderr)
|
||||
sys.exit(130)
|
||||
except Exception as e:
|
||||
if '--debug' in args or '-d' in args:
|
||||
raise
|
||||
else:
|
||||
print(encodeutils.safe_encode(six.text_type(e)), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
78
senlinclient/tests/unit/test_cliargs.py
Normal file
78
senlinclient/tests/unit/test_cliargs.py
Normal file
@ -0,0 +1,78 @@
|
||||
# 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 mock
|
||||
import testtools
|
||||
|
||||
from senlinclient import cliargs
|
||||
|
||||
|
||||
class TestCliArgs(testtools.TestCase):
|
||||
|
||||
def test_add_global_identity_args(self):
|
||||
parser = mock.Mock()
|
||||
|
||||
cliargs.add_global_identity_args(parser)
|
||||
expected = [
|
||||
'--os-auth-plugin',
|
||||
'--os-auth-url',
|
||||
'--os-project-id',
|
||||
'--os-project-name',
|
||||
'--os-tenant-id',
|
||||
'--os-tenant-name',
|
||||
'--os-domain-id',
|
||||
'--os-domain-name',
|
||||
'--os-project-domain-id',
|
||||
'--os-project-domain-name',
|
||||
'--os-user-domain-id',
|
||||
'--os-user-domain-name',
|
||||
'--os-username',
|
||||
'--os-user-id',
|
||||
'--os-password',
|
||||
'--os-trust-id',
|
||||
'--os-token',
|
||||
'--os-access-info',
|
||||
'--os-api-name',
|
||||
'--os-api-region',
|
||||
'--os-api-version',
|
||||
'--os-api-interface'
|
||||
]
|
||||
|
||||
options = [arg[0][0] for arg in parser.add_argument.call_args_list]
|
||||
self.assertEqual(expected, options)
|
||||
|
||||
parser.add_mutually_exclusive_group.assert_called_once_with()
|
||||
group = parser.add_mutually_exclusive_group.return_value
|
||||
|
||||
verify_opts = [arg[0][0] for arg in group.add_argument.call_args_list]
|
||||
verify_args = [
|
||||
'--os-cacert',
|
||||
'--verify',
|
||||
'--insecure'
|
||||
]
|
||||
self.assertEqual(verify_args, verify_opts)
|
||||
|
||||
def test_add_global_args(self):
|
||||
parser = mock.Mock()
|
||||
|
||||
cliargs.add_global_args(parser, '1')
|
||||
expected = [
|
||||
'-h',
|
||||
'--version',
|
||||
'-d',
|
||||
'-v',
|
||||
'--api-timeout',
|
||||
'--senlin-api-version'
|
||||
]
|
||||
|
||||
options = [arg[0][0] for arg in parser.add_argument.call_args_list]
|
||||
self.assertEqual(expected, options)
|
373
senlinclient/tests/unit/test_shell.py
Normal file
373
senlinclient/tests/unit/test_shell.py
Normal file
@ -0,0 +1,373 @@
|
||||
# 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 argparse
|
||||
import logging
|
||||
import sys
|
||||
|
||||
import mock
|
||||
import six
|
||||
from six.moves import builtins
|
||||
import testtools
|
||||
|
||||
from senlinclient import client as senlin_client
|
||||
from senlinclient.common import exc
|
||||
from senlinclient.common.i18n import _
|
||||
from senlinclient.common import sdk
|
||||
from senlinclient.common import utils
|
||||
from senlinclient import shell
|
||||
from senlinclient.tests.unit import fakes
|
||||
|
||||
|
||||
class HelpFormatterTest(testtools.TestCase):
|
||||
|
||||
def test_start_section(self):
|
||||
fmtr = shell.HelpFormatter('senlin')
|
||||
res = fmtr.start_section(('heading', 'text1', 30))
|
||||
self.assertIsNone(res)
|
||||
h = fmtr._current_section.heading
|
||||
self.assertEqual("HEADING('text1', 30)", h)
|
||||
|
||||
|
||||
class TestArgs(testtools.TestCase):
|
||||
|
||||
def __init__(self):
|
||||
self.auth_url = 'http://fakeurl/v3'
|
||||
self.auth_plugin = 'test_plugin'
|
||||
self.username = 'test_user_name'
|
||||
self.user_id = 'test_user_id'
|
||||
self.token = 'test_token'
|
||||
self.project_id = 'test_project_id'
|
||||
self.project_name = 'test_project_name'
|
||||
self.tenant_id = 'test_tenant_id'
|
||||
self.tenant_name = 'test_tenant_name'
|
||||
self.password = 'test_password'
|
||||
self.user_domain_id = 'test_user_domain_id'
|
||||
self.user_domain_name = 'test_user_domain_name'
|
||||
self.project_domain_id = 'test_project_domain_id'
|
||||
self.project_domain_name = 'test_project_domain_name'
|
||||
self.domain_name = 'test_domain_name'
|
||||
self.domain_id = 'test_domain_id'
|
||||
self.verify = 'test_verify'
|
||||
self.user_preferences = 'test_preferences'
|
||||
self.trust_id = 'test_trust'
|
||||
|
||||
|
||||
class ShellTest(testtools.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(ShellTest, self).setUp()
|
||||
|
||||
def SHELL(self, func, *args, **kwargs):
|
||||
orig_out = sys.stdout
|
||||
sys.stdout = six.StringIO()
|
||||
func(*args, **kwargs)
|
||||
output = sys.stdout.getvalue()
|
||||
sys.stdout.close()
|
||||
sys.stdout = orig_out
|
||||
|
||||
return output
|
||||
|
||||
@mock.patch.object(logging, 'basicConfig')
|
||||
@mock.patch.object(logging, 'getLogger')
|
||||
def test_setup_logging_debug(self, x_get, x_config):
|
||||
sh = shell.SenlinShell()
|
||||
sh._setup_logging(True)
|
||||
|
||||
x_config.assert_called_once_with(
|
||||
format="%(levelname)s (%(module)s) %(message)s",
|
||||
level=logging.DEBUG)
|
||||
mock_calls = [
|
||||
mock.call('iso8601'),
|
||||
mock.call().setLevel(logging.WARNING),
|
||||
mock.call('urllib3.connectionpool'),
|
||||
mock.call().setLevel(logging.WARNING),
|
||||
]
|
||||
x_get.assert_has_calls(mock_calls)
|
||||
|
||||
@mock.patch.object(logging, 'basicConfig')
|
||||
@mock.patch.object(logging, 'getLogger')
|
||||
def test_setup_logging_no_debug(self, x_get, x_config):
|
||||
sh = shell.SenlinShell()
|
||||
sh._setup_logging(False)
|
||||
|
||||
x_config.assert_called_once_with(
|
||||
format="%(levelname)s (%(module)s) %(message)s",
|
||||
level=logging.WARNING)
|
||||
mock_calls = [
|
||||
mock.call('iso8601'),
|
||||
mock.call().setLevel(logging.WARNING),
|
||||
mock.call('urllib3.connectionpool'),
|
||||
mock.call().setLevel(logging.WARNING),
|
||||
]
|
||||
x_get.assert_has_calls(mock_calls)
|
||||
|
||||
def test_setup_verbose(self):
|
||||
sh = shell.SenlinShell()
|
||||
sh._setup_verbose(True)
|
||||
self.assertEqual(1, exc.verbose)
|
||||
|
||||
sh._setup_verbose(False)
|
||||
self.assertEqual(1, exc.verbose)
|
||||
|
||||
def test_find_actions(self):
|
||||
sh = shell.SenlinShell()
|
||||
sh.subcommands = {}
|
||||
subparsers = mock.Mock()
|
||||
x_subparser1 = mock.Mock()
|
||||
x_subparser2 = mock.Mock()
|
||||
x_add_parser = mock.MagicMock(side_effect=[x_subparser1, x_subparser2])
|
||||
subparsers.add_parser = x_add_parser
|
||||
|
||||
# subparsers.add_parser = mock.Mock(return_value=x_subparser)
|
||||
sh._find_actions(subparsers, fakes)
|
||||
|
||||
self.assertEqual({'command-bar': x_subparser1,
|
||||
'command-foo': x_subparser2},
|
||||
sh.subcommands)
|
||||
add_calls = [
|
||||
mock.call('command-bar', help='This is the command doc.',
|
||||
description='This is the command doc.',
|
||||
add_help=False,
|
||||
formatter_class=shell.HelpFormatter),
|
||||
mock.call('command-foo', help='Pydoc for command foo.',
|
||||
description='Pydoc for command foo.',
|
||||
add_help=False,
|
||||
formatter_class=shell.HelpFormatter),
|
||||
]
|
||||
x_add_parser.assert_has_calls(add_calls)
|
||||
|
||||
calls_1 = [
|
||||
mock.call('-h', '--help', action='help',
|
||||
help=argparse.SUPPRESS),
|
||||
mock.call('-F', '--flag', metavar='<FLAG>',
|
||||
help='Flag desc.'),
|
||||
mock.call('arg1', metavar='<ARG1>',
|
||||
help='Arg1 desc')
|
||||
]
|
||||
x_subparser1.add_argument.assert_has_calls(calls_1)
|
||||
x_subparser1.set_defaults.assert_called_once_with(
|
||||
func=fakes.do_command_bar)
|
||||
|
||||
calls_2 = [
|
||||
mock.call('-h', '--help', action='help',
|
||||
help=argparse.SUPPRESS),
|
||||
]
|
||||
x_subparser2.add_argument.assert_has_calls(calls_2)
|
||||
x_subparser2.set_defaults.assert_called_once_with(
|
||||
func=fakes.do_command_foo)
|
||||
|
||||
def test_do_bash_completion(self):
|
||||
sh = shell.SenlinShell()
|
||||
sc1 = mock.Mock()
|
||||
sc2 = mock.Mock()
|
||||
sc1._optionals._option_string_actions = ('A1', 'A2', 'C')
|
||||
sc2._optionals._option_string_actions = ('B1', 'B2', 'C')
|
||||
sh.subcommands = {
|
||||
'command-foo': sc1,
|
||||
'command-bar': sc2,
|
||||
'bash-completion': None,
|
||||
'bash_completion': None,
|
||||
}
|
||||
|
||||
output = self.SHELL(sh.do_bash_completion, None)
|
||||
|
||||
output = output.split('\n')[0]
|
||||
output_list = output.split(' ')
|
||||
for option in ('A1', 'A2', 'C', 'B1', 'B2',
|
||||
'command-foo', 'command-bar'):
|
||||
self.assertIn(option, output_list)
|
||||
|
||||
def test_do_add_profiler_args(self):
|
||||
sh = shell.SenlinShell()
|
||||
parser = mock.Mock()
|
||||
|
||||
sh.add_profiler_args(parser)
|
||||
|
||||
self.assertEqual(0, parser.add_argument.call_count)
|
||||
|
||||
def test_add_bash_completion_subparser(self):
|
||||
sh = shell.SenlinShell()
|
||||
sh.subcommands = {}
|
||||
x_subparser = mock.Mock()
|
||||
x_subparsers = mock.Mock()
|
||||
x_subparsers.add_parser.return_value = x_subparser
|
||||
|
||||
sh._add_bash_completion_subparser(x_subparsers)
|
||||
|
||||
x_subparsers.add_parser.assert_called_once_with(
|
||||
'bash_completion', add_help=False,
|
||||
formatter_class=shell.HelpFormatter)
|
||||
self.assertEqual({'bash_completion': x_subparser}, sh.subcommands)
|
||||
x_subparser.set_defaults.assert_called_once_with(
|
||||
func=sh.do_bash_completion)
|
||||
|
||||
@mock.patch.object(utils, 'import_versioned_module')
|
||||
@mock.patch.object(shell.SenlinShell, '_find_actions')
|
||||
@mock.patch.object(shell.SenlinShell, '_add_bash_completion_subparser')
|
||||
def test_get_subcommand_parser(self, x_add, x_find, x_import):
|
||||
x_base = mock.Mock()
|
||||
x_module = mock.Mock()
|
||||
x_import.return_value = x_module
|
||||
sh = shell.SenlinShell()
|
||||
|
||||
res = sh.get_subcommand_parser(x_base, 'v100')
|
||||
|
||||
self.assertEqual(x_base, res)
|
||||
x_base.add_subparsers.assert_called_once_with(
|
||||
metavar='<subcommand>')
|
||||
x_subparsers = x_base.add_subparsers.return_value
|
||||
x_import.assert_called_once_with('v100', 'shell')
|
||||
find_calls = [
|
||||
mock.call(x_subparsers, x_module),
|
||||
mock.call(x_subparsers, sh)
|
||||
]
|
||||
|
||||
x_find.assert_has_calls(find_calls)
|
||||
x_add.assert_called_once_with(x_subparsers)
|
||||
|
||||
@mock.patch.object(argparse.ArgumentParser, 'print_help')
|
||||
def test_do_help(self, mock_print):
|
||||
sh = shell.SenlinShell()
|
||||
args = mock.Mock()
|
||||
args.command = mock.Mock()
|
||||
sh.subcommands = {args.command: argparse.ArgumentParser}
|
||||
sh.do_help(args)
|
||||
self.assertTrue(mock_print.called)
|
||||
|
||||
sh.subcommands = {}
|
||||
ex = self.assertRaises(exc.CommandError,
|
||||
sh.do_help, args)
|
||||
msg = _("'%s' is not a valid subcommand") % args.command
|
||||
self.assertEqual(msg, six.text_type(ex))
|
||||
|
||||
@mock.patch.object(builtins, 'print')
|
||||
def test_check_identity_arguments(self, mock_print):
|
||||
sh = shell.SenlinShell()
|
||||
# auth_url is not specified.
|
||||
args = TestArgs()
|
||||
args.auth_url = None
|
||||
ex = self.assertRaises(exc.CommandError,
|
||||
sh._check_identity_arguments, args)
|
||||
msg = _('You must provide an auth url via --os-auth-url (or '
|
||||
' env[OS_AUTH_URL])')
|
||||
self.assertEqual(msg, six.text_type(ex))
|
||||
# username, user_id and token are not specified.
|
||||
args = TestArgs()
|
||||
args.username = None
|
||||
args.user_id = None
|
||||
args.token = None
|
||||
msg = _('You must provide a user name, a user_id or a '
|
||||
'token for authentication')
|
||||
ex = self.assertRaises(exc.CommandError,
|
||||
sh._check_identity_arguments, args)
|
||||
self.assertEqual(msg, six.text_type(ex))
|
||||
# Both username and user_id are specified.
|
||||
args = TestArgs()
|
||||
args.project_id = None
|
||||
args.tenant_id = None
|
||||
sh._check_identity_arguments(args)
|
||||
msg = _('WARNING: Both user name and user ID are specified, '
|
||||
'Senlin will use user ID for authentication')
|
||||
mock_print.assert_called_with(msg)
|
||||
|
||||
# 'v3' in auth_url but neither user_domain_id nor user_domain_name
|
||||
# is specified.
|
||||
args = TestArgs()
|
||||
args.user_id = None
|
||||
args.user_domain_id = None
|
||||
args.user_domain_name = None
|
||||
msg = _('Either user domain ID (--user-domain-id / '
|
||||
'env[OS_USER_DOMAIN_ID]) or user domain name '
|
||||
'(--user-domain-name / env[OS_USER_DOMAIN_NAME]) '
|
||||
'must be specified, because user name may not be '
|
||||
'unique.')
|
||||
ex = self.assertRaises(exc.CommandError,
|
||||
sh._check_identity_arguments, args)
|
||||
self.assertEqual(msg, six.text_type(ex))
|
||||
# user_id, project_id, project_name, tenant_id and tenant_name are all
|
||||
# not specified.
|
||||
args = TestArgs()
|
||||
args.project_id = None
|
||||
args.project_name = None
|
||||
args.tenant_id = None
|
||||
args.tenant_name = None
|
||||
args.user_id = None
|
||||
msg = _('Either project/tenant ID or project/tenant name '
|
||||
'must be specified, or else Senlin cannot know '
|
||||
'which project to use.')
|
||||
ex = self.assertRaises(exc.CommandError,
|
||||
sh._check_identity_arguments, args)
|
||||
self.assertEqual(msg, six.text_type(ex))
|
||||
args.user_id = 'test_user_id'
|
||||
sh._check_identity_arguments(args)
|
||||
msg = _('Neither project ID nor project name is specified. '
|
||||
'Senlin will use user\'s default project which may '
|
||||
'result in authentication error.')
|
||||
mock_print.assert_called_with(_('WARNING: %s') % msg)
|
||||
|
||||
# Both project_name and project_id are specified
|
||||
args = TestArgs()
|
||||
args.user_id = None
|
||||
sh._check_identity_arguments(args)
|
||||
msg = _('Both project/tenant name and project/tenant ID are '
|
||||
'specified, Senlin will use project ID for '
|
||||
'authentication')
|
||||
mock_print.assert_called_with(_('WARNING: %s') % msg)
|
||||
# Project name may not be unique
|
||||
args = TestArgs()
|
||||
args.user_id = None
|
||||
args.project_id = None
|
||||
args.tenant_id = None
|
||||
args.project_domain_id = None
|
||||
args.project_domain_name = None
|
||||
msg = _('Either project domain ID (--project-domain-id / '
|
||||
'env[OS_PROJECT_DOMAIN_ID]) orr project domain name '
|
||||
'(--project-domain-name / env[OS_PROJECT_DOMAIN_NAME '
|
||||
'must be specified, because project/tenant name may '
|
||||
'not be unique.')
|
||||
ex = self.assertRaises(exc.CommandError,
|
||||
sh._check_identity_arguments, args)
|
||||
self.assertEqual(msg, six.text_type(ex))
|
||||
|
||||
@mock.patch.object(sdk, 'create_connection')
|
||||
def test_setup_senlinclient(self, mock_conn):
|
||||
USER_AGENT = 'python-senlinclient'
|
||||
args = TestArgs()
|
||||
kwargs = {
|
||||
'auth_plugin': args.auth_plugin,
|
||||
'auth_url': args.auth_url,
|
||||
'project_name': args.project_name or args.tenant_name,
|
||||
'project_id': args.project_id or args.tenant_id,
|
||||
'domain_name': args.domain_name,
|
||||
'domain_id': args.domain_id,
|
||||
'project_domain_name': args.project_domain_name,
|
||||
'project_domain_id': args.project_domain_id,
|
||||
'user_domain_name': args.user_domain_name,
|
||||
'user_domain_id': args.user_domain_id,
|
||||
'username': args.username,
|
||||
'user_id': args.user_id,
|
||||
'password': args.password,
|
||||
'verify': args.verify,
|
||||
'token': args.token,
|
||||
'trust_id': args.trust_id,
|
||||
}
|
||||
sh = shell.SenlinShell()
|
||||
conn = mock.Mock()
|
||||
mock_conn.return_value = conn
|
||||
conn.session = mock.Mock()
|
||||
sh._setup_senlin_client('1', args)
|
||||
mock_conn.assert_called_once_with(args.user_preferences, USER_AGENT,
|
||||
**kwargs)
|
||||
client = mock.Mock()
|
||||
senlin_client.Client = mock.MagicMock(return_value=client)
|
||||
self.assertEqual(client, sh._setup_senlin_client('1', args))
|
1348
senlinclient/tests/unit/v1/test_shell.py
Normal file
1348
senlinclient/tests/unit/v1/test_shell.py
Normal file
File diff suppressed because it is too large
Load Diff
1314
senlinclient/v1/shell.py
Normal file
1314
senlinclient/v1/shell.py
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user