Added a ton of CLI options, plus fixed a CI bug.

* Renamed the auth_type "basic" to the more apt "auth1.1".
* Made it possible to pass an "token" and "service_url" argument alone to
  the client. It wouldn't work with just this before.
* The client now saves all arguments you give it to the pickled file,
  including the auth strategy, and preserves the token and service_url
  (which it didn't before) which makes exotic auth types such as "fake"
  easier to work with.
* Not raising an error for a lack of an auth_url until auth occurs
  (which is usually right after creation of the client anyway for most
   auth types).
* Moved oparser code into CliOption class. This is where the options live
  plus is the name of that pickled file that gets stored on login.
* Added a "debug" option which avoids swallowing stack traces if
  something goes wrong with the CLI. Should make client work much easier.
* Added a "verbose" option which changes the output to instead show the
  simulated CURL statement plus the request and response headers and
  bodies, which is useful because I...
* Added an "xml" option which does all the communication in XML.
* Fixed a bug which was affecting the CI tests where the client would fail
  if the response body could not be parsed.
* Added all of Ed's work to update the mgmt CLI module with his newer
  named parameters.
This commit is contained in:
Tim Simpson
2012-08-09 11:04:28 -05:00
parent e0b9db2aee
commit 88f9530151
7 changed files with 410 additions and 239 deletions

View File

@@ -24,6 +24,8 @@ def get_authenticator_cls(cls_or_name):
return KeyStoneV2Authenticator return KeyStoneV2Authenticator
elif cls_or_name == "rax": elif cls_or_name == "rax":
return RaxAuthenticator return RaxAuthenticator
elif cls_or_name == "auth1.1":
return Auth1_1
elif cls_or_name == "fake": elif cls_or_name == "fake":
return FakeAuth return FakeAuth
@@ -40,6 +42,8 @@ class Authenticator(object):
""" """
URL_REQUIRED=True
def __init__(self, client, type, url, username, password, tenant, def __init__(self, client, type, url, username, password, tenant,
region=None, service_type=None, service_name=None, region=None, service_type=None, service_name=None,
service_url=None): service_url=None):
@@ -54,7 +58,7 @@ class Authenticator(object):
self.service_name = service_name self.service_name = service_name
self.service_url = service_url self.service_url = service_url
def _authenticate(self, url, body): def _authenticate(self, url, body, root_key='access'):
"""Authenticate and extract the service catalog.""" """Authenticate and extract the service catalog."""
# Make sure we follow redirects when trying to reach Keystone # Make sure we follow redirects when trying to reach Keystone
tmp_follow_all_redirects = self.client.follow_all_redirects tmp_follow_all_redirects = self.client.follow_all_redirects
@@ -70,7 +74,8 @@ class Authenticator(object):
return ServiceCatalog(body, region=self.region, return ServiceCatalog(body, region=self.region,
service_type=self.service_type, service_type=self.service_type,
service_name=self.service_name, service_name=self.service_name,
service_url=self.service_url) service_url=self.service_url,
root_key=root_key)
except exceptions.AmbiguousEndpoints: except exceptions.AmbiguousEndpoints:
print "Found more than one valid endpoint. Use a more "\ print "Found more than one valid endpoint. Use a more "\
"restrictive filter" "restrictive filter"
@@ -93,6 +98,8 @@ class Authenticator(object):
class KeyStoneV2Authenticator(Authenticator): class KeyStoneV2Authenticator(Authenticator):
def authenticate(self): def authenticate(self):
if self.url is None:
raise exceptions.AuthUrlNotGiven()
return self._v2_auth(self.url) return self._v2_auth(self.url)
def _v2_auth(self, url): def _v2_auth(self, url):
@@ -110,9 +117,40 @@ class KeyStoneV2Authenticator(Authenticator):
return self._authenticate(url, body) return self._authenticate(url, body)
class Auth1_1(Authenticator):
def authenticate(self):
"""Authenticate against a v2.0 auth service."""
if self.url is None:
raise exceptions.AuthUrlNotGiven()
auth_url = self.url
body = {"credentials": {"username": self.username,
"key": self.password}}
return self._authenticate(auth_url, body, root_key='auth')
try:
print(resp_body)
self.auth_token = resp_body['auth']['token']['id']
except KeyError:
raise nova_exceptions.AuthorizationFailure()
catalog = resp_body['auth']['serviceCatalog']
if 'cloudDatabases' not in catalog:
raise nova_exceptions.EndpointNotFound()
endpoints = catalog['cloudDatabases']
for endpoint in endpoints:
if self.region_name is None or \
endpoint['region'] == self.region_name:
self.management_url = endpoint['publicURL']
return
raise nova_exceptions.EndpointNotFound()
class RaxAuthenticator(Authenticator): class RaxAuthenticator(Authenticator):
def authenticate(self): def authenticate(self):
if self.url is None:
raise exceptions.AuthUrlNotGiven()
return self._rax_auth(self.url) return self._rax_auth(self.url)
def _rax_auth(self, url): def _rax_auth(self, url):
@@ -155,7 +193,7 @@ class ServiceCatalog(object):
""" """
def __init__(self, resource_dict, region=None, service_type=None, def __init__(self, resource_dict, region=None, service_type=None,
service_name=None, service_url=None): service_name=None, service_url=None, root_key='access'):
self.catalog = resource_dict self.catalog = resource_dict
self.region = region self.region = region
self.service_type = service_type self.service_type = service_type
@@ -163,6 +201,7 @@ class ServiceCatalog(object):
self.service_url = service_url self.service_url = service_url
self.management_url = None self.management_url = None
self.public_url = None self.public_url = None
self.root_key = root_key
self._load() self._load()
def _load(self): def _load(self):
@@ -178,7 +217,7 @@ class ServiceCatalog(object):
self.management_url = self.service_url self.management_url = self.service_url
def get_token(self): def get_token(self):
return self.catalog['access']['token']['id'] return self.catalog[self.root_key]['token']['id']
def get_management_url(self): def get_management_url(self):
return self.management_url return self.management_url
@@ -202,11 +241,11 @@ class ServiceCatalog(object):
raise exceptions.EndpointNotFound() raise exceptions.EndpointNotFound()
# We don't always get a service catalog back ... # We don't always get a service catalog back ...
if not 'serviceCatalog' in self.catalog['access']: if not 'serviceCatalog' in self.catalog[self.root_key]:
raise exceptions.EndpointNotFound() raise exceptions.EndpointNotFound()
# Full catalog ... # Full catalog ...
catalog = self.catalog['access']['serviceCatalog'] catalog = self.catalog[self.root_key]['serviceCatalog']
for service in catalog: for service in catalog:
if service.get("type") != self.service_type: if service.get("type") != self.service_type:

View File

@@ -18,6 +18,7 @@
Reddwarf Command line tool Reddwarf Command line tool
""" """
#TODO(tim.simpson): optparse is deprecated. Replace with argparse.
import optparse import optparse
import os import os
import sys import sys
@@ -36,7 +37,7 @@ if os.path.exists(os.path.join(possible_topdir, 'reddwarfclient',
from reddwarfclient import common from reddwarfclient import common
class InstanceCommands(common.CommandsBase): class InstanceCommands(common.AuthedCommandsBase):
"""Commands to perform various instances operations and actions""" """Commands to perform various instances operations and actions"""
params = [ params = [
@@ -93,17 +94,17 @@ class InstanceCommands(common.CommandsBase):
self._pretty_print(self.dbaas.instances.restart, self.id) self._pretty_print(self.dbaas.instances.restart, self.id)
class FlavorsCommands(common.CommandsBase): class FlavorsCommands(common.AuthedCommandsBase):
"""Commands for listing Flavors""" """Commands for listing Flavors"""
params = [] params = []
def list(self): def list(self):
"""List the available flavors""" """List the available flavors"""
self._pretty_print(self.dbaas.flavors.list) self._pretty_list(self.dbaas.flavors.list)
class DatabaseCommands(common.CommandsBase): class DatabaseCommands(common.AuthedCommandsBase):
"""Database CRUD operations on an instance""" """Database CRUD operations on an instance"""
params = [ params = [
@@ -130,7 +131,7 @@ class DatabaseCommands(common.CommandsBase):
self._pretty_paged(self.dbaas.databases.list, self.id) self._pretty_paged(self.dbaas.databases.list, self.id)
class UserCommands(common.CommandsBase): class UserCommands(common.AuthedCommandsBase):
"""User CRUD operations on an instance""" """User CRUD operations on an instance"""
params = [ params = [
'id', 'id',
@@ -159,7 +160,7 @@ class UserCommands(common.CommandsBase):
self._pretty_paged(self.dbaas.users.list, self.id) self._pretty_paged(self.dbaas.users.list, self.id)
class RootCommands(common.CommandsBase): class RootCommands(common.AuthedCommandsBase):
"""Root user related operations on an instance""" """Root user related operations on an instance"""
params = [ params = [
@@ -181,7 +182,7 @@ class RootCommands(common.CommandsBase):
self._pretty_print(self.dbaas.root.is_root_enabled, self.id) self._pretty_print(self.dbaas.root.is_root_enabled, self.id)
class VersionCommands(common.CommandsBase): class VersionCommands(common.AuthedCommandsBase):
"""List available versions""" """List available versions"""
params = [ params = [
@@ -194,31 +195,6 @@ class VersionCommands(common.CommandsBase):
self._pretty_print(self.dbaas.versions.index, self.url) self._pretty_print(self.dbaas.versions.index, self.url)
def config_options(oparser):
oparser.add_option("--auth_url", default="http://localhost:5000/v2.0",
help="Auth API endpoint URL with port and version. \
Default: http://localhost:5000/v2.0")
oparser.add_option("--username", help="Login username")
oparser.add_option("--apikey", help="Api key")
oparser.add_option("--tenant_id",
help="Tenant Id associated with the account")
oparser.add_option("--auth_type", default="keystone",
help="Auth type to support different auth environments, \
Supported values are 'keystone', 'rax'.")
oparser.add_option("--service_type", default="reddwarf",
help="Service type is a name associated for the catalog")
oparser.add_option("--service_name", default="Reddwarf",
help="Service name as provided in the service catalog")
oparser.add_option("--service_url", default="",
help="Service endpoint to use if the catalog doesn't \
have one")
oparser.add_option("--region", default="RegionOne",
help="Region the service is located in")
oparser.add_option("-i", "--insecure", action="store_true",
dest="insecure", default=False,
help="Run in insecure mode for https endpoints.")
COMMANDS = {'auth': common.Auth, COMMANDS = {'auth': common.Auth,
'instance': InstanceCommands, 'instance': InstanceCommands,
'flavor': FlavorsCommands, 'flavor': FlavorsCommands,
@@ -230,10 +206,7 @@ COMMANDS = {'auth': common.Auth,
def main(): def main():
# Parse arguments # Parse arguments
oparser = optparse.OptionParser(usage="%prog [options] <cmd> <action> <args>", oparser = common.CliOptions.create_optparser()
version='1.0',
conflict_handler='resolve')
config_options(oparser)
for k, v in COMMANDS.items(): for k, v in COMMANDS.items():
v._prepare_parser(oparser) v._prepare_parser(oparser)
(options, args) = oparser.parse_args() (options, args) = oparser.parse_args()
@@ -241,11 +214,21 @@ def main():
if not args: if not args:
common.print_commands(COMMANDS) common.print_commands(COMMANDS)
if options.verbose:
os.environ['RDC_PP'] = "True"
os.environ['REDDWARFCLIENT_DEBUG'] = "True"
# Pop the command and check if it's in the known commands # Pop the command and check if it's in the known commands
cmd = args.pop(0) cmd = args.pop(0)
if cmd in COMMANDS: if cmd in COMMANDS:
fn = COMMANDS.get(cmd) fn = COMMANDS.get(cmd)
command_object = None
try:
command_object = fn(oparser) command_object = fn(oparser)
except Exception as ex:
if options.debug:
raise
print(ex)
# Get a list of supported actions for the command # Get a list of supported actions for the command
actions = common.methods_of(command_object) actions = common.methods_of(command_object)
@@ -256,10 +239,15 @@ def main():
# Check for a valid action and perform that action # Check for a valid action and perform that action
action = args.pop(0) action = args.pop(0)
if action in actions: if action in actions:
if not options.debug:
try: try:
getattr(command_object, action)() getattr(command_object, action)()
except Exception as ex: except Exception as ex:
if options.debug:
raise
print ex print ex
else:
getattr(command_object, action)()
else: else:
common.print_actions(cmd, actions) common.print_actions(cmd, actions)
else: else:

View File

@@ -18,6 +18,7 @@ import logging
import os import os
import time import time
import urlparse import urlparse
import sys
try: try:
import json import json
@@ -37,12 +38,17 @@ _logger = logging.getLogger(__name__)
RDC_PP = os.environ.get("RDC_PP", "False") == "True" RDC_PP = os.environ.get("RDC_PP", "False") == "True"
if 'REDDWARFCLIENT_DEBUG' in os.environ and os.environ['REDDWARFCLIENT_DEBUG']: def log_to_streamhandler(stream=None):
ch = logging.StreamHandler() stream = stream or sys.stderr
ch = logging.StreamHandler(stream)
_logger.setLevel(logging.DEBUG) _logger.setLevel(logging.DEBUG)
_logger.addHandler(ch) _logger.addHandler(ch)
if 'REDDWARFCLIENT_DEBUG' in os.environ and os.environ['REDDWARFCLIENT_DEBUG']:
log_to_streamhandler()
class ReddwarfHTTPClient(httplib2.Http): class ReddwarfHTTPClient(httplib2.Http):
USER_AGENT = 'python-reddwarfclient' USER_AGENT = 'python-reddwarfclient'
@@ -60,7 +66,10 @@ class ReddwarfHTTPClient(httplib2.Http):
self.username = user self.username = user
self.password = password self.password = password
self.tenant = tenant self.tenant = tenant
if auth_url:
self.auth_url = auth_url.rstrip('/') self.auth_url = auth_url.rstrip('/')
else:
self.auth_url = None
self.region_name = region_name self.region_name = region_name
self.endpoint_type = endpoint_type self.endpoint_type = endpoint_type
self.service_url = service_url self.service_url = service_url
@@ -165,7 +174,13 @@ class ReddwarfHTTPClient(httplib2.Http):
self.http_log(args, kwargs, resp, body) self.http_log(args, kwargs, resp, body)
if body: if body:
try:
body = self.morph_response_body(body) body = self.morph_response_body(body)
except exceptions.ResponseFormatError:
# Acceptable only if the response status is an error code.
# Otherwise its the API or client misbehaving.
self.raise_error_from_status(resp, None)
raise # Not accepted!
else: else:
body = None body = None
@@ -174,6 +189,10 @@ class ReddwarfHTTPClient(httplib2.Http):
return resp, body return resp, body
def raise_error_from_status(self, resp, body):
if resp.status in (400, 401, 403, 404, 408, 409, 413, 500, 501):
raise exceptions.from_response(resp, body)
def morph_request(self, kwargs): def morph_request(self, kwargs):
kwargs['headers']['Accept'] = 'application/json' kwargs['headers']['Accept'] = 'application/json'
kwargs['headers']['Content-Type'] = 'application/json' kwargs['headers']['Content-Type'] = 'application/json'
@@ -181,7 +200,10 @@ class ReddwarfHTTPClient(httplib2.Http):
kwargs['body'] = json.dumps(kwargs['body']) kwargs['body'] = json.dumps(kwargs['body'])
def morph_response_body(self, body_string): def morph_response_body(self, body_string):
try:
return json.loads(body_string) return json.loads(body_string)
except ValueError:
raise exceptions.ResponseFormatError()
def _time_request(self, url, method, **kwargs): def _time_request(self, url, method, **kwargs):
start_time = time.time() start_time = time.time()
@@ -236,12 +258,23 @@ class ReddwarfHTTPClient(httplib2.Http):
""" """
catalog = self.authenticator.authenticate() catalog = self.authenticator.authenticate()
self.auth_token = catalog.get_token() if self.service_url:
if not self.service_url: possible_service_url = None
else:
if self.endpoint_type == "publicURL": if self.endpoint_type == "publicURL":
self.service_url = catalog.get_public_url() possible_service_url = catalog.get_public_url()
elif self.endpoint_type == "adminURL": elif self.endpoint_type == "adminURL":
self.service_url = catalog.get_management_url() possible_service_url = catalog.get_management_url()
self.authenticate_with_token(catalog.get_token(), possible_service_url)
def authenticate_with_token(self, token, service_url=None):
self.auth_token = token
if not self.service_url:
if not service_url:
raise exceptions.ServiceUrlNotGiven()
else:
self.service_url = service_url
class Dbaas(object): class Dbaas(object):

View File

@@ -12,42 +12,18 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import copy
import json import json
import optparse
import os import os
import pickle import pickle
import sys import sys
from reddwarfclient.client import Dbaas from reddwarfclient import client
from reddwarfclient.xml import ReddwarfXmlClient
from reddwarfclient import exceptions from reddwarfclient import exceptions
APITOKEN = os.path.expanduser("~/.apitoken")
def get_client():
"""Load an existing apitoken if available"""
try:
with open(APITOKEN, 'rb') as token:
apitoken = pickle.load(token)
dbaas = Dbaas(apitoken._user, apitoken._apikey,
tenant=apitoken._tenant, auth_url=apitoken._auth_url,
auth_strategy=apitoken._auth_strategy,
service_type=apitoken._service_type,
service_name=apitoken._service_name,
service_url=apitoken._service_url,
insecure=apitoken._insecure,
region_name=apitoken._region_name)
dbaas.client.auth_token = apitoken._token
return dbaas
except IOError:
print "ERROR: You need to login first and get an auth token\n"
sys.exit(1)
except:
print "ERROR: There was an error using your existing auth token, " \
"please login again.\n"
sys.exit(1)
def methods_of(obj): def methods_of(obj):
"""Get all callable methods of an object that don't start with underscore """Get all callable methods of an object that don't start with underscore
returns a list of tuples of the form (method_name, method)""" returns a list of tuples of the form (method_name, method)"""
@@ -92,24 +68,110 @@ def limit_url(url, limit=None, marker=None):
return url + query return url + query
class APIToken(object): class CliOptions(object):
"""A token object containing the user, apikey and token which """A token object containing the user, apikey and token which
is pickleable.""" is pickleable."""
def __init__(self, user, apikey, tenant, token, auth_url, auth_strategy, APITOKEN = os.path.expanduser("~/.apitoken")
service_type, service_name, service_url, region_name,
insecure): DEFAULT_VALUES = {
self._user = user 'username':None,
self._apikey = apikey 'apikey':None,
self._tenant = tenant 'tenant_id':None,
self._token = token 'auth_url':None,
self._auth_url = auth_url 'auth_type':'keystone',
self._auth_strategy = auth_strategy 'service_type':'reddwarf',
self._service_type = service_type 'service_name':'Reddwarf',
self._service_name = service_name 'region':'RegionOne',
self._service_url = service_url 'service_url':None,
self._region_name = region_name 'insecure':False,
self._insecure = insecure '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):
oparser = optparse.OptionParser(
usage="%prog [options] <cmd> <action> <args>",
version='1.0', conflict_handler='resolve')
file = cls.load_from_file()
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): class ArgumentRequired(Exception):
@@ -123,15 +185,49 @@ class ArgumentRequired(Exception):
class CommandsBase(object): class CommandsBase(object):
params = [] 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 = ReddwarfXmlClient
else:
client_cls = client.ReddwarfHTTPClient
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 @classmethod
def _prepare_parser(cls, parser): def _prepare_parser(cls, parser):
for param in cls.params: for param in cls.params:
parser.add_option("--%s" % param) parser.add_option("--%s" % param)
def __init__(self, parser):
self.dbaas = get_client()
self._parse_options(parser)
def _parse_options(self, parser): def _parse_options(self, parser):
opts, args = parser.parse_args() opts, args = parser.parse_args()
for param in opts.__dict__: for param in opts.__dict__:
@@ -140,8 +236,7 @@ class CommandsBase(object):
def _require(self, *params): def _require(self, *params):
for param in params: for param in params:
if any([param not in self.params, if not hasattr(self, param):
not hasattr(self, param)]):
raise ArgumentRequired(param) raise ArgumentRequired(param)
if not getattr(self, param): if not getattr(self, param):
raise ArgumentRequired(param) raise ArgumentRequired(param)
@@ -156,11 +251,23 @@ class CommandsBase(object):
setattr(self, param, raw) setattr(self, param, raw)
def _pretty_print(self, func, *args, **kwargs): def _pretty_print(self, func, *args, **kwargs):
try: 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) result = func(*args, **kwargs)
print json.dumps(result._info, sort_keys=True, indent=4) print json.dumps(result._info, sort_keys=True, indent=4)
except: self._safe_exec(wrapped_func)
print sys.exc_info()[1]
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
for item in result:
print self._dumps(item._info)
def _pretty_paged(self, func, *args, **kwargs): def _pretty_paged(self, func, *args, **kwargs):
try: try:
@@ -168,12 +275,16 @@ class CommandsBase(object):
if limit: if limit:
limit = int(limit, 10) limit = int(limit, 10)
result = func(*args, limit=limit, marker=self.marker, **kwargs) result = func(*args, limit=limit, marker=self.marker, **kwargs)
if self.verbose:
return # Verbose already shows the output, so skip this.
for item in result: for item in result:
print json.dumps(item._info, sort_keys=True, indent=4) print self._dumps(item._info)
if result.links: if result.links:
for link in result.links: for link in result.links:
print json.dumps(link, sort_keys=True, indent=4) print self._dumps((link))
except: except:
if self.debug:
raise
print sys.exc_info()[1] print sys.exc_info()[1]
@@ -195,37 +306,52 @@ class Auth(CommandsBase):
] ]
def __init__(self, parser): def __init__(self, parser):
self.parser = parser super(Auth, self).__init__(parser)
self.dbaas = None self.dbaas = None
self._parse_options(parser)
def login(self): def login(self):
"""Login to retrieve an auth token to use for other api calls""" """Login to retrieve an auth token to use for other api calls"""
self._require('username', 'apikey', 'tenant_id') self._require('username', 'apikey', 'tenant_id', 'auth_url')
try: try:
self.dbaas = Dbaas(self.username, self.apikey, self.tenant_id, self.dbaas = self.get_client()
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)
self.dbaas.authenticate() self.dbaas.authenticate()
apitoken = APIToken(self.username, self.apikey, self.token = self.dbaas.client.auth_token
self.tenant_id, self.dbaas.client.auth_token, self.service_url = self.dbaas.client.service_url
self.auth_url, self.auth_type, CliOptions.save_from_instance_fields(self)
self.service_type, self.service_name, print(self.token)
self.service_url, self.region,
self.insecure)
with open(APITOKEN, 'wb') as token:
pickle.dump(apitoken, token, protocol=2)
print apitoken._token
except: except:
if self.debug:
raise
print sys.exc_info()[1] 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): class Paginated(object):
""" Pretends to be a list if you iterate over it, but also keeps a """ 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. """ next property you can use to get the next page of data. """

View File

@@ -41,6 +41,20 @@ class EndpointNotFound(Exception):
pass pass
class AuthUrlNotGiven(EndpointNotFound):
"""The auth url was not given."""
pass
class ServiceUrlNotGiven(EndpointNotFound):
"""The service url was not given."""
pass
class ResponseFormatError(Exception):
"""Could not parse the response format."""
pass
class AmbiguousEndpoints(Exception): class AmbiguousEndpoints(Exception):
"""Found more than one matching endpoint in Service Catalog.""" """Found more than one matching endpoint in Service Catalog."""
def __init__(self, endpoints=None): def __init__(self, endpoints=None):
@@ -159,4 +173,5 @@ def from_response(response, body):
details = error.get('details', None) details = error.get('details', None)
return cls(code=response.status, message=message, details=details) return cls(code=response.status, message=message, details=details)
else: else:
request_id = response.get('x-compute-request-id')
return cls(code=response.status, request_id=request_id) return cls(code=response.status, request_id=request_id)

View File

@@ -44,143 +44,112 @@ def _pretty_print(info):
print json.dumps(info, sort_keys=True, indent=4) print json.dumps(info, sort_keys=True, indent=4)
class HostCommands(object): class HostCommands(common.AuthedCommandsBase):
"""Commands to list info on hosts""" """Commands to list info on hosts"""
def __init__(self): params = [
pass 'name',
]
def get(self, name): def update_all(self):
"""Update all instances on a host"""
self._require('name')
self.dbaas.hosts.update_all(self.name)
def get(self):
"""List details for the specified host""" """List details for the specified host"""
dbaas = common.get_client() self._require('name')
try: self._pretty_print(self.dbaas.hosts.get, self.name)
_pretty_print(dbaas.hosts.get(name)._info)
except:
print sys.exc_info()[1]
def list(self): def list(self):
"""List all compute hosts""" """List all compute hosts"""
dbaas = common.get_client() self._pretty_list(self.dbaas.hosts.index)
try:
for host in dbaas.hosts.index():
_pretty_print(host._info)
except:
print sys.exc_info()[1]
class RootCommands(object): class RootCommands(common.AuthedCommandsBase):
"""List details about the root info for an instance.""" """List details about the root info for an instance."""
def __init__(self): params = [
pass 'id',
]
def history(self, id): def history(self):
"""List root history for the instance.""" """List root history for the instance."""
dbaas = common.get_client() self._require('id')
try: self._pretty_print(self.dbaas.management.root_enabled_history, self.id)
result = dbaas.management.root_enabled_history(id)
_pretty_print(result._info)
except:
print sys.exc_info()[1]
class AccountCommands(object): class AccountCommands(common.AuthedCommandsBase):
"""Commands to list account info""" """Commands to list account info"""
def __init__(self): params = [
pass 'id',
]
def list(self): def list(self):
"""List all accounts with non-deleted instances""" """List all accounts with non-deleted instances"""
dbaas = common.get_client() self._pretty_print(self.dbaas.accounts.index)
try:
_pretty_print(dbaas.accounts.index()._info)
except:
print sys.exc_info()[1]
def get(self, acct): def get(self):
"""List details for the account provided""" """List details for the account provided"""
dbaas = common.get_client() self._require('id')
try: self._pretty_print(self.dbaas.accounts.show, self.id)
_pretty_print(dbaas.accounts.show(acct)._info)
except:
print sys.exc_info()[1]
class InstanceCommands(object): class InstanceCommands(common.AuthedCommandsBase):
"""List details about an instance.""" """List details about an instance."""
def __init__(self): params = [
pass 'deleted',
'id',
'limit',
'marker',
]
def get(self, id): def get(self):
"""List details for the instance.""" """List details for the instance."""
dbaas = common.get_client() self._require('id')
try: self._pretty_print(self.dbaas.management.show, self.id)
result = dbaas.management.show(id)
_pretty_print(result._info)
except:
print sys.exc_info()[1]
def list(self, deleted=None, limit=None, marker=None): def list(self):
"""List all instances for account""" """List all instances for account"""
dbaas = common.get_client() deleted = None
if limit: if self.deleted is not None:
limit = int(limit, 10) if self.deleted.lower() in ['true']:
try: deleted = True
instances = dbaas.management.index(deleted, limit, marker) elif self.deleted.lower() in ['false']:
for instance in instances: deleted = False
_pretty_print(instance._info) self._pretty_paged(self.dbaas.management.index, deleted=deleted)
if instances.links:
for link in instances.links:
_pretty_print(link)
except:
print sys.exc_info()[1]
def diagnostic(self, id): def diagnostic(self):
"""List diagnostic details about an instance.""" """List diagnostic details about an instance."""
self._require('id')
dbaas = common.get_client() dbaas = common.get_client()
try: self._pretty_print(self.dbaas.diagnostics.get, self.id)
result = dbaas.diagnostics.get(id)
_pretty_print(result._info)
except:
print sys.exc_info()[1]
def stop(self, id): def stop(self):
"""Stop MySQL on the given instance.""" """Stop MySQL on the given instance."""
dbaas = common.get_client() self._require('id')
try: self._pretty_print(self.dbaas.management.stop, self.id)
result = dbaas.management.stop(id)
except:
print sys.exc_info()[1]
def reboot(self, id): def reboot(self, id):
"""Reboot the instance.""" """Reboot the instance."""
dbaas = common.get_client() self._require('id')
try: self._pretty_print(self.dbaas.management.reboot, self.id)
result = dbaas.management.reboot(id)
except:
print sys.exec_info()[1]
class StorageCommands(object): class StorageCommands(common.AuthedCommandsBase):
"""Commands to list devices info""" """Commands to list devices info"""
def __init__(self): params = []
pass
def list(self): def list(self):
"""List details for the storage device""" """List details for the storage device"""
dbaas = common.get_client() dbaas = common.get_client()
try: self._pretty_list(self.dbaas.storage.index)
for storage in dbaas.storage.index():
_pretty_print(storage._info)
except:
print sys.exc_info()[1]
def config_options(): def config_options(oparser):
global oparser
oparser.add_option("-u", "--url", default="http://localhost:5000/v1.1", oparser.add_option("-u", "--url", default="http://localhost:5000/v1.1",
help="Auth API endpoint URL with port and version. \ help="Auth API endpoint URL with port and version. \
Default: http://localhost:5000/v1.1") Default: http://localhost:5000/v1.1")
@@ -196,10 +165,9 @@ COMMANDS = {'account': AccountCommands,
def main(): def main():
# Parse arguments # Parse arguments
global oparser oparser = common.CliOptions.create_optparser()
oparser = optparse.OptionParser("%prog [options] <cmd> <action> <args>", for k, v in COMMANDS.items():
version='1.0') v._prepare_parser(oparser)
config_options()
(options, args) = oparser.parse_args() (options, args) = oparser.parse_args()
if not args: if not args:
@@ -209,7 +177,13 @@ def main():
cmd = args.pop(0) cmd = args.pop(0)
if cmd in COMMANDS: if cmd in COMMANDS:
fn = COMMANDS.get(cmd) fn = COMMANDS.get(cmd)
command_object = fn() command_object = None
try:
command_object = fn(oparser)
except Exception as ex:
if options.debug:
raise
print(ex)
# Get a list of supported actions for the command # Get a list of supported actions for the command
actions = common.methods_of(command_object) actions = common.methods_of(command_object)
@@ -220,20 +194,12 @@ def main():
# Check for a valid action and perform that action # Check for a valid action and perform that action
action = args.pop(0) action = args.pop(0)
if action in actions: if action in actions:
fn = actions.get(action)
try: try:
fn(*args) getattr(command_object, action)()
sys.exit(0) except Exception as ex:
except TypeError as err: if options.debug:
print "Possible wrong number of arguments supplied."
print "%s %s: %s" % (cmd, action, fn.__doc__)
print "\t\t", [fn.func_code.co_varnames[i] for i in
range(fn.func_code.co_argcount)]
print "ERROR: %s" % err
except Exception:
print "Command failed, please check the log for more info."
raise raise
print ex
else: else:
common.print_actions(cmd, actions) common.print_actions(cmd, actions)
else: else:

View File

@@ -2,6 +2,7 @@ from lxml import etree
import json import json
from numbers import Number from numbers import Number
from reddwarfclient import exceptions
from reddwarfclient.client import ReddwarfHTTPClient from reddwarfclient.client import ReddwarfHTTPClient
@@ -196,7 +197,10 @@ class ReddwarfXmlClient(ReddwarfHTTPClient):
# The root XML element always becomes a dictionary with a single # The root XML element always becomes a dictionary with a single
# field, which has the same key as the elements name. # field, which has the same key as the elements name.
result = {} result = {}
try:
root_element = etree.XML(body_string) root_element = etree.XML(body_string)
except etree.XMLSyntaxError:
raise exceptions.ResponseFormatError()
root_name = normalize_tag(root_element) root_name = normalize_tag(root_element)
root_value, links = root_element_to_json(root_name, root_element) root_value, links = root_element_to_json(root_name, root_element)
result = { root_name:root_value } result = { root_name:root_value }