diff --git a/bin/cli b/bin/cli new file mode 100755 index 0000000000..6fd3e3ad54 --- /dev/null +++ b/bin/cli @@ -0,0 +1,165 @@ +#!/usr/bin/env python +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 Nicira Networks, Inc. +# Copyright 2011 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 +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# @author: Somik Behera, Nicira Networks, Inc. +# @author: Brad Hall, Nicira Networks, Inc. +# @author: Salvatore Orlando, Citrix + +import Cheetah.Template as cheetah_template +import gettext +import logging +import logging.handlers +import os +import sys + +from optparse import OptionParser + + +possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]), + os.pardir, + os.pardir)) +if os.path.exists(os.path.join(possible_topdir, 'quantum', '__init__.py')): + sys.path.insert(0, possible_topdir) + +gettext.install('quantum', unicode=1) + +from quantum import cli_lib +from quantum.client import Client + +#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') +FORMAT = 'json' +commands = { + "list_nets": { + "func": cli_lib.list_nets, + "args": ["tenant-id"]}, + "create_net": { + "func": cli_lib.create_net, + "args": ["tenant-id", "net-name"]}, + "delete_net": { + "func": cli_lib.delete_net, + "args": ["tenant-id", "net-id"]}, + "show_net": { + "func": cli_lib.show_net, + "args": ["tenant-id", "net-id"]}, + "rename_net": { + "func": cli_lib.rename_net, + "args": ["tenant-id", "net-id", "new-name"]}, + "list_ports": { + "func": cli_lib.list_ports, + "args": ["tenant-id", "net-id"]}, + "create_port": { + "func": cli_lib.create_port, + "args": ["tenant-id", "net-id"]}, + "delete_port": { + "func": cli_lib.delete_port, + "args": ["tenant-id", "net-id", "port-id"]}, + "set_port_state": { + "func": cli_lib.set_port_state, + "args": ["tenant-id", "net-id", "port-id", "new_state"]}, + "show_port": { + "func": cli_lib.show_port, + "args": ["tenant-id", "net-id", "port-id"]}, + "plug_iface": { + "func": cli_lib.plug_iface, + "args": ["tenant-id", "net-id", "port-id", "iface-id"]}, + "unplug_iface": { + "func": cli_lib.unplug_iface, + "args": ["tenant-id", "net-id", "port-id"]}, } + + +def help(): + print "\nCommands:" + for k in commands.keys(): + print " %s %s" % (k, + " ".join(["<%s>" % y for y in commands[k]["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 + return args + + +if __name__ == "__main__": + usagestr = "Usage: %prog [OPTIONS] [args]" + parser = OptionParser(usage=usagestr) + parser.add_option("-H", "--host", dest="host", + type="string", default="127.0.0.1", help="ip address of api host") + parser.add_option("-p", "--port", dest="port", + type="int", default=9696, help="api poort") + parser.add_option("-s", "--ssl", dest="ssl", + action="store_true", default=False, help="use ssl") + parser.add_option("-v", "--verbose", dest="verbose", + action="store_true", default=False, help="turn on verbose logging") + parser.add_option("-f", "--logfile", dest="logfile", + type="string", default="syslog", help="log file path") + 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')) + else: + LOG.addHandler(logging.handlers.WatchedFileHandler(options.logfile)) + # Set permissions on log file + os.chmod(options.logfile, 0644) + + if len(args) < 1: + parser.print_help() + help() + sys.exit(1) + + cmd = args[0] + if cmd not in commands.keys(): + LOG.error("Unknown command: %s" % cmd) + help() + sys.exit(1) + + args = build_args(cmd, commands[cmd]["args"], args[1:]) + 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) + commands[cmd]["func"](client, *args) + + LOG.info("Command execution completed") + sys.exit(0) diff --git a/quantum/api/attachments.py b/quantum/api/attachments.py index 14f4d94058..e40aabba46 100644 --- a/quantum/api/attachments.py +++ b/quantum/api/attachments.py @@ -63,6 +63,7 @@ class Controller(common.QuantumController): except exc.HTTPError as e: return faults.Fault(e) try: + LOG.debug("PLUGGING INTERFACE:%s", request_params['id']) self._plugin.plug_interface(tenant_id, network_id, id, request_params['id']) return exc.HTTPNoContent() diff --git a/quantum/api/views/networks.py b/quantum/api/views/networks.py index 4b2e77df28..0de4e7eb93 100644 --- a/quantum/api/views/networks.py +++ b/quantum/api/views/networks.py @@ -23,7 +23,7 @@ def get_view_builder(req): class ViewBuilder(object): - def __init__(self, base_url): + def __init__(self, base_url=None): """ :param base_url: url of the root wsgi application """ diff --git a/quantum/api/views/ports.py b/quantum/api/views/ports.py index 164726eb3c..dd3f18ea23 100644 --- a/quantum/api/views/ports.py +++ b/quantum/api/views/ports.py @@ -23,7 +23,7 @@ def get_view_builder(req): class ViewBuilder(object): - def __init__(self, base_url): + def __init__(self, base_url=None): """ :param base_url: url of the root wsgi application """ diff --git a/quantum/cli.py b/quantum/cli.py deleted file mode 100644 index 4c40ab2a89..0000000000 --- a/quantum/cli.py +++ /dev/null @@ -1,392 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 Nicira Networks, Inc. -# Copyright 2011 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 -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -# @author: Somik Behera, Nicira Networks, Inc. -# @author: Brad Hall, Nicira Networks, Inc. - -import httplib -import logging as LOG -import json -import socket -import sys -import urllib - -from manager import QuantumManager -from optparse import OptionParser -from client import Client - -FORMAT = "json" - -### -- Core CLI functions - - -def list_nets(manager, *args): - tenant_id = args[0] - networks = manager.get_all_networks(tenant_id) - print "Virtual Networks on Tenant:%s\n" % tenant_id - for net in networks: - id = net["net-id"] - name = net["net-name"] - print "\tNetwork ID:%s \n\tNetwork Name:%s \n" % (id, name) - - -def api_list_nets(client, *args): - tenant_id = args[0] - res = client.list_networks() - LOG.debug(res) - print "Virtual Networks on Tenant:%s\n" % tenant_id - for n in res["networks"]: - net_id = n["id"] - print "\tNetwork ID:%s\n" % (net_id) - # TODO(bgh): we should make this call pass back the name too - # name = n["net-name"] - # LOG.info("\tNetwork ID:%s \n\tNetwork Name:%s \n" % (id, name)) - - -def create_net(manager, *args): - tid, name = args - new_net_id = manager.create_network(tid, name) - print "Created a new Virtual Network with ID:%s\n" % new_net_id - - -def api_create_net(client, *args): - tid, name = args - data = {'network': {'net-name': '%s' % name}} - res = client.create_network(data) - LOG.debug(res) - nid = None - try: - nid = res["network"]["id"] - except Exception, e: - print "Failed to create network" - # TODO(bgh): grab error details from ws request result - return - print "Created a new Virtual Network with ID:%s\n" % nid - - -def delete_net(manager, *args): - tid, nid = args - manager.delete_network(tid, nid) - print "Deleted Virtual Network with ID:%s" % nid - - -def api_delete_net(client, *args): - tid, nid = args - try: - res = client.delete_network(nid) - print "Deleted Virtual Network with ID:%s" % nid - except Exception, e: - print "Failed to delete network" - LOG.error("Failed to delete network: %s" % e) - - -def detail_net(manager, *args): - tid, nid = args - iface_list = manager.get_network_details(tid, nid) - print "Remote Interfaces on Virtual Network:%s\n" % nid - for iface in iface_list: - print "\tRemote interface:%s" % iface - - -def api_detail_net(client, *args): - tid, nid = args - try: - res = client.show_network_details(nid)["network"] - except Exception, e: - LOG.error("Failed to get network details: %s" % e) - return - - try: - ports = client.list_ports(nid) - except Exception, e: - LOG.error("Failed to list ports: %s" % e) - return - - print "Network %s (%s)" % (res['name'], res['id']) - print "Remote Interfaces on Virtual Network:%s\n" % nid - for port in ports["ports"]: - pid = port["id"] - res = client.show_port_attachment(nid, pid) - LOG.debug(res) - remote_iface = res["attachment"]["id"] - print "\tRemote interface:%s" % remote_iface - - -def rename_net(manager, *args): - tid, nid, name = args - manager.rename_network(tid, nid, name) - print "Renamed Virtual Network with ID:%s" % nid - - -def api_rename_net(client, *args): - tid, nid, name = args - data = {'network': {'name': '%s' % name}} - try: - res = client.update_network(nid, data) - except Exception, e: - LOG.error("Failed to rename network %s: %s" % (nid, e)) - return - LOG.debug(res) - print "Renamed Virtual Network with ID:%s" % nid - - -def list_ports(manager, *args): - tid, nid = args - ports = manager.get_all_ports(tid, nid) - print "Ports on Virtual Network:%s\n" % nid - for port in ports: - print "\tVirtual Port:%s" % port["port-id"] - - -def api_list_ports(client, *args): - tid, nid = args - try: - ports = client.list_ports(nid) - except Exception, e: - LOG.error("Failed to list ports: %s" % e) - return - - LOG.debug(ports) - print "Ports on Virtual Network:%s\n" % nid - for port in ports["ports"]: - print "\tVirtual Port:%s" % port["id"] - - -def create_port(manager, *args): - tid, nid = args - new_port = manager.create_port(tid, nid) - print "Created Virtual Port:%s " \ - "on Virtual Network:%s" % (new_port, nid) - - -def api_create_port(client, *args): - tid, nid = args - try: - res = client.create_port(nid) - except Exception, e: - LOG.error("Failed to create port: %s" % e) - return - new_port = res["port"]["id"] - print "Created Virtual Port:%s " \ - "on Virtual Network:%s" % (new_port, nid) - - -def delete_port(manager, *args): - tid, nid, pid = args - manager.delete_port(tid, nid, pid) - LOG.info("Deleted Virtual Port:%s " \ - "on Virtual Network:%s" % (pid, nid)) - - -def api_delete_port(client, *args): - tid, nid, pid = args - try: - res = client.delete_port(nid, pid) - except Exception, e: - LOG.error("Failed to delete port: %s" % e) - return - LOG.info("Deleted Virtual Port:%s " \ - "on Virtual Network:%s" % (pid, nid)) - print "Deleted Virtual Port:%s " \ - "on Virtual Network:%s" % (pid, nid) - - -def detail_port(manager, *args): - tid, nid, pid = args - port_detail = manager.get_port_details(tid, nid, pid) - print "Virtual Port:%s on Virtual Network:%s " \ - "contains remote interface:%s" % (pid, nid, port_detail) - - -def api_detail_port(client, *args): - tid, nid, pid = args - try: - port = client.show_port_details(nid, pid)["port"] - att = client.show_port_attachment(nid, pid) - except Exception, e: - LOG.error("Failed to get port details: %s" % e) - return - - id = port['id'] - interface_id = att['id'] - LOG.debug(port) - print "Virtual Port:%s on Virtual Network:%s " \ - "contains remote interface:%s" % (pid, nid, interface_id) - - -def plug_iface(manager, *args): - tid, nid, pid, vid = args - manager.plug_interface(tid, nid, pid, vid) - print "Plugged remote interface:%s " \ - "into Virtual Network:%s" % (vid, nid) - - -def api_plug_iface(client, *args): - tid, nid, pid, vid = args - try: - data = {'attachment': {'id': '%s' % vid}} - res = client.attach_resource(nid, pid, data) - except Exception, e: - LOG.error("Failed to plug iface \"%s\" to port \"%s\": %s" % (vid, - pid, e)) - return - LOG.debug(res) - print "Plugged interface \"%s\" to port:%s on network:%s" % (vid, pid, nid) - - -def unplug_iface(manager, *args): - tid, nid, pid = args - manager.unplug_interface(tid, nid, pid) - print "UnPlugged remote interface " \ - "from Virtual Port:%s Virtual Network:%s" % (pid, nid) - - -def api_unplug_iface(client, *args): - tid, nid, pid = args - try: - res = client.detach_resource(nid, pid) - except Exception, e: - LOG.error("Failed to unplug iface from port \"%s\": %s" % (pid, e)) - return - LOG.debug(res) - print "Unplugged interface from port:%s on network:%s" % (pid, nid) - - -commands = { - "list_nets": { - "func": list_nets, - "api_func": api_list_nets, - "args": ["tenant-id"]}, - "create_net": { - "func": create_net, - "api_func": api_create_net, - "args": ["tenant-id", "net-name"]}, - "delete_net": { - "func": delete_net, - "api_func": api_delete_net, - "args": ["tenant-id", "net-id"]}, - "detail_net": { - "func": detail_net, - "api_func": api_detail_net, - "args": ["tenant-id", "net-id"]}, - "rename_net": { - "func": rename_net, - "api_func": api_rename_net, - "args": ["tenant-id", "net-id", "new-name"]}, - "list_ports": { - "func": list_ports, - "api_func": api_list_ports, - "args": ["tenant-id", "net-id"]}, - "create_port": { - "func": create_port, - "api_func": api_create_port, - "args": ["tenant-id", "net-id"]}, - "delete_port": { - "func": delete_port, - "api_func": api_delete_port, - "args": ["tenant-id", "net-id", "port-id"]}, - "detail_port": { - "func": detail_port, - "api_func": api_detail_port, - "args": ["tenant-id", "net-id", "port-id"]}, - "plug_iface": { - "func": plug_iface, - "api_func": api_plug_iface, - "args": ["tenant-id", "net-id", "port-id", "iface-id"]}, - "unplug_iface": { - "func": unplug_iface, - "api_func": api_unplug_iface, - "args": ["tenant-id", "net-id", "port-id"]}, } - - -def help(): - print "\nCommands:" - for k in commands.keys(): - print " %s %s" % (k, - " ".join(["<%s>" % y for y in commands[k]["args"]])) - - -def build_args(cmd, cmdargs, arglist): - args = [] - orig_arglist = arglist[:] - try: - for x in cmdargs: - args.append(arglist[0]) - del arglist[0] - except Exception, e: - 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 - return args - - -if __name__ == "__main__": - usagestr = "Usage: %prog [OPTIONS] [args]" - parser = OptionParser(usage=usagestr) - parser.add_option("-l", "--load-plugin", dest="load_plugin", - action="store_true", default=False, - help="Load plugin directly instead of using WS API") - parser.add_option("-H", "--host", dest="host", - type="string", default="127.0.0.1", help="ip address of api host") - parser.add_option("-p", "--port", dest="port", - type="int", default=9696, help="api poort") - parser.add_option("-s", "--ssl", dest="ssl", - action="store_true", default=False, help="use ssl") - parser.add_option("-v", "--verbose", dest="verbose", - action="store_true", default=False, help="turn on verbose logging") - - options, args = parser.parse_args() - - if options.verbose: - LOG.basicConfig(level=LOG.DEBUG) - else: - LOG.basicConfig(level=LOG.WARN) - - if len(args) < 1: - parser.print_help() - help() - sys.exit(1) - - cmd = args[0] - if cmd not in commands.keys(): - LOG.error("Unknown command: %s" % cmd) - help() - sys.exit(1) - - args = build_args(cmd, commands[cmd]["args"], args[1:]) - if not args: - sys.exit(1) - LOG.debug("Executing command \"%s\" with args: %s" % (cmd, args)) - if not options.load_plugin: - client = Client(options.host, options.port, options.ssl, - args[0], FORMAT) - if "api_func" not in commands[cmd]: - LOG.error("API version of \"%s\" is not yet implemented" % cmd) - sys.exit(1) - commands[cmd]["api_func"](client, *args) - else: - quantum = QuantumManager() - manager = quantum.get_plugin() - commands[cmd]["func"](manager, *args) - sys.exit(0) diff --git a/quantum/cli_lib.py b/quantum/cli_lib.py new file mode 100755 index 0000000000..bfee9ac43c --- /dev/null +++ b/quantum/cli_lib.py @@ -0,0 +1,228 @@ +#!/usr/bin/env python +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 Nicira Networks, Inc. +# Copyright 2011 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 +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# @author: Somik Behera, Nicira Networks, Inc. +# @author: Brad Hall, Nicira Networks, Inc. +# @author: Salvatore Orlando, Citrix + +import Cheetah.Template as cheetah_template +import logging +import os +import sys + +FORMAT = "json" +CLI_TEMPLATE = "cli_output.template" +LOG = logging.getLogger('quantum.cli_lib') + + +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 + if ex.args and isinstance(ex.args[-1][0], dict): + status_code = ex.args[-1][0].get('status_code', None) + message = ex.args[-1][0].get('message', None) + msg_1 = "Command failed with error code: %s" \ + % (status_code or '') + msg_2 = "Error message:%s" % (message or '') + LOG.exception(msg_1 + "-" + msg_2) + print msg_1 + print msg_2 + + +def prepare_output(cmd, tenant_id, response): + """ Fills a cheetah template with the response """ + #add command and tenant to response for output generation + LOG.debug("Preparing output for response:%s", response) + response['cmd'] = cmd + response['tenant_id'] = tenant_id + template_path = os.path.join(os.path.dirname(__file__), CLI_TEMPLATE) + template_file = open(template_path).read() + output = str(cheetah_template.Template(template_file, + searchList=response)) + 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 + + +def create_net(client, *args): + tenant_id, name = 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)) + print output + except Exception as ex: + _handle_exception(ex) + + +def delete_net(client, *args): + tenant_id, network_id = 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)) + print output + except Exception as ex: + _handle_exception(ex) + + +def show_net(client, *args): + tenant_id, network_id = 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)) + print output + except Exception as ex: + _handle_exception(ex) + + +def rename_net(client, *args): + tenant_id, network_id, name = args + data = {'network': {'name': '%s' % name}} + try: + client.update_network(network_id, data) + LOG.debug("Operation 'update_network' executed.") + # Response has no body. Use data for populating output + data['network']['id'] = network_id + output = prepare_output("rename_net", tenant_id, data) + print output + except Exception as ex: + _handle_exception(ex) + + +def list_ports(client, *args): + tenant_id, network_id = 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) + print output + except Exception as ex: + _handle_exception(ex) + + +def create_port(client, *args): + tenant_id, network_id = 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, + dict(network_id=network_id, + port_id=new_port_id)) + print output + except Exception as ex: + _handle_exception(ex) + + +def delete_port(client, *args): + tenant_id, network_id, port_id = args + try: + client.delete_port(network_id, port_id) + LOG.debug("Operation 'delete_port' executed.") + output = prepare_output("delete_port", tenant_id, + dict(network_id=network_id, + port_id=port_id)) + print output + except Exception as ex: + _handle_exception(ex) + return + + +def show_port(client, *args): + tenant_id, network_id, port_id = args + try: + port = client.show_port_details(network_id, port_id)["port"] + LOG.debug("Operation 'list_port_details' executed.") + #NOTE(salvatore-orland): current API implementation does not + #return attachment with GET operation on port. Once API alignment + #branch is merged, update client to use the detail action. + # (danwent) Until then, just make additonal webservice call. + attach = client.show_port_attachment(network_id, port_id)['attachment'] + if "id" in attach: + port['attachment'] = attach['id'] + else: + port['attachment'] = '' + output = prepare_output("show_port", tenant_id, + dict(network_id=network_id, + port=port)) + print output + except Exception as ex: + _handle_exception(ex) + + +def set_port_state(client, *args): + tenant_id, network_id, port_id, new_state = args + data = {'port': {'state': '%s' % new_state}} + try: + client.set_port_state(network_id, port_id, data) + LOG.debug("Operation 'set_port_state' executed.") + # Response has no body. Use data for populating output + data['network_id'] = network_id + data['port']['id'] = port_id + output = prepare_output("set_port_state", tenant_id, data) + print output + except Exception as ex: + _handle_exception(ex) + + +def plug_iface(client, *args): + tenant_id, network_id, port_id, attachment = args + try: + data = {'attachment': {'id': '%s' % attachment}} + client.attach_resource(network_id, port_id, data) + LOG.debug("Operation 'attach_resource' executed.") + output = prepare_output("plug_iface", tenant_id, + dict(network_id=network_id, + port_id=port_id, + attachment=attachment)) + print output + except Exception as ex: + _handle_exception(ex) + + +def unplug_iface(client, *args): + tenant_id, network_id, port_id = args + try: + client.detach_resource(network_id, port_id) + LOG.debug("Operation 'detach_resource' executed.") + output = prepare_output("unplug_iface", tenant_id, + dict(network_id=network_id, + port_id=port_id)) + print output + except Exception as ex: + _handle_exception(ex) diff --git a/quantum/cli_output.template b/quantum/cli_output.template new file mode 100644 index 0000000000..107dceb93c --- /dev/null +++ b/quantum/cli_output.template @@ -0,0 +1,56 @@ +## Cheetah template for cli output +#if $cmd == 'list_nets' +Virtual Networks for Tenant $tenant_id +#for $network in $networks + Network ID: $network.id +#end for +#elif $cmd == 'create_net' +Created a new Virtual Network with ID: $network_id +for Tenant $tenant_id +#elif $cmd == 'delete_net' +Deleted Virtual Network with ID: $network_id +for Tenant $tenant_id +#elif $cmd == 'show_net' +Network ID: $network.id +Network Name: $network.name +for Tenant: $tenant_id +#elif $cmd == 'rename_net' +Renamed Virtual Network with ID: $network.id +New name is: $network.name +for Tenant $tenant_id, +#elif $cmd == 'list_ports' +Ports on Virtual Network: $network_id +for Tenant: $tenant_id +#for $port in $ports + Logical Port: $port.id +#end for +#elif $cmd == 'create_port' +Created new Logical Port with ID: $port_id +on Virtual Network: $network_id +for Tenant: $tenant_id +#elif $cmd == 'delete_port' +Deleted Logical Port with ID: $port_id +on Virtual Network: $network_id +for Tenant: $tenant_id +#elif $cmd == 'set_port_state' +Updated state for Logical Port with ID: $port.id +new state is: $port.state +on Virtual Network: $network_id +for tenant: $tenant_id +#elif $cmd == 'show_port' +Logical Port ID: $port.id +administrative State: $port.state +interface: $port.attachment +on Virtual Network: $network_id +for Tenant: $tenant_id +#elif $cmd == 'plug_iface' +Plugged interface $attachment +into Logical Port: $port_id +on Virtual Network: $network_id +for Tenant: $tenant_id +#elif $cmd == 'unplug_iface' +Unplugged interface from Logical Port: $port_id +on Virtual Network: $network_id +for Tenant: $tenant_id +#end if + diff --git a/quantum/client.py b/quantum/client.py index c75b5a243e..92c1be23d1 100644 --- a/quantum/client.py +++ b/quantum/client.py @@ -16,12 +16,15 @@ # under the License. # @author: Tyler Smith, Cisco Systems +import logging import httplib import socket import urllib -from quantum.common.wsgi import Serializer -from quantum.common import exceptions +from quantum.common import exceptions +from quantum.common.wsgi import Serializer + +LOG = logging.getLogger('quantum.client') EXCEPTIONS = { 400: exceptions.BadInputError, 401: exceptions.NotAuthorized, @@ -60,6 +63,17 @@ class Client(object): """A base client class - derived from Glance.BaseClient""" + #Metadata for deserializing xml + _serialization_metadata = { + "application/xml": { + "attributes": { + "network": ["id", "name"], + "port": ["id", "state"], + "attachment": ["id"]}, + "plurals": {"networks": "network", + "ports": "port"}}, + } + # Action query strings networks_path = "/networks" network_path = "/networks/%s" @@ -105,6 +119,17 @@ class Client(object): else: return httplib.HTTPConnection + def _send_request(self, conn, method, action, body, headers): + # Salvatore: Isolating this piece of code in its own method to + # facilitate stubout for testing + if self.logger: + self.logger.debug("Quantum Client Request:\n" \ + + method + " " + action + "\n") + if body: + self.logger.debug(body) + conn.request(method, action, body, headers) + return conn.getresponse() + def do_request(self, method, action, body=None, headers=None, params=None): """ @@ -119,7 +144,7 @@ class Client(object): to action """ - + LOG.debug("Client issuing request: %s", action) # Ensure we have a tenant id if not self.tenant: raise Exception("Tenant ID not set") @@ -131,7 +156,6 @@ class Client(object): if type(params) is dict: action += '?' + urllib.urlencode(params) - if body: body = self.serialize(body) @@ -139,7 +163,6 @@ class Client(object): connection_type = self.get_connection_type() headers = headers or {"Content-Type": "application/%s" % self.format} - # 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) @@ -148,15 +171,7 @@ class Client(object): conn = connection_type(self.host, self.port, **certs) else: conn = connection_type(self.host, self.port) - - if self.logger: - self.logger.debug("Quantum Client Request:\n" \ - + method + " " + action + "\n") - if body: - self.logger.debug(body) - - conn.request(method, action, body, headers) - res = conn.getresponse() + res = self._send_request(conn, method, action, body, headers) status_code = self.get_status_code(res) data = res.read() @@ -170,13 +185,21 @@ 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]() - raise Exception("Server returned error: %s" % res.read()) - + # 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 except (socket.error, IOError), e: - raise Exception("Unable to connect to " - "server. Got error: %s" % e) + msg = "Unable to connect to server. Got error: %s" % e + LOG.exception(msg) + raise Exception(msg) def get_status_code(self, response): """ @@ -205,9 +228,10 @@ class Client(object): """ Deserializes a an xml or json string into a dictionary """ - if status_code == 202: + if status_code in (202, 204): return data - return Serializer().deserialize(data, self.content_type()) + return Serializer(self._serialization_metadata).\ + deserialize(data, self.content_type()) def content_type(self, format=None): """ diff --git a/tests/unit/client_tools/__init__.py b/tests/unit/client_tools/__init__.py new file mode 100644 index 0000000000..848908a953 --- /dev/null +++ b/tests/unit/client_tools/__init__.py @@ -0,0 +1,15 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. diff --git a/tests/unit/client_tools/stubs.py b/tests/unit/client_tools/stubs.py new file mode 100644 index 0000000000..081436bd1a --- /dev/null +++ b/tests/unit/client_tools/stubs.py @@ -0,0 +1,65 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +""" Stubs for client tools unit tests """ + + +from quantum import api as server +from tests.unit import testlib_api + + +class FakeStdout: + + def __init__(self): + self.content = [] + + def write(self, text): + self.content.append(text) + + def make_string(self): + result = '' + for line in self.content: + result = result + line + return result + + +class FakeHTTPConnection: + """ stub HTTP connection class for CLI testing """ + def __init__(self, _1, _2): + # Ignore host and port parameters + self._req = None + options = \ + dict(plugin_provider='quantum.plugins.SamplePlugin.FakePlugin') + self._api = server.APIRouterV01(options) + + def request(self, method, action, body, headers): + # TODO: remove version prefix from action! + parts = action.split('/', 2) + path = '/' + parts[2] + self._req = testlib_api.create_request(path, body, "application/json", + method) + + def getresponse(self): + res = self._req.get_response(self._api) + + def _fake_read(): + """ Trick for making a webob.Response look like a + httplib.Response + + """ + return res.body + + setattr(res, 'read', _fake_read) + return res diff --git a/tests/unit/test_cli.py b/tests/unit/test_cli.py new file mode 100644 index 0000000000..8fe388c6e5 --- /dev/null +++ b/tests/unit/test_cli.py @@ -0,0 +1,420 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010-2011 ???? +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# @author: Salvatore Orlando, Citrix Systems + +""" Module containing unit tests for Quantum + command line interface + +""" + + +import logging +import sys +import unittest + +from quantum import api as server +from quantum import cli_lib as cli +from quantum.client import Client +from quantum.db import api as db +from tests.unit.client_tools import stubs as client_stubs + +LOG = logging.getLogger('quantum.tests.test_cli') +FORMAT = 'json' + + +class CLITest(unittest.TestCase): + + def setUp(self): + """Prepare the test environment""" + options = {} + options['plugin_provider'] = 'quantum.plugins.SamplePlugin.FakePlugin' + self.api = server.APIRouterV01(options) + + self.tenant_id = "test_tenant" + self.network_name_1 = "test_network_1" + self.network_name_2 = "test_network_2" + # Prepare client and plugin manager + self.client = Client(tenant=self.tenant_id, format=FORMAT, + testingStub=client_stubs.FakeHTTPConnection) + # Redirect stdout + self.fake_stdout = client_stubs.FakeStdout() + sys.stdout = self.fake_stdout + + def tearDown(self): + """Clear the test environment""" + db.clear_db() + sys.stdout = sys.__stdout__ + + 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] + # Fill CLI template + output = cli.prepare_output('list_nets', self.tenant_id, + dict(networks=networks)) + # Verify! + # Must add newline at the end to match effect of print call + self.assertEquals(self.fake_stdout.make_string(), output + '\n') + + def _verify_create_network(self): + # Verification - get raw result from db + nw_list = db.network_list(self.tenant_id) + if len(nw_list) != 1: + 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)) + # Verify! + # Must add newline at the end to match effect of print call + self.assertEquals(self.fake_stdout.make_string(), output + '\n') + + def _verify_delete_network(self, network_id): + # Verification - get raw result from db + nw_list = db.network_list(self.tenant_id) + 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)) + # Verify! + # Must add newline at the end to match effect of print call + self.assertEquals(self.fake_stdout.make_string(), output + '\n') + + def _verify_rename_network(self): + # 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} + # Fill CLI template + output = cli.prepare_output('rename_net', self.tenant_id, + dict(network=network_data)) + # Verify! + # Must add newline at the end to match effect of print call + self.assertEquals(self.fake_stdout.make_string(), output + '\n') + + 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) + # Fill CLI template + output = cli.prepare_output('show_net', self.tenant_id, + dict(network=network)) + # Verify! + # Must add newline at the end to match effect of print call + self.assertEquals(self.fake_stdout.make_string(), output + '\n') + + def _verify_list_ports(self, network_id): + # Verification - get raw result from db + port_list = db.port_list(network_id) + 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, + dict(network_id=network_id, + ports=ports)) + # Verify! + # Must add newline at the end to match effect of print call + self.assertEquals(self.fake_stdout.make_string(), output + '\n') + + def _verify_create_port(self, network_id): + # Verification - get raw result from db + port_list = db.port_list(network_id) + if len(port_list) != 1: + self.fail("No port created") + port_id = port_list[0].uuid + # Fill CLI template + output = cli.prepare_output('create_port', self.tenant_id, + dict(network_id=network_id, + port_id=port_id)) + # Verify! + # Must add newline at the end to match effect of print call + self.assertEquals(self.fake_stdout.make_string(), output + '\n') + + def _verify_delete_port(self, network_id, port_id): + # Verification - get raw result from db + port_list = db.port_list(network_id) + 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, + dict(network_id=network_id, + port_id=port_id)) + # Verify! + # Must add newline at the end to match effect of print call + self.assertEquals(self.fake_stdout.make_string(), output + '\n') + + def _verify_set_port_state(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} + # Fill CLI template + output = cli.prepare_output('set_port_state', self.tenant_id, + dict(network_id=network_id, + port=port_data)) + # Verify! + # Must add newline at the end to match effect of print call + self.assertEquals(self.fake_stdout.make_string(), output + '\n') + + def _verify_show_port(self, network_id, port_id): + # Verification - get raw result from db + # 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': ""} + 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, + dict(network_id=network_id, + port=port_data)) + # Verify! + # Must add newline at the end to match effect of print call + self.assertEquals(self.fake_stdout.make_string(), output + '\n') + + def _verify_plug_iface(self, network_id, port_id): + # 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, + dict(network_id=network_id, + port_id=port['uuid'], + attachment=port['interface_id'])) + # Verify! + # Must add newline at the end to match effect of print call + self.assertEquals(self.fake_stdout.make_string(), output + '\n') + + def _verify_unplug_iface(self, network_id, port_id): + # 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, + dict(network_id=network_id, + port_id=port['uuid'])) + # Verify! + # Must add newline at the end to match effect of print call + self.assertEquals(self.fake_stdout.make_string(), output + '\n') + + def test_list_networks(self): + try: + # Pre-populate data for testing using db api + 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) + except: + LOG.exception("Exception caught: %s", sys.exc_info()) + self.fail("test_list_networks failed due to an exception") + + LOG.debug("Operation completed. Verifying result") + LOG.debug(self.fake_stdout.content) + self._verify_list_networks() + + def test_create_network(self): + try: + cli.create_net(self.client, self.tenant_id, "test") + except: + LOG.exception("Exception caught: %s", sys.exc_info()) + self.fail("test_create_network failed due to an exception") + + LOG.debug("Operation completed. Verifying result") + LOG.debug(self.fake_stdout.content) + self._verify_create_network() + + def test_delete_network(self): + 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) + except: + LOG.exception("Exception caught: %s", sys.exc_info()) + self.fail("test_delete_network failed due to an exception") + + LOG.debug("Operation completed. Verifying result") + LOG.debug(self.fake_stdout.content) + self._verify_delete_network(network_id) + + def test_show_network(self): + 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']) + except: + LOG.exception("Exception caught: %s", sys.exc_info()) + self.fail("test_detail_network failed due to an exception") + + LOG.debug("Operation completed. Verifying result") + LOG.debug(self.fake_stdout.content) + self._verify_show_network() + + def test_rename_network(self): + try: + net = db.network_create(self.tenant_id, self.network_name_1) + network_id = net['uuid'] + cli.rename_net(self.client, self.tenant_id, + network_id, self.network_name_2) + except: + LOG.exception("Exception caught: %s", sys.exc_info()) + self.fail("test_rename_network failed due to an exception") + + LOG.debug("Operation completed. Verifying result") + LOG.debug(self.fake_stdout.content) + self._verify_rename_network() + + def test_list_ports(self): + try: + # Pre-populate data for testing using db api + net = db.network_create(self.tenant_id, self.network_name_1) + network_id = net['uuid'] + db.port_create(network_id) + db.port_create(network_id) + cli.list_ports(self.client, self.tenant_id, network_id) + except: + LOG.exception("Exception caught: %s", sys.exc_info()) + self.fail("test_list_ports failed due to an exception") + + LOG.debug("Operation completed. Verifying result") + LOG.debug(self.fake_stdout.content) + self._verify_list_ports(network_id) + + def test_create_port(self): + network_id = None + try: + # 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) + except: + LOG.exception("Exception caught: %s", sys.exc_info()) + self.fail("test_create_port failed due to an exception") + + LOG.debug("Operation completed. Verifying result") + LOG.debug(self.fake_stdout.content) + self._verify_create_port(network_id) + + def test_delete_port(self): + network_id = None + port_id = None + try: + # Pre-populate data for testing using db api + net = db.network_create(self.tenant_id, self.network_name_1) + 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) + except: + LOG.exception("Exception caught: %s", sys.exc_info()) + self.fail("test_delete_port failed due to an exception") + + LOG.debug("Operation completed. Verifying result") + LOG.debug(self.fake_stdout.content) + self._verify_delete_port(network_id, port_id) + + def test_set_port_state(self): + try: + net = db.network_create(self.tenant_id, self.network_name_1) + network_id = net['uuid'] + port = db.port_create(network_id) + port_id = port['uuid'] + # Default state is DOWN - change to ACTIVE. + cli.set_port_state(self.client, self.tenant_id, network_id, + port_id, 'ACTIVE') + except: + LOG.exception("Exception caught: %s", sys.exc_info()) + self.fail("test_set_port_state failed due to an exception") + + LOG.debug("Operation completed. Verifying result") + LOG.debug(self.fake_stdout.content) + self._verify_set_port_state(network_id, port_id) + + def test_show_port_no_attach(self): + network_id = None + port_id = None + try: + # Pre-populate data for testing using db api + net = db.network_create(self.tenant_id, self.network_name_1) + 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) + except: + LOG.exception("Exception caught: %s", sys.exc_info()) + self.fail("test_show_port_no_attach failed due to an exception") + + LOG.debug("Operation completed. Verifying result") + LOG.debug(self.fake_stdout.content) + self._verify_show_port(network_id, port_id) + + def test_show_port_with_attach(self): + network_id = None + port_id = None + iface_id = "flavor crystals" + try: + # Pre-populate data for testing using db api + net = db.network_create(self.tenant_id, self.network_name_1) + network_id = net['uuid'] + 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) + except: + LOG.exception("Exception caught: %s", sys.exc_info()) + self.fail("test_show_port_with_attach failed due to an exception") + + LOG.debug("Operation completed. Verifying result") + LOG.debug(self.fake_stdout.content) + self._verify_show_port(network_id, port_id) + + def test_plug_iface(self): + network_id = None + port_id = None + try: + # Load some data into the datbase + net = db.network_create(self.tenant_id, self.network_name_1) + 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") + except: + LOG.exception("Exception caught: %s", sys.exc_info()) + self.fail("test_plug_iface failed due to an exception") + + LOG.debug("Operation completed. Verifying result") + LOG.debug(self.fake_stdout.content) + self._verify_plug_iface(network_id, port_id) + + def test_unplug_iface(self): + network_id = None + port_id = None + try: + # Load some data into the datbase + net = db.network_create(self.tenant_id, self.network_name_1) + network_id = net['uuid'] + 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) + except: + LOG.exception("Exception caught: %s", sys.exc_info()) + self.fail("test_plug_iface failed due to an exception") + + LOG.debug("Operation completed. Verifying result") + LOG.debug(self.fake_stdout.content) + self._verify_unplug_iface(network_id, port_id) diff --git a/tools/pip-requires b/tools/pip-requires index 31b66c77fe..baa07210c1 100644 --- a/tools/pip-requires +++ b/tools/pip-requires @@ -1,5 +1,6 @@ eventlet>=0.9.12 Routes>=1.12.3 +Cheetah>=2.0.1 nose Paste PasteDeploy