From 88f9530151b5252487b260cec8e4cdb61f07acec Mon Sep 17 00:00:00 2001 From: Tim Simpson Date: Thu, 9 Aug 2012 11:04:28 -0500 Subject: [PATCH] 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. --- reddwarfclient/auth.py | 51 ++++++- reddwarfclient/cli.py | 68 ++++----- reddwarfclient/client.py | 51 +++++-- reddwarfclient/common.py | 278 +++++++++++++++++++++++++---------- reddwarfclient/exceptions.py | 15 ++ reddwarfclient/mcli.py | 180 +++++++++-------------- reddwarfclient/xml.py | 6 +- 7 files changed, 410 insertions(+), 239 deletions(-) diff --git a/reddwarfclient/auth.py b/reddwarfclient/auth.py index 51c901dc..76ef1767 100644 --- a/reddwarfclient/auth.py +++ b/reddwarfclient/auth.py @@ -24,6 +24,8 @@ def get_authenticator_cls(cls_or_name): return KeyStoneV2Authenticator elif cls_or_name == "rax": return RaxAuthenticator + elif cls_or_name == "auth1.1": + return Auth1_1 elif cls_or_name == "fake": return FakeAuth @@ -40,6 +42,8 @@ class Authenticator(object): """ + URL_REQUIRED=True + def __init__(self, client, type, url, username, password, tenant, region=None, service_type=None, service_name=None, service_url=None): @@ -54,7 +58,7 @@ class Authenticator(object): self.service_name = service_name self.service_url = service_url - def _authenticate(self, url, body): + def _authenticate(self, url, body, root_key='access'): """Authenticate and extract the service catalog.""" # Make sure we follow redirects when trying to reach Keystone tmp_follow_all_redirects = self.client.follow_all_redirects @@ -70,7 +74,8 @@ class Authenticator(object): return ServiceCatalog(body, region=self.region, service_type=self.service_type, service_name=self.service_name, - service_url=self.service_url) + service_url=self.service_url, + root_key=root_key) except exceptions.AmbiguousEndpoints: print "Found more than one valid endpoint. Use a more "\ "restrictive filter" @@ -93,6 +98,8 @@ class Authenticator(object): class KeyStoneV2Authenticator(Authenticator): def authenticate(self): + if self.url is None: + raise exceptions.AuthUrlNotGiven() return self._v2_auth(self.url) def _v2_auth(self, url): @@ -110,9 +117,40 @@ class KeyStoneV2Authenticator(Authenticator): 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): def authenticate(self): + if self.url is None: + raise exceptions.AuthUrlNotGiven() return self._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, - service_name=None, service_url=None): + service_name=None, service_url=None, root_key='access'): self.catalog = resource_dict self.region = region self.service_type = service_type @@ -163,6 +201,7 @@ class ServiceCatalog(object): self.service_url = service_url self.management_url = None self.public_url = None + self.root_key = root_key self._load() def _load(self): @@ -178,7 +217,7 @@ class ServiceCatalog(object): self.management_url = self.service_url def get_token(self): - return self.catalog['access']['token']['id'] + return self.catalog[self.root_key]['token']['id'] def get_management_url(self): return self.management_url @@ -202,11 +241,11 @@ class ServiceCatalog(object): raise exceptions.EndpointNotFound() # 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() # Full catalog ... - catalog = self.catalog['access']['serviceCatalog'] + catalog = self.catalog[self.root_key]['serviceCatalog'] for service in catalog: if service.get("type") != self.service_type: diff --git a/reddwarfclient/cli.py b/reddwarfclient/cli.py index 37971f2e..9b4489db 100644 --- a/reddwarfclient/cli.py +++ b/reddwarfclient/cli.py @@ -18,6 +18,7 @@ Reddwarf Command line tool """ +#TODO(tim.simpson): optparse is deprecated. Replace with argparse. import optparse import os import sys @@ -36,7 +37,7 @@ if os.path.exists(os.path.join(possible_topdir, 'reddwarfclient', from reddwarfclient import common -class InstanceCommands(common.CommandsBase): +class InstanceCommands(common.AuthedCommandsBase): """Commands to perform various instances operations and actions""" params = [ @@ -93,17 +94,17 @@ class InstanceCommands(common.CommandsBase): self._pretty_print(self.dbaas.instances.restart, self.id) -class FlavorsCommands(common.CommandsBase): +class FlavorsCommands(common.AuthedCommandsBase): """Commands for listing Flavors""" params = [] def list(self): """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""" params = [ @@ -130,7 +131,7 @@ class DatabaseCommands(common.CommandsBase): self._pretty_paged(self.dbaas.databases.list, self.id) -class UserCommands(common.CommandsBase): +class UserCommands(common.AuthedCommandsBase): """User CRUD operations on an instance""" params = [ 'id', @@ -159,7 +160,7 @@ class UserCommands(common.CommandsBase): self._pretty_paged(self.dbaas.users.list, self.id) -class RootCommands(common.CommandsBase): +class RootCommands(common.AuthedCommandsBase): """Root user related operations on an instance""" params = [ @@ -181,7 +182,7 @@ class RootCommands(common.CommandsBase): self._pretty_print(self.dbaas.root.is_root_enabled, self.id) -class VersionCommands(common.CommandsBase): +class VersionCommands(common.AuthedCommandsBase): """List available versions""" params = [ @@ -194,31 +195,6 @@ class VersionCommands(common.CommandsBase): 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, 'instance': InstanceCommands, 'flavor': FlavorsCommands, @@ -230,10 +206,7 @@ COMMANDS = {'auth': common.Auth, def main(): # Parse arguments - oparser = optparse.OptionParser(usage="%prog [options] ", - version='1.0', - conflict_handler='resolve') - config_options(oparser) + oparser = common.CliOptions.create_optparser() for k, v in COMMANDS.items(): v._prepare_parser(oparser) (options, args) = oparser.parse_args() @@ -241,11 +214,21 @@ def main(): if not args: 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 cmd = args.pop(0) if cmd in COMMANDS: fn = COMMANDS.get(cmd) - command_object = fn(oparser) + 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 actions = common.methods_of(command_object) @@ -256,10 +239,15 @@ def main(): # Check for a valid action and perform that action action = args.pop(0) if action in actions: - try: + if not options.debug: + try: + getattr(command_object, action)() + except Exception as ex: + if options.debug: + raise + print ex + else: getattr(command_object, action)() - except Exception as ex: - print ex else: common.print_actions(cmd, actions) else: diff --git a/reddwarfclient/client.py b/reddwarfclient/client.py index 440e2b90..75dd4574 100644 --- a/reddwarfclient/client.py +++ b/reddwarfclient/client.py @@ -18,6 +18,7 @@ import logging import os import time import urlparse +import sys try: import json @@ -37,12 +38,17 @@ _logger = logging.getLogger(__name__) RDC_PP = os.environ.get("RDC_PP", "False") == "True" -if 'REDDWARFCLIENT_DEBUG' in os.environ and os.environ['REDDWARFCLIENT_DEBUG']: - ch = logging.StreamHandler() +def log_to_streamhandler(stream=None): + stream = stream or sys.stderr + ch = logging.StreamHandler(stream) _logger.setLevel(logging.DEBUG) _logger.addHandler(ch) +if 'REDDWARFCLIENT_DEBUG' in os.environ and os.environ['REDDWARFCLIENT_DEBUG']: + log_to_streamhandler() + + class ReddwarfHTTPClient(httplib2.Http): USER_AGENT = 'python-reddwarfclient' @@ -60,7 +66,10 @@ class ReddwarfHTTPClient(httplib2.Http): self.username = user self.password = password self.tenant = tenant - self.auth_url = auth_url.rstrip('/') + if auth_url: + self.auth_url = auth_url.rstrip('/') + else: + self.auth_url = None self.region_name = region_name self.endpoint_type = endpoint_type self.service_url = service_url @@ -165,7 +174,13 @@ class ReddwarfHTTPClient(httplib2.Http): self.http_log(args, kwargs, resp, body) if body: - body = self.morph_response_body(body) + try: + 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: body = None @@ -174,6 +189,10 @@ class ReddwarfHTTPClient(httplib2.Http): 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): kwargs['headers']['Accept'] = 'application/json' kwargs['headers']['Content-Type'] = 'application/json' @@ -181,7 +200,10 @@ class ReddwarfHTTPClient(httplib2.Http): kwargs['body'] = json.dumps(kwargs['body']) def morph_response_body(self, body_string): - return json.loads(body_string) + try: + return json.loads(body_string) + except ValueError: + raise exceptions.ResponseFormatError() def _time_request(self, url, method, **kwargs): start_time = time.time() @@ -236,12 +258,23 @@ class ReddwarfHTTPClient(httplib2.Http): """ catalog = self.authenticator.authenticate() - self.auth_token = catalog.get_token() - if not self.service_url: + if self.service_url: + possible_service_url = None + else: if self.endpoint_type == "publicURL": - self.service_url = catalog.get_public_url() + possible_service_url = catalog.get_public_url() 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): diff --git a/reddwarfclient/common.py b/reddwarfclient/common.py index 9a766f2e..c06a7e71 100644 --- a/reddwarfclient/common.py +++ b/reddwarfclient/common.py @@ -12,42 +12,18 @@ # 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 reddwarfclient.client import Dbaas +from reddwarfclient import client +from reddwarfclient.xml import ReddwarfXmlClient 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): """Get all callable methods of an object that don't start with underscore 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 -class APIToken(object): +class CliOptions(object): """A token object containing the user, apikey and token which is pickleable.""" - def __init__(self, user, apikey, tenant, token, auth_url, auth_strategy, - service_type, service_name, service_url, region_name, - insecure): - self._user = user - self._apikey = apikey - self._tenant = tenant - self._token = token - self._auth_url = auth_url - self._auth_strategy = auth_strategy - self._service_type = service_type - self._service_name = service_name - self._service_url = service_url - self._region_name = region_name - self._insecure = insecure + APITOKEN = os.path.expanduser("~/.apitoken") + + DEFAULT_VALUES = { + 'username':None, + 'apikey':None, + 'tenant_id':None, + 'auth_url':None, + 'auth_type':'keystone', + 'service_type':'reddwarf', + 'service_name':'Reddwarf', + '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): + oparser = optparse.OptionParser( + usage="%prog [options] ", + 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): @@ -123,15 +185,49 @@ class ArgumentRequired(Exception): 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 = 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 def _prepare_parser(cls, parser): for param in cls.params: parser.add_option("--%s" % param) - def __init__(self, parser): - self.dbaas = get_client() - self._parse_options(parser) - def _parse_options(self, parser): opts, args = parser.parse_args() for param in opts.__dict__: @@ -140,9 +236,8 @@ class CommandsBase(object): def _require(self, *params): for param in params: - if any([param not in self.params, - not hasattr(self, param)]): - raise ArgumentRequired(param) + if not hasattr(self, param): + raise ArgumentRequired(param) if not getattr(self, param): raise ArgumentRequired(param) @@ -156,11 +251,23 @@ class CommandsBase(object): setattr(self, param, raw) 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) print json.dumps(result._info, sort_keys=True, indent=4) - except: - print sys.exc_info()[1] + 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 + for item in result: + print self._dumps(item._info) def _pretty_paged(self, func, *args, **kwargs): try: @@ -168,12 +275,16 @@ class CommandsBase(object): 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. for item in result: - print json.dumps(item._info, sort_keys=True, indent=4) + print self._dumps(item._info) if result.links: for link in result.links: - print json.dumps(link, sort_keys=True, indent=4) + print self._dumps((link)) except: + if self.debug: + raise print sys.exc_info()[1] @@ -195,37 +306,52 @@ class Auth(CommandsBase): ] def __init__(self, parser): - self.parser = parser + super(Auth, self).__init__(parser) self.dbaas = None - self._parse_options(parser) def login(self): """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: - self.dbaas = 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) + self.dbaas = self.get_client() self.dbaas.authenticate() - apitoken = APIToken(self.username, self.apikey, - self.tenant_id, self.dbaas.client.auth_token, - self.auth_url, self.auth_type, - self.service_type, self.service_name, - self.service_url, self.region, - self.insecure) - - with open(APITOKEN, 'wb') as token: - pickle.dump(apitoken, token, protocol=2) - print apitoken._token + self.token = self.dbaas.client.auth_token + self.service_url = self.dbaas.client.service_url + CliOptions.save_from_instance_fields(self) + print(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. """ diff --git a/reddwarfclient/exceptions.py b/reddwarfclient/exceptions.py index 33c25e80..eb305a38 100644 --- a/reddwarfclient/exceptions.py +++ b/reddwarfclient/exceptions.py @@ -41,6 +41,20 @@ class EndpointNotFound(Exception): 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): """Found more than one matching endpoint in Service Catalog.""" def __init__(self, endpoints=None): @@ -159,4 +173,5 @@ def from_response(response, body): details = error.get('details', None) return cls(code=response.status, message=message, details=details) else: + request_id = response.get('x-compute-request-id') return cls(code=response.status, request_id=request_id) diff --git a/reddwarfclient/mcli.py b/reddwarfclient/mcli.py index 394d1432..2cacfa5f 100644 --- a/reddwarfclient/mcli.py +++ b/reddwarfclient/mcli.py @@ -44,143 +44,112 @@ def _pretty_print(info): print json.dumps(info, sort_keys=True, indent=4) -class HostCommands(object): +class HostCommands(common.AuthedCommandsBase): """Commands to list info on hosts""" - def __init__(self): - pass + params = [ + '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""" - dbaas = common.get_client() - try: - _pretty_print(dbaas.hosts.get(name)._info) - except: - print sys.exc_info()[1] + self._require('name') + self._pretty_print(self.dbaas.hosts.get, self.name) def list(self): """List all compute hosts""" - dbaas = common.get_client() - try: - for host in dbaas.hosts.index(): - _pretty_print(host._info) - except: - print sys.exc_info()[1] + self._pretty_list(self.dbaas.hosts.index) -class RootCommands(object): +class RootCommands(common.AuthedCommandsBase): """List details about the root info for an instance.""" - def __init__(self): - pass + params = [ + 'id', + ] - def history(self, id): + def history(self): """List root history for the instance.""" - dbaas = common.get_client() - try: - result = dbaas.management.root_enabled_history(id) - _pretty_print(result._info) - except: - print sys.exc_info()[1] + self._require('id') + self._pretty_print(self.dbaas.management.root_enabled_history, self.id) -class AccountCommands(object): +class AccountCommands(common.AuthedCommandsBase): """Commands to list account info""" - def __init__(self): - pass + params = [ + 'id', + ] def list(self): """List all accounts with non-deleted instances""" - dbaas = common.get_client() - try: - _pretty_print(dbaas.accounts.index()._info) - except: - print sys.exc_info()[1] + self._pretty_print(self.dbaas.accounts.index) - def get(self, acct): + def get(self): """List details for the account provided""" - dbaas = common.get_client() - try: - _pretty_print(dbaas.accounts.show(acct)._info) - except: - print sys.exc_info()[1] + self._require('id') + self._pretty_print(self.dbaas.accounts.show, self.id) -class InstanceCommands(object): +class InstanceCommands(common.AuthedCommandsBase): """List details about an instance.""" - def __init__(self): - pass + params = [ + 'deleted', + 'id', + 'limit', + 'marker', + ] - def get(self, id): + def get(self): """List details for the instance.""" - dbaas = common.get_client() - try: - result = dbaas.management.show(id) - _pretty_print(result._info) - except: - print sys.exc_info()[1] + self._require('id') + self._pretty_print(self.dbaas.management.show, self.id) - def list(self, deleted=None, limit=None, marker=None): + def list(self): """List all instances for account""" - dbaas = common.get_client() - if limit: - limit = int(limit, 10) - try: - instances = dbaas.management.index(deleted, limit, marker) - for instance in instances: - _pretty_print(instance._info) - if instances.links: - for link in instances.links: - _pretty_print(link) - except: - print sys.exc_info()[1] + deleted = None + if self.deleted is not None: + if self.deleted.lower() in ['true']: + deleted = True + elif self.deleted.lower() in ['false']: + deleted = False + self._pretty_paged(self.dbaas.management.index, deleted=deleted) - def diagnostic(self, id): + def diagnostic(self): """List diagnostic details about an instance.""" + self._require('id') dbaas = common.get_client() - try: - result = dbaas.diagnostics.get(id) - _pretty_print(result._info) - except: - print sys.exc_info()[1] + self._pretty_print(self.dbaas.diagnostics.get, self.id) - def stop(self, id): + def stop(self): """Stop MySQL on the given instance.""" - dbaas = common.get_client() - try: - result = dbaas.management.stop(id) - except: - print sys.exc_info()[1] + self._require('id') + self._pretty_print(self.dbaas.management.stop, self.id) def reboot(self, id): """Reboot the instance.""" - dbaas = common.get_client() - try: - result = dbaas.management.reboot(id) - except: - print sys.exec_info()[1] + self._require('id') + self._pretty_print(self.dbaas.management.reboot, self.id) -class StorageCommands(object): +class StorageCommands(common.AuthedCommandsBase): """Commands to list devices info""" - def __init__(self): - pass + params = [] def list(self): """List details for the storage device""" dbaas = common.get_client() - try: - for storage in dbaas.storage.index(): - _pretty_print(storage._info) - except: - print sys.exc_info()[1] + self._pretty_list(self.dbaas.storage.index) -def config_options(): - global oparser +def config_options(oparser): oparser.add_option("-u", "--url", default="http://localhost:5000/v1.1", help="Auth API endpoint URL with port and version. \ Default: http://localhost:5000/v1.1") @@ -196,10 +165,9 @@ COMMANDS = {'account': AccountCommands, def main(): # Parse arguments - global oparser - oparser = optparse.OptionParser("%prog [options] ", - version='1.0') - config_options() + oparser = common.CliOptions.create_optparser() + for k, v in COMMANDS.items(): + v._prepare_parser(oparser) (options, args) = oparser.parse_args() if not args: @@ -209,7 +177,13 @@ def main(): cmd = args.pop(0) if cmd in COMMANDS: 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 actions = common.methods_of(command_object) @@ -220,20 +194,12 @@ def main(): # Check for a valid action and perform that action action = args.pop(0) if action in actions: - fn = actions.get(action) - try: - fn(*args) - sys.exit(0) - except TypeError as err: - 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 + getattr(command_object, action)() + except Exception as ex: + if options.debug: + raise + print ex else: common.print_actions(cmd, actions) else: diff --git a/reddwarfclient/xml.py b/reddwarfclient/xml.py index ae1c226d..d0b7f9b4 100644 --- a/reddwarfclient/xml.py +++ b/reddwarfclient/xml.py @@ -2,6 +2,7 @@ from lxml import etree import json from numbers import Number +from reddwarfclient import exceptions from reddwarfclient.client import ReddwarfHTTPClient @@ -196,7 +197,10 @@ class ReddwarfXmlClient(ReddwarfHTTPClient): # The root XML element always becomes a dictionary with a single # field, which has the same key as the elements name. result = {} - root_element = etree.XML(body_string) + try: + root_element = etree.XML(body_string) + except etree.XMLSyntaxError: + raise exceptions.ResponseFormatError() root_name = normalize_tag(root_element) root_value, links = root_element_to_json(root_name, root_element) result = { root_name:root_value }