diff --git a/senlinclient/cliargs.py b/senlinclient/cliargs.py new file mode 100644 index 00000000..694d0179 --- /dev/null +++ b/senlinclient/cliargs.py @@ -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='=', + 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='=', + 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='=', + 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='=', + 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".')) diff --git a/senlinclient/shell.py b/senlinclient/shell.py new file mode 100644 index 00000000..59549825 --- /dev/null +++ b/senlinclient/shell.py @@ -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='') + 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='', nargs='?', + help=_('Display help for .')) + 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 " 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() diff --git a/senlinclient/tests/unit/test_cliargs.py b/senlinclient/tests/unit/test_cliargs.py new file mode 100644 index 00000000..8bc6df88 --- /dev/null +++ b/senlinclient/tests/unit/test_cliargs.py @@ -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) diff --git a/senlinclient/tests/unit/test_shell.py b/senlinclient/tests/unit/test_shell.py new file mode 100644 index 00000000..dbeaaf71 --- /dev/null +++ b/senlinclient/tests/unit/test_shell.py @@ -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='', + help='Flag desc.'), + mock.call('arg1', metavar='', + 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='') + 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)) diff --git a/senlinclient/tests/unit/v1/test_shell.py b/senlinclient/tests/unit/v1/test_shell.py new file mode 100644 index 00000000..e2d1ef22 --- /dev/null +++ b/senlinclient/tests/unit/v1/test_shell.py @@ -0,0 +1,1348 @@ +# 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 copy +import mock +import six +import testtools + +from openstack import exceptions as oexc +from oslotest import mockpatch +from senlinclient.common import exc +from senlinclient.common.i18n import _ +from senlinclient.common import utils +from senlinclient.v1 import shell as sh + + +class ShellTest(testtools.TestCase): + + def setUp(self): + super(ShellTest, self).setUp() + self.profile_args = { + 'spec_file': mock.Mock(), + 'name': 'stack_spec', + 'metadata': {'user': 'demo'} + } + self.profile_spec = { + 'type': 'os.heat.stack', + 'version': 1.0, + 'properties': { + 'name': 'stack1', + 'template': {"Template": "data"} + } + } + self.patch('senlinclient.v1.shell.show_deprecated') + + # NOTE(pshchelo): this overrides the testtools.TestCase.patch method + # that does simple monkey-patching in favor of mock's patching + def patch(self, target, **kwargs): + mockfixture = self.useFixture(mockpatch.Patch(target, **kwargs)) + return mockfixture.mock + + def _make_args(self, args): + """Convert a dict to an object.""" + class Args(object): + def __init__(self, entries): + self.__dict__.update(entries) + + return Args(args) + + @mock.patch.object(utils, 'print_dict') + def test_do_build_info(self, mock_print): + service = mock.Mock() + result = mock.Mock() + service.get_build_info.return_value = result + sh.do_build_info(service) + formatters = { + 'api': utils.json_formatter, + 'engine': utils.json_formatter, + } + mock_print.assert_called_once_with(result, formatters=formatters) + self.assertTrue(service.get_build_info.called) + + @mock.patch.object(utils, 'print_list') + def test_do_profile_type_list(self, mock_print): + service = mock.Mock() + types = mock.Mock() + service.profile_types.return_value = types + sh.do_profile_type_list(service) + mock_print.assert_called_once_with(types, ['name'], sortby_index=0) + self.assertTrue(service.profile_types.called) + + @mock.patch.object(utils, 'format_output') + def test_do_profile_type_show(self, mock_format): + service = mock.Mock() + fake_pt = mock.Mock() + fake_pt.to_dict.return_value = {'foo': 'bar'} + service.get_profile_type = mock.Mock(return_value=fake_pt) + args_dict = { + 'format': 'json', + 'type_name': 'os.nova.server' + } + args = self._make_args(args_dict) + sh.do_profile_type_show(service, args) + mock_format.assert_called_with({'foo': 'bar'}, format=args.format) + service.get_profile_type.assert_called_with('os.nova.server') + args.format = None + sh.do_profile_type_show(service, args) + mock_format.assert_called_with({'foo': 'bar'}) + + def test_do_profile_type_show_type_not_found(self): + service = mock.Mock() + args = { + 'type_name': 'wrong_type', + 'format': 'json' + } + args = self._make_args(args) + ex = oexc.ResourceNotFound + service.get_profile_type = mock.Mock(side_effect=ex) + ex = self.assertRaises(exc.CommandError, + sh.do_profile_type_show, + service, args) + self.assertEqual(_('Profile Type not found: wrong_type'), + six.text_type(ex)) + + @mock.patch.object(utils, 'print_list') + def test_do_profile_list(self, mock_print): + service = mock.Mock() + profiles = mock.Mock() + service.profiles.return_value = profiles + fields = ['id', 'name', 'type', 'created_at'] + args = { + 'limit': 20, + 'marker': 'mark_id', + 'sort': 'key:dir', + 'global_project': True, + 'filters': ['name=stack_spec'] + } + queries = copy.deepcopy(args) + del queries['filters'] + queries['name'] = 'stack_spec' + formatters = {} + args = self._make_args(args) + args.full_id = True + sh.do_profile_list(service, args) + service.profiles.assert_called_once_with(**queries) + mock_print.assert_called_with(profiles, fields, formatters=formatters, + sortby_index=None) + + args.sort = None + sh.do_profile_list(service, args) + mock_print.assert_called_with(profiles, fields, formatters=formatters, + sortby_index=1) + + @mock.patch.object(utils, 'nested_dict_formatter') + @mock.patch.object(utils, 'print_dict') + def test_show_profile(self, mock_print, mock_dict): + service = mock.Mock() + profile = mock.Mock() + profile_id = mock.Mock() + service.get_profile.return_value = profile + pro_to_dict = mock.Mock() + profile.to_dict.return_value = pro_to_dict + json_formatter = mock.Mock() + utils.json_formatter = json_formatter + dict_formatter = mock.Mock() + mock_dict.return_value = dict_formatter + formatters = { + 'metadata': json_formatter, + 'spec': dict_formatter + } + sh._show_profile(service, profile_id) + service.get_profile.assert_called_once_with(profile_id) + mock_dict.assert_called_once_with(['type', 'version', 'properties'], + ['property', 'value']) + mock_print.assert_called_once_with(pro_to_dict, formatters=formatters) + + def test_show_profile_not_found(self): + service = mock.Mock() + ex = oexc.ResourceNotFound + service.get_profile.side_effect = ex + profile_id = 'wrong_id' + ex = self.assertRaises(exc.CommandError, + sh._show_profile, + service, profile_id) + self.assertEqual(_('Profile not found: wrong_id'), six.text_type(ex)) + service.get_profile.assert_called_once_with(profile_id) + + @mock.patch.object(sh, '_show_profile') + @mock.patch.object(utils, 'format_parameters') + @mock.patch.object(utils, 'process_stack_spec') + @mock.patch.object(utils, 'get_spec_content') + def test_do_profile_create(self, mock_get, mock_proc, mock_format, + mock_show): + args = copy.deepcopy(self.profile_args) + args = self._make_args(args) + spec = copy.deepcopy(self.profile_spec) + mock_get.return_value = spec + stack_properties = mock.Mock() + mock_proc.return_value = stack_properties + mock_format.return_value = {'user': 'demo'} + params = { + 'name': 'stack_spec', + 'spec': spec, + 'metadata': {'user': 'demo'}, + } + service = mock.Mock() + profile = mock.Mock() + profile_id = mock.Mock() + profile.id = profile_id + service.create_profile.return_value = profile + + sh.do_profile_create(service, args) + + mock_get.assert_called_once_with(args.spec_file) + mock_proc.assert_called_once_with(self.profile_spec['properties']) + mock_format.assert_called_once_with(args.metadata) + service.create_profile.assert_called_once_with(**params) + mock_show.assert_called_once_with(service, profile_id) + + # Miss 'type' key in spec file + del spec['type'] + ex = self.assertRaises(exc.CommandError, + sh.do_profile_create, + service, args) + self.assertEqual(_("Missing 'type' key in spec file."), + six.text_type(ex)) + # Miss 'version' key in spec file + spec['type'] = 'os.heat.stack' + del spec['version'] + ex = self.assertRaises(exc.CommandError, + sh.do_profile_create, + service, args) + self.assertEqual(_("Missing 'version' key in spec file."), + six.text_type(ex)) + # Miss 'properties' key in spec file + spec['version'] = 1.0 + del spec['properties'] + ex = self.assertRaises(exc.CommandError, + sh.do_profile_create, + service, args) + self.assertEqual(_("Missing 'properties' key in spec file."), + six.text_type(ex)) + + @mock.patch.object(sh, '_show_profile') + def test_do_profile_show(self, mock_show): + service = mock.Mock() + args = {'id': 'profile_id'} + args = self._make_args(args) + sh.do_profile_show(service, args) + mock_show.assert_called_once_with(service, args.id) + + @mock.patch.object(sh, '_show_profile') + @mock.patch.object(utils, 'format_parameters') + def test_do_profile_update(self, mock_format, mock_show): + args = copy.deepcopy(self.profile_args) + args = self._make_args(args) + mock_format.return_value = {'user': 'demo'} + service = mock.Mock() + profile = mock.Mock() + profile_id = mock.Mock() + profile.id = profile_id + args.id = 'FAKE_ID' + service.get_profile.return_value = profile + + sh.do_profile_update(service, args) + + mock_format.assert_called_once_with(args.metadata) + service.get_profile.assert_called_once_with('FAKE_ID') + params = { + 'name': 'stack_spec', + 'metadata': {'user': 'demo'}, + } + service.update_profile.assert_called_once_with(profile_id, **params) + mock_show.assert_called_once_with(service, profile_id) + + @mock.patch.object(utils, 'format_parameters') + def test_do_profile_update_not_found(self, mock_format): + service = mock.Mock() + args = copy.deepcopy(self.profile_args) + args = self._make_args(args) + args.id = 'FAKE_ID' + ex = oexc.ResourceNotFound + service.get_profile.side_effect = ex + ex = self.assertRaises(exc.CommandError, + sh.do_profile_update, + service, args) + self.assertEqual(_('Profile not found: FAKE_ID'), + six.text_type(ex)) + mock_format.assert_called_once_with(args.metadata) + + def test_do_profile_delete(self): + service = mock.Mock() + args = {'id': ['profile_id']} + args = self._make_args(args) + sh.do_profile_delete(service, args) + service.delete_profile.assert_called_with('profile_id', False) + + def test_do_profile_delete_not_found(self): + service = mock.Mock() + args = {'id': ['profile1', 'profile2']} + args = self._make_args(args) + sh.do_profile_delete(service, args) + service.delete_profile.side_effect = oexc.ResourceNotFound + ex = self.assertRaises(exc.CommandError, + sh.do_profile_delete, + service, args) + msg = _("Failed to delete some of the specified profile(s).") + self.assertEqual(msg, six.text_type(ex)) + service.delete_profile.assert_called_with('profile2', False) + + @mock.patch.object(utils, 'print_list') + def test_do_policy_type_list(self, mock_print): + service = mock.Mock() + args = mock.Mock() + types = mock.Mock() + service.policy_types.return_value = types + sh.do_policy_type_list(service, args) + mock_print.assert_called_once_with(types, ['name'], sortby_index=0) + + @mock.patch.object(utils, 'format_output') + def test_do_policy_type_show(self, mock_format): + service = mock.Mock() + args = { + 'type_name': 'senlin.policy.deletion', + 'format': 'yaml' + } + args = self._make_args(args) + res = mock.Mock() + pt = mock.Mock() + res.to_dict.return_value = pt + service.get_policy_type.return_value = res + sh.do_policy_type_show(service, args) + mock_format.assert_called_with(pt, format=args.format) + + # no format attribute + args = { + 'type_name': 'senlin.policy.deletion', + 'format': None + } + args = self._make_args(args) + service.get_policy_type.return_value = res + sh.do_policy_type_show(service, args) + mock_format.assert_called_with(pt) + + def test_do_policy_type_show_not_found(self): + service = mock.Mock() + args = {'type_name': 'BAD'} + args = self._make_args(args) + + service.get_policy_type.side_effect = oexc.ResourceNotFound + ex = self.assertRaises(exc.CommandError, + sh.do_policy_type_show, service, args) + msg = _('Policy type not found: BAD') + self.assertEqual(msg, six.text_type(ex)) + + @mock.patch.object(utils, 'print_list') + def test_do_receiver_list(self, mock_print): + service = mock.Mock() + params = { + 'limit': 10, + 'marker': 'fake_id', + 'sort': 'key:dir', + 'filters': ['filter_key=filter_value'], + 'global_project': False, + 'full_id': False, + } + fields = ['id', 'name', 'type', 'cluster_id', 'action', 'created_at'] + args = self._make_args(params) + queries = copy.deepcopy(params) + del queries['filters'] + queries['filter_key'] = 'filter_value' + r1 = mock.Mock() + r1.id = '01234567-abcd-efgh' + r1.cluster_id = 'abcdefgh-abcd-efgh' + receivers = [r1] + service.receivers.return_value = receivers + formatters = { + 'id': mock.ANY, + 'cluster_id': mock.ANY + } + sh.do_receiver_list(service, args) + mock_print.assert_called_with(receivers, fields, + formatters=formatters, + sortby_index=None) + # full_id is requested + args.full_id = True + sh.do_receiver_list(service, args) + mock_print.assert_called_with(receivers, fields, + formatters={}, + sortby_index=None) + + # default sorting + args.sort = None + sh.do_receiver_list(service, args) + mock_print.assert_called_with(receivers, fields, + formatters={}, + sortby_index=0) + + @mock.patch.object(utils, 'print_dict') + def test_show_receiver(self, mock_print): + service = mock.Mock() + receiver = mock.Mock() + receiver_id = '01234567-abcd-abcd-abcdef' + receiver.id = receiver_id + service.get_receiver.return_value = receiver + receiver_dict = mock.Mock() + receiver.to_dict.return_value = receiver_dict + sh._show_receiver(service, receiver_id) + formatters = { + 'actor': utils.json_formatter, + 'params': utils.json_formatter, + 'channel': utils.json_formatter, + } + service.get_receiver.assert_called_once_with(receiver_id) + mock_print.assert_called_once_with(receiver_dict, + formatters=formatters) + + def test_show_receiver_not_found(self): + service = mock.Mock() + receiver = mock.Mock() + receiver_id = 'wrong_id' + receiver.id = receiver_id + + service.get_receiver.side_effect = oexc.ResourceNotFound + ex = self.assertRaises(exc.CommandError, + sh._show_receiver, service, receiver_id) + self.assertEqual(_('Receiver not found: wrong_id'), six.text_type(ex)) + + @mock.patch.object(sh, '_show_receiver') + def test_do_receiver_show(self, mock_show): + service = mock.Mock() + args = {'id': 'receiver_id'} + args = self._make_args(args) + sh.do_receiver_show(service, args) + mock_show.assert_called_once_with(service, + receiver_id='receiver_id') + + @mock.patch.object(sh, '_show_receiver') + def test_do_receiver_create(self, mock_show): + service = mock.Mock() + args = { + 'name': 'receiver1', + 'type': 'webhook', + 'cluster': 'cluster1', + 'action': 'CLUSTER_SCALE_IN', + 'params': {} + } + args = self._make_args(args) + params = { + 'name': 'receiver1', + 'type': 'webhook', + 'cluster_id': 'cluster1', + 'action': 'CLUSTER_SCALE_IN', + 'params': {} + } + receiver = mock.Mock() + receiver.id = 'FAKE_ID' + service.create_receiver.return_value = receiver + sh.do_receiver_create(service, args) + service.create_receiver.assert_called_once_with(**params) + mock_show.assert_called_once_with(service, 'FAKE_ID') + + def test_do_receiver_delete(self): + service = mock.Mock() + args = {'id': ['FAKE']} + args = self._make_args(args) + service.delete_receiver = mock.Mock() + sh.do_receiver_delete(service, args) + service.delete_receiver.assert_called_once_with('FAKE', False) + + def test_do_receiver_delete_not_found(self): + service = mock.Mock() + args = {'id': ['receiver_id']} + args = self._make_args(args) + + service.delete_receiver.side_effect = oexc.ResourceNotFound + ex = self.assertRaises(exc.CommandError, + sh.do_receiver_delete, service, args) + msg = _("Failed to delete some of the specified receiver(s).") + self.assertEqual(msg, six.text_type(ex)) + + @mock.patch.object(utils, 'print_list') + def test_do_policy_list(self, mock_print): + service = mock.Mock() + fields = ['id', 'name', 'type', 'created_at'] + args = { + 'limit': 20, + 'marker': 'fake_id', + 'sort': 'name', + 'global_project': False, + 'full_id': True, + 'filters': ['name=stack_spec'] + } + args = self._make_args(args) + queries = { + 'limit': 20, + 'marker': 'fake_id', + 'sort': 'name', + 'global_project': False, + 'name': 'stack_spec', + } + policies = mock.Mock() + service.policies.return_value = policies + formatters = {} + sh.do_policy_list(service, args) + service.policies.assert_called_once_with(**queries) + mock_print.assert_called_once_with( + policies, fields, formatters=formatters, sortby_index=None) + mock_print.reset_mock() + + args.sort = None + sh.do_policy_list(service, args) + mock_print.assert_called_once_with( + policies, fields, formatters=formatters, sortby_index=1) + + @mock.patch.object(utils, 'print_dict') + def test_show_policy(self, mock_print): + service = mock.Mock() + formatters = { + 'metadata': utils.json_formatter, + 'spec': utils.json_formatter, + } + policy_id = 'fake_policy_id' + policy = mock.Mock() + policy.id = policy_id + service.get_policy.return_value = policy + policy_dict = mock.Mock() + policy.to_dict.return_value = policy_dict + sh._show_policy(service, policy_id) + mock_print.assert_called_once_with(policy_dict, + formatters=formatters) + + # policy not found + ex = oexc.ResourceNotFound + service.get_policy.side_effect = ex + ex = self.assertRaises(exc.CommandError, + sh._show_policy, + service, policy_id) + msg = _('Policy not found: fake_policy_id') + self.assertEqual(msg, six.text_type(ex)) + + @mock.patch.object(sh, '_show_policy') + @mock.patch.object(utils, 'get_spec_content') + def test_do_policy_create(self, mock_get, mock_show): + service = mock.Mock() + spec = mock.Mock() + mock_get.return_value = spec + args = { + 'name': 'new_policy', + 'spec_file': 'policy_file', + } + args = self._make_args(args) + attrs = { + 'name': 'new_policy', + 'spec': spec, + } + policy = mock.Mock() + policy.id = 'policy_id' + service.create_policy.return_value = policy + sh.do_policy_create(service, args) + mock_get.assert_called_once_with(args.spec_file) + service.create_policy.assert_called_once_with(**attrs) + mock_show.assert_called_once_with(service, policy.id) + + @mock.patch.object(sh, '_show_policy') + def test_do_policy_show(self, mock_show): + service = mock.Mock() + args = {'id': 'policy_id'} + args = self._make_args(args) + sh.do_policy_show(service, args) + mock_show.assert_called_once_with(service, policy_id='policy_id') + + @mock.patch.object(sh, '_show_policy') + def test_do_policy_update(self, mock_show): + service = mock.Mock() + args = { + 'name': 'deletion_policy', + 'id': 'policy_id', + } + args = self._make_args(args) + params = { + 'name': 'deletion_policy', + } + policy = mock.Mock() + service.get_policy.return_value = policy + policy.id = 'policy_id' + sh.do_policy_update(service, args) + service.get_policy.assert_called_once_with('policy_id') + service.update_policy.assert_called_once_with( + 'policy_id', **params) + mock_show(service, policy_id=policy.id) + + def test_do_policy_delete(self): + service = mock.Mock() + args = {'id': ['policy_id']} + args = self._make_args(args) + service.delete_policy = mock.Mock() + sh.do_policy_delete(service, args) + service.delete_policy.assert_called_once_with('policy_id', False) + + def test_do_policy_delete_not_found(self): + service = mock.Mock() + args = {'id': ['policy_id']} + args = self._make_args(args) + + service.delete_policy.side_effect = oexc.ResourceNotFound + ex = self.assertRaises(exc.CommandError, + sh.do_policy_delete, service, args) + msg = _("Failed to delete some of the specified policy(s).") + self.assertEqual(msg, six.text_type(ex)) + + @mock.patch.object(utils, 'print_list') + def test_do_cluster_list(self, mock_print): + service = mock.Mock() + fields = ['id', 'name', 'status', 'created_at', 'updated_at'] + args = { + 'limit': 20, + 'marker': 'fake_id', + 'sort': 'key:dir', + 'global_project': False, + 'filters': ['status=ACTIVE'], + } + queries = copy.deepcopy(args) + del queries['filters'] + queries['status'] = 'ACTIVE' + args = self._make_args(args) + clusters = mock.Mock() + service.clusters.return_value = clusters + args.full_id = True + formatters = {} + sh.do_cluster_list(service, args) + service.clusters.assert_called_once_with(**queries) + mock_print.assert_called_once_with(clusters, fields, + formatters=formatters, + sortby_index=None) + args.sort = None + sh.do_cluster_list(service, args) + mock_print.assert_called_with(clusters, fields, + formatters={}, sortby_index=3) + + @mock.patch.object(utils, 'print_dict') + def test_show_cluster(self, mock_print): + service = mock.Mock() + cluster_id = 'cluster_id' + cluster = mock.Mock() + cluster.id = cluster_id + service.get_cluster.return_value = cluster + formatters = { + 'metadata': utils.json_formatter, + 'nodes': utils.list_formatter, + } + cluster_dict = mock.Mock() + cluster.to_dict.return_value = cluster_dict + sh._show_cluster(service, cluster_id) + mock_print.assert_called_once_with(cluster_dict, formatters=formatters) + + @mock.patch.object(sh, '_show_cluster') + def test_do_cluster_create(self, mock_show): + service = mock.Mock() + args = { + 'name': 'CLUSTER1', + 'profile': 'profile1', + 'min_size': 1, + 'max_size': 10, + 'desired_capacity': 5, + 'metadata': ['user=demo'], + 'timeout': 200, + } + attrs = copy.deepcopy(args) + attrs['profile_id'] = args['profile'] + args = self._make_args(args) + del attrs['profile'] + attrs['metadata'] = {'user': 'demo'} + cluster = mock.Mock() + service.create_cluster.return_value = cluster + cluster.id = 'cluster_id' + sh.do_cluster_create(service, args) + service.create_cluster.assert_called_once_with(**attrs) + mock_show.assert_called_once_with(service, 'cluster_id') + + def test_do_cluster_delete(self): + service = mock.Mock() + args = {'id': ['CID']} + args = self._make_args(args) + service.delete_cluster = mock.Mock() + sh.do_cluster_delete(service, args) + service.delete_cluster.assert_called_once_with('CID', False) + + def test_do_cluster_delete_not_found(self): + service = mock.Mock() + args = {'id': ['cluster_id']} + args = self._make_args(args) + + service.delete_cluster.side_effect = oexc.ResourceNotFound + ex = self.assertRaises(exc.CommandError, + sh.do_cluster_delete, service, args) + msg = _('Failed to delete some of the specified clusters.') + self.assertEqual(msg, six.text_type(ex)) + + @mock.patch.object(sh, '_show_cluster') + def test_do_cluster_update(self, mock_show): + service = mock.Mock() + args = { + 'profile': 'test_profile', + 'name': 'CLUSTER1', + 'metadata': ['user=demo'], + 'timeout': 100, + } + attrs = copy.deepcopy(args) + attrs['metadata'] = {'user': 'demo'} + attrs['profile_id'] = 'test_profile' + del attrs['profile'] + args = self._make_args(args) + args.id = 'CID' + cluster = mock.Mock() + cluster.id = 'CID' + service.get_cluster.return_value = cluster + service.update_cluster = mock.Mock() + + sh.do_cluster_update(service, args) + + service.get_cluster.assert_called_once_with('CID') + service.update_cluster.assert_called_once_with('CID', **attrs) + mock_show.assert_called_once_with(service, 'CID') + + @mock.patch.object(sh, '_show_cluster') + def test_do_cluster_show(self, mock_show): + service = mock.Mock() + args = {'id': 'cluster_id'} + args = self._make_args(args) + sh.do_cluster_show(service, args) + mock_show.assert_called_once_with(service, 'cluster_id') + + @mock.patch.object(utils, 'print_list') + def test_do_cluster_node_list(self, mock_print): + service = mock.Mock() + args = { + 'id': 'cluster_id', + 'limit': 20, + 'marker': 'marker_id', + 'filters': ['status=ACTIVE'], + } + queries = copy.deepcopy(args) + queries['cluster_id'] = args['id'] + del queries['id'] + del queries['filters'] + queries['status'] = 'ACTIVE' + args = self._make_args(args) + args.full_id = True + nodes = mock.Mock() + service.nodes.return_value = nodes + formatters = {} + fields = ['id', 'name', 'index', 'status', 'physical_id', 'created_at'] + sh.do_cluster_node_list(service, args) + service.nodes.assert_called_once_with(**queries) + mock_print.assert_called_once_with(nodes, fields, + formatters=formatters, + sortby_index=5) + + def test_do_cluster_node_add(self): + service = mock.Mock() + args = { + 'id': 'cluster_id', + 'nodes': 'node1,node2' + } + args = self._make_args(args) + node_ids = ['node1', 'node2'] + resp = {'action': 'CLUSTER_NODE_ADD'} + service.cluster_add_nodes.return_value = resp + sh.do_cluster_node_add(service, args) + service.cluster_add_nodes.assert_called_once_with( + 'cluster_id', node_ids) + + def test_do_cluster_node_del(self): + service = mock.Mock() + args = { + 'id': 'cluster_id', + 'nodes': 'node1,node2' + } + args = self._make_args(args) + node_ids = ['node1', 'node2'] + resp = {'action': 'CLUSTER_NODE_DEL'} + service.cluster_del_nodes.return_value = resp + + sh.do_cluster_node_del(service, args) + + service.cluster_del_nodes.assert_called_once_with('cluster_id', + node_ids) + + def test_do_cluster_resize(self): + service = mock.Mock() + args = { + 'id': 'cluster_id', + 'capacity': 2, + 'adjustment': 1, + 'percentage': 50.0, + 'min_size': 1, + 'max_size': 10, + 'min_step': 1, + 'strict': True, + } + args = self._make_args(args) + ex = self.assertRaises(exc.CommandError, + sh.do_cluster_resize, + service, args) + msg = _("Only one of 'capacity', 'adjustment' and " + "'percentage' can be specified.") + self.assertEqual(msg, six.text_type(ex)) + + # capacity + args.adjustment = None + args.percentage = None + args.min_step = None + action_args = { + 'adjustment_type': 'EXACT_CAPACITY', + 'number': 2, + 'min_size': 1, + 'max_size': 10, + 'strict': True, + 'min_step': None, + } + resp = {'action': 'action_id'} + service.cluster_resize.return_value = resp + sh.do_cluster_resize(service, args) + service.cluster_resize.assert_called_with('cluster_id', **action_args) + + # capacity is smaller than 0 + args.capacity = -1 + ex = self.assertRaises(exc.CommandError, + sh.do_cluster_resize, + service, args) + msg = _('Cluster capacity must be larger than ' + ' or equal to zero.') + self.assertEqual(msg, six.text_type(ex)) + + # adjustment + args.capacity = None + args.percentage = None + args.adjustment = 1 + action_args['adjustment_type'] = 'CHANGE_IN_CAPACITY' + action_args['number'] = 1 + sh.do_cluster_resize(service, args) + service.cluster_resize.assert_called_with('cluster_id', **action_args) + + # adjustment is 0 + args.adjustment = 0 + ex = self.assertRaises(exc.CommandError, + sh.do_cluster_resize, + service, args) + msg = _('Adjustment cannot be zero.') + self.assertEqual(msg, six.text_type(ex)) + + # percentage + args.capacity = None + args.percentage = 50.0 + args.adjustment = None + action_args['adjustment_type'] = 'CHANGE_IN_PERCENTAGE' + action_args['number'] = 50.0 + sh.do_cluster_resize(service, args) + service.cluster_resize.assert_called_with('cluster_id', **action_args) + + # percentage is 0 + args.percentage = 0 + ex = self.assertRaises(exc.CommandError, + sh.do_cluster_resize, + service, args) + msg = _('Percentage cannot be zero.') + self.assertEqual(msg, six.text_type(ex)) + + # min_step is not None while percentage is None + args.capacity = 2 + args.percentage = None + args.adjustment = None + args.min_step = 1 + ex = self.assertRaises(exc.CommandError, + sh.do_cluster_resize, + service, args) + msg = _('Min step is only used with percentage.') + self.assertEqual(msg, six.text_type(ex)) + + # min_size < 0 + args.capacity = 2 + args.percentage = None + args.adjustment = None + args.min_step = None + args.min_size = -1 + ex = self.assertRaises(exc.CommandError, + sh.do_cluster_resize, + service, args) + msg = _('Min size cannot be less than zero.') + self.assertEqual(msg, six.text_type(ex)) + + # max_size < min_size + args.capacity = 5 + args.percentage = None + args.adjustment = None + args.min_step = None + args.min_size = 5 + args.max_size = 4 + ex = self.assertRaises(exc.CommandError, + sh.do_cluster_resize, + service, args) + msg = _('Min size cannot be larger than max size.') + self.assertEqual(msg, six.text_type(ex)) + + # min_size > capacity + args.capacity = 5 + args.percentage = None + args.adjustment = None + args.min_step = None + args.min_size = 6 + args.max_size = 8 + ex = self.assertRaises(exc.CommandError, + sh.do_cluster_resize, + service, args) + msg = _('Min size cannot be larger than the specified capacity') + self.assertEqual(msg, six.text_type(ex)) + + # max_size < capacity + args.capacity = 5 + args.percentage = None + args.adjustment = None + args.min_step = None + args.min_size = 1 + args.max_size = 4 + ex = self.assertRaises(exc.CommandError, + sh.do_cluster_resize, + service, args) + msg = _('Max size cannot be less than the specified capacity.') + self.assertEqual(msg, six.text_type(ex)) + + def test_do_cluster_scale_out(self): + service = mock.Mock() + args = { + 'id': 'cluster_id', + 'count': 3, + } + args = self._make_args(args) + resp = {'action': 'action_id'} + service.cluster_scale_out.return_value = resp + sh.do_cluster_scale_out(service, args) + service.cluster_scale_out.assert_called_once_with( + 'cluster_id', 3) + + def test_do_cluster_scale_in(self): + service = mock.Mock() + args = { + 'id': 'cluster_id', + 'count': 3, + } + args = self._make_args(args) + resp = {'action': 'action_id'} + service.cluster_scale_in.return_value = resp + + sh.do_cluster_scale_in(service, args) + + service.cluster_scale_in.assert_called_once_with('cluster_id', 3) + + @mock.patch.object(utils, 'print_list') + def test_do_cluster_policy_list(self, mock_print): + fields = ['policy_id', 'policy_name', 'policy_type', 'enabled'] + service = mock.Mock() + args = { + 'id': 'C1', + 'filters': ['enabled=True'], + 'sort': 'enabled:asc', + 'full_id': True, + } + args = self._make_args(args) + queries = { + 'sort': 'enabled:asc', + 'enabled': 'True', + } + cluster = mock.Mock() + cluster.id = 'C1' + service.get_cluster.return_value = cluster + policies = mock.Mock() + service.cluster_policies.return_value = policies + sh.do_cluster_policy_list(service, args) + service.get_cluster.assert_called_once_with('C1') + service.cluster_policies.assert_called_once_with('C1', **queries) + formatters = {} + mock_print.assert_called_once_with(policies, fields, + formatters=formatters, + sortby_index=None) + + @mock.patch.object(utils, 'print_dict') + def test_do_cluster_policy_show(self, mock_print): + class Binding(object): + def to_dict(self): + pass + + service = mock.Mock() + args = { + 'id': 'CC', + 'policy': 'PP', + } + args = self._make_args(args) + binding = Binding() + service.get_cluster_policy.return_value = binding + sh.do_cluster_policy_show(service, args) + service.get_cluster_policy.assert_called_once_with('PP', 'CC') + mock_print.assert_called_once_with(binding.to_dict()) + + def test_do_cluster_policy_attach(self): + service = mock.Mock() + args = { + 'id': 'C1', + 'policy': 'P1', + 'enabled': 'True', + } + args = self._make_args(args) + kwargs = { + 'enabled': 'True', + } + service.cluster_attach_policy.return_value = {'action': 'action_id'} + sh.do_cluster_policy_attach(service, args) + service.cluster_attach_policy.assert_called_once_with('C1', 'P1', + **kwargs) + + def test_do_cluster_policy_detach(self): + args = { + 'id': 'CC', + 'policy': 'PP' + } + service = mock.Mock() + args = self._make_args(args) + resp = {'action': 'action_id'} + service.cluster_detach_policy.return_value = resp + sh.do_cluster_policy_detach(service, args) + service.cluster_detach_policy.assert_called_once_with('CC', 'PP') + + def test_do_cluster_policy_update(self): + service = mock.Mock() + args = { + 'id': 'C1', + 'policy': 'policy1', + 'enabled': 'True', + } + args = self._make_args(args) + kwargs = { + 'enabled': 'True', + } + service.cluster_update_policy.return_value = {'action': 'action_id'} + + sh.do_cluster_policy_update(service, args) + + service.cluster_update_policy.assert_called_once_with('C1', 'policy1', + **kwargs) + + def test_do_cluster_check(self): + service = mock.Mock() + args = self._make_args({'id': ['cluster1']}) + service.check_cluster = mock.Mock() + service.check_cluster.return_value = {'action': 'action_id'} + sh.do_cluster_check(service, args) + + service.check_cluster.assert_called_once_with('cluster1') + + def test_do_cluster_recover(self): + service = mock.Mock() + args = self._make_args({'id': ['cluster1']}) + service.recover_cluster = mock.Mock() + service.recover_cluster.return_value = {'action': 'action_id'} + + sh.do_cluster_recover(service, args) + + service.recover_cluster.assert_called_once_with('cluster1') + + @mock.patch.object(utils, 'print_list') + def test_do_node_list(self, mock_print): + service = mock.Mock() + fields = ['id', 'name', 'index', 'status', 'cluster_id', 'physical_id', + 'profile_name', 'created_at', 'updated_at'] + args = { + 'cluster': 'cluster1', + 'sort': 'name:asc', + 'limit': 20, + 'marker': 'marker_id', + 'global_project': True, + 'filters': ['status=active'], + 'full_id': True, + } + queries = { + 'cluster_id': 'cluster1', + 'sort': 'name:asc', + 'limit': 20, + 'marker': 'marker_id', + 'global_project': True, + 'status': 'active', + } + args = self._make_args(args) + nodes = mock.Mock() + service.nodes.return_value = nodes + formatters = {} + sh.do_node_list(service, args) + mock_print.assert_called_once_with(nodes, fields, + formatters=formatters, + sortby_index=None) + service.nodes.assert_called_once_with(**queries) + + @mock.patch.object(utils, 'print_dict') + @mock.patch.object(utils, 'nested_dict_formatter') + def test_show_node(self, mock_nested, mock_print): + service = mock.Mock() + node_id = 'node1' + node = mock.Mock() + service.get_node.return_value = node + formatters = { + 'metadata': utils.json_formatter, + 'data': utils.json_formatter, + } + data = mock.Mock() + node.to_dict.return_value = data + + sh._show_node(service, node_id, show_details=False) + + service.get_node.assert_called_once_with(node_id, args=None) + mock_print.assert_called_once_with(data, formatters=formatters) + + @mock.patch.object(sh, '_show_node') + def test_do_node_create(self, mock_show): + args = { + 'name': 'node1', + 'cluster': 'cluster1', + 'profile': 'profile1', + 'role': 'master', + 'metadata': ['user=demo'], + } + args = self._make_args(args) + attrs = { + 'name': 'node1', + 'cluster_id': 'cluster1', + 'profile_id': 'profile1', + 'role': 'master', + 'metadata': {'user': 'demo'}, + } + service = mock.Mock() + node = mock.Mock() + node.id = 'node_id' + service.create_node.return_value = node + sh.do_node_create(service, args) + service.create_node.assert_called_once_with(**attrs) + mock_show.assert_called_once_with(service, 'node_id') + + @mock.patch.object(sh, '_show_node') + def test_do_node_show(self, mock_show): + service = mock.Mock() + args = { + 'id': 'node1', + 'details': False + } + args = self._make_args(args) + sh.do_node_show(service, args) + mock_show.assert_called_once_with(service, 'node1', False) + + def test_do_node_delete(self): + service = mock.Mock() + args = self._make_args({'id': ['node1']}) + service.delete_node = mock.Mock() + + sh.do_node_delete(service, args) + + service.delete_node.assert_called_once_with('node1', False) + + def test_do_node_delete_not_found(self): + service = mock.Mock() + ex = oexc.ResourceNotFound + service.delete_node.side_effect = ex + + args = self._make_args({'id': ['node1']}) + ex = self.assertRaises(exc.CommandError, + sh.do_node_delete, service, args) + msg = _('Failed to delete some of the specified nodes.') + self.assertEqual(msg, six.text_type(ex)) + + def test_do_node_check(self): + service = mock.Mock() + args = self._make_args({'id': ['node1']}) + service.check_node = mock.Mock() + + sh.do_node_check(service, args) + + service.check_node.assert_called_once_with('node1') + + def test_do_node_check_not_found(self): + service = mock.Mock() + ex = exc.HTTPNotFound + service.check_node.side_effect = ex + + args = self._make_args({'id': ['node1']}) + ex = self.assertRaises(exc.CommandError, + sh.do_node_check, service, args) + msg = _('Failed to check some of the specified nodes.') + self.assertEqual(msg, six.text_type(ex)) + + def test_do_node_recover(self): + service = mock.Mock() + args = self._make_args({'id': ['node1']}) + service.check_node = mock.Mock() + + sh.do_node_recover(service, args) + + service.recover_node.assert_called_once_with('node1') + + def test_do_node_recover_not_found(self): + service = mock.Mock() + ex = exc.HTTPNotFound + service.recover_node.side_effect = ex + + args = self._make_args({'id': ['node1']}) + ex = self.assertRaises(exc.CommandError, + sh.do_node_recover, service, args) + msg = _('Failed to recover some of the specified nodes.') + self.assertEqual(msg, six.text_type(ex)) + + @mock.patch.object(sh, '_show_node') + def test_do_node_update(self, mock_show): + service = mock.Mock() + args = { + 'id': 'node_id', + 'name': 'node1', + 'role': 'master', + 'profile': 'profile1', + 'metadata': ['user=demo'], + } + args = self._make_args(args) + attrs = { + 'name': 'node1', + 'role': 'master', + 'profile_id': 'profile1', + 'metadata': {'user': 'demo'}, + } + node = mock.Mock() + node.id = 'node_id' + service.get_node.return_value = node + sh.do_node_update(service, args) + service.get_node.assert_called_once_with('node_id') + service.update_node.assert_called_once_with('node_id', **attrs) + mock_show.assert_called_once_with(service, 'node_id') + + @mock.patch.object(utils, 'print_list') + def test_do_event_list(self, mock_print): + service = mock.Mock() + fields = ['id', 'timestamp', 'obj_type', 'obj_id', 'obj_name', + 'action', 'status', 'status_reason', 'level'] + args = { + 'sort': 'timestamp:asc', + 'limit': 20, + 'marker': 'marker_id', + 'global_project': True, + 'filters': ['action=NODE_DELETE'], + 'full_id': True, + } + queries = copy.deepcopy(args) + del queries['full_id'] + del queries['filters'] + queries['action'] = 'NODE_DELETE' + args = self._make_args(args) + formatters = {} + events = mock.Mock() + service.events.return_value = events + + sh.do_event_list(service, args) + + service.events.assert_called_once_with(**queries) + mock_print.assert_called_once_with(events, fields, + formatters=formatters) + + @mock.patch.object(utils, 'print_dict') + def test_do_event_show(self, mock_print): + class FakeEvent(object): + def to_dict(self): + pass + + service = mock.Mock() + args = { + 'id': 'event_id' + } + args = self._make_args(args) + + event = FakeEvent() + service.get_event.return_value = event + sh.do_event_show(service, args) + service.get_event.assert_called_once_with('event_id') + mock_print.assert_called_once_with(event.to_dict()) + + def test_do_event_show_not_found(self): + service = mock.Mock() + args = self._make_args({'id': 'FAKE'}) + # event not found + ex = exc.CommandError + service.get_event.side_effect = oexc.ResourceNotFound + ex = self.assertRaises(ex, + sh.do_event_show, + service, args) + + @mock.patch.object(utils, 'print_list') + def test_do_action_list(self, mock_print): + service = mock.Mock() + fields = ['id', 'name', 'action', 'status', 'target', 'depends_on', + 'depended_by', 'created_at'] + args = { + 'sort': 'status', + 'limit': 20, + 'marker': 'marker_id', + } + queries = copy.deepcopy(args) + args = self._make_args(args) + args.filters = ['status=ACTIVE'] + queries['status'] = 'ACTIVE' + actions = mock.Mock() + service.actions.return_value = actions + formatters = { + 'depends_on': mock.ANY, + 'depended_by': mock.ANY + } + args.full_id = True + sortby_index = None + sh.do_action_list(service, args) + service.actions.assert_called_once_with(**queries) + mock_print.assert_called_once_with(actions, fields, + formatters=formatters, + sortby_index=sortby_index) + + @mock.patch.object(utils, 'print_dict') + def test_do_action_show(self, mock_print): + class FakeAction(object): + def to_dict(self): + pass + + service = mock.Mock() + args = self._make_args({'id': 'action_id'}) + + action = FakeAction() + service.get_action.return_value = action + formatters = { + 'inputs': utils.json_formatter, + 'outputs': utils.json_formatter, + 'metadata': utils.json_formatter, + 'data': utils.json_formatter, + 'depends_on': utils.list_formatter, + 'depended_by': utils.list_formatter, + } + sh.do_action_show(service, args) + service.get_action.assert_called_once_with('action_id') + mock_print.assert_called_once_with(action.to_dict(), + formatters=formatters) + + def test_do_action_show_not_found(self): + service = mock.Mock() + args = self._make_args({'id': 'fake_id'}) + + service.get_action.side_effect = oexc.ResourceNotFound + ex = self.assertRaises(exc.CommandError, + sh.do_action_show, + service, args) + msg = _('Action not found: fake_id') + self.assertEqual(msg, six.text_type(ex)) diff --git a/senlinclient/v1/shell.py b/senlinclient/v1/shell.py new file mode 100644 index 00000000..6ef78e31 --- /dev/null +++ b/senlinclient/v1/shell.py @@ -0,0 +1,1314 @@ +# 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 logging + +from openstack import exceptions as sdk_exc +from senlinclient.common import exc +from senlinclient.common.i18n import _ +from senlinclient.common.i18n import _LW +from senlinclient.common import utils + +logger = logging.getLogger(__name__) + + +def show_deprecated(deprecated, recommended): + logger.warning(_LW('"%(old)s" is deprecated, ' + 'please use "%(new)s" instead.'), + {'old': deprecated, + 'new': recommended} + ) + + +def do_build_info(service, args=None): + """Retrieve build information. + + :param sc: Instance of senlinclient. + :param args: Additional command line arguments, if any. + """ + show_deprecated('senlin build-info', 'openstack cluster build info') + result = service.get_build_info() + + formatters = { + 'api': utils.json_formatter, + 'engine': utils.json_formatter, + } + utils.print_dict(result, formatters=formatters) + + +# PROFILE TYPES + + +def do_profile_type_list(service, args=None): + """List the available profile types. + + :param sc: Instance of senlinclient. + :param args: Additional command line arguments, if any. + """ + show_deprecated('senlin profile-type-list', + 'openstack cluster profile type list') + types = service.profile_types() + utils.print_list(types, ['name'], sortby_index=0) + + +@utils.arg('type_name', metavar='', + help=_('Profile type to retrieve.')) +@utils.arg('-F', '--format', metavar='', + choices=utils.supported_formats.keys(), + help=_("The template output format, one of: %s.") + % ', '.join(utils.supported_formats.keys())) +def do_profile_type_show(service, args): + """Get the details about a profile type.""" + show_deprecated('senlin profile-type-show', + 'openstack cluster profile type show') + try: + res = service.get_profile_type(args.type_name) + except sdk_exc.ResourceNotFound: + raise exc.CommandError( + _('Profile Type not found: %s') % args.type_name) + + pt = res.to_dict() + + if args.format: + print(utils.format_output(pt, format=args.format)) + else: + print(utils.format_output(pt)) + + +# PROFILES + +@utils.arg('-f', '--filters', metavar='', + help=_('Filter parameters to apply on returned profiles. ' + 'This can be specified multiple times, or once with ' + 'parameters separated by a semicolon.'), + action='append') +@utils.arg('-l', '--limit', metavar='', + help=_('Limit the number of profiles returned.')) +@utils.arg('-m', '--marker', metavar='', + help=_('Only return profiles that appear after the given ID.')) +@utils.arg('-o', '--sort', metavar='', + help=_('Sorting option which is a string containing a list of keys ' + 'separated by commas. Each key can be optionally appended ' + 'by a sort direction (:asc or :desc)')) +@utils.arg('-g', '--global-project', default=False, action="store_true", + help=_('Indicate that the list should include profiles from' + ' all projects. This option is subject to access policy ' + 'checking. Default is False.')) +@utils.arg('-F', '--full-id', default=False, action="store_true", + help=_('Print full IDs in list.')) +def do_profile_list(service, args=None): + """List profiles that meet the criteria.""" + show_deprecated('senlin profile-list', 'openstack cluster profile list') + fields = ['id', 'name', 'type', 'created_at'] + queries = { + 'limit': args.limit, + 'marker': args.marker, + 'sort': args.sort, + 'global_project': args.global_project, + } + if args.filters: + queries.update(utils.format_parameters(args.filters)) + + sortby_index = None if args.sort else 1 + + profiles = service.profiles(**queries) + formatters = {} + if not args.full_id: + formatters = { + 'id': lambda x: x.id[:8], + } + utils.print_list(profiles, fields, formatters=formatters, + sortby_index=sortby_index) + + +def _show_profile(service, profile_id): + try: + profile = service.get_profile(profile_id) + except sdk_exc.ResourceNotFound: + raise exc.CommandError(_('Profile not found: %s') % profile_id) + + formatters = { + 'metadata': utils.json_formatter, + } + + formatters['spec'] = utils.nested_dict_formatter( + ['type', 'version', 'properties'], + ['property', 'value']) + + utils.print_dict(profile.to_dict(), formatters=formatters) + + +@utils.arg('-s', '--spec-file', metavar='', required=True, + help=_('The spec file used to create the profile.')) +@utils.arg('-M', '--metadata', metavar='', + help=_('Metadata values to be attached to the profile. ' + 'This can be specified multiple times, or once with ' + 'key-value pairs separated by a semicolon.'), + action='append') +@utils.arg('name', metavar='', + help=_('Name of the profile to create.')) +def do_profile_create(service, args): + """Create a profile.""" + show_deprecated('senlin profile-create', + 'openstack cluster profile create') + spec = utils.get_spec_content(args.spec_file) + type_name = spec.get('type', None) + type_version = spec.get('version', None) + properties = spec.get('properties', None) + if type_name is None: + raise exc.CommandError(_("Missing 'type' key in spec file.")) + if type_version is None: + raise exc.CommandError(_("Missing 'version' key in spec file.")) + if properties is None: + raise exc.CommandError(_("Missing 'properties' key in spec file.")) + + if type_name == 'os.heat.stack': + stack_properties = utils.process_stack_spec(properties) + spec['properties'] = stack_properties + + params = { + 'name': args.name, + 'spec': spec, + 'metadata': utils.format_parameters(args.metadata), + } + + profile = service.create_profile(**params) + _show_profile(service, profile.id) + + +@utils.arg('id', metavar='', + help=_('Name or ID of profile to show.')) +def do_profile_show(service, args): + """Show the profile details.""" + show_deprecated('senlin profile-show', 'openstack cluster profile show') + _show_profile(service, args.id) + + +@utils.arg('-n', '--name', metavar='', + help=_('The new name for the profile.')) +@utils.arg('-M', '--metadata', metavar='', + help=_('Metadata values to be attached to the profile. ' + 'This can be specified multiple times, or once with ' + 'key-value pairs separated by a semicolon.'), + action='append') +@utils.arg('id', metavar='', + help=_('Name or ID of the profile to update.')) +def do_profile_update(service, args): + """Update a profile.""" + show_deprecated('senlin profile-update', + 'openstack cluster profile update') + params = { + 'name': args.name, + } + if args.metadata: + params['metadata'] = utils.format_parameters(args.metadata) + + # Find the profile first, we need its id + try: + profile = service.get_profile(args.id) + except sdk_exc.ResourceNotFound: + raise exc.CommandError(_('Profile not found: %s') % args.id) + service.update_profile(profile.id, **params) + _show_profile(service, profile.id) + + +@utils.arg('id', metavar='', nargs='+', + help=_('Name or ID of profile(s) to delete.')) +def do_profile_delete(service, args): + """Delete profile(s).""" + show_deprecated('senlin profile-delete', + 'openstack cluster profile delete') + failure_count = 0 + + for pid in args.id: + try: + service.delete_profile(pid, False) + except Exception as ex: + failure_count += 1 + print(ex) + if failure_count > 0: + msg = _('Failed to delete some of the specified profile(s).') + raise exc.CommandError(msg) + print('Profile deleted: %s' % args.id) + + +# POLICY TYPES + + +def do_policy_type_list(service, args): + """List the available policy types.""" + show_deprecated('senlin policy-type-list', + 'openstack cluster policy type list') + types = service.policy_types() + utils.print_list(types, ['name'], sortby_index=0) + + +@utils.arg('type_name', metavar='', + help=_('Policy type to retrieve.')) +@utils.arg('-F', '--format', metavar='', + choices=utils.supported_formats.keys(), + help=_("The template output format, one of: %s.") + % ', '.join(utils.supported_formats.keys())) +def do_policy_type_show(service, args): + """Get the details about a policy type.""" + show_deprecated('senlin policy-type-show', + 'openstack cluster policy type show') + try: + res = service.get_policy_type(args.type_name) + except sdk_exc.ResourceNotFound: + raise exc.CommandError(_('Policy type not found: %s') % args.type_name) + + pt = res.to_dict() + if args.format: + print(utils.format_output(pt, format=args.format)) + else: + print(utils.format_output(pt)) + + +# POLICIES + +@utils.arg('-f', '--filters', metavar='', + help=_('Filter parameters to apply on returned policies. ' + 'This can be specified multiple times, or once with ' + 'parameters separated by a semicolon.'), + action='append') +@utils.arg('-l', '--limit', metavar='', + help=_('Limit the number of policies returned.')) +@utils.arg('-m', '--marker', metavar='', + help=_('Only return policies that appear after the given ID.')) +@utils.arg('-o', '--sort', metavar='', + help=_('Sorting option which is a string containing a list of keys ' + 'separated by commas. Each key can be optionally appended ' + 'by a sort direction (:asc or :desc)')) +@utils.arg('-g', '--global-project', default=False, action="store_true", + help=_('Indicate that the list should include policies from' + ' all projects. This option is subject to access policy ' + 'checking. Default is False.')) +@utils.arg('-F', '--full-id', default=False, action="store_true", + help=_('Print full IDs in list.')) +def do_policy_list(service, args=None): + """List policies that meet the criteria.""" + show_deprecated('senlin policy-list', 'openstack cluster policy list') + fields = ['id', 'name', 'type', 'created_at'] + queries = { + 'limit': args.limit, + 'marker': args.marker, + 'sort': args.sort, + 'global_project': args.global_project, + } + if args.filters: + queries.update(utils.format_parameters(args.filters)) + + sortby_index = None if args.sort else 1 + policies = service.policies(**queries) + formatters = {} + if not args.full_id: + formatters = { + 'id': lambda x: x.id[:8] + } + utils.print_list(policies, fields, formatters=formatters, + sortby_index=sortby_index) + + +def _show_policy(service, policy_id): + try: + policy = service.get_policy(policy_id) + except sdk_exc.ResourceNotFound: + raise exc.CommandError(_('Policy not found: %s') % policy_id) + + formatters = { + 'metadata': utils.json_formatter, + 'spec': utils.json_formatter, + } + utils.print_dict(policy.to_dict(), formatters=formatters) + + +@utils.arg('-s', '--spec-file', metavar='', required=True, + help=_('The spec file used to create the policy.')) +@utils.arg('name', metavar='', + help=_('Name of the policy to create.')) +def do_policy_create(service, args): + """Create a policy.""" + show_deprecated('senlin policy-create', 'openstack cluster policy create') + spec = utils.get_spec_content(args.spec_file) + attrs = { + 'name': args.name, + 'spec': spec, + } + + policy = service.create_policy(**attrs) + _show_policy(service, policy.id) + + +@utils.arg('id', metavar='', + help=_('Name of the policy to be updated.')) +def do_policy_show(service, args): + """Show the policy details.""" + show_deprecated('senlin policy-show', 'openstack cluster policy show') + _show_policy(service, policy_id=args.id) + + +@utils.arg('-n', '--name', metavar='', + help=_('New name of the policy to be updated.')) +@utils.arg('id', metavar='', + help=_('Name of the policy to be updated.')) +def do_policy_update(service, args): + """Update a policy.""" + show_deprecated('senlin policy-update', 'openstack cluster policy update') + params = { + 'name': args.name, + } + + policy = service.get_policy(args.id) + if policy is not None: + service.update_policy(policy.id, **params) + _show_policy(service, policy_id=policy.id) + + +@utils.arg('id', metavar='', nargs='+', + help=_('Name or ID of policy(s) to delete.')) +def do_policy_delete(service, args): + """Delete policy(s).""" + show_deprecated('senlin policy-delete', 'openstack cluster policy delete') + failure_count = 0 + + for pid in args.id: + try: + service.delete_policy(pid, False) + except Exception as ex: + failure_count += 1 + print(ex) + if failure_count > 0: + msg = _('Failed to delete some of the specified policy(s).') + raise exc.CommandError(msg) + print('Policy deleted: %s' % args.id) + + +# CLUSTERS + + +@utils.arg('-f', '--filters', metavar='', + help=_('Filter parameters to apply on returned clusters. ' + 'This can be specified multiple times, or once with ' + 'parameters separated by a semicolon.'), + action='append') +@utils.arg('-o', '--sort', metavar='', + help=_('Sorting option which is a string containing a list of keys ' + 'separated by commas. Each key can be optionally appended ' + 'by a sort direction (:asc or :desc)')) +@utils.arg('-l', '--limit', metavar='', + help=_('Limit the number of clusters returned.')) +@utils.arg('-m', '--marker', metavar='', + help=_('Only return clusters that appear after the given cluster ' + 'ID.')) +@utils.arg('-g', '--global-project', default=False, action="store_true", + help=_('Indicate that the cluster list should include clusters from' + ' all projects. This option is subject to access policy ' + 'checking. Default is False.')) +@utils.arg('-F', '--full-id', default=False, action="store_true", + help=_('Print full IDs in list.')) +def do_cluster_list(service, args=None): + """List the user's clusters.""" + show_deprecated('senlin cluster-list', 'openstack cluster list') + fields = ['id', 'name', 'status', 'created_at', 'updated_at'] + queries = { + 'limit': args.limit, + 'marker': args.marker, + 'sort': args.sort, + 'global_project': args.global_project, + } + if args.filters: + queries.update(utils.format_parameters(args.filters)) + + sortby_index = None if args.sort else 3 + + clusters = service.clusters(**queries) + formatters = {} + if not args.full_id: + formatters = { + 'id': lambda x: x.id[:8] + } + utils.print_list(clusters, fields, formatters=formatters, + sortby_index=sortby_index) + + +def _show_cluster(service, cluster_id): + try: + cluster = service.get_cluster(cluster_id) + except sdk_exc.ResourceNotFound: + raise exc.CommandError(_('Cluster not found: %s') % cluster_id) + + formatters = { + 'metadata': utils.json_formatter, + 'nodes': utils.list_formatter, + } + utils.print_dict(cluster.to_dict(), formatters=formatters) + + +@utils.arg('-p', '--profile', metavar='', required=True, + help=_('Profile Id used for this cluster.')) +@utils.arg('-n', '--min-size', metavar='', default=0, + help=_('Min size of the cluster. Default to 0.')) +@utils.arg('-m', '--max-size', metavar='', default=-1, + help=_('Max size of the cluster. Default to -1, means unlimited.')) +@utils.arg('-c', '--desired-capacity', metavar='', default=0, + help=_('Desired capacity of the cluster. Default to min_size if ' + 'min_size is specified else 0.')) +@utils.arg('-t', '--timeout', metavar='', type=int, + help=_('Cluster creation timeout in seconds.')) +@utils.arg('-M', '--metadata', metavar='', + help=_('Metadata values to be attached to the cluster. ' + 'This can be specified multiple times, or once with ' + 'key-value pairs separated by a semicolon.'), + action='append') +@utils.arg('name', metavar='', + help=_('Name of the cluster to create.')) +def do_cluster_create(service, args): + """Create the cluster.""" + show_deprecated('senlin cluster-create', 'openstack cluster create') + if args.min_size and not args.desired_capacity: + args.desired_capacity = args.min_size + attrs = { + 'name': args.name, + 'profile_id': args.profile, + 'min_size': args.min_size, + 'max_size': args.max_size, + 'desired_capacity': args.desired_capacity, + 'metadata': utils.format_parameters(args.metadata), + 'timeout': args.timeout + } + + cluster = service.create_cluster(**attrs) + _show_cluster(service, cluster.id) + + +@utils.arg('id', metavar='', nargs='+', + help=_('Name or ID of cluster(s) to delete.')) +def do_cluster_delete(service, args): + """Delete the cluster(s).""" + show_deprecated('senlin cluster-delete', 'openstack cluster delete') + failure_count = 0 + + for cid in args.id: + try: + service.delete_cluster(cid, False) + except Exception as ex: + failure_count += 1 + print(ex) + if failure_count > 0: + msg = _('Failed to delete some of the specified clusters.') + raise exc.CommandError(msg) + print('Request accepted') + + +@utils.arg('-p', '--profile', metavar='', + help=_('ID of new profile to use.')) +@utils.arg('-t', '--timeout', metavar='', + help=_('New timeout (in seconds) value for the cluster.')) +@utils.arg('-M', '--metadata', metavar='', + help=_('Metadata values to be attached to the cluster. ' + 'This can be specified multiple times, or once with ' + 'key-value pairs separated by a semicolon.'), + action='append') +@utils.arg('-n', '--name', metavar='', + help=_('New name for the cluster to update.')) +@utils.arg('id', metavar='', + help=_('Name or ID of cluster to be updated.')) +def do_cluster_update(service, args): + """Update the cluster.""" + show_deprecated('senlin cluster-update', 'openstack cluster update') + cluster = service.get_cluster(args.id) + attrs = { + 'name': args.name, + 'profile_id': args.profile, + 'metadata': utils.format_parameters(args.metadata), + 'timeout': args.timeout, + } + + service.update_cluster(cluster.id, **attrs) + _show_cluster(service, cluster.id) + + +@utils.arg('id', metavar='', + help=_('Name or ID of cluster to show.')) +def do_cluster_show(service, args): + """Show details of the cluster.""" + show_deprecated('senlin cluster-show', 'openstack cluster show') + _show_cluster(service, args.id) + + +@utils.arg('-f', '--filters', metavar='', + help=_('Filter parameters to apply on returned nodes. ' + 'This can be specified multiple times, or once with ' + 'parameters separated by a semicolon.'), + action='append') +@utils.arg('-l', '--limit', metavar='', + help=_('Limit the number of nodes returned.')) +@utils.arg('-m', '--marker', metavar='', + help=_('Only return nodes that appear after the given node ID.')) +@utils.arg('-F', '--full-id', default=False, action="store_true", + help=_('Print full IDs in list.')) +@utils.arg('id', metavar='', + help=_('Name or ID of cluster to nodes from.')) +def do_cluster_node_list(service, args): + """List nodes from cluster.""" + show_deprecated('senlin cluster-node-list', + 'openstack cluster node members list') + queries = { + 'cluster_id': args.id, + 'limit': args.limit, + 'marker': args.marker, + } + if args.filters: + queries.update(utils.format_parameters(args.filters)) + + nodes = service.nodes(**queries) + if not args.full_id: + formatters = { + 'id': lambda x: x.id[:8], + 'physical_id': lambda x: x.physical_id[:8] if x.physical_id else '' + } + else: + formatters = {} + + fields = ['id', 'name', 'index', 'status', 'physical_id', 'created_at'] + utils.print_list(nodes, fields, formatters=formatters, sortby_index=5) + + +@utils.arg('-n', '--nodes', metavar='', required=True, + help=_('ID of nodes to be added; multiple nodes can be separated ' + 'with ","')) +@utils.arg('id', metavar='', + help=_('Name or ID of cluster to operate on.')) +def do_cluster_node_add(service, args): + """Add specified nodes to cluster.""" + show_deprecated('senlin cluster-node-add', + 'openstack cluster node members add') + node_ids = args.nodes.split(',') + resp = service.cluster_add_nodes(args.id, node_ids) + print('Request accepted by action: %s' % resp['action']) + + +@utils.arg('-n', '--nodes', metavar='', required=True, + help=_('ID of nodes to be deleted; multiple nodes can be separated ' + 'with ",".')) +@utils.arg('id', metavar='', + help=_('Name or ID of cluster to operate on.')) +def do_cluster_node_del(service, args): + """Delete specified nodes from cluster.""" + show_deprecated('senlin cluster-node-del', + 'openstack cluster node members del') + node_ids = args.nodes.split(',') + resp = service.cluster_del_nodes(args.id, node_ids) + print('Request accepted by action: %s' % resp['action']) + + +@utils.arg('-c', '--capacity', metavar='', type=int, + help=_('The desired number of nodes of the cluster.')) +@utils.arg('-a', '--adjustment', metavar='', type=int, + help=_('A positive integer meaning the number of nodes to add, ' + 'or a negative integer indicating the number of nodes to ' + 'remove.')) +@utils.arg('-p', '--percentage', metavar='', type=float, + help=_('A value that is interpreted as the percentage of size ' + 'adjustment. This value can be positive or negative.')) +@utils.arg('-t', '--min-step', metavar='', type=int, + help=_('An integer specifying the number of nodes for adjustment ' + 'when is specified.')) +@utils.arg('-s', '--strict', action='store_true', default=False, + help=_('A boolean specifying whether the resize should be ' + 'performed on a best-effort basis when the new capacity ' + 'may go beyond size constraints.')) +@utils.arg('-n', '--min-size', metavar='MIN', type=int, + help=_('New lower bound of cluster size.')) +@utils.arg('-m', '--max-size', metavar='MAX', type=int, + help=_('New upper bound of cluster size. A value of -1 indicates ' + 'no upper limit on cluster size.')) +@utils.arg('id', metavar='', + help=_('Name or ID of cluster to operate on.')) +def do_cluster_resize(service, args): + """Resize a cluster.""" + # validate parameters + # NOTE: this will be much simpler if cliutils supports exclusive groups + show_deprecated('senlin cluster-resize', 'openstack cluster resize') + action_args = {} + + capacity = args.capacity + adjustment = args.adjustment + percentage = args.percentage + min_size = args.min_size + max_size = args.max_size + min_step = args.min_step + + if sum(v is not None for v in (capacity, adjustment, percentage)) > 1: + raise exc.CommandError(_("Only one of 'capacity', 'adjustment' and " + "'percentage' can be specified.")) + + action_args['adjustment_type'] = None + action_args['number'] = None + + if capacity is not None: + if capacity < 0: + raise exc.CommandError(_('Cluster capacity must be larger than ' + ' or equal to zero.')) + action_args['adjustment_type'] = 'EXACT_CAPACITY' + action_args['number'] = capacity + + if adjustment is not None: + if adjustment == 0: + raise exc.CommandError(_('Adjustment cannot be zero.')) + action_args['adjustment_type'] = 'CHANGE_IN_CAPACITY' + action_args['number'] = adjustment + + if percentage is not None: + if (percentage == 0 or percentage == 0.0): + raise exc.CommandError(_('Percentage cannot be zero.')) + action_args['adjustment_type'] = 'CHANGE_IN_PERCENTAGE' + action_args['number'] = percentage + + if min_step is not None: + if percentage is None: + raise exc.CommandError(_('Min step is only used with percentage.')) + + if min_size is not None: + if min_size < 0: + raise exc.CommandError(_('Min size cannot be less than zero.')) + if max_size is not None and max_size >= 0 and min_size > max_size: + raise exc.CommandError(_('Min size cannot be larger than ' + 'max size.')) + if capacity is not None and min_size > capacity: + raise exc.CommandError(_('Min size cannot be larger than the ' + 'specified capacity')) + + if max_size is not None: + if capacity is not None and max_size > 0 and max_size < capacity: + raise exc.CommandError(_('Max size cannot be less than the ' + 'specified capacity.')) + # do a normalization + if max_size < 0: + max_size = -1 + + action_args['min_size'] = min_size + action_args['max_size'] = max_size + action_args['min_step'] = min_step + action_args['strict'] = args.strict + + resp = service.cluster_resize(args.id, **action_args) + print('Request accepted by action: %s' % resp['action']) + + +@utils.arg('-c', '--count', metavar='', + help=_('Number of nodes to be added to the specified cluster.')) +@utils.arg('id', metavar='', + help=_('Name or ID of cluster to operate on.')) +def do_cluster_scale_out(service, args): + """Scale out a cluster by the specified number of nodes.""" + show_deprecated('senlin cluster-scale-out', 'openstack cluster scale out') + resp = service.cluster_scale_out(args.id, args.count) + print('Request accepted by action %s' % resp['action']) + + +@utils.arg('-c', '--count', metavar='', + help=_('Number of nodes to be deleted from the specified cluster.')) +@utils.arg('id', metavar='', + help=_('Name or ID of cluster to operate on.')) +def do_cluster_scale_in(service, args): + """Scale in a cluster by the specified number of nodes.""" + show_deprecated('senlin cluster-scale-in', 'openstack cluster scale in') + resp = service.cluster_scale_in(args.id, args.count) + print('Request accepted by action %s' % resp['action']) + + +@utils.arg('-f', '--filters', metavar='', + help=_('Filter parameters to apply on returned results. ' + 'This can be specified multiple times, or once with ' + 'parameters separated by a semicolon.'), + action='append') +@utils.arg('-o', '--sort', metavar='', + help=_('Sorting option which is a string containing a list of keys ' + 'separated by commas. Each key can be optionally appended ' + 'by a sort direction (:asc or :desc)')) +@utils.arg('-F', '--full-id', default=False, action="store_true", + help=_('Print full IDs in list.')) +@utils.arg('id', metavar='', + help=_('Name or ID of cluster to query on.')) +def do_cluster_policy_list(service, args): + """List policies from cluster.""" + show_deprecated('senlin cluster-policy-list', + 'openstack cluster policy binding list') + fields = ['policy_id', 'policy_name', 'policy_type', 'enabled'] + + cluster = service.get_cluster(args.id) + queries = { + 'sort': args.sort, + } + + if args.filters: + queries.update(utils.format_parameters(args.filters)) + + sortby_index = None if args.sort else 3 + policies = service.cluster_policies(cluster.id, **queries) + formatters = {} + if not args.full_id: + formatters = { + 'policy_id': lambda x: x.id[:8] + } + + utils.print_list(policies, fields, formatters=formatters, + sortby_index=sortby_index) + + +@utils.arg('-p', '--policy', metavar='', required=True, + help=_('ID or name of the policy to query on.')) +@utils.arg('id', metavar='', + help=_('ID or name of the cluster to query on.')) +def do_cluster_policy_show(service, args): + """Show a specific policy that is bound to the specified cluster.""" + show_deprecated('senlin cluster-policy-show', + 'openstack cluster policy binding show') + binding = service.get_cluster_policy(args.policy, args.id) + utils.print_dict(binding.to_dict()) + + +@utils.arg('-p', '--policy', metavar='', required=True, + help=_('ID or name of policy to be attached.')) +@utils.arg('-e', '--enabled', default=True, action="store_true", + help=_('Whether the policy should be enabled once attached. ' + 'Default to enabled.')) +@utils.arg('id', metavar='', + help=_('Name or ID of cluster to operate on.')) +def do_cluster_policy_attach(service, args): + """Attach policy to cluster.""" + show_deprecated('senlin cluster-policy-attach', + 'openstack cluster policy attach') + kwargs = { + 'enabled': args.enabled, + } + + resp = service.cluster_attach_policy(args.id, args.policy, **kwargs) + print('Request accepted by action: %s' % resp['action']) + + +@utils.arg('-p', '--policy', metavar='', required=True, + help=_('ID or name of policy to be detached.')) +@utils.arg('id', metavar='', + help=_('Name or ID of cluster to operate on.')) +def do_cluster_policy_detach(service, args): + """Detach policy from cluster.""" + show_deprecated('senlin cluster-policy-detach', + 'openstack cluster policy detach') + resp = service.cluster_detach_policy(args.id, args.policy) + print('Request accepted by action %s' % resp['action']) + + +@utils.arg('-p', '--policy', metavar='', required=True, + help=_('ID or name of policy to be updated.')) +@utils.arg('-e', '--enabled', metavar='', + help=_('Whether the policy should be enabled.')) +@utils.arg('id', metavar='', + help=_('Name or ID of cluster to operate on.')) +def do_cluster_policy_update(service, args): + """Update a policy's properties on a cluster.""" + show_deprecated('senlin cluster-policy-update', + 'openstack cluster policy binding update') + kwargs = { + 'enabled': args.enabled, + } + + resp = service.cluster_update_policy(args.id, args.policy, **kwargs) + print('Request accepted by action: %s' % resp['action']) + + +@utils.arg('id', metavar='', nargs='+', + help=_('ID or name of cluster(s) to operate on.')) +def do_cluster_check(service, args): + """Check the cluster(s).""" + show_deprecated('senlin cluster-check', 'openstack cluster check') + for cid in args.id: + resp = service.check_cluster(cid) + print('Cluster check request on cluster %(cid)s is accepted by ' + 'action %(action)s.' % {'cid': cid, 'action': resp['action']}) + + +@utils.arg('id', metavar='', nargs='+', + help=_('ID or name of cluster(s) to operate on.')) +def do_cluster_recover(service, args): + """Recover the cluster(s).""" + show_deprecated('senlin cluster-recover', 'openstack cluster recover') + for cid in args.id: + resp = service.recover_cluster(cid) + print('Cluster recover request on cluster %(cid)s is accepted by ' + 'action %(action)s.' % {'cid': cid, 'action': resp['action']}) + + +# NODES + + +@utils.arg('-c', '--cluster', metavar='', + help=_('ID or name of cluster from which nodes are to be listed.')) +@utils.arg('-f', '--filters', metavar='', + help=_('Filter parameters to apply on returned nodes. ' + 'This can be specified multiple times, or once with ' + 'parameters separated by a semicolon.'), + action='append') +@utils.arg('-o', '--sort', metavar='', + help=_('Sorting option which is a string containing a list of keys ' + 'separated by commas. Each key can be optionally appended ' + 'by a sort direction (:asc or :desc)')) +@utils.arg('-l', '--limit', metavar='', + help=_('Limit the number of nodes returned.')) +@utils.arg('-m', '--marker', metavar='', + help=_('Only return nodes that appear after the given node ID.')) +@utils.arg('-g', '--global-project', default=False, action="store_true", + help=_('Indicate that this node list should include nodes from ' + 'all projects. This option is subject to access policy ' + 'checking. Default is False.')) +@utils.arg('-F', '--full-id', default=False, action="store_true", + help=_('Print full IDs in list.')) +def do_node_list(service, args): + """Show list of nodes.""" + show_deprecated('senlin node-list', 'openstack cluster node list') + + fields = ['id', 'name', 'index', 'status', 'cluster_id', 'physical_id', + 'profile_name', 'created_at', 'updated_at'] + queries = { + 'cluster_id': args.cluster, + 'sort': args.sort, + 'limit': args.limit, + 'marker': args.marker, + 'global_project': args.global_project, + } + + if args.filters: + queries.update(utils.format_parameters(args.filters)) + + sortby_index = None if args.sort else 6 + + nodes = service.nodes(**queries) + + if not args.full_id: + formatters = { + 'id': lambda x: x.id[:8], + 'cluster_id': lambda x: x.cluster_id[:8] if x.cluster_id else '', + 'physical_id': lambda x: x.physical_id[:8] if x.physical_id else '' + } + else: + formatters = {} + + utils.print_list(nodes, fields, formatters=formatters, + sortby_index=sortby_index) + + +def _show_node(service, node_id, show_details=False): + """Show detailed info about the specified node.""" + args = {'show_details': True} if show_details else None + try: + node = service.get_node(node_id, args=args) + except sdk_exc.ResourceNotFound: + raise exc.CommandError(_('Node not found: %s') % node_id) + + formatters = { + 'metadata': utils.json_formatter, + 'data': utils.json_formatter, + } + data = node.to_dict() + if show_details: + formatters['details'] = utils.nested_dict_formatter( + list(node['details'].keys()), ['property', 'value']) + + utils.print_dict(data, formatters=formatters) + + +@utils.arg('-p', '--profile', metavar='', required=True, + help=_('Profile Id used for this node.')) +@utils.arg('-c', '--cluster', metavar='', + help=_('Cluster Id for this node.')) +@utils.arg('-r', '--role', metavar='', + help=_('Role for this node in the specific cluster.')) +@utils.arg('-M', '--metadata', metavar='', + help=_('Metadata values to be attached to the node. ' + 'This can be specified multiple times, or once with ' + 'key-value pairs separated by a semicolon.'), + action='append') +@utils.arg('name', metavar='', + help=_('Name of the node to create.')) +def do_node_create(service, args): + """Create the node.""" + show_deprecated('senlin node-create', 'openstack cluster node create') + attrs = { + 'name': args.name, + 'cluster_id': args.cluster, + 'profile_id': args.profile, + 'role': args.role, + 'metadata': utils.format_parameters(args.metadata), + } + + node = service.create_node(**attrs) + _show_node(service, node.id) + + +@utils.arg('-D', '--details', default=False, action="store_true", + help=_('Include physical object details.')) +@utils.arg('id', metavar='', + help=_('Name or ID of the node to show the details for.')) +def do_node_show(service, args): + """Show detailed info about the specified node.""" + show_deprecated('senlin node-show', 'openstack cluster node show') + _show_node(service, args.id, args.details) + + +@utils.arg('id', metavar='', nargs='+', + help=_('Name or ID of node(s) to delete.')) +def do_node_delete(service, args): + """Delete the node(s).""" + show_deprecated('senlin node-delete', 'openstack cluster node delete') + failure_count = 0 + + for nid in args.id: + try: + service.delete_node(nid, False) + except Exception as ex: + failure_count += 1 + print(ex) + if failure_count > 0: + msg = _('Failed to delete some of the specified nodes.') + raise exc.CommandError(msg) + print('Request accepted') + + +@utils.arg('-n', '--name', metavar='', + help=_('New name for the node.')) +@utils.arg('-p', '--profile', metavar='', + help=_('ID of new profile to use.')) +@utils.arg('-r', '--role', metavar='', + help=_('Role for this node in the specific cluster.')) +@utils.arg('-M', '--metadata', metavar='', + help=_('Metadata values to be attached to the node. ' + 'Metadata can be specified multiple times, or once with ' + 'key-value pairs separated by a semicolon.'), + action='append') +@utils.arg('id', metavar='', + help=_('Name or ID of node to update.')) +def do_node_update(service, args): + """Update the node.""" + show_deprecated('senlin node-update', 'openstack cluster node update') + # Find the node first, we need its UUID + try: + node = service.get_node(args.id) + except sdk_exc.ResourceNotFound: + raise exc.CommandError(_('Node not found: %s') % args.id) + + attrs = { + 'name': args.name, + 'role': args.role, + 'profile_id': args.profile, + 'metadata': utils.format_parameters(args.metadata), + } + + service.update_node(args.id, **attrs) + _show_node(service, node.id) + + +@utils.arg('id', metavar='', nargs='+', + help=_('ID of node(s) to check.')) +def do_node_check(service, args): + """Check the node(s).""" + show_deprecated('senlin node-check', 'openstack cluster node check') + failure_count = 0 + + for nid in args.id: + try: + service.check_node(nid) + except exc.HTTPNotFound: + failure_count += 1 + print('Node id "%s" not found' % nid) + if failure_count > 0: + msg = _('Failed to check some of the specified nodes.') + raise exc.CommandError(msg) + print('Request accepted') + + +@utils.arg('id', metavar='', nargs='+', + help=_('ID of node(s) to recover.')) +def do_node_recover(service, args): + """Recover the node(s).""" + show_deprecated('senlin node-recover', 'openstack cluster node recover') + failure_count = 0 + + for nid in args.id: + try: + service.recover_node(nid) + except exc.HTTPNotFound: + failure_count += 1 + print('Node id "%s" not found' % nid) + if failure_count > 0: + msg = _('Failed to recover some of the specified nodes.') + raise exc.CommandError(msg) + print('Request accepted') + + +# RECEIVERS + + +@utils.arg('-f', '--filters', metavar='', + help=_('Filter parameters to apply on returned receivers. ' + 'This can be specified multiple times, or once with ' + 'parameters separated by a semicolon.'), + action='append') +@utils.arg('-l', '--limit', metavar='', + help=_('Limit the number of receivers returned.')) +@utils.arg('-m', '--marker', metavar='', + help=_('Only return receivers that appear after the given ID.')) +@utils.arg('-o', '--sort', metavar='', + help=_('Sorting option which is a string containing a list of keys ' + 'separated by commas. Each key can be optionally appended ' + 'by a sort direction (:asc or :desc)')) +@utils.arg('-g', '--global-project', default=False, action="store_true", + help=_('Indicate that the list should include receivers from' + ' all projects. This option is subject to access policy ' + 'checking. Default is False.')) +@utils.arg('-F', '--full-id', default=False, action="store_true", + help=_('Print full IDs in list.')) +def do_receiver_list(service, args): + """List receivers that meet the criteria.""" + show_deprecated('senlin receiver-list', 'openstack cluster receiver list') + fields = ['id', 'name', 'type', 'cluster_id', 'action', 'created_at'] + queries = { + 'limit': args.limit, + 'marker': args.marker, + 'sort': args.sort, + 'global_project': args.global_project, + } + + if args.filters: + queries.update(utils.format_parameters(args.filters)) + + sortby_index = None if args.sort else 0 + + receivers = service.receivers(**queries) + formatters = {} + if not args.full_id: + formatters = { + 'id': lambda x: x.id[:8], + 'cluster_id': lambda x: x.cluster_id[:8], + } + utils.print_list(receivers, fields, formatters=formatters, + sortby_index=sortby_index) + + +def _show_receiver(service, receiver_id): + try: + receiver = service.get_receiver(receiver_id) + except sdk_exc.ResourceNotFound: + raise exc.CommandError(_('Receiver not found: %s') % receiver_id) + + formatters = { + 'actor': utils.json_formatter, + 'params': utils.json_formatter, + 'channel': utils.json_formatter, + } + + utils.print_dict(receiver.to_dict(), formatters=formatters) + + +@utils.arg('id', metavar='', + help=_('Name or ID of the receiver to show.')) +def do_receiver_show(service, args): + """Show the receiver details.""" + show_deprecated('senlin receiver-show', 'openstack cluster receiver show') + _show_receiver(service, receiver_id=args.id) + + +@utils.arg('-t', '--type', metavar='', default='webhook', + help=_('Type of the receiver to create.')) +@utils.arg('-c', '--cluster', metavar='', required=True, + help=_('Targeted cluster for this receiver.')) +@utils.arg('-a', '--action', metavar='', required=True, + help=_('Name or ID of the targeted action to be triggered.')) +@utils.arg('-P', '--params', metavar='', + help=_('A dictionary of parameters that will be passed to target ' + 'action when the receiver is triggered.'), + action='append') +@utils.arg('name', metavar='', + help=_('Name of the receiver to create.')) +def do_receiver_create(service, args): + """Create a receiver.""" + show_deprecated('senlin receiver-create', + 'openstack cluster receiver create') + + params = { + 'name': args.name, + 'type': args.type, + 'cluster_id': args.cluster, + 'action': args.action, + 'params': utils.format_parameters(args.params) + } + + receiver = service.create_receiver(**params) + _show_receiver(service, receiver.id) + + +@utils.arg('id', metavar='', nargs='+', + help=_('Name or ID of receiver(s) to delete.')) +def do_receiver_delete(service, args): + """Delete receiver(s).""" + show_deprecated('senlin receiver-delete', + 'openstack cluster receiver delete') + failure_count = 0 + + for wid in args.id: + try: + service.delete_receiver(wid, False) + except Exception as ex: + failure_count += 1 + print(ex) + if failure_count > 0: + msg = _('Failed to delete some of the specified receiver(s).') + raise exc.CommandError(msg) + print('Receivers deleted: %s' % args.id) + + +# EVENTS + + +@utils.arg('-f', '--filters', metavar='', + help=_('Filter parameters to apply on returned events. ' + 'This can be specified multiple times, or once with ' + 'parameters separated by a semicolon.'), + action='append') +@utils.arg('-l', '--limit', metavar='', + help=_('Limit the number of events returned.')) +@utils.arg('-m', '--marker', metavar='', + help=_('Only return events that appear after the given event ID.')) +@utils.arg('-o', '--sort', metavar='', + help=_('Sorting option which is a string containing a list of keys ' + 'separated by commas. Each key can be optionally appended ' + 'by a sort direction (:asc or :desc)')) +@utils.arg('-g', '--global-project', default=False, action="store_true", + help=_('Whether events from all projects should be listed. ' + ' Default to False. Setting this to True may demand ' + 'for an admin privilege.')) +@utils.arg('-F', '--full-id', default=False, action="store_true", + help=_('Print full IDs in list.')) +def do_event_list(service, args): + """List events.""" + show_deprecated('senlin event-list', 'openstack cluster event list') + fields = ['id', 'timestamp', 'obj_type', 'obj_id', 'obj_name', 'action', + 'status', 'status_reason', 'level'] + queries = { + 'sort': args.sort, + 'limit': args.limit, + 'marker': args.marker, + 'global_project': args.global_project, + } + + if args.filters: + queries.update(utils.format_parameters(args.filters)) + + formatters = {} + if not args.full_id: + formatters['id'] = lambda x: x.id[:8] + formatters['obj_id'] = lambda x: x.obj_id[:8] if x.obj_id else '' + + events = service.events(**queries) + utils.print_list(events, fields, formatters=formatters) + + +@utils.arg('id', metavar='', + help=_('ID of event to display details for.')) +def do_event_show(service, args): + """Describe the event.""" + show_deprecated('senlin event-show', 'openstack cluster event show') + try: + event = service.get_event(args.id) + except sdk_exc.ResourceNotFound: + raise exc.CommandError(_("Event not found: %s") % args.id) + + utils.print_dict(event.to_dict()) + + +# ACTIONS + + +@utils.arg('-f', '--filters', metavar='', + help=_('Filter parameters to apply on returned actions. ' + 'This can be specified multiple times, or once with ' + 'parameters separated by a semicolon.'), + action='append') +@utils.arg('-o', '--sort', metavar='', + help=_('Sorting option which is a string containing a list of keys ' + 'separated by commas. Each key can be optionally appended ' + 'by a sort direction (:asc or :desc)')) +@utils.arg('-l', '--limit', metavar='', + help=_('Limit the number of actions returned.')) +@utils.arg('-m', '--marker', metavar='', + help=_('Only return actions that appear after the given node ID.')) +@utils.arg('-F', '--full-id', default=False, action="store_true", + help=_('Print full IDs in list.')) +def do_action_list(service, args): + """List actions.""" + show_deprecated('senlin action-list', 'openstack cluster action list') + fields = ['id', 'name', 'action', 'status', 'target', 'depends_on', + 'depended_by', 'created_at'] + + queries = { + 'sort': args.sort, + 'limit': args.limit, + 'marker': args.marker, + } + + if args.filters: + queries.update(utils.format_parameters(args.filters)) + + sortby_index = None if args.sort else 0 + + actions = service.actions(**queries) + + formatters = {} + if args.full_id: + f_depon = lambda x: '\n'.join(a for a in x.depends_on) + f_depby = lambda x: '\n'.join(a for a in x.depended_by) + + formatters['depends_on'] = f_depon + formatters['depended_by'] = f_depby + else: + formatters['id'] = lambda x: x.id[:8] + formatters['target'] = lambda x: x.target[:8] + f_depon = lambda x: '\n'.join(a[:8] for a in x.depends_on) + f_depby = lambda x: '\n'.join(a[:8] for a in x.depended_by) + formatters['depends_on'] = f_depon + formatters['depended_by'] = f_depby + + utils.print_list(actions, fields, formatters=formatters, + sortby_index=sortby_index) + + +@utils.arg('id', metavar='', + help=_('Name or ID of the action to show the details for.')) +def do_action_show(service, args): + """Show detailed info about the specified action.""" + show_deprecated('senlin action-show', 'openstack cluster action show') + try: + action = service.get_action(args.id) + except sdk_exc.ResourceNotFound: + raise exc.CommandError(_('Action not found: %s') % args.id) + + formatters = { + 'inputs': utils.json_formatter, + 'outputs': utils.json_formatter, + 'metadata': utils.json_formatter, + 'data': utils.json_formatter, + 'depends_on': utils.list_formatter, + 'depended_by': utils.list_formatter, + } + + utils.print_dict(action.to_dict(), formatters=formatters) diff --git a/setup.cfg b/setup.cfg index b4027d49..3f4141c4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -23,6 +23,9 @@ packages = senlinclient [entry_points] +console_scripts = + senlin = senlinclient.shell:main + openstack.cli.extension = clustering = senlinclient.plugin