diff --git a/masakariclient/cliargs.py b/masakariclient/cliargs.py new file mode 100644 index 0000000..f776d63 --- /dev/null +++ b/masakariclient/cliargs.py @@ -0,0 +1,158 @@ +# Copyright(c) 2016 Nippon Telegraph and Telephone Corporation +# +# 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 masakariclient.common.i18n import _ +from masakariclient.common import utils + + +def add_global_args(parser, version): + # GLOBAL ARGUMENTS + parser.add_argument( + '-h', '--help', action='store_true', + help=argparse.SUPPRESS) + + parser.add_argument( + '--masakari-api-version', action='version', version=version, + default=utils.env('MASAKARI_API_VERSION', default='1'), + help=_('Version number for Masakari API to use, Default to "1".')) + + parser.add_argument( + '--debug', default=False, action='store_true', + help=_('Print debugging output.')) + + +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 masakari 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 masakariclient 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]')) diff --git a/masakariclient/client.py b/masakariclient/client.py new file mode 100644 index 0000000..586b2f8 --- /dev/null +++ b/masakariclient/client.py @@ -0,0 +1,28 @@ +# Copyright(c) 2016 Nippon Telegraph and Telephone Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from masakariclient.common import utils + + +def Client(api_ver, *args, **kwargs): + """Import versioned client module. + + :param api_ver: API version required. + :param args: API args. + :param kwargs: the auth parameters for client. + """ + module = utils.import_versioned_module(api_ver, 'client') + cls = getattr(module, 'Client') + + return cls(*args, **kwargs) diff --git a/masakariclient/common/utils.py b/masakariclient/common/utils.py index dd3adb8..8335834 100644 --- a/masakariclient/common/utils.py +++ b/masakariclient/common/utils.py @@ -12,6 +12,15 @@ # See the License for the specific language governing permissions and # limitations under the License. +import os +import prettytable +import six +import textwrap + +from oslo_serialization import jsonutils +from oslo_utils import encodeutils +from oslo_utils import importutils + from masakariclient.common import exception as exc from masakariclient.common.i18n import _ @@ -51,3 +60,112 @@ def remove_unspecified_items(attrs): if not value: del attrs[key] return attrs + + +def import_versioned_module(version, submodule=None): + module = 'masakariclient.v%s' % version + if submodule: + module = '.'.join((module, submodule)) + return importutils.import_module(module) + + +def arg(*args, **kwargs): + """Decorator for CLI args.""" + + def _decorator(func): + if not hasattr(func, 'arguments'): + func.arguments = [] + + if (args, kwargs) not in func.arguments: + func.arguments.insert(0, (args, kwargs)) + + return func + + return _decorator + + +def env(*args, **kwargs): + """Returns the first environment variable set. + + If all are empty, defaults to '' or keyword arg `default`. + """ + for arg in args: + value = os.environ.get(arg) + if value: + return value + return kwargs.get('default', '') + + +def print_list(objs, fields, formatters={}, sortby_index=None): + """Print list data by PrettyTable.""" + + if sortby_index is None: + sortby = None + else: + sortby = fields[sortby_index] + mixed_case_fields = ['serverId'] + pt = prettytable.PrettyTable([f for f in fields], caching=False) + pt.align = 'l' + + for o in objs: + row = [] + for field in fields: + if field in formatters: + row.append(formatters[field](o)) + else: + if field in mixed_case_fields: + field_name = field.replace(' ', '_') + else: + field_name = field.lower().replace(' ', '_') + data = getattr(o, field_name, '') + if data is None: + data = '-' + # '\r' would break the table, so remove it. + data = six.text_type(data).replace("\r", "") + row.append(data) + pt.add_row(row) + + if sortby is not None: + result = encodeutils.safe_encode(pt.get_string(sortby=sortby)) + else: + result = encodeutils.safe_encode(pt.get_string()) + + if six.PY3: + result = result.decode() + + print(result) + + +def print_dict(d, dict_property="Property", dict_value="Value", wrap=0): + """Print dictionary data (eg. show) by PrettyTable.""" + + pt = prettytable.PrettyTable([dict_property, dict_value], caching=False) + pt.align = 'l' + for k, v in sorted(d.items()): + # convert dict to str to check length + if isinstance(v, (dict, list)): + v = jsonutils.dumps(v) + if wrap > 0: + v = textwrap.fill(six.text_type(v), wrap) + # if value has a newline, add in multiple rows + # e.g. fault with stacktrace + if v and isinstance(v, six.string_types) and (r'\n' in v or '\r' in v): + # '\r' would break the table, so remove it. + if '\r' in v: + v = v.replace('\r', '') + lines = v.strip().split(r'\n') + col1 = k + for line in lines: + pt.add_row([col1, line]) + col1 = '' + else: + if v is None: + v = '-' + pt.add_row([k, v]) + + result = encodeutils.safe_encode(pt.get_string()) + + if six.PY3: + result = result.decode() + + print(result) diff --git a/masakariclient/sdk/vmha/connection.py b/masakariclient/sdk/vmha/connection.py index 91f3551..c2ebb42 100644 --- a/masakariclient/sdk/vmha/connection.py +++ b/masakariclient/sdk/vmha/connection.py @@ -15,12 +15,16 @@ from openstack import connection from openstack import profile +from masakariclient.sdk.vmha import vmha_service + def create_connection(prof=None, user_agent=None, **kwargs): + """Create connection to masakari_api.""" if not prof: prof = profile.Profile() + prof._add_service(vmha_service.VMHAService(version="v1")) interface = kwargs.pop('interface', None) region_name = kwargs.pop('region_name', None) if interface: diff --git a/masakariclient/shell.py b/masakariclient/shell.py new file mode 100644 index 0000000..7642687 --- /dev/null +++ b/masakariclient/shell.py @@ -0,0 +1,187 @@ +# Copyright(c) 2016 Nippon Telegraph and Telephone Corporation +# +# 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 sys + +from oslo_utils import encodeutils +import six + +import masakariclient +from masakariclient import cliargs +from masakariclient import client as masakari_client +from masakariclient.common import exception as exc +from masakariclient.common.i18n import _ +from masakariclient.common import utils + +USER_AGENT = 'python-masakariclient' + + +class MasakariShell(object): + def __init__(self): + pass + + def do_bash_completion(self, args): + """All of the commands and options to stdout.""" + 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_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._add_bash_completion_subparser(subparsers) + + return parser + + 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) + + 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 _setup_masakari_client(self, api_ver, args): + """Create masakari 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 masakari_client.Client(api_ver, user_agent=USER_AGENT, **kwargs) + + def main(self, argv): + + parser = argparse.ArgumentParser( + prog='masakari', + description="masakari shell", + epilog='Type "masakari help " for help on a specific ' + 'command.', + add_help=False, + ) + # add add arguments + cliargs.add_global_args(parser, masakariclient.__version__) + cliargs.add_global_identity_args(parser) + + # parse main arguments + (options, args) = parser.parse_known_args(argv) + + base_parser = parser + api_ver = options.masakari_api_version + + # add subparser + subcommand_parser = self._get_subcommand_parser(base_parser, api_ver) + self.parser = subcommand_parser + + # --help/-h or no arguments + if not args and options.help or not argv: + self.do_help(options) + return 0 + + args = subcommand_parser.parse_args(argv) + + sc = self._setup_masakari_client(api_ver, args) + # call specified function + args.func(sc.service, args) + + @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() + + +class HelpFormatter(argparse.HelpFormatter): + def start_section(self, heading): + heading = '%s%s' % (heading[0].upper(), heading[1:]) + super(HelpFormatter, self).start_section(heading) + + +def main(args=None): + try: + if args is None: + args = sys.argv[1:] + + MasakariShell().main(args) + except KeyboardInterrupt: + print(_("KeyboardInterrupt masakari client"), sys.stderr) + return 130 + except Exception as e: + if '--debug' in args: + raise + else: + print(encodeutils.safe_encode(six.text_type(e)), sys.stderr) + return 1 + +if __name__ == "__main__": + sys.exit(main()) diff --git a/masakariclient/v1/__init__.py b/masakariclient/v1/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/masakariclient/v1/client.py b/masakariclient/v1/client.py new file mode 100644 index 0000000..7692dab --- /dev/null +++ b/masakariclient/v1/client.py @@ -0,0 +1,23 @@ +# Copyright(c) 2016 Nippon Telegraph and Telephone Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from masakariclient.sdk.vmha import connection + + +class Client(object): + + def __init__(self, prof=None, user_agent=None, **kwargs): + self.con = connection.create_connection( + prof=prof, user_agent=user_agent, **kwargs) + self.service = self.con.vmha diff --git a/masakariclient/v1/shell.py b/masakariclient/v1/shell.py new file mode 100644 index 0000000..e45b1d0 --- /dev/null +++ b/masakariclient/v1/shell.py @@ -0,0 +1,15 @@ +# Copyright(c) 2016 Nippon Telegraph and Telephone Corporation +# +# 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. + +"""Implement sub commands in this file after that.""" diff --git a/setup.cfg b/setup.cfg index 6a23fd9..b81ed43 100644 --- a/setup.cfg +++ b/setup.cfg @@ -24,6 +24,9 @@ packages = masakariclient [entry_points] +console_scripts = + masakari = masakariclient.shell:main + openstack.cli.extension = vmha = masakariclient.plugin