639 lines
25 KiB
Python
639 lines
25 KiB
Python
# Copyright 2011 OpenStack Foundation
|
|
# Copyright 2013 Rackspace Hosting
|
|
# All Rights Reserved.
|
|
#
|
|
# 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 OpenStack Trove API.
|
|
"""
|
|
|
|
from __future__ import print_function
|
|
|
|
import argparse
|
|
import glob
|
|
import imp
|
|
import itertools
|
|
import os
|
|
import pkgutil
|
|
import sys
|
|
import logging
|
|
|
|
import pkg_resources
|
|
import six
|
|
|
|
import troveclient
|
|
import troveclient.extension
|
|
from troveclient import client
|
|
from troveclient.openstack.common import strutils
|
|
from troveclient.openstack.common.apiclient import exceptions as exc
|
|
from troveclient.openstack.common import gettextutils as gtu
|
|
from troveclient import utils
|
|
from troveclient.v1 import shell as shell_v1
|
|
|
|
DEFAULT_OS_DATABASE_API_VERSION = "1.0"
|
|
DEFAULT_TROVE_ENDPOINT_TYPE = 'publicURL'
|
|
DEFAULT_TROVE_SERVICE_TYPE = 'database'
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class TroveClientArgumentParser(argparse.ArgumentParser):
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super(TroveClientArgumentParser, self).__init__(*args, **kwargs)
|
|
|
|
def error(self, message):
|
|
"""error(message: string)
|
|
|
|
Prints a usage message incorporating the message to stderr and
|
|
exits.
|
|
"""
|
|
self.print_usage(sys.stderr)
|
|
#FIXME(lzyeval): if changes occur in argparse.ArgParser._check_value
|
|
choose_from = ' (choose from'
|
|
progparts = self.prog.partition(' ')
|
|
self.exit(2, "error: %(errmsg)s\nTry '%(mainp)s help %(subp)s'"
|
|
" for more information.\n" %
|
|
{'errmsg': message.split(choose_from)[0],
|
|
'mainp': progparts[0],
|
|
'subp': progparts[2]})
|
|
|
|
|
|
class OpenStackTroveShell(object):
|
|
|
|
def get_base_parser(self):
|
|
parser = TroveClientArgumentParser(
|
|
prog='trove',
|
|
description=__doc__.strip(),
|
|
epilog='See "trove help COMMAND" '
|
|
'for help on a specific command.',
|
|
add_help=False,
|
|
formatter_class=OpenStackHelpFormatter,
|
|
)
|
|
|
|
# Global arguments
|
|
parser.add_argument('-h', '--help',
|
|
action='store_true',
|
|
help=argparse.SUPPRESS)
|
|
|
|
parser.add_argument('--version',
|
|
action='version',
|
|
version=troveclient.__version__)
|
|
|
|
parser.add_argument('--debug',
|
|
action='store_true',
|
|
default=utils.env('TROVECLIENT_DEBUG',
|
|
default=False),
|
|
help="Print debugging output.")
|
|
|
|
parser.add_argument('--os-username',
|
|
metavar='<auth-user-name>',
|
|
default=utils.env('OS_USERNAME',
|
|
'TROVE_USERNAME'),
|
|
help='Defaults to env[OS_USERNAME].')
|
|
parser.add_argument('--os_username',
|
|
help=argparse.SUPPRESS)
|
|
|
|
parser.add_argument('--os-password',
|
|
metavar='<auth-password>',
|
|
default=utils.env('OS_PASSWORD',
|
|
'TROVE_PASSWORD'),
|
|
help='Defaults to env[OS_PASSWORD].')
|
|
parser.add_argument('--os_password',
|
|
help=argparse.SUPPRESS)
|
|
|
|
parser.add_argument('--os-tenant-name',
|
|
metavar='<auth-tenant-name>',
|
|
default=utils.env('OS_TENANT_NAME',
|
|
'TROVE_PROJECT_ID'),
|
|
help='Defaults to env[OS_TENANT_NAME].')
|
|
parser.add_argument('--os_tenant_name',
|
|
help=argparse.SUPPRESS)
|
|
|
|
parser.add_argument('--os-tenant-id',
|
|
metavar='<auth-tenant-id>',
|
|
default=utils.env('OS_TENANT_ID',
|
|
'TROVE_TENANT_ID'),
|
|
help='Defaults to env[OS_TENANT_ID].')
|
|
parser.add_argument('--os_tenant_id',
|
|
help=argparse.SUPPRESS)
|
|
|
|
parser.add_argument('--os-auth-url',
|
|
metavar='<auth-url>',
|
|
default=utils.env('OS_AUTH_URL',
|
|
'TROVE_URL'),
|
|
help='Defaults to env[OS_AUTH_URL].')
|
|
parser.add_argument('--os_auth_url',
|
|
help=argparse.SUPPRESS)
|
|
|
|
parser.add_argument('--os-region-name',
|
|
metavar='<region-name>',
|
|
default=utils.env('OS_REGION_NAME',
|
|
'TROVE_REGION_NAME'),
|
|
help='Defaults to env[OS_REGION_NAME].')
|
|
parser.add_argument('--os_region_name',
|
|
help=argparse.SUPPRESS)
|
|
|
|
parser.add_argument('--service-type',
|
|
metavar='<service-type>',
|
|
default=utils.env('OS_SERVICE_TYPE',
|
|
'TROVE_SERVICE_TYPE'),
|
|
help='Defaults to database for most actions.')
|
|
parser.add_argument('--service_type',
|
|
help=argparse.SUPPRESS)
|
|
|
|
parser.add_argument('--service-name',
|
|
metavar='<service-name>',
|
|
default=utils.env('TROVE_SERVICE_NAME'),
|
|
help='Defaults to env[TROVE_SERVICE_NAME].')
|
|
parser.add_argument('--service_name',
|
|
help=argparse.SUPPRESS)
|
|
|
|
parser.add_argument('--bypass-url',
|
|
metavar='<bypass-url>',
|
|
default=utils.env('TROVE_BYPASS_URL'),
|
|
help='Defaults to env[TROVE_BYPASS_URL].')
|
|
parser.add_argument('--bypass_url',
|
|
help=argparse.SUPPRESS)
|
|
|
|
parser.add_argument('--database-service-name',
|
|
metavar='<database-service-name>',
|
|
default=utils.env('TROVE_DATABASE_SERVICE_NAME'),
|
|
help='Defaults to env'
|
|
'[TROVE_DATABASE_SERVICE_NAME].')
|
|
parser.add_argument('--database_service_name',
|
|
help=argparse.SUPPRESS)
|
|
|
|
parser.add_argument('--endpoint-type',
|
|
metavar='<endpoint-type>',
|
|
default=utils.env(
|
|
'TROVE_ENDPOINT_TYPE',
|
|
default=DEFAULT_TROVE_ENDPOINT_TYPE),
|
|
help='Defaults to env[TROVE_ENDPOINT_TYPE] or '
|
|
+ DEFAULT_TROVE_ENDPOINT_TYPE + '.')
|
|
parser.add_argument('--endpoint_type',
|
|
help=argparse.SUPPRESS)
|
|
|
|
parser.add_argument('--os-database-api-version',
|
|
metavar='<database-api-ver>',
|
|
default=utils.env(
|
|
'OS_DATABASE_API_VERSION',
|
|
default=DEFAULT_OS_DATABASE_API_VERSION),
|
|
help='Accepts 1, defaults '
|
|
'to env[OS_DATABASE_API_VERSION].')
|
|
parser.add_argument('--os_database_api_version',
|
|
help=argparse.SUPPRESS)
|
|
|
|
parser.add_argument('--os-cacert',
|
|
metavar='<ca-certificate>',
|
|
default=utils.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',
|
|
default=utils.env('TROVECLIENT_INSECURE',
|
|
default=False),
|
|
action='store_true',
|
|
help=argparse.SUPPRESS)
|
|
|
|
parser.add_argument('--retries',
|
|
metavar='<retries>',
|
|
type=int,
|
|
default=0,
|
|
help='Number of retries.')
|
|
|
|
parser.add_argument('--json', '--os-json-output',
|
|
dest='json',
|
|
action='store_true',
|
|
default=utils.env('OS_JSON_OUTPUT',
|
|
default=False),
|
|
help='Output json instead of prettyprint. '
|
|
'Defaults to env[OS_JSON_OUTPUT].')
|
|
|
|
return parser
|
|
|
|
def get_subcommand_parser(self, version):
|
|
parser = self.get_base_parser()
|
|
|
|
self.subcommands = {}
|
|
subparsers = parser.add_subparsers(metavar='<subcommand>')
|
|
|
|
try:
|
|
actions_module = {
|
|
'1.0': shell_v1,
|
|
}[version]
|
|
except KeyError:
|
|
actions_module = shell_v1
|
|
|
|
self._find_actions(subparsers, actions_module)
|
|
self._find_actions(subparsers, self)
|
|
|
|
for extension in self.extensions:
|
|
self._find_actions(subparsers, extension.module)
|
|
|
|
self._add_bash_completion_subparser(subparsers)
|
|
|
|
return parser
|
|
|
|
def _discover_extensions(self, version):
|
|
extensions = []
|
|
for name, module in itertools.chain(
|
|
self._discover_via_python_path(),
|
|
self._discover_via_contrib_path(version),
|
|
self._discover_via_entry_points()):
|
|
|
|
extension = troveclient.extension.Extension(name, module)
|
|
extensions.append(extension)
|
|
|
|
return extensions
|
|
|
|
def _discover_via_python_path(self):
|
|
for (module_loader, name, _ispkg) in pkgutil.iter_modules():
|
|
if name.endswith('_python_troveclient_ext'):
|
|
if not hasattr(module_loader, 'load_module'):
|
|
# Python 2.6 compat: actually get an ImpImporter obj
|
|
module_loader = module_loader.find_module(name)
|
|
|
|
module = module_loader.load_module(name)
|
|
if hasattr(module, 'extension_name'):
|
|
name = module.extension_name
|
|
|
|
yield name, module
|
|
|
|
def _discover_via_contrib_path(self, version):
|
|
module_path = os.path.dirname(os.path.abspath(__file__))
|
|
version_str = "v%s" % version.replace('.', '_')
|
|
version_pkg = 'v1' if version_str == 'v1_0' else version_str
|
|
ext_path = os.path.join(module_path, version_pkg, 'contrib')
|
|
ext_glob = os.path.join(ext_path, "*.py")
|
|
|
|
for ext_path in glob.iglob(ext_glob):
|
|
name = os.path.basename(ext_path)[:-3]
|
|
|
|
if name == "__init__":
|
|
continue
|
|
|
|
module = imp.load_source(name, ext_path)
|
|
yield name, module
|
|
|
|
def _discover_via_entry_points(self):
|
|
for ep in pkg_resources.iter_entry_points('troveclient.extension'):
|
|
name = ep.name
|
|
module = ep.load()
|
|
|
|
yield name, module
|
|
|
|
def _add_bash_completion_subparser(self, subparsers):
|
|
subparser = subparsers.add_parser(
|
|
'bash_completion',
|
|
add_help=False,
|
|
formatter_class=OpenStackHelpFormatter)
|
|
|
|
self.subcommands['bash_completion'] = subparser
|
|
subparser.set_defaults(func=self.do_bash_completion)
|
|
|
|
def _find_actions(self, subparsers, actions_module):
|
|
for attr in (a for a in dir(actions_module) if a.startswith('do_')):
|
|
# I prefer to be hyphen-separated instead of underscores.
|
|
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=OpenStackHelpFormatter)
|
|
|
|
subparser.add_argument('-h', '--help',
|
|
action='help',
|
|
help=argparse.SUPPRESS,)
|
|
|
|
self.subcommands[command] = subparser
|
|
for (args, kwargs) in arguments:
|
|
subparser.add_argument(*args, **kwargs)
|
|
subparser.set_defaults(func=callback)
|
|
|
|
def setup_debugging(self, debug):
|
|
if not debug:
|
|
return
|
|
|
|
streamhandler = logging.StreamHandler()
|
|
streamformat = "%(levelname)s (%(module)s:%(lineno)d) %(message)s"
|
|
streamhandler.setFormatter(logging.Formatter(streamformat))
|
|
logger.setLevel(logging.DEBUG)
|
|
logger.addHandler(streamhandler)
|
|
|
|
def main(self, argv):
|
|
# Parse args once to find version and debug settings
|
|
parser = self.get_base_parser()
|
|
(options, args) = parser.parse_known_args(argv)
|
|
self.setup_debugging(options.debug)
|
|
|
|
# build available subcommands based on version
|
|
self.extensions = self._discover_extensions(
|
|
options.os_database_api_version)
|
|
self._run_extension_hooks('__pre_parse_args__')
|
|
|
|
subcommand_parser = self.get_subcommand_parser(
|
|
options.os_database_api_version)
|
|
self.parser = subcommand_parser
|
|
|
|
if options.help or not argv:
|
|
subcommand_parser.print_help()
|
|
return 0
|
|
|
|
args = subcommand_parser.parse_args(argv)
|
|
self._run_extension_hooks('__post_parse_args__', args)
|
|
|
|
# Short-circuit and deal with help 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
|
|
|
|
(os_username, os_password, os_tenant_name, os_auth_url,
|
|
os_region_name, os_tenant_id, endpoint_type, insecure,
|
|
service_type, service_name, database_service_name,
|
|
cacert, bypass_url) = (
|
|
args.os_username, args.os_password,
|
|
args.os_tenant_name, args.os_auth_url,
|
|
args.os_region_name, args.os_tenant_id,
|
|
args.endpoint_type, args.insecure,
|
|
args.service_type, args.service_name,
|
|
args.database_service_name,
|
|
args.os_cacert, args.bypass_url)
|
|
|
|
if not endpoint_type:
|
|
endpoint_type = DEFAULT_TROVE_ENDPOINT_TYPE
|
|
|
|
if not service_type:
|
|
service_type = DEFAULT_TROVE_SERVICE_TYPE
|
|
service_type = utils.get_service_type(args.func) or service_type
|
|
|
|
#FIXME(usrleon): Here should be restrict for project id same as
|
|
# for os_username or os_password but for compatibility it is not.
|
|
|
|
if not utils.isunauthenticated(args.func):
|
|
if not os_username:
|
|
raise exc.CommandError(
|
|
"You must provide a username "
|
|
"via either --os-username or env[OS_USERNAME]")
|
|
|
|
if not os_password:
|
|
raise exc.CommandError("You must provide a password "
|
|
"via either --os-password or via "
|
|
"env[OS_PASSWORD]")
|
|
|
|
if not (os_tenant_name or os_tenant_id):
|
|
raise exc.CommandError("You must provide a tenant_id "
|
|
"via either --os-tenant-id or "
|
|
"env[OS_TENANT_ID]")
|
|
|
|
if not os_auth_url:
|
|
raise exc.CommandError(
|
|
"You must provide an auth url "
|
|
"via either --os-auth-url or env[OS_AUTH_URL]")
|
|
|
|
if not (os_tenant_name or os_tenant_id):
|
|
raise exc.CommandError(
|
|
"You must provide a tenant_id "
|
|
"via either --os-tenant-id or env[OS_TENANT_ID]")
|
|
|
|
if not os_auth_url:
|
|
raise exc.CommandError(
|
|
"You must provide an auth url "
|
|
"via either --os-auth-url or env[OS_AUTH_URL]")
|
|
|
|
self.cs = client.Client(options.os_database_api_version, os_username,
|
|
os_password, os_tenant_name, os_auth_url,
|
|
insecure, region_name=os_region_name,
|
|
tenant_id=os_tenant_id,
|
|
endpoint_type=endpoint_type,
|
|
extensions=self.extensions,
|
|
service_type=service_type,
|
|
service_name=service_name,
|
|
database_service_name=database_service_name,
|
|
retries=options.retries,
|
|
http_log_debug=args.debug,
|
|
cacert=cacert,
|
|
bypass_url=bypass_url)
|
|
|
|
try:
|
|
if not utils.isunauthenticated(args.func):
|
|
self.cs.authenticate()
|
|
except exc.Unauthorized:
|
|
raise exc.CommandError("Invalid OpenStack Trove credentials.")
|
|
except exc.AuthorizationFailure:
|
|
raise exc.CommandError("Unable to authorize user")
|
|
|
|
endpoint_api_version = self.cs.get_database_api_version_from_endpoint()
|
|
if endpoint_api_version != options.os_database_api_version:
|
|
msg = (("Database API version is set to %s "
|
|
"but you are accessing a %s endpoint. "
|
|
"Change its value via either --os-database-api-version "
|
|
"or env[OS_DATABASE_API_VERSION]")
|
|
% (options.os_database_api_version, endpoint_api_version))
|
|
#raise exc.InvalidAPIVersion(msg)
|
|
raise exc.UnsupportedVersion(msg)
|
|
|
|
# Override printing to json output
|
|
if args.json:
|
|
utils.json_output = True
|
|
else:
|
|
utils.json_output = False
|
|
|
|
args.func(self.cs, args)
|
|
|
|
def _run_extension_hooks(self, hook_type, *args, **kwargs):
|
|
"""Run hooks for all registered extensions."""
|
|
for extension in self.extensions:
|
|
extension.run_hooks(hook_type, *args, **kwargs)
|
|
|
|
def do_bash_completion(self, args):
|
|
"""Prints arguments for bash_completion.
|
|
|
|
Prints all of the commands and options to stdout so that the
|
|
trove.bash_completion script doesn't have to hard code them.
|
|
"""
|
|
commands = set()
|
|
options = set()
|
|
for sc_str, sc in list(self.subcommands.items()):
|
|
commands.add(sc_str)
|
|
for option in list(sc._optionals._option_string_actions.keys()):
|
|
options.add(option)
|
|
|
|
commands.remove('bash-completion')
|
|
commands.remove('bash_completion')
|
|
print(' '.join(commands | options))
|
|
|
|
@utils.arg('command', metavar='<subcommand>', nargs='?',
|
|
help='Display help for <subcommand>.')
|
|
def do_help(self, args):
|
|
"""
|
|
Displays help about this program or one of its subcommands.
|
|
"""
|
|
if args.command:
|
|
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()
|
|
|
|
|
|
# I'm picky about my shell help.
|
|
class OpenStackHelpFormatter(argparse.HelpFormatter):
|
|
def start_section(self, heading):
|
|
# Title-case the headings
|
|
heading = '%s%s' % (heading[0].upper(), heading[1:])
|
|
super(OpenStackHelpFormatter, self).start_section(heading)
|
|
|
|
def _format_usage(self, usage, actions, groups, prefix):
|
|
"""
|
|
Print positionals before optionals in the usage string to help
|
|
users avoid argparse nargs='*' problems.
|
|
|
|
ex: 'trove create --databases <db_name> <name> <flavor_id>'
|
|
fails with 'error: too few arguments', but this succeeds:
|
|
'trove create <name> <flavor_id> --databases <db_name>'
|
|
"""
|
|
if prefix is None:
|
|
prefix = gtu._('usage: ')
|
|
|
|
# if usage is specified, use that
|
|
if usage is not None:
|
|
usage = usage % dict(prog=self._prog)
|
|
|
|
# if no optionals or positionals are available, usage is just prog
|
|
elif usage is None and not actions:
|
|
usage = '%(prog)s' % dict(prog=self._prog)
|
|
|
|
# if optionals and positionals are available, calculate usage
|
|
elif usage is None:
|
|
prog = '%(prog)s' % dict(prog=self._prog)
|
|
|
|
# split optionals from positionals
|
|
optionals = []
|
|
positionals = []
|
|
for action in actions:
|
|
if action.option_strings:
|
|
optionals.append(action)
|
|
else:
|
|
positionals.append(action)
|
|
|
|
# build full usage string
|
|
format = self._format_actions_usage
|
|
action_usage = format(optionals + positionals, groups)
|
|
usage = ' '.join([s for s in [prog, action_usage] if s])
|
|
|
|
# wrap the usage parts if it's too long
|
|
text_width = self._width - self._current_indent
|
|
if len(prefix) + len(usage) > text_width:
|
|
|
|
# break usage into wrappable parts
|
|
part_regexp = r'\(.*?\)+|\[.*?\]+|\S+'
|
|
opt_usage = format(optionals, groups)
|
|
pos_usage = format(positionals, groups)
|
|
opt_parts = argparse._re.findall(part_regexp, opt_usage)
|
|
pos_parts = argparse._re.findall(part_regexp, pos_usage)
|
|
assert ' '.join(opt_parts) == opt_usage
|
|
assert ' '.join(pos_parts) == pos_usage
|
|
|
|
# helper for wrapping lines
|
|
def get_lines(parts, indent, prefix=None):
|
|
lines = []
|
|
line = []
|
|
if prefix is not None:
|
|
line_len = len(prefix) - 1
|
|
else:
|
|
line_len = len(indent) - 1
|
|
for part in parts:
|
|
if line_len + 1 + len(part) > text_width:
|
|
lines.append(indent + ' '.join(line))
|
|
line = []
|
|
line_len = len(indent) - 1
|
|
line.append(part)
|
|
line_len += len(part) + 1
|
|
if line:
|
|
lines.append(indent + ' '.join(line))
|
|
if prefix is not None:
|
|
lines[0] = lines[0][len(indent):]
|
|
return lines
|
|
|
|
# if prog is short, follow it with optionals or positionals
|
|
if len(prefix) + len(prog) <= 0.75 * text_width:
|
|
indent = ' ' * (len(prefix) + len(prog) + 1)
|
|
if pos_parts:
|
|
if prog == 'trove':
|
|
# "trove help" called without any subcommand
|
|
lines = get_lines([prog] + opt_parts, indent,
|
|
prefix)
|
|
lines.extend(get_lines(pos_parts, indent))
|
|
else:
|
|
lines = get_lines([prog] + pos_parts, indent,
|
|
prefix)
|
|
lines.extend(get_lines(opt_parts, indent))
|
|
elif opt_parts:
|
|
lines = get_lines([prog] + opt_parts, indent, prefix)
|
|
else:
|
|
lines = [prog]
|
|
|
|
# if prog is long, put it on its own line
|
|
else:
|
|
indent = ' ' * len(prefix)
|
|
parts = pos_parts + opt_parts
|
|
lines = get_lines(parts, indent)
|
|
if len(lines) > 1:
|
|
lines = []
|
|
lines.extend(get_lines(pos_parts, indent))
|
|
lines.extend(get_lines(opt_parts, indent))
|
|
lines = [prog] + lines
|
|
|
|
# join lines into usage
|
|
usage = '\n'.join(lines)
|
|
|
|
# prefix with 'usage:'
|
|
return '%s%s\n\n' % (prefix, usage)
|
|
|
|
|
|
def main():
|
|
try:
|
|
if sys.version_info >= (3, 0):
|
|
OpenStackTroveShell().main(sys.argv[1:])
|
|
else:
|
|
OpenStackTroveShell().main(map(strutils.safe_decode,
|
|
sys.argv[1:]))
|
|
except KeyboardInterrupt:
|
|
print("... terminating trove client", file=sys.stderr)
|
|
sys.exit(130)
|
|
except Exception as e:
|
|
logger.debug(e, exc_info=1)
|
|
message = e.message
|
|
if not isinstance(message, six.string_types):
|
|
message = str(message)
|
|
print("ERROR: %s" % strutils.safe_encode(message), file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|