
The new client adheres to the standards of the other clients now. It prints out tables, uses ENVVAR's for auth, no longer stores pickled json in a login token, uses openstack common, and moves the cli operations into a v1 module for the future of trove when it has a v2 api. Please note for compatibility, the troveclient.compat module has the old cli. In order to deploy it, amend the setup.cfg to include the compat module. implements blueprint cli-compliance-upgrade Change-Id: Ie69d9dbc75ce90496da316244c97acca1877a327
430 lines
14 KiB
Python
430 lines
14 KiB
Python
# Copyright 2011 OpenStack Foundation
|
|
#
|
|
# 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 json
|
|
import optparse
|
|
import os
|
|
import pickle
|
|
import sys
|
|
|
|
from troveclient.compat import client
|
|
from troveclient.compat.xml import TroveXmlClient
|
|
from troveclient.compat import exceptions
|
|
|
|
from urllib import quote
|
|
|
|
|
|
def methods_of(obj):
|
|
"""Get all callable methods of an object that don't start with underscore
|
|
returns a list of tuples of the form (method_name, method)"""
|
|
result = {}
|
|
for i in dir(obj):
|
|
if callable(getattr(obj, i)) and not i.startswith('_'):
|
|
result[i] = getattr(obj, i)
|
|
return result
|
|
|
|
|
|
def check_for_exceptions(resp, body):
|
|
if resp.status in (400, 422, 500):
|
|
raise exceptions.from_response(resp, body)
|
|
|
|
|
|
def print_actions(cmd, actions):
|
|
"""Print help for the command with list of options and description"""
|
|
print ("Available actions for '%s' cmd:") % cmd
|
|
for k, v in actions.iteritems():
|
|
print "\t%-20s%s" % (k, v.__doc__)
|
|
sys.exit(2)
|
|
|
|
|
|
def print_commands(commands):
|
|
"""Print the list of available commands and description"""
|
|
|
|
print "Available commands"
|
|
for k, v in commands.iteritems():
|
|
print "\t%-20s%s" % (k, v.__doc__)
|
|
sys.exit(2)
|
|
|
|
|
|
def limit_url(url, limit=None, marker=None):
|
|
if not limit and not marker:
|
|
return url
|
|
query = []
|
|
if marker:
|
|
query.append("marker=%s" % marker)
|
|
if limit:
|
|
query.append("limit=%s" % limit)
|
|
query = '?' + '&'.join(query)
|
|
return url + query
|
|
|
|
|
|
def quote_user_host(user, host):
|
|
quoted = ''
|
|
if host:
|
|
quoted = quote("%s@%s" % (user, host))
|
|
else:
|
|
quoted = quote("%s" % user)
|
|
return quoted.replace('.', '%2e')
|
|
|
|
|
|
class CliOptions(object):
|
|
"""A token object containing the user, apikey and token which
|
|
is pickleable."""
|
|
|
|
APITOKEN = os.path.expanduser("~/.apitoken")
|
|
|
|
DEFAULT_VALUES = {
|
|
'username': None,
|
|
'apikey': None,
|
|
'tenant_id': None,
|
|
'auth_url': None,
|
|
'auth_type': 'keystone',
|
|
'service_type': 'database',
|
|
'service_name': '',
|
|
'region': 'RegionOne',
|
|
'service_url': None,
|
|
'insecure': False,
|
|
'verbose': False,
|
|
'debug': False,
|
|
'token': None,
|
|
'xml': None,
|
|
}
|
|
|
|
def __init__(self, **kwargs):
|
|
for key, value in self.DEFAULT_VALUES.items():
|
|
setattr(self, key, value)
|
|
|
|
@classmethod
|
|
def default(cls):
|
|
kwargs = copy.deepcopy(cls.DEFAULT_VALUES)
|
|
return cls(**kwargs)
|
|
|
|
@classmethod
|
|
def load_from_file(cls):
|
|
try:
|
|
with open(cls.APITOKEN, 'rb') as token:
|
|
return pickle.load(token)
|
|
except IOError:
|
|
pass # File probably not found.
|
|
except:
|
|
print("ERROR: Token file found at %s was corrupt." % cls.APITOKEN)
|
|
return cls.default()
|
|
|
|
@classmethod
|
|
def save_from_instance_fields(cls, instance):
|
|
apitoken = cls.default()
|
|
for key, default_value in cls.DEFAULT_VALUES.items():
|
|
final_value = getattr(instance, key, default_value)
|
|
setattr(apitoken, key, final_value)
|
|
with open(cls.APITOKEN, 'wb') as token:
|
|
pickle.dump(apitoken, token, protocol=2)
|
|
|
|
@classmethod
|
|
def create_optparser(cls, load_file):
|
|
oparser = optparse.OptionParser(
|
|
usage="%prog [options] <cmd> <action> <args>",
|
|
version='1.0', conflict_handler='resolve')
|
|
if load_file:
|
|
file = cls.load_from_file()
|
|
else:
|
|
file = cls.default()
|
|
|
|
def add_option(*args, **kwargs):
|
|
if len(args) == 1:
|
|
name = args[0]
|
|
else:
|
|
name = args[1]
|
|
kwargs['default'] = getattr(file, name, cls.DEFAULT_VALUES[name])
|
|
oparser.add_option("--%s" % name, **kwargs)
|
|
|
|
add_option("verbose", action="store_true",
|
|
help="Show equivalent curl statement along "
|
|
"with actual HTTP communication.")
|
|
add_option("debug", action="store_true",
|
|
help="Show the stack trace on errors.")
|
|
add_option("auth_url", help="Auth API endpoint URL with port and "
|
|
"version. Default: http://localhost:5000/v2.0")
|
|
add_option("username", help="Login username")
|
|
add_option("apikey", help="Api key")
|
|
add_option("tenant_id",
|
|
help="Tenant Id associated with the account")
|
|
add_option("auth_type",
|
|
help="Auth type to support different auth environments, \
|
|
Supported values are 'keystone', 'rax'.")
|
|
add_option("service_type",
|
|
help="Service type is a name associated for the catalog")
|
|
add_option("service_name",
|
|
help="Service name as provided in the service catalog")
|
|
add_option("service_url",
|
|
help="Service endpoint to use "
|
|
"if the catalog doesn't have one.")
|
|
add_option("region", help="Region the service is located in")
|
|
add_option("insecure", action="store_true",
|
|
help="Run in insecure mode for https endpoints.")
|
|
add_option("token", help="Token from a prior login.")
|
|
add_option("xml", action="store_true", help="Changes format to XML.")
|
|
|
|
oparser.add_option("--secure", action="store_false", dest="insecure",
|
|
help="Run in insecure mode for https endpoints.")
|
|
oparser.add_option("--json", action="store_false", dest="xml",
|
|
help="Changes format to JSON.")
|
|
oparser.add_option("--terse", action="store_false", dest="verbose",
|
|
help="Toggles verbose mode off.")
|
|
oparser.add_option("--hide-debug", action="store_false", dest="debug",
|
|
help="Toggles debug mode off.")
|
|
return oparser
|
|
|
|
|
|
class ArgumentRequired(Exception):
|
|
def __init__(self, param):
|
|
self.param = param
|
|
|
|
def __str__(self):
|
|
return 'Argument "--%s" required.' % self.param
|
|
|
|
|
|
class ArgumentsRequired(ArgumentRequired):
|
|
def __init__(self, *params):
|
|
self.params = params
|
|
|
|
def __str__(self):
|
|
returnstring = 'Specify at least one of these arguments: '
|
|
for param in self.params:
|
|
returnstring = returnstring + '"--%s" ' % param
|
|
return returnstring
|
|
|
|
|
|
class CommandsBase(object):
|
|
params = []
|
|
|
|
def __init__(self, parser):
|
|
self._parse_options(parser)
|
|
|
|
def _get_client(self):
|
|
"""Creates the all important client object."""
|
|
try:
|
|
if self.xml:
|
|
client_cls = TroveXmlClient
|
|
else:
|
|
client_cls = client.TroveHTTPClient
|
|
if self.verbose:
|
|
client.log_to_streamhandler(sys.stdout)
|
|
client.RDC_PP = True
|
|
return client.Dbaas(self.username, self.apikey, self.tenant_id,
|
|
auth_url=self.auth_url,
|
|
auth_strategy=self.auth_type,
|
|
service_type=self.service_type,
|
|
service_name=self.service_name,
|
|
region_name=self.region,
|
|
service_url=self.service_url,
|
|
insecure=self.insecure,
|
|
client_cls=client_cls)
|
|
except:
|
|
if self.debug:
|
|
raise
|
|
print sys.exc_info()[1]
|
|
|
|
def _safe_exec(self, func, *args, **kwargs):
|
|
if not self.debug:
|
|
try:
|
|
return func(*args, **kwargs)
|
|
except:
|
|
print(sys.exc_info()[1])
|
|
return None
|
|
else:
|
|
return func(*args, **kwargs)
|
|
|
|
@classmethod
|
|
def _prepare_parser(cls, parser):
|
|
for param in cls.params:
|
|
parser.add_option("--%s" % param)
|
|
|
|
def _parse_options(self, parser):
|
|
opts, args = parser.parse_args()
|
|
for param in opts.__dict__:
|
|
value = getattr(opts, param)
|
|
setattr(self, param, value)
|
|
|
|
def _require(self, *params):
|
|
for param in params:
|
|
if not hasattr(self, param):
|
|
raise ArgumentRequired(param)
|
|
if not getattr(self, param):
|
|
raise ArgumentRequired(param)
|
|
|
|
def _require_at_least_one_of(self, *params):
|
|
# One or more of params is required to be present.
|
|
argument_present = False
|
|
for param in params:
|
|
if hasattr(self, param):
|
|
if getattr(self, param):
|
|
argument_present = True
|
|
if argument_present is False:
|
|
raise ArgumentsRequired(*params)
|
|
|
|
def _make_list(self, *params):
|
|
# Convert the listed params to lists.
|
|
for param in params:
|
|
raw = getattr(self, param)
|
|
if isinstance(raw, list):
|
|
return
|
|
raw = [item.strip() for item in raw.split(',')]
|
|
setattr(self, param, raw)
|
|
|
|
def _pretty_print(self, func, *args, **kwargs):
|
|
if self.verbose:
|
|
self._safe_exec(func, *args, **kwargs)
|
|
return # Skip this, since the verbose stuff will show up anyway.
|
|
|
|
def wrapped_func():
|
|
result = func(*args, **kwargs)
|
|
if result:
|
|
print(json.dumps(result._info, sort_keys=True, indent=4))
|
|
else:
|
|
print("OK")
|
|
|
|
self._safe_exec(wrapped_func)
|
|
|
|
def _dumps(self, item):
|
|
return json.dumps(item, sort_keys=True, indent=4)
|
|
|
|
def _pretty_list(self, func, *args, **kwargs):
|
|
result = self._safe_exec(func, *args, **kwargs)
|
|
if self.verbose:
|
|
return
|
|
if result and len(result) > 0:
|
|
for item in result:
|
|
print(self._dumps(item._info))
|
|
else:
|
|
print("OK")
|
|
|
|
def _pretty_paged(self, func, *args, **kwargs):
|
|
try:
|
|
limit = self.limit
|
|
if limit:
|
|
limit = int(limit, 10)
|
|
result = func(*args, limit=limit, marker=self.marker, **kwargs)
|
|
if self.verbose:
|
|
return # Verbose already shows the output, so skip this.
|
|
if result and len(result) > 0:
|
|
for item in result:
|
|
print self._dumps(item._info)
|
|
if result.links:
|
|
print("Links:")
|
|
for link in result.links:
|
|
print self._dumps((link))
|
|
else:
|
|
print("OK")
|
|
except:
|
|
if self.debug:
|
|
raise
|
|
print sys.exc_info()[1]
|
|
|
|
|
|
class Auth(CommandsBase):
|
|
"""Authenticate with your username and api key"""
|
|
params = [
|
|
'apikey',
|
|
'auth_strategy',
|
|
'auth_type',
|
|
'auth_url',
|
|
'options',
|
|
'region',
|
|
'service_name',
|
|
'service_type',
|
|
'service_url',
|
|
'tenant_id',
|
|
'username',
|
|
]
|
|
|
|
def __init__(self, parser):
|
|
super(Auth, self).__init__(parser)
|
|
self.dbaas = None
|
|
|
|
def login(self):
|
|
"""Login to retrieve an auth token to use for other api calls"""
|
|
self._require('username', 'apikey', 'tenant_id', 'auth_url')
|
|
try:
|
|
self.dbaas = self._get_client()
|
|
self.dbaas.authenticate()
|
|
self.token = self.dbaas.client.auth_token
|
|
self.service_url = self.dbaas.client.service_url
|
|
CliOptions.save_from_instance_fields(self)
|
|
print("Token aquired! Saving to %s..." % CliOptions.APITOKEN)
|
|
print(" service_url = %s" % self.service_url)
|
|
print(" token = %s" % self.token)
|
|
except:
|
|
if self.debug:
|
|
raise
|
|
print sys.exc_info()[1]
|
|
|
|
|
|
class AuthedCommandsBase(CommandsBase):
|
|
"""Commands that work only with an authicated client."""
|
|
|
|
def __init__(self, parser):
|
|
"""Makes sure a token is available somehow and logs in."""
|
|
super(AuthedCommandsBase, self).__init__(parser)
|
|
try:
|
|
self._require('token')
|
|
except ArgumentRequired:
|
|
if self.debug:
|
|
raise
|
|
print('No token argument supplied. Use the "auth login" command '
|
|
'to log in and get a token.\n')
|
|
sys.exit(1)
|
|
try:
|
|
self._require('service_url')
|
|
except ArgumentRequired:
|
|
if self.debug:
|
|
raise
|
|
print('No service_url given.\n')
|
|
sys.exit(1)
|
|
self.dbaas = self._get_client()
|
|
# Actually set the token to avoid a re-auth.
|
|
self.dbaas.client.auth_token = self.token
|
|
self.dbaas.client.authenticate_with_token(self.token, self.service_url)
|
|
|
|
|
|
class Paginated(object):
|
|
""" Pretends to be a list if you iterate over it, but also keeps a
|
|
next property you can use to get the next page of data. """
|
|
|
|
def __init__(self, items=[], next_marker=None, links=[]):
|
|
self.items = items
|
|
self.next = next_marker
|
|
self.links = links
|
|
|
|
def __len__(self):
|
|
return len(self.items)
|
|
|
|
def __iter__(self):
|
|
return self.items.__iter__()
|
|
|
|
def __getitem__(self, key):
|
|
return self.items[key]
|
|
|
|
def __setitem__(self, key, value):
|
|
self.items[key] = value
|
|
|
|
def __delitem__(self, key):
|
|
del self.items[key]
|
|
|
|
def __reversed__(self):
|
|
return reversed(self.items)
|
|
|
|
def __contains__(self, needle):
|
|
return needle in self.items
|