# Copyright (c) 2013 Mirantis Inc. # # 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 Blazar APIs """ from __future__ import print_function import argparse import logging import os import sys from cliff import app from cliff import commandmanager from keystoneauth1 import identity from keystoneauth1 import session from oslo_utils import encodeutils import six from blazarclient import client as blazar_client from blazarclient import exception from blazarclient import utils from blazarclient.v1.shell_commands import floatingips from blazarclient.v1.shell_commands import hosts from blazarclient.v1.shell_commands import leases from blazarclient import version as base_version COMMANDS_V1 = { 'lease-list': leases.ListLeases, 'lease-show': leases.ShowLease, 'lease-create': leases.CreateLease, 'lease-update': leases.UpdateLease, 'lease-delete': leases.DeleteLease, 'host-list': hosts.ListHosts, 'host-show': hosts.ShowHost, 'host-create': hosts.CreateHost, 'host-update': hosts.UpdateHost, 'host-delete': hosts.DeleteHost, 'floatingip-list': floatingips.ListFloatingIPs, 'floatingip-show': floatingips.ShowFloatingIP, 'floatingip-create': floatingips.CreateFloatingIP, 'floatingip-delete': floatingips.DeleteFloatingIP, } VERSION = 1 DEFAULT_API_VERSION = 1 COMMANDS = {'v1': COMMANDS_V1} def run_command(cmd, cmd_parser, sub_argv): _argv = sub_argv index = -1 values_specs = [] if '--' in sub_argv: index = sub_argv.index('--') _argv = sub_argv[:index] values_specs = sub_argv[index:] known_args, _values_specs = cmd_parser.parse_known_args(_argv) cmd.values_specs = (index == -1 and _values_specs or values_specs) return cmd.run(known_args) def env(*_vars, **kwargs): """Search for the first defined of possibly many env vars. Returns the first environment variable defined in vars, or returns the default defined in kwargs. """ for v in _vars: value = os.environ.get(v, None) if value: return value return kwargs.get('default', '') class HelpAction(argparse.Action): """Provide a custom action so the -h and --help options to the main app will print a list of the commands. The commands are determined by checking the CommandManager instance, passed in as the "default" value for the action. """ def __call__(self, parser, namespace, values, option_string=None): outputs = [] max_len = 0 app = self.default parser.print_help(app.stdout) app.stdout.write('\nCommands for API %s:\n' % app.api_version) command_manager = app.command_manager for name, ep in sorted(command_manager): factory = ep.load() cmd = factory(self, None) one_liner = cmd.get_description().split('\n')[0] outputs.append((name, one_liner)) max_len = max(len(name), max_len) for (name, one_liner) in outputs: app.stdout.write(' %s %s\n' % (name.ljust(max_len), one_liner)) sys.exit(0) class BlazarShell(app.App): """Manager class for the Blazar CLI.""" CONSOLE_MESSAGE_FORMAT = '%(message)s' DEBUG_MESSAGE_FORMAT = '%(levelname)s: %(name)s %(message)s' log = logging.getLogger(__name__) def __init__(self): super(BlazarShell, self).__init__( description=__doc__.strip(), version=VERSION, command_manager=commandmanager.CommandManager('blazar.cli'), ) self.commands = COMMANDS def build_option_parser(self, description, version, argparse_kwargs=None): """Return an argparse option parser for this application. Subclasses may override this method to extend the parser with more global options. """ parser = argparse.ArgumentParser( description=description, add_help=False) parser.add_argument( '--version', action='version', version=base_version.__version__) parser.add_argument( '-v', '--verbose', action='count', dest='verbose_level', default=self.DEFAULT_VERBOSE_LEVEL, help='Increase verbosity of output. Can be repeated.') parser.add_argument( '-q', '--quiet', action='store_const', dest='verbose_level', const=0, help='suppress output except warnings and errors') help_action = parser.add_argument( '-h', '--help', action=HelpAction, nargs=0, default=self, help="show this help message and exit") parser.add_argument( '--debug', default=False, action='store_true', help='Print debugging output') # Removes help action to defer its execution self.deferred_help_action = help_action parser._actions.remove(help_action) del parser._option_string_actions['-h'] del parser._option_string_actions['--help'] parser.add_argument( '-h', '--help', action='store_true', dest='deferred_help', default=False, help="Show this help message and exit", ) # Global arguments parser.add_argument( '--os-reservation-api-version', default=env('OS_RESERVATION_API_VERSION', default=DEFAULT_API_VERSION), help='Accepts 1 now, defaults to 1.') parser.add_argument( '--os_reservation_api_version', help=argparse.SUPPRESS) parser.add_argument( '--os-auth-strategy', metavar='<auth-strategy>', default=env('OS_AUTH_STRATEGY', default='keystone'), help='Authentication strategy (Env: OS_AUTH_STRATEGY' ', default keystone). For now, any other value will' ' disable the authentication') parser.add_argument( '--os_auth_strategy', help=argparse.SUPPRESS) parser.add_argument( '--os-auth-url', metavar='<auth-url>', default=env('OS_AUTH_URL'), help='Authentication URL (Env: OS_AUTH_URL)') parser.add_argument( '--os_auth_url', help=argparse.SUPPRESS) parser.add_argument( '--os-project-name', metavar='<auth-project-name>', default=env('OS_PROJECT_NAME'), help='Authentication project name (Env: OS_PROJECT_NAME)') parser.add_argument( '--os_project_name', help=argparse.SUPPRESS) parser.add_argument( '--os-project-id', metavar='<auth-project-id>', default=env('OS_PROJECT_ID'), help='Authentication project ID (Env: OS_PROJECT_ID)') parser.add_argument( '--os_project_id', help=argparse.SUPPRESS) parser.add_argument( '--os-project-domain-name', metavar='<auth-project-domain-name>', default=env('OS_PROJECT_DOMAIN_NAME'), help='Authentication project domain name ' '(Env: OS_PROJECT_DOMAIN_NAME)') parser.add_argument( '--os_project_domain_name', help=argparse.SUPPRESS) parser.add_argument( '--os-project-domain-id', metavar='<auth-project-domain-id>', default=env('OS_PROJECT_DOMAIN_ID'), help='Authentication project domain ID ' '(Env: OS_PROJECT_DOMAIN_ID)') parser.add_argument( '--os_project_domain_id', help=argparse.SUPPRESS) parser.add_argument( '--os-tenant-name', metavar='<auth-tenant-name>', default=env('OS_TENANT_NAME'), help='Authentication tenant name (Env: OS_TENANT_NAME)') parser.add_argument( '--os_tenant_name', help=argparse.SUPPRESS) parser.add_argument( '--os-tenant-id', metavar='<auth-tenant-id>', default=env('OS_TENANT_ID'), help='Authentication tenant name (Env: OS_TENANT_ID)') parser.add_argument( '--os-username', metavar='<auth-username>', default=utils.env('OS_USERNAME'), help='Authentication username (Env: OS_USERNAME)') parser.add_argument( '--os_username', help=argparse.SUPPRESS) parser.add_argument( '--os-user-domain-name', metavar='<auth-user-domain-name>', default=env('OS_USER_DOMAIN_NAME'), help='Authentication user domain name (Env: OS_USER_DOMAIN_NAME)') parser.add_argument( '--os_user_domain_name', help=argparse.SUPPRESS) parser.add_argument( '--os-user-domain-id', metavar='<auth-user-domain-id>', default=env('OS_USER_DOMAIN_ID'), help='Authentication user domain ID (Env: OS_USER_DOMAIN_ID)') parser.add_argument( '--os_user_domain_id', help=argparse.SUPPRESS) parser.add_argument( '--os-password', metavar='<auth-password>', default=utils.env('OS_PASSWORD'), help='Authentication password (Env: OS_PASSWORD)') parser.add_argument( '--os_password', help=argparse.SUPPRESS) parser.add_argument( '--os-region-name', metavar='<auth-region-name>', default=env('OS_REGION_NAME'), help='Authentication region name (Env: OS_REGION_NAME)') parser.add_argument( '--os_region_name', help=argparse.SUPPRESS) parser.add_argument( '--os-token', metavar='<token>', default=env('OS_TOKEN'), help='Defaults to env[OS_TOKEN]') parser.add_argument( '--os_token', help=argparse.SUPPRESS) parser.add_argument( '--service-type', metavar='<service-type>', default=env('BLAZAR_SERVICE_TYPE', default='reservation'), help='Defaults to env[BLAZAR_SERVICE_TYPE] or reservation.') parser.add_argument( '--endpoint-type', metavar='<endpoint-type>', default=env('OS_ENDPOINT_TYPE', default='publicURL'), help='Defaults to env[OS_ENDPOINT_TYPE] or publicURL.') parser.add_argument( '--os-cacert', metavar='<ca-certificate>', default=env('OS_CACERT', default=None), help="Specify a CA bundle file to use in " "verifying a TLS (https) server certificate. " "Defaults to env[OS_CACERT]") parser.add_argument( '--insecure', action='store_true', default=env('BLAZARCLIENT_INSECURE', default=False), help="Explicitly allow blazarclient 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.") return parser def _bash_completion(self): """Prints all of the commands and options for bash-completion.""" commands = set() options = set() for option, _action in self.parser._option_string_actions.items(): options.add(option) for command_name, command in self.command_manager: commands.add(command_name) cmd_factory = command.load() cmd = cmd_factory(self, None) cmd_parser = cmd.get_parser('') for option, _action in cmd_parser._option_string_actions.items(): options.add(option) print(' '.join(commands | options)) def run(self, argv): """Equivalent to the main program for the application. :param argv: input arguments and options :paramtype argv: list of str """ try: self.options, remainder = self.parser.parse_known_args(argv) self.api_version = 'v%s' % self.options.os_reservation_api_version for k, v in self.commands[self.api_version].items(): self.command_manager.add_command(k, v) index = 0 command_pos = -1 help_pos = -1 help_command_pos = -1 for arg in argv: if arg == 'bash-completion': self._bash_completion() return 0 if arg in self.commands[self.api_version]: if command_pos == -1: command_pos = index elif arg in ('-h', '--help'): if help_pos == -1: help_pos = index elif arg == 'help': if help_command_pos == -1: help_command_pos = index index += 1 if -1 < command_pos < help_pos: argv = ['help', argv[command_pos]] if help_command_pos > -1 and command_pos == -1: argv[help_command_pos] = '--help' if self.options.deferred_help: self.deferred_help_action(self.parser, self.parser, None, None) self.configure_logging() self.interactive_mode = not remainder self.initialize_app(remainder) except Exception as err: if self.options.debug: self.log.exception(six.text_type(err)) raise else: self.log.error(six.text_type(err)) return 1 if self.interactive_mode: _argv = [sys.argv[0]] sys.argv = _argv result = self.interact() else: result = self.run_subcommand(remainder) return result def run_subcommand(self, argv): subcommand = self.command_manager.find_command(argv) cmd_factory, cmd_name, sub_argv = subcommand cmd = cmd_factory(self, self.options) result = 1 try: self.prepare_to_run_command(cmd) full_name = (cmd_name if self.interactive_mode else ' '.join([self.NAME, cmd_name])) cmd_parser = cmd.get_parser(full_name) return run_command(cmd, cmd_parser, sub_argv) except Exception as err: if self.options.debug: self.log.exception(six.text_type(err)) else: self.log.error(six.text_type(err)) try: self.clean_up(cmd, result, err) except Exception as err2: if self.options.debug: self.log.exception(six.text_type(err2)) else: self.log.error('Could not clean up: %s', six.text_type(err2)) if self.options.debug: raise else: try: self.clean_up(cmd, result, None) except Exception as err3: if self.options.debug: self.log.exception(six.text_type(err3)) else: self.log.error('Could not clean up: %s', six.text_type(err3)) return result def authenticate_user(self): """Authenticate user and set client by using passed params.""" if self.options.os_token: auth = identity.Token( auth_url=self.options.os_auth_url, token=self.options.os_token, tenant_id=self.options.os_tenant_id, tenant_name=self.options.os_tenant_name, project_id=self.options.os_project_id, project_name=self.options.os_project_name, project_domain_id=self.options.os_project_domain_id, project_domain_name=self.options.os_project_domain_name ) else: auth = identity.Password( auth_url=self.options.os_auth_url, username=self.options.os_username, tenant_id=self.options.os_tenant_id, tenant_name=self.options.os_tenant_name, password=self.options.os_password, project_id=self.options.os_project_id, project_name=self.options.os_project_name, project_domain_id=self.options.os_project_domain_id, project_domain_name=self.options.os_project_domain_name, user_domain_id=self.options.os_user_domain_id, user_domain_name=self.options.os_user_domain_name ) sess = session.Session( auth=auth, verify=(self.options.os_cacert or not self.options.insecure) ) self.client = blazar_client.Client( self.options.os_reservation_api_version, session=sess, service_type=self.options.service_type, interface=self.options.endpoint_type, region_name=self.options.os_region_name, ) return def initialize_app(self, argv): """Global app init bits: * set up API versions * validate authentication info """ super(BlazarShell, self).initialize_app(argv) cmd_name = None if argv: cmd_info = self.command_manager.find_command(argv) cmd_factory, cmd_name, sub_argv = cmd_info if self.interactive_mode or cmd_name != 'help': self.authenticate_user() def clean_up(self, cmd, result, err): self.log.debug('clean_up %s', cmd.__class__.__name__) if err: self.log.debug('got an error: %s', six.text_type(err)) def configure_logging(self): """Create logging handlers for any log output.""" root_logger = logging.getLogger('') # Set up logging to a file root_logger.setLevel(logging.DEBUG) # Send higher-level messages to the console via stderr console = logging.StreamHandler(self.stderr) if self.options.debug: console_level = logging.DEBUG else: console_level = {0: logging.WARNING, 1: logging.INFO, 2: logging.DEBUG}.get(self.options.verbose_level, logging.DEBUG) console.setLevel(console_level) if logging.DEBUG == console_level: formatter = logging.Formatter(self.DEBUG_MESSAGE_FORMAT) else: formatter = logging.Formatter(self.CONSOLE_MESSAGE_FORMAT) console.setFormatter(formatter) root_logger.addHandler(console) return def main(argv=sys.argv[1:]): try: return BlazarShell().run(list(map(encodeutils.safe_decode, argv))) except exception.BlazarClientException: return 1 except Exception as e: print(six.text_type(e)) return 1 if __name__ == "__main__": sys.exit(main(sys.argv[1:]))