osc-lib: shell
Convert to using ClientManager and OpenStackShell from osc-lib. * Change all internal uses of ClientManager private attributes that are now public in osc-lib's ClientManager. Leave back-compat copies in place in OSC's clientManager so we don't break plugins. * Put some work-arounds in place for changes in osc-lib that we need until a new release makes it through the g-r and u-c change process. * Add a test for Unicode decoding of argv in shell.main() to parallel the one in osc-lib. Change-Id: I85289740d4ca081f2aca8c9b40ec422ad25d302c
This commit is contained in:
@ -16,30 +16,17 @@
|
||||
|
||||
"""Command-line interface to the OpenStack APIs"""
|
||||
|
||||
import argparse
|
||||
import getpass
|
||||
import locale
|
||||
import logging
|
||||
import six
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
from cliff import app
|
||||
from cliff import command
|
||||
from cliff import complete
|
||||
from cliff import help
|
||||
from osc_lib.cli import client_config as cloud_config
|
||||
from osc_lib.command import timing
|
||||
from osc_lib import exceptions as exc
|
||||
from osc_lib import logs
|
||||
from osc_lib import utils
|
||||
from osc_lib.api import auth
|
||||
from osc_lib import shell
|
||||
from oslo_utils import importutils
|
||||
from oslo_utils import strutils
|
||||
import six
|
||||
|
||||
import openstackclient
|
||||
from openstackclient.common import clientmanager
|
||||
from openstackclient.common import commandmanager
|
||||
from openstackclient.i18n import _
|
||||
|
||||
osprofiler_profiler = importutils.try_import("osprofiler.profiler")
|
||||
|
||||
@ -47,47 +34,9 @@ osprofiler_profiler = importutils.try_import("osprofiler.profiler")
|
||||
DEFAULT_DOMAIN = 'default'
|
||||
|
||||
|
||||
def prompt_for_password(prompt=None):
|
||||
"""Prompt user for a password
|
||||
|
||||
Prompt for a password if stdin is a tty.
|
||||
"""
|
||||
|
||||
if not prompt:
|
||||
prompt = 'Password: '
|
||||
pw = None
|
||||
# If stdin is a tty, try prompting for the password
|
||||
if hasattr(sys.stdin, 'isatty') and sys.stdin.isatty():
|
||||
# Check for Ctl-D
|
||||
try:
|
||||
pw = getpass.getpass(prompt)
|
||||
except EOFError:
|
||||
pass
|
||||
# No password because we did't have a tty or nothing was entered
|
||||
if not pw:
|
||||
raise exc.CommandError(_("No password entered, or found via"
|
||||
" --os-password or OS_PASSWORD"),)
|
||||
return pw
|
||||
|
||||
|
||||
class OpenStackShell(app.App):
|
||||
|
||||
CONSOLE_MESSAGE_FORMAT = '%(levelname)s: %(name)s %(message)s'
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
timing_data = []
|
||||
class OpenStackShell(shell.OpenStackShell):
|
||||
|
||||
def __init__(self):
|
||||
# Patch command.Command to add a default auth_required = True
|
||||
command.Command.auth_required = True
|
||||
|
||||
# Some commands do not need authentication
|
||||
help.HelpCommand.auth_required = False
|
||||
complete.CompleteCommand.auth_required = False
|
||||
|
||||
# Slight change to the meaning of --debug
|
||||
self.DEFAULT_DEBUG_VALUE = None
|
||||
self.DEFAULT_DEBUG_HELP = 'Set debug logging and traceback on errors.'
|
||||
|
||||
super(OpenStackShell, self).__init__(
|
||||
description=__doc__.strip(),
|
||||
@ -97,281 +46,28 @@ class OpenStackShell(app.App):
|
||||
|
||||
self.api_version = {}
|
||||
|
||||
# Until we have command line arguments parsed, dump any stack traces
|
||||
self.dump_stack_trace = True
|
||||
|
||||
# Assume TLS host certificate verification is enabled
|
||||
self.verify = True
|
||||
|
||||
self.client_manager = None
|
||||
self.command_options = None
|
||||
|
||||
self.do_profile = False
|
||||
|
||||
def configure_logging(self):
|
||||
"""Configure logging for the app."""
|
||||
self.log_configurator = logs.LogConfigurator(self.options)
|
||||
self.dump_stack_trace = self.log_configurator.dump_trace
|
||||
|
||||
def run(self, argv):
|
||||
ret_val = 1
|
||||
self.command_options = argv
|
||||
try:
|
||||
ret_val = super(OpenStackShell, self).run(argv)
|
||||
return ret_val
|
||||
except Exception as e:
|
||||
if not logging.getLogger('').handlers:
|
||||
logging.basicConfig()
|
||||
if self.dump_stack_trace:
|
||||
self.log.error(traceback.format_exc())
|
||||
else:
|
||||
self.log.error('Exception raised: ' + str(e))
|
||||
|
||||
return ret_val
|
||||
|
||||
finally:
|
||||
self.log.info("END return value: %s", ret_val)
|
||||
|
||||
def init_profile(self):
|
||||
# NOTE(dtroyer): Remove this 'if' block when the --profile global
|
||||
# option is removed
|
||||
if osprofiler_profiler and self.options.old_profile:
|
||||
self.log.warning(
|
||||
'The --profile option is deprecated, '
|
||||
'please use --os-profile instead'
|
||||
)
|
||||
if not self.options.profile:
|
||||
self.options.profile = self.options.old_profile
|
||||
|
||||
self.do_profile = osprofiler_profiler and self.options.profile
|
||||
if self.do_profile:
|
||||
osprofiler_profiler.init(self.options.profile)
|
||||
|
||||
def close_profile(self):
|
||||
if self.do_profile:
|
||||
trace_id = osprofiler_profiler.get().get_base_id()
|
||||
|
||||
# NOTE(dbelova): let's use warning log level to see these messages
|
||||
# printed. In fact we can define custom log level here with value
|
||||
# bigger than most big default one (CRITICAL) or something like
|
||||
# that (PROFILE = 60 for instance), but not sure we need it here.
|
||||
self.log.warning("Trace ID: %s" % trace_id)
|
||||
self.log.warning("Display trace with command:\n"
|
||||
"osprofiler trace show --html %s " % trace_id)
|
||||
|
||||
def run_subcommand(self, argv):
|
||||
self.init_profile()
|
||||
try:
|
||||
ret_value = super(OpenStackShell, self).run_subcommand(argv)
|
||||
finally:
|
||||
self.close_profile()
|
||||
return ret_value
|
||||
|
||||
def interact(self):
|
||||
self.init_profile()
|
||||
try:
|
||||
ret_value = super(OpenStackShell, self).interact()
|
||||
finally:
|
||||
self.close_profile()
|
||||
return ret_value
|
||||
|
||||
def build_option_parser(self, description, version):
|
||||
parser = super(OpenStackShell, self).build_option_parser(
|
||||
description,
|
||||
version)
|
||||
parser = clientmanager.build_plugin_option_parser(parser)
|
||||
parser = auth.build_auth_plugins_option_parser(parser)
|
||||
return parser
|
||||
|
||||
# service token auth argument
|
||||
parser.add_argument(
|
||||
'--os-cloud',
|
||||
metavar='<cloud-config-name>',
|
||||
dest='cloud',
|
||||
default=utils.env('OS_CLOUD'),
|
||||
help=_('Cloud name in clouds.yaml (Env: OS_CLOUD)'),
|
||||
)
|
||||
# Global arguments
|
||||
parser.add_argument(
|
||||
'--os-region-name',
|
||||
metavar='<auth-region-name>',
|
||||
dest='region_name',
|
||||
default=utils.env('OS_REGION_NAME'),
|
||||
help=_('Authentication region name (Env: OS_REGION_NAME)'),
|
||||
)
|
||||
parser.add_argument(
|
||||
'--os-cacert',
|
||||
metavar='<ca-bundle-file>',
|
||||
dest='cacert',
|
||||
default=utils.env('OS_CACERT'),
|
||||
help=_('CA certificate bundle file (Env: OS_CACERT)'),
|
||||
)
|
||||
parser.add_argument(
|
||||
'--os-cert',
|
||||
metavar='<certificate-file>',
|
||||
dest='cert',
|
||||
default=utils.env('OS_CERT'),
|
||||
help=_('Client certificate bundle file (Env: OS_CERT)'),
|
||||
)
|
||||
parser.add_argument(
|
||||
'--os-key',
|
||||
metavar='<key-file>',
|
||||
dest='key',
|
||||
default=utils.env('OS_KEY'),
|
||||
help=_('Client certificate key file (Env: OS_KEY)'),
|
||||
)
|
||||
verify_group = parser.add_mutually_exclusive_group()
|
||||
verify_group.add_argument(
|
||||
'--verify',
|
||||
action='store_true',
|
||||
default=None,
|
||||
help=_('Verify server certificate (default)'),
|
||||
)
|
||||
verify_group.add_argument(
|
||||
'--insecure',
|
||||
action='store_true',
|
||||
default=None,
|
||||
help=_('Disable server certificate verification'),
|
||||
)
|
||||
parser.add_argument(
|
||||
'--os-default-domain',
|
||||
metavar='<auth-domain>',
|
||||
dest='default_domain',
|
||||
default=utils.env(
|
||||
'OS_DEFAULT_DOMAIN',
|
||||
default=DEFAULT_DOMAIN),
|
||||
help=_('Default domain ID, default=%s. '
|
||||
'(Env: OS_DEFAULT_DOMAIN)') % DEFAULT_DOMAIN,
|
||||
)
|
||||
parser.add_argument(
|
||||
'--os-interface',
|
||||
metavar='<interface>',
|
||||
dest='interface',
|
||||
choices=['admin', 'public', 'internal'],
|
||||
default=utils.env('OS_INTERFACE'),
|
||||
help=_('Select an interface type.'
|
||||
' Valid interface types: [admin, public, internal].'
|
||||
' (Env: OS_INTERFACE)'),
|
||||
)
|
||||
parser.add_argument(
|
||||
'--timing',
|
||||
default=False,
|
||||
action='store_true',
|
||||
help=_("Print API call timing info"),
|
||||
)
|
||||
parser.add_argument(
|
||||
'--os-beta-command',
|
||||
action='store_true',
|
||||
help=_("Enable beta commands which are subject to change"),
|
||||
)
|
||||
def _final_defaults(self):
|
||||
super(OpenStackShell, self)._final_defaults()
|
||||
|
||||
# osprofiler HMAC key argument
|
||||
if osprofiler_profiler:
|
||||
parser.add_argument(
|
||||
'--os-profile',
|
||||
metavar='hmac-key',
|
||||
dest='profile',
|
||||
help=_('HMAC key for encrypting profiling context data'),
|
||||
)
|
||||
# NOTE(dtroyer): This global option should have been named
|
||||
# --os-profile as --profile interferes with at
|
||||
# least one existing command option. Deprecate
|
||||
# --profile and remove after Apr 2017.
|
||||
parser.add_argument(
|
||||
'--profile',
|
||||
metavar='hmac-key',
|
||||
dest='old_profile',
|
||||
help=argparse.SUPPRESS,
|
||||
)
|
||||
# Set default auth type to password
|
||||
self._auth_type = 'password'
|
||||
|
||||
return clientmanager.build_plugin_option_parser(parser)
|
||||
def _load_plugins(self):
|
||||
"""Load plugins via stevedore
|
||||
|
||||
def initialize_app(self, argv):
|
||||
"""Global app init bits:
|
||||
|
||||
* set up API versions
|
||||
* validate authentication info
|
||||
* authenticate against Identity if requested
|
||||
osc-lib has no opinion on what plugins should be loaded
|
||||
"""
|
||||
|
||||
# Parent __init__ parses argv into self.options
|
||||
super(OpenStackShell, self).initialize_app(argv)
|
||||
self.log.info("START with options: %s",
|
||||
strutils.mask_password(self.command_options))
|
||||
self.log.debug("options: %s",
|
||||
strutils.mask_password(self.options))
|
||||
|
||||
# Set the default plugin to token_endpoint if url and token are given
|
||||
if (self.options.url and self.options.token):
|
||||
# Use service token authentication
|
||||
auth_type = 'token_endpoint'
|
||||
else:
|
||||
auth_type = 'password'
|
||||
|
||||
project_id = getattr(self.options, 'project_id', None)
|
||||
project_name = getattr(self.options, 'project_name', None)
|
||||
tenant_id = getattr(self.options, 'tenant_id', None)
|
||||
tenant_name = getattr(self.options, 'tenant_name', None)
|
||||
|
||||
# Save default domain
|
||||
self.default_domain = self.options.default_domain
|
||||
|
||||
# handle some v2/v3 authentication inconsistencies by just acting like
|
||||
# both the project and tenant information are both present. This can
|
||||
# go away if we stop registering all the argparse options together.
|
||||
if project_id and not tenant_id:
|
||||
self.options.tenant_id = project_id
|
||||
if project_name and not tenant_name:
|
||||
self.options.tenant_name = project_name
|
||||
if tenant_id and not project_id:
|
||||
self.options.project_id = tenant_id
|
||||
if tenant_name and not project_name:
|
||||
self.options.project_name = tenant_name
|
||||
|
||||
# Do configuration file handling
|
||||
# Ignore the default value of interface. Only if it is set later
|
||||
# will it be used.
|
||||
try:
|
||||
cc = cloud_config.OSC_Config(
|
||||
override_defaults={
|
||||
'interface': None,
|
||||
'auth_type': auth_type,
|
||||
},
|
||||
)
|
||||
except (IOError, OSError):
|
||||
self.log.critical("Could not read clouds.yaml configuration file")
|
||||
self.print_help_if_requested()
|
||||
raise
|
||||
|
||||
# TODO(thowe): Change cliff so the default value for debug
|
||||
# can be set to None.
|
||||
if not self.options.debug:
|
||||
self.options.debug = None
|
||||
self.cloud = cc.get_one_cloud(
|
||||
cloud=self.options.cloud,
|
||||
argparse=self.options,
|
||||
)
|
||||
|
||||
self.log_configurator.configure(self.cloud)
|
||||
self.dump_stack_trace = self.log_configurator.dump_trace
|
||||
self.log.debug("defaults: %s", cc.defaults)
|
||||
self.log.debug("cloud cfg: %s",
|
||||
strutils.mask_password(self.cloud.config))
|
||||
|
||||
# Set up client TLS
|
||||
# NOTE(dtroyer): --insecure is the non-default condition that
|
||||
# overrides any verify setting in clouds.yaml
|
||||
# so check it first, then fall back to any verify
|
||||
# setting provided.
|
||||
self.verify = not self.cloud.config.get(
|
||||
'insecure',
|
||||
not self.cloud.config.get('verify', True),
|
||||
)
|
||||
|
||||
# NOTE(dtroyer): Per bug https://bugs.launchpad.net/bugs/1447784
|
||||
# --insecure now overrides any --os-cacert setting,
|
||||
# where before --insecure was ignored if --os-cacert
|
||||
# was set.
|
||||
if self.verify and self.cloud.cacert:
|
||||
self.verify = self.cloud.cacert
|
||||
|
||||
# Loop through extensions to get API versions
|
||||
for mod in clientmanager.PLUGIN_MODULES:
|
||||
default_version = getattr(mod, 'DEFAULT_API_VERSION', None)
|
||||
@ -406,6 +102,11 @@ class OpenStackShell(app.App):
|
||||
{'name': api, 'version': version_opt, 'group': cmd_group}
|
||||
)
|
||||
|
||||
def _load_commands(self):
|
||||
"""Load commands via cliff/stevedore
|
||||
|
||||
osc-lib has no opinion on what commands should be loaded
|
||||
"""
|
||||
# Commands that span multiple APIs
|
||||
self.command_manager.add_command_group(
|
||||
'openstack.common')
|
||||
@ -422,59 +123,19 @@ class OpenStackShell(app.App):
|
||||
# }
|
||||
self.command_manager.add_command_group(
|
||||
'openstack.extension')
|
||||
# call InitializeXxx() here
|
||||
# set up additional clients to stuff in to client_manager??
|
||||
|
||||
# Handle deferred help and exit
|
||||
self.print_help_if_requested()
|
||||
def initialize_app(self, argv):
|
||||
super(OpenStackShell, self).initialize_app(argv)
|
||||
|
||||
# For now we need to build our own ClientManager so re-do what
|
||||
# has already been done :(
|
||||
# TODO(dtroyer): remove when osc-lib is fixed
|
||||
self.client_manager = clientmanager.ClientManager(
|
||||
cli_options=self.cloud,
|
||||
api_version=self.api_version,
|
||||
pw_func=prompt_for_password,
|
||||
pw_func=shell.prompt_for_password,
|
||||
)
|
||||
|
||||
def prepare_to_run_command(self, cmd):
|
||||
"""Set up auth and API versions"""
|
||||
self.log.info(
|
||||
'command: %s -> %s.%s',
|
||||
getattr(cmd, 'cmd_name', '<none>'),
|
||||
cmd.__class__.__module__,
|
||||
cmd.__class__.__name__,
|
||||
)
|
||||
if cmd.auth_required:
|
||||
self.client_manager.setup_auth()
|
||||
if hasattr(cmd, 'required_scope') and cmd.required_scope:
|
||||
# let the command decide whether we need a scoped token
|
||||
self.client_manager.validate_scope()
|
||||
# Trigger the Identity client to initialize
|
||||
self.client_manager.auth_ref
|
||||
|
||||
def clean_up(self, cmd, result, err):
|
||||
self.log.debug('clean_up %s: %s', cmd.__class__.__name__, err or '')
|
||||
|
||||
# Process collected timing data
|
||||
if self.options.timing:
|
||||
# Get session data
|
||||
self.timing_data.extend(
|
||||
self.client_manager.session.get_timings(),
|
||||
)
|
||||
|
||||
# Use the Timing pseudo-command to generate the output
|
||||
tcmd = timing.Timing(self, self.options)
|
||||
tparser = tcmd.get_parser('Timing')
|
||||
|
||||
# If anything other than prettytable is specified, force csv
|
||||
format = 'table'
|
||||
# Check the formatter used in the actual command
|
||||
if hasattr(cmd, 'formatter') \
|
||||
and cmd.formatter != cmd._formatter_plugins['table'].obj:
|
||||
format = 'csv'
|
||||
|
||||
sys.stdout.write('\n')
|
||||
targs = tparser.parse_args(['-f', format])
|
||||
tcmd.run(targs)
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
if argv is None:
|
||||
|
Reference in New Issue
Block a user