diff --git a/quantum/client/__init__.py b/quantum/client/__init__.py index 70a629c8d..861d4a0a4 100644 --- a/quantum/client/__init__.py +++ b/quantum/client/__init__.py @@ -25,19 +25,104 @@ from quantum.common import exceptions from quantum.common.serializer import Serializer LOG = logging.getLogger('quantum.client') -EXCEPTIONS = { - 400: exceptions.BadInputError, - 401: exceptions.NotAuthorized, - 420: exceptions.NetworkNotFound, - 421: exceptions.NetworkInUse, - 430: exceptions.PortNotFound, - 431: exceptions.StateInvalid, - 432: exceptions.PortInUseClient, - 440: exceptions.AlreadyAttachedClient, - 501: exceptions.NotImplementedError} AUTH_TOKEN_HEADER = "X-Auth-Token" +def exception_handler_v10(status_code, error_content): + """ Exception handler for API v1.0 client + + This routine generates the appropriate + Quantum exception according to the contents of the + response body + + :param status_code: HTTP error status code + :param error_content: deserialized body of error response + """ + + quantum_error_types = { + 420: 'networkNotFound', + 421: 'networkInUse', + 430: 'portNotFound', + 431: 'requestedStateInvalid', + 432: 'portInUse', + 440: 'alreadyAttached' + } + + quantum_errors = { + 400: exceptions.BadInputError, + 401: exceptions.NotAuthorized, + 404: exceptions.NotFound, + 420: exceptions.NetworkNotFoundClient, + 421: exceptions.NetworkInUseClient, + 430: exceptions.PortNotFoundClient, + 431: exceptions.StateInvalidClient, + 432: exceptions.PortInUseClient, + 440: exceptions.AlreadyAttachedClient, + 501: NotImplementedError + } + + # Find real error type + error_type = None + if isinstance(error_content, dict): + error_type = quantum_error_types.get(status_code) + if error_type: + error_dict = error_content[error_type] + error_message = error_dict['message'] + "\n" +\ + error_dict['detail'] + else: + error_message = error_content + # raise the appropriate error! + ex = quantum_errors[status_code](message=error_message) + ex.args = ([dict(status_code=status_code, + message=error_message)],) + raise ex + + +def exception_handler_v11(status_code, error_content): + """ Exception handler for API v1.1 client + + This routine generates the appropriate + Quantum exception according to the contents of the + response body + + :param status_code: HTTP error status code + :param error_content: deserialized body of error response + """ + + quantum_errors = { + 'NetworkNotFound': exceptions.NetworkNotFoundClient, + 'NetworkInUse': exceptions.NetworkInUseClient, + 'PortNotFound': exceptions.PortNotFoundClient, + 'RequestedStateInvalid': exceptions.StateInvalidClient, + 'PortInUse': exceptions.PortInUseClient, + 'AlreadyAttached': exceptions.AlreadyAttachedClient, + } + + error_dict = None + if isinstance(error_content, dict): + error_dict = error_content.get('QuantumError') + # Find real error type + if error_dict: + # If QuantumError key is found, it will definitely contain + # a 'message' and 'type' keys + error_type = error_dict['type'] + error_message = (error_dict['message'] + "\n" + + error_dict['detail']) + # raise the appropriate error! + ex = quantum_errors[error_type](message=error_message) + ex.args = ([dict(status_code=status_code, + message=error_message)],) + raise ex + # If we end up here the exception was not a quantum error + raise exceptions.QuantumClientException(status_code + "-" + error_content) + + +EXCEPTION_HANDLERS = { + '1.0': exception_handler_v10, + '1.1': exception_handler_v11 +} + + class ApiCall(object): """A Decorator to add support for format and tenant overriding""" def __init__(self, function): @@ -86,7 +171,8 @@ class Client(object): def __init__(self, host="127.0.0.1", port=9696, use_ssl=False, tenant=None, format="xml", testingStub=None, key_file=None, cert_file=None, auth_token=None, logger=None, - action_prefix="/v1.0/tenants/{tenant_id}"): + version="1.1", + uri_prefix="/tenants/{tenant_id}"): """ Creates a new client to some service. @@ -113,7 +199,24 @@ class Client(object): self.cert_file = cert_file self.logger = logger self.auth_token = auth_token - self.action_prefix = action_prefix + self.version = version + self.action_prefix = "/v%s%s" % (version, uri_prefix) + + def _handle_fault_response(self, status_code, response_body): + # Create exception with HTTP status code and message + error_message = response_body + LOG.debug("Server returned error: %s", status_code) + LOG.debug("Error message: %s", error_message) + # Add deserialized error message to exception arguments + try: + des_error_body = Serializer().deserialize(error_message, + self.content_type()) + except: + # If unable to deserialized body it is probably not a + # Quantum error + des_error_body = {'message': error_message} + # Raise the appropriate exception + EXCEPTION_HANDLERS[self.version](status_code, des_error_body) def get_connection_type(self): """ @@ -138,7 +241,7 @@ class Client(object): return conn.getresponse() def do_request(self, method, action, body=None, - headers=None, params=None, exception_args={}): + headers=None, params=None): """ Connects to the server and issues a request. Returns the result data, or raises an appropriate exception if @@ -155,7 +258,6 @@ class Client(object): # Ensure we have a tenant id if not self.tenant: raise Exception("Tenant ID not set") - # Add format and tenant_id action += ".%s" % self.format action = self.action_prefix + action @@ -165,7 +267,6 @@ class Client(object): action += '?' + urllib.urlencode(params) if body: body = self.serialize(body) - try: connection_type = self.get_connection_type() headers = headers or {"Content-Type": @@ -176,7 +277,6 @@ class Client(object): # Open connection and send request, handling SSL certs certs = {'key_file': self.key_file, 'cert_file': self.cert_file} certs = dict((x, certs[x]) for x in certs if certs[x] != None) - if self.use_ssl and len(certs): conn = connection_type(self.host, self.port, **certs) else: @@ -184,7 +284,6 @@ class Client(object): res = self._send_request(conn, method, action, body, headers) status_code = self.get_status_code(res) data = res.read() - if self.logger: self.logger.debug("Quantum Client Reply (code = %s) :\n %s" \ % (str(status_code), data)) @@ -194,17 +293,7 @@ class Client(object): httplib.NO_CONTENT): return self.deserialize(data, status_code) else: - error_message = res.read() - LOG.debug("Server returned error: %s", status_code) - LOG.debug("Error message: %s", error_message) - # Create exception with HTTP status code and message - if res.status in EXCEPTIONS: - raise EXCEPTIONS[res.status](**exception_args) - # Add error code and message to exception arguments - ex = Exception("Server returned error: %s" % status_code) - ex.args = ([dict(status_code=status_code, - message=error_message)],) - raise ex + self._handle_fault_response(status_code, data) except (socket.error, IOError), e: msg = "Unable to connect to server. Got error: %s" % e LOG.exception(msg) @@ -263,8 +352,7 @@ class Client(object): """ Fetches the details of a certain network """ - return self.do_request("GET", self.network_path % (network), - exception_args={"net_id": network}) + return self.do_request("GET", self.network_path % (network)) @ApiCall def create_network(self, body=None): @@ -278,16 +366,14 @@ class Client(object): """ Updates a network """ - return self.do_request("PUT", self.network_path % (network), body=body, - exception_args={"net_id": network}) + return self.do_request("PUT", self.network_path % (network), body=body) @ApiCall def delete_network(self, network): """ Deletes the specified network """ - return self.do_request("DELETE", self.network_path % (network), - exception_args={"net_id": network}) + return self.do_request("DELETE", self.network_path % (network)) @ApiCall def list_ports(self, network): @@ -301,24 +387,21 @@ class Client(object): """ Fetches the details of a certain port """ - return self.do_request("GET", self.port_path % (network, port), - exception_args={"net_id": network, "port_id": port}) + return self.do_request("GET", self.port_path % (network, port)) @ApiCall def create_port(self, network, body=None): """ Creates a new port on a given network """ - return self.do_request("POST", self.ports_path % (network), body=body, - exception_args={"net_id": network}) + return self.do_request("POST", self.ports_path % (network), body=body) @ApiCall def delete_port(self, network, port): """ Deletes the specified port from a network """ - return self.do_request("DELETE", self.port_path % (network, port), - exception_args={"net_id": network, "port_id": port}) + return self.do_request("DELETE", self.port_path % (network, port)) @ApiCall def update_port(self, network, port, body=None): @@ -326,17 +409,14 @@ class Client(object): Sets the attributes of the specified port """ return self.do_request("PUT", - self.port_path % (network, port), body=body, - exception_args={"net_id": network, - "port_id": port}) + self.port_path % (network, port), body=body) @ApiCall def show_port_attachment(self, network, port): """ Fetches the attachment-id associated with the specified port """ - return self.do_request("GET", self.attachment_path % (network, port), - exception_args={"net_id": network, "port_id": port}) + return self.do_request("GET", self.attachment_path % (network, port)) @ApiCall def attach_resource(self, network, port, body=None): @@ -344,10 +424,7 @@ class Client(object): Sets the attachment-id of the specified port """ return self.do_request("PUT", - self.attachment_path % (network, port), body=body, - exception_args={"net_id": network, - "port_id": port, - "attach_id": str(body)}) + self.attachment_path % (network, port), body=body) @ApiCall def detach_resource(self, network, port): @@ -355,5 +432,28 @@ class Client(object): Removes the attachment-id of the specified port """ return self.do_request("DELETE", - self.attachment_path % (network, port), - exception_args={"net_id": network, "port_id": port}) + self.attachment_path % (network, port)) + + +class ClientV11(Client): + """ + This class overiddes some methods of the Client class in order to deal with + features specific to API v1.1 such as filters + """ + + @ApiCall + def list_networks(self, **filters): + """ + Fetches a list of all networks for a tenant + """ + # Pass filters in "params" argument to do_request + return self.do_request("GET", self.networks_path, params=filters) + + @ApiCall + def list_ports(self, network, **filters): + """ + Fetches a list of ports on a given network + """ + # Pass filters in "params" argument to do_request + return self.do_request("GET", self.ports_path % (network), + params=filters) diff --git a/quantum/client/cli.py b/quantum/client/cli.py index 99057368a..d2337fe7e 100755 --- a/quantum/client/cli.py +++ b/quantum/client/cli.py @@ -1,7 +1,7 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 -# Copyright 2011 Nicira Networks, Inc. -# Copyright 2011 Citrix Systems +# Copyright 2012 Nicira Networks, Inc. +# Copyright 2012 Citrix Systems # # 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 @@ -26,6 +26,7 @@ import sys from optparse import OptionParser +from quantum.common import exceptions possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]), os.pardir, @@ -37,13 +38,16 @@ gettext.install('quantum', unicode=1) from quantum.client import cli_lib from quantum.client import Client +from quantum.client import ClientV11 +from quantum.common import utils #Configure logger for client - cli logger is a child of it #NOTE(salvatore-orlando): logger name does not map to package #this is deliberate. Simplifies logger configuration LOG = logging.getLogger('quantum') +DEFAULT_QUANTUM_VERSION = '1.1' FORMAT = 'json' -commands = { +commands_v10 = { "list_nets": { "func": cli_lib.list_nets, "args": ["tenant-id"]}, @@ -81,36 +85,122 @@ commands = { "func": cli_lib.unplug_iface, "args": ["tenant-id", "net-id", "port-id"]}, } +commands_v11 = commands_v10.copy() +commands_v11.update({ + "list_nets": { + "func": cli_lib.list_nets_v11, + "args": ["tenant-id"], + "filters": ["name", "op-status", "port-op-status", "port-state", + "has-attachment", "attachment", "port"]}, + "list_ports": { + "func": cli_lib.list_ports_v11, + "args": ["tenant-id", "net-id"], + "filters": ["name", "op-status", "has-attachment", "attachment"]}, + }) +commands = { + '1.0': commands_v10, + '1.1': commands_v11 + } +clients = { + '1.0': Client, + '1.1': ClientV11 + } -def help(): + +def help(version): print "\nCommands:" - for k in commands.keys(): - print " %s %s" % (k, - " ".join(["<%s>" % y for y in commands[k]["args"]])) + cmds = commands[version] + for k in cmds.keys(): + print " %s %s %s" % (k, + " ".join(["<%s>" % y for y in cmds[k]["args"]]), + 'filters' in cmds[k] and "[filterspec ...]" or "") + + +def print_usage(cmd, version): + cmds = commands[version] + print "Usage:\n %s %s" % (cmd, + " ".join(["<%s>" % y for y in cmds[cmd]["args"]])) def build_args(cmd, cmdargs, arglist): - args = [] - orig_arglist = arglist[:] - try: - for x in cmdargs: - args.append(arglist[0]) - del arglist[0] - except: - LOG.error("Not enough arguments for \"%s\" (expected: %d, got: %d)" % ( - cmd, len(cmdargs), len(orig_arglist))) - print "Usage:\n %s %s" % (cmd, - " ".join(["<%s>" % y for y in commands[cmd]["args"]])) - return None - if len(arglist) > 0: - LOG.error("Too many arguments for \"%s\" (expected: %d, got: %d)" % ( - cmd, len(cmdargs), len(orig_arglist))) - print "Usage:\n %s %s" % (cmd, - " ".join(["<%s>" % y for y in commands[cmd]["args"]])) - return None + arglist_len = len(arglist) + cmdargs_len = len(cmdargs) + if arglist_len < cmdargs_len: + message = "Not enough arguments for \"%s\" (expected: %d, got: %d)"\ + % (cmd, len(cmdargs), arglist_len) + raise exceptions.QuantumCLIError(message=message) + args = arglist[:cmdargs_len] return args +def build_filters(cmd, cmd_filters, filter_list, version): + filters = {} + # Each filter is expected to be in the <key>=<value> format + for flt in filter_list: + split_filter = flt.split("=") + if len(split_filter) != 2: + message = "Invalid filter argument detected (%s)" % flt + raise exceptions.QuantumCLIError(message=message) + filter_key, filter_value = split_filter + # Ensure the filter is allowed + if not filter_key in cmd_filters: + message = "Invalid filter key (%s)" % filter_key + raise exceptions.QuantumCLIError(message=message) + filters[filter_key] = filter_value + return filters + + +def build_cmd(cmd, cmd_args, cmd_filters, arglist, version): + """ + Builds arguments and filters to be passed to the cli library routines + + :param cmd: Command to be executed + :param cmd_args: List of arguments required by the command + :param cmd_filters: List of filters allowed by the command + :param arglist: Command line arguments (includes both arguments and + filter specifications) + :param version: API version + """ + arglist_len = len(arglist) + try: + # Parse arguments + args = build_args(cmd, cmd_args, arglist) + # Parse filters + filters = None + if cmd_filters: + # Pop consumed arguments + arglist = arglist[len(args):] + filters = build_filters(cmd, cmd_filters, arglist, version) + except exceptions.QuantumCLIError as cli_ex: + LOG.error(cli_ex.message) + print " Error in command line:%s" % cli_ex.message + print_usage(cmd, version) + return None, None + filter_len = (filters is not None) and len(filters) or 0 + if len(arglist) - len(args) - filter_len > 0: + message = "Too many arguments for \"%s\" (expected: %d, got: %d)"\ + % (cmd, len(cmd_args), arglist_len) + LOG.error(message) + print "Error in command line: %s " % message + print "Usage:\n %s %s" % (cmd, + " ".join(["<%s>" % y for y in commands[version][cmd]["args"]])) + return None, None + # Append version to arguments for cli functions + args.append(version) + return args, filters + + +def instantiate_client(host, port, ssl, tenant, token, version): + client = clients[version](host, + port, + ssl, + tenant, + FORMAT, + auth_token=token, + version=version) + return client + + def main(): usagestr = "Usage: %prog [OPTIONS] <command> [args]" parser = OptionParser(usage=usagestr) @@ -126,13 +216,15 @@ def main(): type="string", default="syslog", help="log file path") parser.add_option("-t", "--token", dest="token", type="string", default=None, help="authentication token") + parser.add_option('--version', + default=utils.env('QUANTUM_VERSION', default=DEFAULT_QUANTUM_VERSION), + help='Accepts 1.1 and 1.0, defaults to env[QUANTUM_VERSION].') options, args = parser.parse_args() if options.verbose: LOG.setLevel(logging.DEBUG) else: LOG.setLevel(logging.WARN) - #logging.handlers.WatchedFileHandler if options.logfile == "syslog": LOG.addHandler(logging.handlers.SysLogHandler(address='/dev/log')) @@ -141,29 +233,45 @@ def main(): # Set permissions on log file os.chmod(options.logfile, 0644) + version = options.version + if not version in commands: + LOG.error("Unknown API version specified:%s", version) + print "Unknown API version: %s" % version + if len(args) < 1: parser.print_help() - help() + help(version) sys.exit(1) cmd = args[0] - if cmd not in commands.keys(): + if cmd not in commands[version].keys(): LOG.error("Unknown command: %s" % cmd) - help() + help(version) sys.exit(1) - args = build_args(cmd, commands[cmd]["args"], args[1:]) + # Build argument list for CLI command + # The argument list will include the version number as well + args, filters = build_cmd(cmd, + commands[version][cmd]["args"], + commands[version][cmd].get("filters", None), + args[1:], + options.version) if not args: sys.exit(1) LOG.info("Executing command \"%s\" with args: %s" % (cmd, args)) - client = Client(options.host, options.port, options.ssl, - args[0], FORMAT, - auth_token=options.token) - commands[cmd]["func"](client, *args) + client = instantiate_client(options.host, + options.port, + options.ssl, + args[0], + options.token, + options.version) + # append filters to arguments + # this will allow for using the same prototype for v10 and v11 + # TODO: Use **kwargs instead of *args (keyword is better than positional) + if filters: + args.append(filters) + commands[version][cmd]["func"](client, *args) LOG.info("Command execution completed") sys.exit(0) - -if __name__ == "__main__": - main() diff --git a/quantum/client/cli_lib.py b/quantum/client/cli_lib.py index 6e202ff6a..8d73695e1 100755 --- a/quantum/client/cli_lib.py +++ b/quantum/client/cli_lib.py @@ -91,7 +91,7 @@ class CmdOutputTemplate(OutputTemplate): Extends OutputTemplate loading a different template for each command. """ - _templates = { + _templates_v10 = { "list_nets": "Virtual Networks for Tenant %(tenant_id)s\n" + "%(networks|\tNetwork ID: %(id)s)s", "show_net": "Network ID: %(network.id)s\n" + @@ -130,13 +130,31 @@ class CmdOutputTemplate(OutputTemplate): "on Virtual Network: %(network_id)s\n" + "for Tenant: %(tenant_id)s"} - def __init__(self, cmd, data): - super(CmdOutputTemplate, self).__init__(self._templates[cmd], data) + _templates_v11 = _templates_v10.copy() + _templates_v11.update({ + "show_net": "Network ID: %(network.id)s\n" + + "network Name: %(network.name)s\n" + + "operational Status: %(network.op-status)s", + "show_port": "Logical Port ID: %(port.id)s\n" + + "administrative state: %(port.state)s\n" + + "operational status: %(port.op-status)s\n" + + "interface: %(port.attachment)s\n" + + "on Virtual Network: %(network_id)s\n" + + "for Tenant: %(tenant_id)s", + }) + + _templates = { + '1.0': _templates_v10, + '1.1': _templates_v11 + } + + def __init__(self, cmd, data, version): + super(CmdOutputTemplate, self).__init__( + self._templates[version][cmd], data) def _handle_exception(ex): LOG.exception(sys.exc_info()) - print "Exception:%s - %s" % (sys.exc_info()[0], sys.exc_info()[1]) status_code = None message = None # Retrieve dict at 1st element of tuple at last argument @@ -149,67 +167,94 @@ def _handle_exception(ex): LOG.exception(msg_1 + "-" + msg_2) print msg_1 print msg_2 + else: + print "An unexpected exception occured:%s (%s)" % (sys.exc_info()[1], + sys.exc_info()[0]) -def prepare_output(cmd, tenant_id, response): - LOG.debug("Preparing output for response:%s", response) +def prepare_output(cmd, tenant_id, response, version): + LOG.debug("Preparing output for response:%s, version:%s" + % (response, version)) response['tenant_id'] = tenant_id - output = str(CmdOutputTemplate(cmd, response)) + output = str(CmdOutputTemplate(cmd, response, version)) LOG.debug("Finished preparing output for command:%s", cmd) return output def list_nets(client, *args): - tenant_id = args[0] - res = client.list_networks() - LOG.debug("Operation 'list_networks' executed.") - output = prepare_output("list_nets", tenant_id, res) - print output + tenant_id, version = args + try: + res = client.list_networks() + LOG.debug("Operation 'list_networks' executed.") + output = prepare_output("list_nets", tenant_id, res, version) + print output + except Exception as ex: + _handle_exception(ex) + + +def list_nets_v11(client, *args): + filters = {} + tenant_id, version = args[:2] + if len(args) > 2: + filters = args[2] + try: + res = client.list_networks(**filters) + LOG.debug("Operation 'list_networks' executed.") + output = prepare_output("list_nets", tenant_id, res, version) + print output + except Exception as ex: + _handle_exception(ex) def create_net(client, *args): - tenant_id, name = args + tenant_id, name, version = args data = {'network': {'name': name}} new_net_id = None try: res = client.create_network(data) new_net_id = res["network"]["id"] LOG.debug("Operation 'create_network' executed.") - output = prepare_output("create_net", tenant_id, - dict(network_id=new_net_id)) + output = prepare_output("create_net", + tenant_id, + dict(network_id=new_net_id), + version) print output except Exception as ex: _handle_exception(ex) def delete_net(client, *args): - tenant_id, network_id = args + tenant_id, network_id, version = args try: client.delete_network(network_id) LOG.debug("Operation 'delete_network' executed.") - output = prepare_output("delete_net", tenant_id, - dict(network_id=network_id)) + output = prepare_output("delete_net", + tenant_id, + dict(network_id=network_id), + version) print output except Exception as ex: _handle_exception(ex) def show_net(client, *args): - tenant_id, network_id = args + tenant_id, network_id, version = args try: #NOTE(salvatore-orlando) changed for returning exclusively # output for GET /networks/{net-id} API operation res = client.show_network_details(network_id)["network"] LOG.debug("Operation 'show_network_details' executed.") - output = prepare_output("show_net", tenant_id, - dict(network=res)) + output = prepare_output("show_net", + tenant_id, + dict(network=res), + version) print output except Exception as ex: _handle_exception(ex) def update_net(client, *args): - tenant_id, network_id, param_data = args + tenant_id, network_id, param_data, version = args data = {'network': {}} for kv in param_data.split(","): k, v = kv.split("=") @@ -219,47 +264,67 @@ def update_net(client, *args): client.update_network(network_id, data) LOG.debug("Operation 'update_network' executed.") # Response has no body. Use data for populating output - output = prepare_output("update_net", tenant_id, data) + output = prepare_output("update_net", tenant_id, data, version) print output except Exception as ex: _handle_exception(ex) def list_ports(client, *args): - tenant_id, network_id = args + tenant_id, network_id, version = args try: ports = client.list_ports(network_id) LOG.debug("Operation 'list_ports' executed.") data = ports data['network_id'] = network_id - output = prepare_output("list_ports", tenant_id, data) + output = prepare_output("list_ports", tenant_id, data, version) + print output + except Exception as ex: + _handle_exception(ex) + + +def list_ports_v11(client, *args): + filters = {} + tenant_id, network_id, version = args[:3] + if len(args) > 3: + filters = args[3] + try: + ports = client.list_ports(network_id, **filters) + LOG.debug("Operation 'list_ports' executed.") + data = ports + data['network_id'] = network_id + output = prepare_output("list_ports", tenant_id, data, version) print output except Exception as ex: _handle_exception(ex) def create_port(client, *args): - tenant_id, network_id = args + tenant_id, network_id, version = args try: res = client.create_port(network_id) LOG.debug("Operation 'create_port' executed.") new_port_id = res["port"]["id"] - output = prepare_output("create_port", tenant_id, + output = prepare_output("create_port", + tenant_id, dict(network_id=network_id, - port_id=new_port_id)) + port_id=new_port_id), + version) print output except Exception as ex: _handle_exception(ex) def delete_port(client, *args): - tenant_id, network_id, port_id = args + tenant_id, network_id, port_id, version = args try: client.delete_port(network_id, port_id) LOG.debug("Operation 'delete_port' executed.") - output = prepare_output("delete_port", tenant_id, + output = prepare_output("delete_port", + tenant_id, dict(network_id=network_id, - port_id=port_id)) + port_id=port_id), + version) print output except Exception as ex: _handle_exception(ex) @@ -267,7 +332,7 @@ def delete_port(client, *args): def show_port(client, *args): - tenant_id, network_id, port_id = args + tenant_id, network_id, port_id, version = args try: port = client.show_port_details(network_id, port_id)["port"] LOG.debug("Operation 'list_port_details' executed.") @@ -280,16 +345,18 @@ def show_port(client, *args): port['attachment'] = attach['id'] else: port['attachment'] = '<none>' - output = prepare_output("show_port", tenant_id, + output = prepare_output("show_port", + tenant_id, dict(network_id=network_id, - port=port)) + port=port), + version) print output except Exception as ex: _handle_exception(ex) def update_port(client, *args): - tenant_id, network_id, port_id, param_data = args + tenant_id, network_id, port_id, param_data, version = args data = {'port': {}} for kv in param_data.split(","): k, v = kv.split("=") @@ -300,14 +367,14 @@ def update_port(client, *args): client.update_port(network_id, port_id, data) LOG.debug("Operation 'udpate_port' executed.") # Response has no body. Use data for populating output - output = prepare_output("update_port", tenant_id, data) + output = prepare_output("update_port", tenant_id, data, version) print output except Exception as ex: _handle_exception(ex) def plug_iface(client, *args): - tenant_id, network_id, port_id, attachment = args + tenant_id, network_id, port_id, attachment, version = args try: data = {'attachment': {'id': '%s' % attachment}} client.attach_resource(network_id, port_id, data) @@ -315,20 +382,23 @@ def plug_iface(client, *args): output = prepare_output("plug_iface", tenant_id, dict(network_id=network_id, port_id=port_id, - attachment=attachment)) + attachment=attachment), + version) print output except Exception as ex: _handle_exception(ex) def unplug_iface(client, *args): - tenant_id, network_id, port_id = args + tenant_id, network_id, port_id, version = args try: client.detach_resource(network_id, port_id) LOG.debug("Operation 'detach_resource' executed.") - output = prepare_output("unplug_iface", tenant_id, + output = prepare_output("unplug_iface", + tenant_id, dict(network_id=network_id, - port_id=port_id)) + port_id=port_id), + version) print output except Exception as ex: _handle_exception(ex) diff --git a/quantum/client/tests/unit/test_cli.py b/quantum/client/tests/unit/test_cli.py index 09c84453b..bfd2f02d6 100644 --- a/quantum/client/tests/unit/test_cli.py +++ b/quantum/client/tests/unit/test_cli.py @@ -33,6 +33,7 @@ from quantum.db import api as db from quantum.tests.unit.client_tools import stubs as client_stubs LOG = logging.getLogger('quantum.tests.test_cli') +API_VERSION = "1.1" FORMAT = 'json' @@ -49,9 +50,12 @@ class CLITest(unittest.TestCase): self.tenant_id = "test_tenant" self.network_name_1 = "test_network_1" self.network_name_2 = "test_network_2" + self.version = API_VERSION # Prepare client and plugin manager - self.client = Client(tenant=self.tenant_id, format=FORMAT, - testingStub=client_stubs.FakeHTTPConnection) + self.client = Client(tenant=self.tenant_id, + format=FORMAT, + testingStub=client_stubs.FakeHTTPConnection, + version=self.version) # Redirect stdout self.fake_stdout = client_stubs.FakeStdout() sys.stdout = self.fake_stdout @@ -64,10 +68,13 @@ class CLITest(unittest.TestCase): def _verify_list_networks(self): # Verification - get raw result from db nw_list = db.network_list(self.tenant_id) - networks = [dict(id=nw.uuid, name=nw.name) for nw in nw_list] + networks = [{'id': nw.uuid, 'name': nw.name} + for nw in nw_list] # Fill CLI template - output = cli.prepare_output('list_nets', self.tenant_id, - dict(networks=networks)) + output = cli.prepare_output('list_nets', + self.tenant_id, + dict(networks=networks), + self.version) # Verify! # Must add newline at the end to match effect of print call self.assertEquals(self.fake_stdout.make_string(), output + '\n') @@ -79,8 +86,10 @@ class CLITest(unittest.TestCase): self.fail("No network created") network_id = nw_list[0].uuid # Fill CLI template - output = cli.prepare_output('create_net', self.tenant_id, - dict(network_id=network_id)) + output = cli.prepare_output('create_net', + self.tenant_id, + dict(network_id=network_id), + self.version) # Verify! # Must add newline at the end to match effect of print call self.assertEquals(self.fake_stdout.make_string(), output + '\n') @@ -91,8 +100,10 @@ class CLITest(unittest.TestCase): if len(nw_list) != 0: self.fail("DB should not contain any network") # Fill CLI template - output = cli.prepare_output('delete_net', self.tenant_id, - dict(network_id=network_id)) + output = cli.prepare_output('delete_net', + self.tenant_id, + dict(network_id=network_id), + self.version) # Verify! # Must add newline at the end to match effect of print call self.assertEquals(self.fake_stdout.make_string(), output + '\n') @@ -101,10 +112,13 @@ class CLITest(unittest.TestCase): # Verification - get raw result from db nw_list = db.network_list(self.tenant_id) network_data = {'id': nw_list[0].uuid, - 'name': nw_list[0].name} + 'name': nw_list[0].name, + 'op-status': nw_list[0].op_status} # Fill CLI template - output = cli.prepare_output('update_net', self.tenant_id, - dict(network=network_data)) + output = cli.prepare_output('update_net', + self.tenant_id, + dict(network=network_data), + self.version) # Verify! # Must add newline at the end to match effect of print call self.assertEquals(self.fake_stdout.make_string(), output + '\n') @@ -112,10 +126,14 @@ class CLITest(unittest.TestCase): def _verify_show_network(self): # Verification - get raw result from db nw = db.network_list(self.tenant_id)[0] - network = dict(id=nw.uuid, name=nw.name) + network = {'id': nw.uuid, + 'name': nw.name, + 'op-status': nw.op_status} # Fill CLI template - output = cli.prepare_output('show_net', self.tenant_id, - dict(network=network)) + output = cli.prepare_output('show_net', + self.tenant_id, + dict(network=network), + self.version) # Verify! # Must add newline at the end to match effect of print call self.assertEquals(self.fake_stdout.make_string(), output + '\n') @@ -126,9 +144,11 @@ class CLITest(unittest.TestCase): ports = [dict(id=port.uuid, state=port.state) for port in port_list] # Fill CLI template - output = cli.prepare_output('list_ports', self.tenant_id, + output = cli.prepare_output('list_ports', + self.tenant_id, dict(network_id=network_id, - ports=ports)) + ports=ports), + self.version) # Verify! # Must add newline at the end to match effect of print call self.assertEquals(self.fake_stdout.make_string(), output + '\n') @@ -140,9 +160,11 @@ class CLITest(unittest.TestCase): self.fail("No port created") port_id = port_list[0].uuid # Fill CLI template - output = cli.prepare_output('create_port', self.tenant_id, + output = cli.prepare_output('create_port', + self.tenant_id, dict(network_id=network_id, - port_id=port_id)) + port_id=port_id), + self.version) # Verify! # Must add newline at the end to match effect of print call self.assertEquals(self.fake_stdout.make_string(), output + '\n') @@ -153,9 +175,11 @@ class CLITest(unittest.TestCase): if len(port_list) != 0: self.fail("DB should not contain any port") # Fill CLI template - output = cli.prepare_output('delete_port', self.tenant_id, + output = cli.prepare_output('delete_port', + self.tenant_id, dict(network_id=network_id, - port_id=port_id)) + port_id=port_id), + self.version) # Verify! # Must add newline at the end to match effect of print call self.assertEquals(self.fake_stdout.make_string(), output + '\n') @@ -163,11 +187,15 @@ class CLITest(unittest.TestCase): def _verify_update_port(self, network_id, port_id): # Verification - get raw result from db port = db.port_get(port_id, network_id) - port_data = {'id': port.uuid, 'state': port.state} + port_data = {'id': port.uuid, + 'state': port.state, + 'op-status': port.op_status} # Fill CLI template - output = cli.prepare_output('update_port', self.tenant_id, + output = cli.prepare_output('update_port', + self.tenant_id, dict(network_id=network_id, - port=port_data)) + port=port_data), + self.version) # Verify! # Must add newline at the end to match effect of print call self.assertEquals(self.fake_stdout.make_string(), output + '\n') @@ -177,15 +205,19 @@ class CLITest(unittest.TestCase): # TODO(salvatore-orlando): Must resolve this issue with # attachment in separate bug fix. port = db.port_get(port_id, network_id) - port_data = {'id': port.uuid, 'state': port.state, - 'attachment': "<none>"} + port_data = {'id': port.uuid, + 'state': port.state, + 'attachment': "<none>", + 'op-status': port.op_status} if port.interface_id is not None: port_data['attachment'] = port.interface_id # Fill CLI template - output = cli.prepare_output('show_port', self.tenant_id, + output = cli.prepare_output('show_port', + self.tenant_id, dict(network_id=network_id, - port=port_data)) + port=port_data), + self.version) # Verify! # Must add newline at the end to match effect of print call self.assertEquals(self.fake_stdout.make_string(), output + '\n') @@ -194,10 +226,12 @@ class CLITest(unittest.TestCase): # Verification - get raw result from db port = db.port_get(port_id, network_id) # Fill CLI template - output = cli.prepare_output("plug_iface", self.tenant_id, + output = cli.prepare_output("plug_iface", + self.tenant_id, dict(network_id=network_id, port_id=port['uuid'], - attachment=port['interface_id'])) + attachment=port['interface_id']), + self.version) # Verify! # Must add newline at the end to match effect of print call self.assertEquals(self.fake_stdout.make_string(), output + '\n') @@ -206,9 +240,11 @@ class CLITest(unittest.TestCase): # Verification - get raw result from db port = db.port_get(port_id, network_id) # Fill CLI template - output = cli.prepare_output("unplug_iface", self.tenant_id, + output = cli.prepare_output("unplug_iface", + self.tenant_id, dict(network_id=network_id, - port_id=port['uuid'])) + port_id=port['uuid']), + self.version) # Verify! # Must add newline at the end to match effect of print call self.assertEquals(self.fake_stdout.make_string(), output + '\n') @@ -219,7 +255,9 @@ class CLITest(unittest.TestCase): db.network_create(self.tenant_id, self.network_name_1) db.network_create(self.tenant_id, self.network_name_2) - cli.list_nets(self.client, self.tenant_id) + cli.list_nets(self.client, + self.tenant_id, + self.version) except: LOG.exception("Exception caught: %s", sys.exc_info()) self.fail("test_list_networks failed due to an exception") @@ -230,7 +268,10 @@ class CLITest(unittest.TestCase): def test_create_network(self): try: - cli.create_net(self.client, self.tenant_id, "test") + cli.create_net(self.client, + self.tenant_id, + "test", + self.version) except: LOG.exception("Exception caught: %s", sys.exc_info()) self.fail("test_create_network failed due to an exception") @@ -243,7 +284,10 @@ class CLITest(unittest.TestCase): try: db.network_create(self.tenant_id, self.network_name_1) network_id = db.network_list(self.tenant_id)[0]['uuid'] - cli.delete_net(self.client, self.tenant_id, network_id) + cli.delete_net(self.client, + self.tenant_id, + network_id, + self.version) except: LOG.exception("Exception caught: %s", sys.exc_info()) self.fail("test_delete_network failed due to an exception") @@ -256,7 +300,10 @@ class CLITest(unittest.TestCase): try: # Load some data into the datbase net = db.network_create(self.tenant_id, self.network_name_1) - cli.show_net(self.client, self.tenant_id, net['uuid']) + cli.show_net(self.client, + self.tenant_id, + net['uuid'], + self.version) except: LOG.exception("Exception caught: %s", sys.exc_info()) self.fail("test_detail_network failed due to an exception") @@ -269,8 +316,11 @@ class CLITest(unittest.TestCase): try: net = db.network_create(self.tenant_id, self.network_name_1) network_id = net['uuid'] - cli.update_net(self.client, self.tenant_id, - network_id, 'name=%s' % self.network_name_2) + cli.update_net(self.client, + self.tenant_id, + network_id, + 'name=%s' % self.network_name_2, + self.version) except: LOG.exception("Exception caught: %s", sys.exc_info()) self.fail("test_update_network failed due to an exception") @@ -286,7 +336,10 @@ class CLITest(unittest.TestCase): network_id = net['uuid'] db.port_create(network_id) db.port_create(network_id) - cli.list_ports(self.client, self.tenant_id, network_id) + cli.list_ports(self.client, + self.tenant_id, + network_id, + self.version) except: LOG.exception("Exception caught: %s", sys.exc_info()) self.fail("test_list_ports failed due to an exception") @@ -301,7 +354,10 @@ class CLITest(unittest.TestCase): # Pre-populate data for testing using db api net = db.network_create(self.tenant_id, self.network_name_1) network_id = net['uuid'] - cli.create_port(self.client, self.tenant_id, network_id) + cli.create_port(self.client, + self.tenant_id, + network_id, + self.version) except: LOG.exception("Exception caught: %s", sys.exc_info()) self.fail("test_create_port failed due to an exception") @@ -319,7 +375,11 @@ class CLITest(unittest.TestCase): network_id = net['uuid'] port = db.port_create(network_id) port_id = port['uuid'] - cli.delete_port(self.client, self.tenant_id, network_id, port_id) + cli.delete_port(self.client, + self.tenant_id, + network_id, + port_id, + self.version) except: LOG.exception("Exception caught: %s", sys.exc_info()) self.fail("test_delete_port failed due to an exception") @@ -335,8 +395,12 @@ class CLITest(unittest.TestCase): port = db.port_create(network_id) port_id = port['uuid'] # Default state is DOWN - change to ACTIVE. - cli.update_port(self.client, self.tenant_id, network_id, - port_id, 'state=ACTIVE') + cli.update_port(self.client, + self.tenant_id, + network_id, + port_id, + 'state=ACTIVE', + self.version) except: LOG.exception("Exception caught: %s", sys.exc_info()) self.fail("test_update_port failed due to an exception") @@ -354,7 +418,11 @@ class CLITest(unittest.TestCase): network_id = net['uuid'] port = db.port_create(network_id) port_id = port['uuid'] - cli.show_port(self.client, self.tenant_id, network_id, port_id) + cli.show_port(self.client, + self.tenant_id, + network_id, + port_id, + self.version) except: LOG.exception("Exception caught: %s", sys.exc_info()) self.fail("test_show_port_no_attach failed due to an exception") @@ -374,7 +442,11 @@ class CLITest(unittest.TestCase): port = db.port_create(network_id) port_id = port['uuid'] db.port_set_attachment(port_id, network_id, iface_id) - cli.show_port(self.client, self.tenant_id, network_id, port_id) + cli.show_port(self.client, + self.tenant_id, + network_id, + port_id, + self.version) except: LOG.exception("Exception caught: %s", sys.exc_info()) self.fail("test_show_port_with_attach failed due to an exception") @@ -392,8 +464,12 @@ class CLITest(unittest.TestCase): network_id = net['uuid'] port = db.port_create(net['uuid']) port_id = port['uuid'] - cli.plug_iface(self.client, self.tenant_id, network_id, - port_id, "test_iface_id") + cli.plug_iface(self.client, + self.tenant_id, + network_id, + port_id, + "test_iface_id", + self.version) except: LOG.exception("Exception caught: %s", sys.exc_info()) self.fail("test_plug_iface failed due to an exception") @@ -412,7 +488,11 @@ class CLITest(unittest.TestCase): port = db.port_create(net['uuid']) port_id = port['uuid'] db.port_set_attachment(port_id, network_id, "test_iface_id") - cli.unplug_iface(self.client, self.tenant_id, network_id, port_id) + cli.unplug_iface(self.client, + self.tenant_id, + network_id, + port_id, + self.version) except: LOG.exception("Exception caught: %s", sys.exc_info()) self.fail("test_plug_iface failed due to an exception") diff --git a/quantum/common/exceptions.py b/quantum/common/exceptions.py index f4cf55efe..043d4f1ed 100644 --- a/quantum/common/exceptions.py +++ b/quantum/common/exceptions.py @@ -49,30 +49,6 @@ class QuantumException(Exception): return self._error_string -class ProcessExecutionError(IOError): - def __init__(self, stdout=None, stderr=None, exit_code=None, cmd=None, - description=None): - if description is None: - description = "Unexpected error while running command." - if exit_code is None: - exit_code = '-' - message = "%s\nCommand: %s\nExit code: %s\nStdout: %r\nStderr: %r" % ( - description, cmd, exit_code, stdout, stderr) - IOError.__init__(self, message) - - -class Error(Exception): - def __init__(self, message=None): - super(Error, self).__init__(message) - - -class ApiError(Error): - def __init__(self, message='Unknown', code='Unknown'): - self.message = message - self.code = code - super(ApiError, self).__init__('%s: %s' % (code, message)) - - class NotFound(QuantumException): pass @@ -111,34 +87,87 @@ class AlreadyAttached(QuantumException): "already plugged into port %(att_port_id)s") -# NOTE: on the client side, we often do not know all of the information -# that is known on the server, thus, we create separate exception for -# those scenarios -class PortInUseClient(QuantumException): - message = _("Unable to complete operation on port %(port_id)s " \ - "for network %(net_id)s. An attachment " \ - "is plugged into the logical port.") +class QuantumClientException(QuantumException): + + def __init__(self, **kwargs): + self.message = kwargs.get('message', "") + super(QuantumClientException, self).__init__(**kwargs) -class AlreadyAttachedClient(QuantumException): - message = _("Unable to plug the attachment %(att_id)s into port " \ - "%(port_id)s for network %(net_id)s. The attachment is " \ - "already plugged into another port.") +# NOTE: on the client side, we use different exception types in order +# to allow client library users to handle server exceptions in try...except +# blocks. The actual error message is the one generated on the server side +class NetworkNotFoundClient(QuantumClientException): + pass -class MalformedRequestBody(QuantumException): - message = _("Malformed request body: %(reason)s") +class PortNotFoundClient(QuantumClientException): + pass class MalformedResponseBody(QuantumException): message = _("Malformed response body: %(reason)s") -class Duplicate(Error): +class StateInvalidClient(QuantumClientException): pass -class NotAuthorized(Error): +class NetworkInUseClient(QuantumClientException): + pass + + +class PortInUseClient(QuantumClientException): + pass + + +class AlreadyAttachedClient(QuantumClientException): + pass + + +class NotAuthorized(QuantumClientException): + pass + + +class QuantumCLIError(QuantumClientException): + """ Exception raised when command line parsing fails """ + pass + + +class BadInputError(Exception): + """Error resulting from a client sending bad input to a server""" + pass + + +class ProcessExecutionError(IOError): + def __init__(self, stdout=None, stderr=None, exit_code=None, cmd=None, + description=None): + if description is None: + description = "Unexpected error while running command." + if exit_code is None: + exit_code = '-' + message = "%s\nCommand: %s\nExit code: %s\nStdout: %r\nStderr: %r" % ( + description, cmd, exit_code, stdout, stderr) + IOError.__init__(self, message) + + +class Error(Exception): + def __init__(self, message=None): + super(Error, self).__init__(message) + + +class ApiError(Error): + def __init__(self, message='Unknown', code='Unknown'): + self.message = message + self.code = code + super(ApiError, self).__init__('%s: %s' % (code, message)) + + +class MalformedRequestBody(QuantumException): + message = _("Malformed request body: %(reason)s") + + +class Duplicate(Error): pass @@ -154,11 +183,6 @@ class InvalidContentType(Invalid): message = _("Invalid content type %(content_type)s.") -class BadInputError(Exception): - """Error resulting from a client sending bad input to a server""" - pass - - class MissingArgumentError(Error): pass diff --git a/quantum/common/utils.py b/quantum/common/utils.py index 1d425a033..acd72b737 100644 --- a/quantum/common/utils.py +++ b/quantum/common/utils.py @@ -22,7 +22,6 @@ import ConfigParser import datetime -import exceptions as exception import inspect import logging import os @@ -44,6 +43,18 @@ from quantum.common import exceptions as exception from quantum.common.exceptions import ProcessExecutionError +def env(*vars, **kwargs): + """ + returns the first environment variable set + if none are non-empty, defaults to '' or keyword arg default + """ + for v in vars: + value = os.environ.get(v) + if value: + return value + return kwargs.get('default', '') + + def import_class(import_str): """Returns a class from a string including module and class.""" mod_str, _sep, class_str = import_str.rpartition('.')