diff --git a/README b/README new file mode 100644 index 0000000000..f3e973faac --- /dev/null +++ b/README @@ -0,0 +1,108 @@ +# -- Welcome! + + You have come across a cloud computing network fabric controller. It has + identified itself as "Quantum." It aims to tame your (cloud) networking! + +# -- Basics: + +1) Quantum REST API: Quantum supports a REST-ful programmatic interface to + manage your cloud networking fabric. + +2) Quantum Plugins: Quantum sports a plug-able architecture that allows + Quantum's REST API to be backed by various entities that can create a + cloud-class virtual networking fabric. The advantages of this plug-able + architecture is two-folds: + + a) Allows for ANY open-source project or commercial vendor to write a + Quantum plug-in. + + b) Allows Quantum users to not be tied down to a single Quantum + implementation and enables them to switch out a plug-in by simple editing a + config file - plugins.ini + +# -- Dependencies + + The following python packages are required to run quantum. These can be + installed using pip: + + eventlet>=0.9.12 + nose + Paste + PasteDeploy + pep8==0.5.0 + python-gflags + routes + simplejson + webob + webtest + +1) Install easy_install (there is probably a distribution specific package for +this) + +2) Install pip: + $ easy_install pip==dev +3) Install packages with pip: + $ pip install + +# -- Configuring Quantum plug-in + +1) Identify your desired plug-in. Choose a plugin from one of he options in + the quantum/plugins directory. + +2) Update plug-in configuration by editing the quantum/plugins.ini file and + modify "provider" property to point to the location of the Quantum plug-in. + It should specify the class path to the plugin and the class name (i.e. for + a plugin class MyPlugin in quantum/plugins/myplugin/myplugin.py the + provider would be: quantum.plugins.myplugin.myplugin.MyPlugin) + +3) Read the plugin specific README, this is usually found in the same + directory as your Quantum plug-in, and follow configuration instructions. + +# -- Launching the Quantum Service + +1) Start quantum using the following command [on the quantum service host]: +~/src/quantum$ PYTHONPATH=.:$PYTHONPATH python bin/quantum etc/quantum.conf + +# -- Making requests against the Quantum Service + +Please refer to sample Web Service client code in: + +../quantum/test_scripts/miniclient.py + +# -- CLI tools to program the Quantum-managed Cloud networking fabric + +Quantum comes with a programmatic CLI that is driven by the Quantum Web +Service. You can use the CLI by issuing the following command: + +~/src/quantum$ PYTHONPATH=.:$PYTHONPATH python quantum/cli.py + +This will show help all of the available commands. + +An example session looks like this: + +$ export TENANT=t1 +$ PYTHONPATH=. python quantum/cli.py -v create_net $TENANT network1 +Created a new Virtual Network with ID:e754e7c0-a8eb-40e5-861a-b182d30c3441 + +# -- Writing your own Quantum plug-in + +If you wish the write your own Quantum plugin, please refer to some concrete as +well as sample plugins available in: + +../quantum/quantum/plugins/.. directory. + +There are a few requirements to writing your own plugin: + +1) Your plugin should implement all methods defined in the + quantum/quantum_plugin_base.QuantumPluginBase class + +2) Copy your Quantum plug-in over to the quantum/quantum/plugins/.. directory + +3) The next step is to edit the plugins.ini file in the same directory + as QuantumPluginBase class and specify the location of your custom plugin + as the "provider" + +4) Launch the Quantum Service, and your plug-in is configured and ready to + manage a Cloud Networking Fabric. + + diff --git a/bin/quantum b/bin/quantum index 0913c31c05..48abd18ae9 100755 --- a/bin/quantum +++ b/bin/quantum @@ -33,9 +33,10 @@ if os.path.exists(os.path.join(possible_topdir, 'quantum', '__init__.py')): gettext.install('quantum', unicode=1) -from quantum import service +from quantum import service from quantum.common import config + def create_options(parser): """ Sets up the CLI and config-file options that may be @@ -58,4 +59,3 @@ if __name__ == '__main__': service.wait() except RuntimeError, e: sys.exit("ERROR: %s" % e) - diff --git a/etc/quantum.conf.test b/etc/quantum.conf.test index 3e532b04dd..b1c266246a 100644 --- a/etc/quantum.conf.test +++ b/etc/quantum.conf.test @@ -12,4 +12,4 @@ paste.app_factory = quantum.l2Network.service:app_factory bind_host = 0.0.0.0 # Port the bind the API server to -bind_port = 9696 \ No newline at end of file +bind_port = 9696 diff --git a/quantum/api/__init__.py b/quantum/api/__init__.py index 41bc6f5a3d..f00d7bcb2c 100644 --- a/quantum/api/__init__.py +++ b/quantum/api/__init__.py @@ -20,6 +20,7 @@ Quantum API controllers. """ import logging +import pprint import routes import webob.dec import webob.exc diff --git a/quantum/api/faults.py b/quantum/api/faults.py index 6f68f561e9..35f6c1073d 100644 --- a/quantum/api/faults.py +++ b/quantum/api/faults.py @@ -36,8 +36,7 @@ class Fault(webob.exc.HTTPException): 432: "portInUse", 440: "alreadyAttached", 470: "serviceUnavailable", - 471: "pluginFault" - } + 471: "pluginFault"} def __init__(self, exception): """Create a Fault for the given webob.exc.exception.""" diff --git a/quantum/api/networks.py b/quantum/api/networks.py index b959b527bd..bcaef0f9ff 100644 --- a/quantum/api/networks.py +++ b/quantum/api/networks.py @@ -45,10 +45,10 @@ class Controller(common.QuantumController): self._resource_name = 'network' super(Controller, self).__init__() - def index(self, req, tenant_id): + def index(self, request, tenant_id): """ Returns a list of network ids """ #TODO: this should be for a given tenant!!! - return self._items(req, tenant_id, net_detail=False) + return self._items(request, tenant_id, net_detail=False) def _item(self, req, tenant_id, network_id, net_details, port_details): @@ -66,60 +66,63 @@ class Controller(common.QuantumController): for network in networks] return dict(networks=result) - def show(self, req, tenant_id, id): + def show(self, request, tenant_id, id): """ Returns network details for the given network id """ try: - return self._item(req, tenant_id, id, + return self._item(request, tenant_id, id, net_details=True, port_details=False) except exception.NetworkNotFound as e: return faults.Fault(faults.NetworkNotFound(e)) - def detail(self, req, **kwargs): + def detail(self, request, **kwargs): tenant_id = kwargs.get('tenant_id') network_id = kwargs.get('id') try: if network_id: - return self._item(req, tenant_id, network_id, + return self._item(request, tenant_id, network_id, net_details=True, port_details=True) else: #do like show but with detaik - return self._items(req, tenant_id, + return self._items(request, tenant_id, net_details=True, port_details=False) except exception.NetworkNotFound as e: return faults.Fault(faults.NetworkNotFound(e)) - def create(self, req, tenant_id): + def create(self, request, tenant_id): """ Creates a new network for a given tenant """ #look for network name in request try: - req_params = \ - self._parse_request_params(req, self._network_ops_param_list) + request_params = \ + self._parse_request_params(request, + self._network_ops_param_list) except exc.HTTPError as e: return faults.Fault(e) network = self.network_manager.\ - create_network(tenant_id, req_params['network-name']) - builder = networks_view.get_view_builder(req) + create_network(tenant_id, + request_params['network-name']) + builder = networks_view.get_view_builder(request) result = builder.build(network) return dict(networks=result) - def update(self, req, tenant_id, id): + def update(self, request, tenant_id, id): """ Updates the name for the network with the given id """ try: - req_params = \ - self._parse_request_params(req, self._network_ops_param_list) + request_params = \ + self._parse_request_params(request, + self._network_ops_param_list) except exc.HTTPError as e: return faults.Fault(e) try: network = self.network_manager.rename_network(tenant_id, - id, req_params['network-name']) + id, request_params['network-name']) - builder = networks_view.get_view_builder(req) + builder = networks_view.get_view_builder(request) result = builder.build(network, True) return dict(networks=result) except exception.NetworkNotFound as e: return faults.Fault(faults.NetworkNotFound(e)) - def delete(self, req, tenant_id, id): + def delete(self, request, tenant_id, id): """ Destroys the network with the given id """ try: self.network_manager.delete_network(tenant_id, id) diff --git a/quantum/api/ports.py b/quantum/api/ports.py index bbe6d3f520..2a066255de 100644 --- a/quantum/api/ports.py +++ b/quantum/api/ports.py @@ -31,47 +31,43 @@ class Controller(common.QuantumController): _port_ops_param_list = [{ 'param-name': 'port-state', 'default-value': 'DOWN', - 'required': False}, - ] + 'required': False}, ] _attachment_ops_param_list = [{ 'param-name': 'attachment-id', - 'required': True}, - ] + 'required': True}, ] _serialization_metadata = { "application/xml": { "attributes": { - "port": ["id", "state"], - }, - }, - } + "port": ["id", "state"], }, }, } + def __init__(self, plugin_conf_file=None): self._resource_name = 'port' super(Controller, self).__init__() - def index(self, req, tenant_id, network_id): + def index(self, request, tenant_id, network_id): """ Returns a list of port ids for a given network """ - return self._items(req, tenant_id, network_id, is_detail=False) + return self._items(request, tenant_id, network_id, is_detail=False) - def _items(self, req, tenant_id, network_id, is_detail): + def _items(self, request, tenant_id, network_id, is_detail): """ Returns a list of networks. """ try: ports = self.network_manager.get_all_ports(tenant_id, network_id) - builder = ports_view.get_view_builder(req) + builder = ports_view.get_view_builder(request) result = [builder.build(port, is_detail)['port'] for port in ports] return dict(ports=result) except exception.NetworkNotFound as e: return faults.Fault(faults.NetworkNotFound(e)) - def show(self, req, tenant_id, network_id, id): + def show(self, request, tenant_id, network_id, id): """ Returns port details for given port and network """ try: port = self.network_manager.get_port_details( tenant_id, network_id, id) - builder = ports_view.get_view_builder(req) + builder = ports_view.get_view_builder(request) #build response with details result = builder.build(port, True) return dict(ports=result) @@ -80,19 +76,19 @@ class Controller(common.QuantumController): except exception.PortNotFound as e: return faults.Fault(faults.PortNotFound(e)) - def create(self, req, tenant_id, network_id): + def create(self, request, tenant_id, network_id): """ Creates a new port for a given network """ #look for port state in request try: - req_params = \ - self._parse_request_params(req, self._port_ops_param_list) + request_params = \ + self._parse_request_params(request, self._port_ops_param_list) except exc.HTTPError as e: return faults.Fault(e) try: port = self.network_manager.create_port(tenant_id, - network_id, - req_params['port-state']) - builder = ports_view.get_view_builder(req) + network_id, + request_params['port-state']) + builder = ports_view.get_view_builder(request) result = builder.build(port) return dict(ports=result) except exception.NetworkNotFound as e: @@ -100,18 +96,19 @@ class Controller(common.QuantumController): except exception.StateInvalid as e: return faults.Fault(faults.RequestedStateInvalid(e)) - def update(self, req, tenant_id, network_id, id): + def update(self, request, tenant_id, network_id, id): """ Updates the state of a port for a given network """ #look for port state in request try: - req_params = \ - self._parse_request_params(req, self._port_ops_param_list) + request_params = \ + self._parse_request_params(request, self._port_ops_param_list) except exc.HTTPError as e: return faults.Fault(e) try: - port = self.network_manager.update_port(tenant_id, network_id, id, - req_params['port-state']) - builder = ports_view.get_view_builder(req) + port = self.network_manager.\ + update_port(tenant_id, network_id, id, + request_params['port-state']) + builder = ports_view.get_view_builder(request) result = builder.build(port, True) return dict(ports=result) except exception.NetworkNotFound as e: @@ -121,7 +118,7 @@ class Controller(common.QuantumController): except exception.StateInvalid as e: return faults.Fault(faults.RequestedStateInvalid(e)) - def delete(self, req, tenant_id, network_id, id): + def delete(self, request, tenant_id, network_id, id): """ Destroys the port with the given id """ #look for port state in request try: @@ -135,7 +132,7 @@ class Controller(common.QuantumController): except exception.PortInUse as e: return faults.Fault(faults.PortInUse(e)) - def get_resource(self, req, tenant_id, network_id, id): + def get_resource(self, request, tenant_id, network_id, id): try: result = self.network_manager.get_interface_details( tenant_id, network_id, id) @@ -145,17 +142,20 @@ class Controller(common.QuantumController): except exception.PortNotFound as e: return faults.Fault(faults.PortNotFound(e)) - def attach_resource(self, req, tenant_id, network_id, id): + + def attach_resource(self, request, tenant_id, network_id, id): + content_type = request.best_match_content_type() + print "Content type:%s" % content_type try: - req_params = \ - self._parse_request_params(req, + request_params = \ + self._parse_request_params(request, self._attachment_ops_param_list) except exc.HTTPError as e: return faults.Fault(e) try: self.network_manager.plug_interface(tenant_id, - network_id, id, - req_params['attachment-id']) + network_id, id, + request_params['attachment-id']) return exc.HTTPAccepted() except exception.NetworkNotFound as e: return faults.Fault(faults.NetworkNotFound(e)) @@ -166,7 +166,7 @@ class Controller(common.QuantumController): except exception.AlreadyAttached as e: return faults.Fault(faults.AlreadyAttached(e)) - def detach_resource(self, req, tenant_id, network_id, id): + def detach_resource(self, request, tenant_id, network_id, id): try: self.network_manager.unplug_interface(tenant_id, network_id, id) diff --git a/quantum/cli.py b/quantum/cli.py index dc6cf8240a..8663d548b1 100644 --- a/quantum/cli.py +++ b/quantum/cli.py @@ -1,6 +1,7 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 -# Copyright 2011, Nicira Networks, Inc. +# 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 @@ -14,96 +15,436 @@ # 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 quantum.common.wsgi import Serializer + +FORMAT = "json" +CONTENT_TYPE = "application/" + FORMAT -def usage(): - print "\nUsage:" - print "list_nets " - print "create_net " - print "delete_net " - print "detail_net " - print "rename_net " - print "list_ports " - print "create_port " - print "delete_port " - print "detail_port " - print "plug_iface " - print "unplug_iface " - print "detail_iface " - print "list_iface \n" +### --- Miniclient (taking from the test directory) +### TODO(bgh): move this to a library within quantum +class MiniClient(object): + """A base client class - derived from Glance.BaseClient""" + action_prefix = '/v0.1/tenants/{tenant_id}' -if len(sys.argv) < 2 or len(sys.argv) > 6: - usage() - exit(1) + def __init__(self, host, port, use_ssl): + self.host = host + self.port = port + self.use_ssl = use_ssl + self.connection = None -quantum = QuantumManager() -manager = quantum.get_manager() + def get_connection_type(self): + if self.use_ssl: + return httplib.HTTPSConnection + else: + return httplib.HTTPConnection -if sys.argv[1] == "list_nets" and len(sys.argv) == 3: - network_on_tenant = manager.get_all_networks(sys.argv[2]) - print "Virtual Networks on Tenant:%s\n" % sys.argv[2] - for k, v in network_on_tenant.iteritems(): - print"\tNetwork ID:%s \n\tNetwork Name:%s \n" % (k, v) -elif sys.argv[1] == "create_net" and len(sys.argv) == 4: - new_net_id = manager.create_network(sys.argv[2], sys.argv[3]) + def do_request(self, tenant, method, action, body=None, + headers=None, params=None): + action = MiniClient.action_prefix + action + action = action.replace('{tenant_id}', tenant) + if type(params) is dict: + action += '?' + urllib.urlencode(params) + try: + connection_type = self.get_connection_type() + headers = headers or {} + # Open connection and send request + c = connection_type(self.host, self.port) + c.request(method, action, body, headers) + res = c.getresponse() + status_code = self.get_status_code(res) + if status_code in (httplib.OK, httplib.CREATED, + httplib.ACCEPTED, httplib.NO_CONTENT): + return res + else: + raise Exception("Server returned error: %s" % res.read()) + except (socket.error, IOError), e: + raise Exception("Unable to connect to server. Got error: %s" % e) + + def get_status_code(self, response): + if hasattr(response, 'status_int'): + return response.status_int + else: + return response.status +### -- end of miniclient + +### -- 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.do_request(tenant_id, 'GET', "/networks." + FORMAT) + resdict = json.loads(res.read()) + LOG.debug(resdict) + print "Virtual Networks on Tenant:%s\n" % tenant_id + for n in resdict["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 -elif sys.argv[1] == "delete_net" and len(sys.argv) == 4: - manager.delete_network(sys.argv[2], sys.argv[3]) - print "Deleted Virtual Network with ID:%s" % sys.argv[3] -elif sys.argv[1] == "detail_net" and len(sys.argv) == 4: - vif_list = manager.get_network_details(sys.argv[2], sys.argv[3]) - print "Remote Interfaces on Virtual Network:%s\n" % sys.argv[3] - for iface in vif_list: - print "\tRemote interface :%s" % iface -elif sys.argv[1] == "rename_net" and len(sys.argv) == 5: - manager.rename_network(sys.argv[2], sys.argv[3], sys.argv[4]) - print "Renamed Virtual Network with ID:%s" % sys.argv[3] -elif sys.argv[1] == "list_ports" and len(sys.argv) == 4: - ports = manager.get_all_ports(sys.argv[2], sys.argv[3]) - print " Virtual Ports on Virtual Network:%s\n" % sys.argv[3] - for port in ports: - print "\tVirtual Port:%s" % port -elif sys.argv[1] == "create_port" and len(sys.argv) == 4: - new_port = manager.create_port(sys.argv[2], sys.argv[3]) - print "Created Virtual Port:%s " \ - "on Virtual Network:%s" % (new_port, sys.argv[3]) -elif sys.argv[1] == "delete_port" and len(sys.argv) == 5: - manager.delete_port(sys.argv[2], sys.argv[3], sys.argv[4]) - print "Deleted Virtual Port:%s " \ - "on Virtual Network:%s" % (sys.argv[3], sys.argv[4]) -elif sys.argv[1] == "detail_port" and len(sys.argv) == 5: - port_detail = manager.get_port_details(sys.argv[2], - sys.argv[3], sys.argv[4]) - print "Virtual Port:%s on Virtual Network:%s " \ - "contains remote interface:%s" % (sys.argv[3], - sys.argv[4], - port_detail) -elif sys.argv[1] == "plug_iface" and len(sys.argv) == 6: - manager.plug_interface(sys.argv[2], sys.argv[3], sys.argv[4], sys.argv[5]) - print "Plugged remote interface:%s " \ - "into Virtual Network:%s" % (sys.argv[5], sys.argv[3]) -elif sys.argv[1] == "unplug_iface" and len(sys.argv) == 5: - manager.unplug_interface(sys.argv[2], sys.argv[3], sys.argv[4]) - print "UnPlugged remote interface " \ - "from Virtual Port:%s Virtual Network:%s" % (sys.argv[4], - sys.argv[3]) -elif sys.argv[1] == "detail_iface" and len(sys.argv) == 5: - remote_iface = manager.get_interface_details(sys.argv[2], - sys.argv[3], sys.argv[4]) - print "Remote interface on Virtual Port:%s " \ - "Virtual Network:%s is %s" % (sys.argv[4], - sys.argv[3], remote_iface) -elif sys.argv[1] == "list_iface" and len(sys.argv) == 4: - iface_list = manager.get_all_attached_interfaces(sys.argv[2], sys.argv[3]) - print "Remote Interfaces on Virtual Network:%s\n" % sys.argv[3] + + +def api_create_net(client, *args): + tid, name = args + data = {'network': {'network-name': '%s' % name}} + body = Serializer().serialize(data, CONTENT_TYPE) + res = client.do_request(tid, 'POST', "/networks." + FORMAT, body=body) + rd = json.loads(res.read()) + LOG.debug(rd) + nid = None + try: + nid = rd["networks"]["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 + res = client.do_request(tid, 'DELETE', "/networks/" + nid + "." + FORMAT) + status = res.status + if status != 202: + print "Failed to delete network" + output = res.read() + print output + else: + print "Deleted Virtual Network with ID:%s" % nid + + +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 -elif sys.argv[1] == "all" and len(sys.argv) == 2: - print "Not Implemented" -else: - print "invalid arguments: %s" % str(sys.argv) - usage() + print "\tRemote interface:%s" % iface + + +def api_detail_net(client, *args): + tid, nid = args + res = client.do_request(tid, 'GET', + "/networks/%s/ports.%s" % (nid, FORMAT)) + output = res.read() + if res.status != 200: + LOG.error("Failed to list ports: %s" % output) + return + rd = json.loads(output) + LOG.debug(rd) + print "Remote Interfaces on Virtual Network:%s\n" % nid + for port in rd["ports"]: + pid = port["id"] + res = client.do_request(tid, 'GET', + "/networks/%s/ports/%s/attachment.%s" % (nid, pid, FORMAT)) + output = res.read() + rd = json.loads(output) + LOG.debug(rd) + remote_iface = rd["attachment"] + 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': {'network-name': '%s' % name}} + body = Serializer().serialize(data, CONTENT_TYPE) + res = client.do_request(tid, 'PUT', "/networks/%s.%s" % (nid, FORMAT), + body=body) + resdict = json.loads(res.read()) + LOG.debug(resdict) + 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 + res = client.do_request(tid, 'GET', + "/networks/%s/ports.%s" % (nid, FORMAT)) + output = res.read() + if res.status != 200: + LOG.error("Failed to list ports: %s" % output) + return + rd = json.loads(output) + LOG.debug(rd) + print "Ports on Virtual Network:%s\n" % nid + for port in rd["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 + res = client.do_request(tid, 'POST', + "/networks/%s/ports.%s" % (nid, FORMAT)) + output = res.read() + if res.status != 200: + LOG.error("Failed to create port: %s" % output) + return + rd = json.loads(output) + new_port = rd["ports"]["port"]["id"] + print "Created Virtual Port:%s " \ + "on Virtual Network:%s" % (new_port, nid) + + +def delete_port(manager, *args): + tid, nid, pid = args + LOG.info("Deleted Virtual Port:%s " \ + "on Virtual Network:%s" % (pid, nid)) + + +def api_delete_port(client, *args): + tid, nid, pid = args + res = client.do_request(tid, 'DELETE', + "/networks/%s/ports/%s.%s" % (nid, pid, FORMAT)) + output = res.read() + if res.status != 202: + LOG.error("Failed to delete port: %s" % output) + return + LOG.info("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 + res = client.do_request(tid, 'GET', + "/networks/%s/ports/%s.%s" % (nid, pid, FORMAT)) + output = res.read() + if res.status != 200: + LOG.error("Failed to get port details: %s" % output) + return + rd = json.loads(output) + port = rd["ports"]["port"] + id = port["id"] + attachment = port["attachment"] + LOG.debug(port) + print "Virtual Port:%s on Virtual Network:%s " \ + "contains remote interface:%s" % (pid, nid, attachment) + + +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 + data = {'port': {'attachment-id': '%s' % vid}} + body = Serializer().serialize(data, CONTENT_TYPE) + res = client.do_request(tid, 'PUT', + "/networks/%s/ports/%s/attachment.%s" % (nid, pid, FORMAT), body=body) + output = res.read() + LOG.debug(output) + if res.status != 202: + LOG.error("Failed to plug iface \"%s\" to port \"%s\": %s" % (vid, + pid, output)) + return + 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 + data = {'port': {'attachment-id': ''}} + body = Serializer().serialize(data, CONTENT_TYPE) + res = client.do_request(tid, 'DELETE', + "/networks/%s/ports/%s/attachment.%s" % (nid, pid, FORMAT), body=body) + output = res.read() + LOG.debug(output) + if res.status != 202: + LOG.error("Failed to unplug iface from port \"%s\": %s" % (vid, + pid, output)) + return + 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 = MiniClient(options.host, options.port, options.ssl) + 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_manager() + commands[cmd]["func"](manager, *args) + sys.exit(0) diff --git a/quantum/common/config.py b/quantum/common/config.py index a497f0dd44..320c3b2cf0 100644 --- a/quantum/common/config.py +++ b/quantum/common/config.py @@ -255,7 +255,7 @@ def load_paste_config(app_name, options, args): % (conf_file, e)) -def load_paste_app(conf_file, app_name): +def load_paste_app(app_name, options, args): """ Builds and returns a WSGI app from a paste config file. @@ -276,16 +276,15 @@ def load_paste_app(conf_file, app_name): :raises RuntimeError when config file cannot be located or application cannot be loaded from config file """ - #conf_file, conf = load_paste_config(app_name, options, args) + conf_file, conf = load_paste_config(app_name, options, args) try: - conf_file = os.path.abspath(conf_file) app = deploy.loadapp("config:%s" % conf_file, name=app_name) except (LookupError, ImportError), e: raise RuntimeError("Unable to load %(app_name)s from " "configuration file %(conf_file)s." "\nGot: %(e)r" % locals()) - return app + return conf, app def get_option(options, option, **kwargs): diff --git a/quantum/common/utils.py b/quantum/common/utils.py index d3730ba61a..3a2455963f 100644 --- a/quantum/common/utils.py +++ b/quantum/common/utils.py @@ -192,7 +192,7 @@ def parse_isotime(timestr): def getPluginFromConfig(file="config.ini"): Config = ConfigParser.ConfigParser() - Config.read(os.path.join(FLAGS.state_path, file)) + Config.read(file) return Config.get("PLUGIN", "provider") diff --git a/quantum/common/wsgi.py b/quantum/common/wsgi.py index f3873e351c..b1d72c4699 100644 --- a/quantum/common/wsgi.py +++ b/quantum/common/wsgi.py @@ -340,7 +340,7 @@ class Controller(object): del arg_dict['action'] if 'format' in arg_dict: del arg_dict['format'] - arg_dict['req'] = req + arg_dict['request'] = req result = method(**arg_dict) if type(result) is dict: @@ -528,7 +528,7 @@ class Serializer(object): node = self._to_xml_node(doc, metadata, k, v) result.appendChild(node) else: - # Type is atom + # Type is atom. node = doc.createTextNode(str(data)) result.appendChild(node) return result diff --git a/quantum/db/__init__.py b/quantum/db/__init__.py new file mode 100644 index 0000000000..620282b583 --- /dev/null +++ b/quantum/db/__init__.py @@ -0,0 +1,17 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# Copyright 2011 Nicira Networks, Inc. +# 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: Somik Behera, Nicira Networks, Inc. +# @author: Brad Hall, Nicira Networks, Inc. diff --git a/quantum/db/api.py b/quantum/db/api.py new file mode 100644 index 0000000000..2a296f2f07 --- /dev/null +++ b/quantum/db/api.py @@ -0,0 +1,187 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# Copyright 2011 Nicira Networks, Inc. +# 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: Somik Behera, Nicira Networks, Inc. +# @author: Brad Hall, Nicira Networks, Inc. +# @author: Dan Wendlandt, Nicira Networks, Inc. + +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker, exc +import models + +_ENGINE = None +_MAKER = None +BASE = models.BASE + + +def configure_db(options): + """ + Establish the database, create an engine if needed, and + register the models. + + :param options: Mapping of configuration options + """ + global _ENGINE + if not _ENGINE: + _ENGINE = create_engine(options['sql_connection'], + echo=False, + echo_pool=True, + pool_recycle=3600) + register_models() + + +def get_session(autocommit=True, expire_on_commit=False): + """Helper method to grab session""" + global _MAKER, _ENGINE + if not _MAKER: + assert _ENGINE + _MAKER = sessionmaker(bind=_ENGINE, + autocommit=autocommit, + expire_on_commit=expire_on_commit) + return _MAKER() + + +def register_models(): + """Register Models and create properties""" + global _ENGINE + assert _ENGINE + BASE.metadata.create_all(_ENGINE) + + +def unregister_models(): + """Unregister Models, useful clearing out data before testing""" + global _ENGINE + assert _ENGINE + BASE.metadata.drop_all(_ENGINE) + + +def network_create(tenant_id, name): + session = get_session() + net = None + try: + net = session.query(models.Network).\ + filter_by(name=name).\ + one() + raise Exception("Network with name \"%s\" already exists" % name) + except exc.NoResultFound: + with session.begin(): + net = models.Network(tenant_id, name) + session.add(net) + session.flush() + return net + + +def network_list(tenant_id): + session = get_session() + return session.query(models.Network).\ + filter_by(tenant_id=tenant_id).\ + all() + + +def network_get(net_id): + session = get_session() + try: + return session.query(models.Network).\ + filter_by(uuid=net_id).\ + one() + except exc.NoResultFound: + raise Exception("No net found with id = %s" % net_id) + + +def network_rename(net_id, tenant_id, new_name): + session = get_session() + try: + res = session.query(models.Network).\ + filter_by(name=new_name).\ + one() + except exc.NoResultFound: + net = network_get(net_id) + net.name = new_name + session.merge(net) + session.flush() + return net + raise Exception("A network with name \"%s\" already exists" % new_name) + + +def network_destroy(net_id): + session = get_session() + try: + net = session.query(models.Network).\ + filter_by(uuid=net_id).\ + one() + session.delete(net) + session.flush() + return net + except exc.NoResultFound: + raise Exception("No network found with id = %s" % net_id) + + +def port_create(net_id): + session = get_session() + with session.begin(): + port = models.Port(net_id) + session.add(port) + session.flush() + return port + + +def port_list(net_id): + session = get_session() + return session.query(models.Port).\ + filter_by(network_id=net_id).\ + all() + + +def port_get(port_id): + session = get_session() + try: + return session.query(models.Port).\ + filter_by(uuid=port_id).\ + one() + except exc.NoResultFound: + raise Exception("No port found with id = %s " % port_id) + + +def port_set_attachment(port_id, new_interface_id): + session = get_session() + ports = None + try: + ports = session.query(models.Port).\ + filter_by(interface_id=new_interface_id).\ + all() + except exc.NoResultFound: + pass + if len(ports) == 0: + port = port_get(port_id) + port.interface_id = new_interface_id + session.merge(port) + session.flush() + return port + else: + raise Exception("Port with attachment \"%s\" already exists" + % (new_interface_id)) + + +def port_destroy(port_id): + session = get_session() + try: + port = session.query(models.Port).\ + filter_by(uuid=port_id).\ + one() + session.delete(port) + session.flush() + return port + except exc.NoResultFound: + raise Exception("No port found with id = %s " % port_id) diff --git a/quantum/db/models.py b/quantum/db/models.py new file mode 100644 index 0000000000..115ab282da --- /dev/null +++ b/quantum/db/models.py @@ -0,0 +1,63 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# Copyright 2011 Nicira Networks, Inc. +# 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: Somik Behera, Nicira Networks, Inc. +# @author: Brad Hall, Nicira Networks, Inc. +# @author: Dan Wendlandt, Nicira Networks, Inc. + +import uuid + +from sqlalchemy import Column, Integer, String, ForeignKey +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import relation + +BASE = declarative_base() + + +class Port(BASE): + """Represents a port on a quantum network""" + __tablename__ = 'ports' + + uuid = Column(String(255), primary_key=True) + network_id = Column(String(255), ForeignKey("networks.uuid"), + nullable=False) + interface_id = Column(String(255)) + + def __init__(self, network_id): + self.uuid = uuid.uuid4() + self.network_id = network_id + + def __repr__(self): + return "" % (self.uuid, self.network_id, + self.interface_id) + + +class Network(BASE): + """Represents a quantum network""" + __tablename__ = 'networks' + + uuid = Column(String(255), primary_key=True) + tenant_id = Column(String(255), nullable=False) + name = Column(String(255)) + ports = relation(Port, order_by=Port.uuid, backref="network") + + def __init__(self, tenant_id, name): + self.uuid = uuid.uuid4() + self.tenant_id = tenant_id + self.name = name + + def __repr__(self): + return "" % \ + (self.uuid, self.name, self.tenant_id) diff --git a/quantum/manager.py b/quantum/manager.py index f4b829303d..05762f3a59 100644 --- a/quantum/manager.py +++ b/quantum/manager.py @@ -18,27 +18,39 @@ """ -Quantum's Manager class is responsible for parsing a config file -and instantiating the correct plugin that concretely implement -quantum_plugin_base class - +Quantum's Manager class is responsible for parsing a config file and +instantiating the correct plugin that concretely implement quantum_plugin_base +class. The caller should make sure that QuantumManager is a singleton. """ import gettext +import os gettext.install('quantum', unicode=1) +import os + from common import utils from quantum_plugin_base import QuantumPluginBase -CONFIG_FILE = "quantum/plugins.ini" +CONFIG_FILE = "plugins.ini" + + +def find_config(basepath): + for root, dirs, files in os.walk(basepath): + if CONFIG_FILE in files: + return os.path.join(root, CONFIG_FILE) + return None class QuantumManager(object): - def __init__(self, config=CONFIG_FILE): - self.configuration_file = CONFIG_FILE - plugin_location = utils.getPluginFromConfig(CONFIG_FILE) - print "PLUGIN LOCATION:%s" % plugin_location + def __init__(self, config=None): + if config == None: + self.configuration_file = find_config( + os.path.abspath(os.path.dirname(__file__))) + else: + self.configuration_file = config + plugin_location = utils.getPluginFromConfig(self.configuration_file) plugin_klass = utils.import_class(plugin_location) if not issubclass(plugin_klass, QuantumPluginBase): raise Exception("Configured Quantum plug-in " \ @@ -50,16 +62,3 @@ class QuantumManager(object): def get_manager(self): return self.plugin - - -# TODO(somik): rmove the main class -# Added for temporary testing purposes -def main(): - manager = QuantumManager() - myManager = manager.get_manager() - myManager.get_all_networks("tesst") - #print("is a plugin") - -# Standard boilerplate to call the main() function. -if __name__ == '__main__': - main() diff --git a/quantum/plugins/SamplePlugin.py b/quantum/plugins/SamplePlugin.py index 15b6ab0899..32dc823f99 100644 --- a/quantum/plugins/SamplePlugin.py +++ b/quantum/plugins/SamplePlugin.py @@ -86,6 +86,12 @@ class QuantumEchoPlugin(object): """ print("delete_port() called\n") + def update_port(self, tenant_id, net_id, port_id, port_state): + """ + Updates the state of a port on the specified Virtual Network. + """ + print("update_port() called\n") + def get_port_details(self, tenant_id, net_id, port_id): """ This method allows the user to retrieve a remote interface @@ -161,9 +167,7 @@ class DummyDataPlugin(object): retrieved a list of all the remote vifs that are attached to the network """ - print("get_network_details() called\n") - vifs_on_net = ["/tenant1/networks/net_id/portid/vif2.0", - "/tenant1/networks/10/121/vif1.1"] + vifs_on_net = ["/tenant1/networks/net_id/portid/vif2.0"] return vifs_on_net def rename_network(self, tenant_id, net_id, new_name): @@ -222,27 +226,6 @@ class DummyDataPlugin(object): """ print("unplug_interface() called\n") - def get_interface_details(self, tenant_id, net_id, port_id): - """ - Retrieves the remote interface that is attached at this - particular port. - """ - print("get_interface_details() called\n") - #returns the remote interface UUID - return "/tenant1/networks/net_id/portid/vif2.0" - - def get_all_attached_interfaces(self, tenant_id, net_id): - """ - Retrieves all remote interfaces that are attached to - a particular Virtual Network. - """ - print("get_all_attached_interfaces() called\n") - # returns a list of all attached remote interfaces - vifs_on_net = ["/tenant1/networks/net_id/portid/vif2.0", - "/tenant1/networks/10/121/vif1.1"] - return vifs_on_net - - class FakePlugin(object): """ FakePlugin is a demo plugin that provides @@ -257,16 +240,14 @@ class FakePlugin(object): 'attachment': None}, 2: {'port-id': 2, 'port-state': 'UP', - 'attachment': None} - } + 'attachment': None}} _port_dict_2 = { 1: {'port-id': 1, 'port-state': 'UP', 'attachment': 'SomeFormOfVIFID'}, 2: {'port-id': 2, 'port-state': 'DOWN', - 'attachment': None} - } + 'attachment': None}} _networks = {'001': { 'net-id': '001', @@ -277,8 +258,7 @@ class FakePlugin(object): { 'net-id': '002', 'net-name': 'cicciotest', - 'net-ports': _port_dict_2 - }} + 'net-ports': _port_dict_2}} def __init__(self): FakePlugin._net_counter = len(FakePlugin._networks) @@ -341,6 +321,7 @@ class FakePlugin(object): new_net_dict = {'net-id': new_net_id, 'net-name': net_name, 'net-ports': {}} + FakePlugin._networks[new_net_id] = new_net_dict # return network_id of the created network return new_net_dict @@ -471,7 +452,6 @@ class FakePlugin(object): # Should unplug on port without attachment raise an Error? port['attachment'] = None - # TODO - neeed to update methods from this point onwards def get_all_attached_interfaces(self, tenant_id, net_id): """ Retrieves all remote interfaces that are attached to diff --git a/quantum/plugins/openvswitch/Makefile b/quantum/plugins/openvswitch/Makefile new file mode 100644 index 0000000000..7cd5e620ed --- /dev/null +++ b/quantum/plugins/openvswitch/Makefile @@ -0,0 +1,30 @@ +QUANTUM_PATH=../../../ + +# TODO(bgh): DIST_DIR and target for plugin + +AGENT_DIST_DIR=ovs_quantum_agent +AGENT_DIST_TARBALL=ovs_quantum_agent.tgz + +agent-dist: distclean + mkdir $(AGENT_DIST_DIR) + cp agent/*.py $(AGENT_DIST_DIR) + cp agent/*.sh $(AGENT_DIST_DIR) + cp README $(AGENT_DIST_DIR) + cp ovs_quantum_plugin.ini $(AGENT_DIST_DIR) + tar -zcvf $(AGENT_DIST_TARBALL) $(AGENT_DIST_DIR)/ + @echo "Agent tarball created: $(AGENT_DIST_TARBALL)" + @echo "See README for installation details" + +all: + +clean: + $(find . -name *.pyc | xargs rm) + +distclean: + -rm -rf $(AGENT_DIST_DIR) + -rm -f $(AGENT_DIST_TARBALL) + +check: + PYTHONPATH=$(QUANTUM_PATH):. python ovs_quantum_plugin.py + +PHONY: agent-dist check clean distclean diff --git a/quantum/plugins/openvswitch/README b/quantum/plugins/openvswitch/README new file mode 100644 index 0000000000..ef0660c081 --- /dev/null +++ b/quantum/plugins/openvswitch/README @@ -0,0 +1,107 @@ +# -- Background + +The quantum openvswitch plugin is a simple plugin that allows you to manage +connectivity between VMs on hypervisors running openvswitch. + +The quantum openvswitch plugin consists of two components: + +1) The plugin itself: The plugin uses a database backend (mysql for now) to + store configuration and mappings that are used by the agent. + +2) An agent which runs on the hypervisor (dom0) and communicates with + openvswitch. The agent gathers the configuration and mappings from the + mysql database running on the quantum host. + +The sections below describe how to configure and run the quantum service with +the openvswitch plugin. + +# -- Nova configuration + +- Make sure to set up nova using flat networking. Also, make sure that the + integration bridge (see below under agent configuration) matches the + flat_network_bridge specified in your nova flag file. Here are the relevant + entries from my nova flag file. +--network_manager=nova.network.manager.FlatManager +--flat_network_bridge=xapi1 + +# -- Quantum configuration + +Make the openvswitch plugin the current quantum plugin + +- edit ../../plugins.ini and change the provider line to be: +provider = quantum.plugins.openvswitch.ovs_quantum_plugin.OVSQuantumPlugin + +# -- Database config. The OVS quantum service requires access to a mysql +# database in order to store configuration and mappings that will be used by +# the agent. Here is how to set up the database on the host that you will be +# running the quantum service on. + +MySQL should be installed on the host, and all plugins and clients must be +configured with access to the database. + +To prep mysql, run: + +$ mysql -u root -p -e "create database ovs_quantum" + +Make sure any xenserver running the ovs quantum agent will be able to +communicate with the host running the quantum service: + +//log in to mysql service +$ mysql -u root -p +// grant access to user-remote host combination. Note: if you're going to use +// a wildcard here it should be a management network with only trusted hosts. +mysql> GRANT USAGE ON *.* to root@'yourremotehost' IDENTIFIED BY 'newpassword'; +//force update of authorization changes +mysql> FLUSH PRIVILEGES; + +# -- Plugin configuration. + +- Edit the configuration file (ovs_quantum_plugin.ini). Make sure it matches + your mysql configuration. This file must be updated with the addresses and + credentials to access the database. This file will be included in the agent + distribution tarball (see below) and the agent will use the credentials here + to access the database. + +# -- Agent configuration + +- Create the agent distribution tarball + +$ make agent-dist +- Copy the resulting tarball to your xenserver(s) (copy to dom0, not the nova + compute node) +- Unpack the tarball and run install.sh. This will install all of the + necessary pieces into /etc/xapi.d/plugins. It will also spit out the name + of the integration bridge that you'll need for your nova configuration. + Make sure to specify this in your nova flagfile as --flat_network_bridge. +- Run the agent [on your hypervisor (dom0)]: +$ /etc/xapi.d/plugins/ovs_quantum_agent.py /etc/xapi.d/plugins/ovs_quantum_plugin.ini + +# -- Getting quantum up and running + +- Start quantum [on the quantum service host]: +~/src/quantum- $ PYTHONPATH=.:$PYTHONPATH python bin/quantum etc/quantum.conf +- Run ovs_quantum_plugin.py via the quantum plugin framework cli [on the + quantum service host] +~/src/quantum-framework$ PYTHONPATH=.:$PYTHONPATH python quantum/cli.py + +This will show help all of the available commands. + +An example session looks like this: + +$ export TENANT=t1 +$ PYTHONPATH=. python quantum/cli.py create_net $TENANT network1 +Created a new Virtual Network with ID:e754e7c0-a8eb-40e5-861a-b182d30c3441 +$ export NETWORK=e754e7c0-a8eb-40e5-861a-b182d30c3441 +$ PYTHONPATH=. python quantum/cli.py create_port $TENANT $NETWORK +Created Virtual Port:5a1e121b-ccc8-471d-9445-24f15f9f854c on Virtual Network:e754e7c0-a8eb-40e5-861a-b182d30c3441 +$ export PORT=5a1e121b-ccc8-471d-9445-24f15f9f854c +$ PYTHONPATH=. python quantum/cli.py plug_iface $TENANT $NETWORK $PORT ubuntu1-eth1 +Plugged interface "ubuntu1-eth1" to port:5a1e121b-ccc8-471d-9445-24f15f9f854c on network:e754e7c0-a8eb-40e5-861a-b182d30c3441 + +(.. repeat for more ports and interface combinations..) + +# -- Other items + +- To get a listing of the vif names in the format that the ovs quantum service + will expect them in, issue the following command on the hypervisor (dom0): +$ for vif in `xe vif-list params=uuid --minimal | sed s/,/" "/g`; do echo $(xe vif-list params=vm-name-label uuid=${vif} --minimal)-eth$(xe vif-list params=device uuid=${vif} --minimal); done diff --git a/test_scripts/__init__.py b/quantum/plugins/openvswitch/__init__.py similarity index 100% rename from test_scripts/__init__.py rename to quantum/plugins/openvswitch/__init__.py diff --git a/quantum/plugins/openvswitch/agent/install.sh b/quantum/plugins/openvswitch/agent/install.sh new file mode 100755 index 0000000000..1eebc8fa33 --- /dev/null +++ b/quantum/plugins/openvswitch/agent/install.sh @@ -0,0 +1,38 @@ +#!/bin/bash + +CONF_FILE=/etc/xapi.d/plugins/ovs_quantum_plugin.ini + +if [ ! -d /etc/xapi.d/plugins ]; then + echo "Am I on a xenserver? I can't find the plugins directory!" + exit 1 +fi + +# Make sure we have mysql-python +rpm -qa | grep MySQL-python >/dev/null 2>&1 +if [ $? -ne 0 ]; then + echo "MySQL-python not found" + echo "Please enable the centos repositories and install mysql-python:" + echo "yum --enablerepo=base -y install MySQL-python" + exit 1 +fi + +cp ovs_quantum_agent.py /etc/xapi.d/plugins +cp ovs_quantum_plugin.ini /etc/xapi.d/plugins +cp set_external_ids.sh /etc/xapi.d/plugins + +xe network-list name-label="integration-bridge" | grep xapi >/dev/null 2>&1 +if [ $? -ne 0 ]; then + echo "No integration bridge found. Creating." + xe network-create name-label="integration-bridge" +fi + +BR=$(xe network-list name-label="integration-bridge" | grep "bridge.*:" | awk '{print $4}') +CONF_BR=$(grep integration-bridge ${CONF_FILE} | cut -d= -f2) +if [ "X$BR" != "X$CONF_BR" ]; then + echo "Integration bridge doesn't match configuration file; fixing." + sed -i -e "s/^integration-bridge =.*$/integration-bridge = ${BR}/g" $CONF_FILE +fi + +echo "Using integration bridge: $BR (make sure this is set in the nova configuration)" + +echo "Make sure to edit: $CONF_FILE" diff --git a/quantum/plugins/openvswitch/agent/ovs_quantum_agent.py b/quantum/plugins/openvswitch/agent/ovs_quantum_agent.py new file mode 100755 index 0000000000..a5cd7948d7 --- /dev/null +++ b/quantum/plugins/openvswitch/agent/ovs_quantum_agent.py @@ -0,0 +1,300 @@ +#!/usr/bin/env python +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# Copyright 2011 Nicira Networks, Inc. +# 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: Somik Behera, Nicira Networks, Inc. +# @author: Brad Hall, Nicira Networks, Inc. +# @author: Dan Wendlandt, Nicira Networks, Inc. + +import ConfigParser +import logging as LOG +import MySQLdb +import os +import sys +import time + +from optparse import OptionParser +from subprocess import * + + +# A class to represent a VIF (i.e., a port that has 'iface-id' and 'vif-mac' +# attributes set). +class VifPort: + def __init__(self, port_name, ofport, vif_id, vif_mac, switch): + self.port_name = port_name + self.ofport = ofport + self.vif_id = vif_id + self.vif_mac = vif_mac + self.switch = switch + + def __str__(self): + return "iface-id=" + self.vif_id + ", vif_mac=" + \ + self.vif_mac + ", port_name=" + self.port_name + \ + ", ofport=" + self.ofport + ", bridge name = " + self.switch.br_name + + +class OVSBridge: + def __init__(self, br_name): + self.br_name = br_name + + def run_cmd(self, args): + # LOG.debug("## running command: " + " ".join(args)) + return Popen(args, stdout=PIPE).communicate()[0] + + def run_vsctl(self, args): + full_args = ["ovs-vsctl"] + args + return self.run_cmd(full_args) + + def reset_bridge(self): + self.run_vsctl(["--", "--if-exists", "del-br", self.br_name]) + self.run_vsctl(["add-br", self.br_name]) + + def delete_port(self, port_name): + self.run_vsctl(["--", "--if-exists", "del-port", self.br_name, + port_name]) + + def set_db_attribute(self, table_name, record, column, value): + args = ["set", table_name, record, "%s=%s" % (column, value)] + self.run_vsctl(args) + + def clear_db_attribute(self, table_name, record, column): + args = ["clear", table_name, record, column] + self.run_vsctl(args) + + def run_ofctl(self, cmd, args): + full_args = ["ovs-ofctl", cmd, self.br_name] + args + return self.run_cmd(full_args) + + def remove_all_flows(self): + self.run_ofctl("del-flows", []) + + def get_port_ofport(self, port_name): + return self.db_get_val("Interface", port_name, "ofport") + + def add_flow(self, **dict): + if "actions" not in dict: + raise Exception("must specify one or more actions") + if "priority" not in dict: + dict["priority"] = "0" + + flow_str = "priority=%s" % dict["priority"] + if "match" in dict: + flow_str += "," + dict["match"] + flow_str += ",actions=%s" % (dict["actions"]) + self.run_ofctl("add-flow", [flow_str]) + + def delete_flows(self, **dict): + all_args = [] + if "priority" in dict: + all_args.append("priority=%s" % dict["priority"]) + if "match" in dict: + all_args.append(dict["match"]) + if "actions" in dict: + all_args.append("actions=%s" % (dict["actions"])) + flow_str = ",".join(all_args) + self.run_ofctl("del-flows", [flow_str]) + + def db_get_map(self, table, record, column): + str = self.run_vsctl(["get", table, record, column]).rstrip("\n\r") + return self.db_str_to_map(str) + + def db_get_val(self, table, record, column): + return self.run_vsctl(["get", table, record, column]).rstrip("\n\r") + + def db_str_to_map(self, full_str): + list = full_str.strip("{}").split(", ") + ret = {} + for e in list: + if e.find("=") == -1: + continue + arr = e.split("=") + ret[arr[0]] = arr[1].strip("\"") + return ret + + def get_port_name_list(self): + res = self.run_vsctl(["list-ports", self.br_name]) + return res.split("\n")[0:-1] + + def get_port_stats(self, port_name): + return self.db_get_map("Interface", port_name, "statistics") + + # returns a VIF object for each VIF port + def get_vif_ports(self): + edge_ports = [] + port_names = self.get_port_name_list() + for name in port_names: + external_ids = self.db_get_map("Interface", name, "external_ids") + if "iface-id" in external_ids and "attached-mac" in external_ids: + ofport = self.db_get_val("Interface", name, "ofport") + p = VifPort(name, ofport, external_ids["iface-id"], + external_ids["attached-mac"], self) + edge_ports.append(p) + else: + # iface-id might not be set. See if we can figure it out and + # set it here. + external_ids = self.db_get_map("Interface", name, + "external_ids") + if "attached-mac" not in external_ids: + continue + vif_uuid = external_ids.get("xs-vif-uuid", "") + if len(vif_uuid) == 0: + continue + LOG.debug("iface-id not set, got vif-uuid: %s" % vif_uuid) + res = os.popen("xe vif-param-get param-name=other-config " + "uuid=%s | grep nicira-iface-id | " + "awk '{print $2}'" + % vif_uuid).readline() + res = res.strip() + if len(res) == 0: + continue + external_ids["iface-id"] = res + LOG.info("Setting interface \"%s\" iface-id to \"%s\"" + % (name, res)) + self.set_db_attribute("Interface", name, + "external-ids:iface-id", res) + ofport = self.db_get_val("Interface", name, "ofport") + p = VifPort(name, ofport, external_ids["iface-id"], + external_ids["attached-mac"], self) + edge_ports.append(p) + return edge_ports + + +class OVSNaaSPlugin: + def __init__(self, integ_br): + self.setup_integration_br(integ_br) + + def port_bound(self, port, vlan_id): + self.int_br.set_db_attribute("Port", port.port_name, "tag", + str(vlan_id)) + + def port_unbound(self, port, still_exists): + if still_exists: + self.int_br.clear_db_attribute("Port", port.port_name, "tag") + + def setup_integration_br(self, integ_br): + self.int_br = OVSBridge(integ_br) + self.int_br.remove_all_flows() + # drop all traffic on the 'dead vlan' + self.int_br.add_flow(priority=2, match="dl_vlan=4095", actions="drop") + # switch all other traffic using L2 learning + self.int_br.add_flow(priority=1, actions="normal") + # FIXME send broadcast everywhere, regardless of tenant + #int_br.add_flow(priority=3, match="dl_dst=ff:ff:ff:ff:ff:ff", + # actions="normal") + + def daemon_loop(self, conn): + self.local_vlan_map = {} + old_local_bindings = {} + old_vif_ports = {} + + while True: + cursor = conn.cursor() + cursor.execute("SELECT * FROM network_bindings") + rows = cursor.fetchall() + cursor.close() + all_bindings = {} + for r in rows: + all_bindings[r[2]] = r[1] + + cursor = conn.cursor() + cursor.execute("SELECT * FROM vlan_bindings") + rows = cursor.fetchall() + cursor.close() + vlan_bindings = {} + for r in rows: + vlan_bindings[r[1]] = r[0] + + new_vif_ports = {} + new_local_bindings = {} + vif_ports = self.int_br.get_vif_ports() + for p in vif_ports: + new_vif_ports[p.vif_id] = p + if p.vif_id in all_bindings: + new_local_bindings[p.vif_id] = all_bindings[p.vif_id] + else: + # no binding, put him on the 'dead vlan' + self.int_br.set_db_attribute("Port", p.port_name, "tag", + "4095") + old_b = old_local_bindings.get(p.vif_id, None) + new_b = new_local_bindings.get(p.vif_id, None) + if old_b != new_b: + if old_b is not None: + LOG.info("Removing binding to net-id = %s for %s" + % (old_b, str(p))) + self.port_unbound(p, True) + if new_b is not None: + LOG.info("Adding binding to net-id = %s for %s" \ + % (new_b, str(p))) + # If we don't have a binding we have to stick it on + # the dead vlan + vlan_id = vlan_bindings.get(all_bindings[p.vif_id], + "4095") + self.port_bound(p, vlan_id) + for vif_id in old_vif_ports.keys(): + if vif_id not in new_vif_ports: + LOG.info("Port Disappeared: %s" % vif_id) + if vif_id in old_local_bindings: + old_b = old_local_bindings[vif_id] + self.port_unbound(old_vif_ports[vif_id], False) + + old_vif_ports = new_vif_ports + old_local_bindings = new_local_bindings + self.int_br.run_cmd(["bash", + "/etc/xapi.d/plugins/set_external_ids.sh"]) + time.sleep(2) + +if __name__ == "__main__": + usagestr = "%prog [OPTIONS] " + parser = OptionParser(usage=usagestr) + 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() + sys.exit(1) + + config_file = args[0] + config = ConfigParser.ConfigParser() + try: + config.read(config_file) + except Exception, e: + LOG.error("Unable to parse config file \"%s\": %s" % (config_file, + str(e))) + + integ_br = config.get("OVS", "integration-bridge") + + db_name = config.get("DATABASE", "name") + db_user = config.get("DATABASE", "user") + db_pass = config.get("DATABASE", "pass") + db_host = config.get("DATABASE", "host") + conn = None + try: + LOG.info("Connecting to database \"%s\" on %s" % (db_name, db_host)) + conn = MySQLdb.connect(host=db_host, user=db_user, + passwd=db_pass, db=db_name) + plugin = OVSNaaSPlugin(integ_br) + plugin.daemon_loop(conn) + finally: + if conn: + conn.close() + + sys.exit(0) diff --git a/quantum/plugins/openvswitch/agent/set_external_ids.sh b/quantum/plugins/openvswitch/agent/set_external_ids.sh new file mode 100755 index 0000000000..2fae05f0a1 --- /dev/null +++ b/quantum/plugins/openvswitch/agent/set_external_ids.sh @@ -0,0 +1,15 @@ +#!/bin/sh +VIFLIST=`xe vif-list params=uuid --minimal | sed s/,/" "/g` +for VIF_UUID in $VIFLIST; do +DEVICE_NUM=`xe vif-list params=device uuid=$VIF_UUID --minimal` + VM_NAME=`xe vif-list params=vm-name-label uuid=$VIF_UUID --minimal` + NAME="$VM_NAME-eth$DEVICE_NUM" + echo "Vif: $VIF_UUID is '$NAME'" + xe vif-param-set uuid=$VIF_UUID other-config:nicira-iface-id="$NAME" +done + +ps auxw | grep -v grep | grep ovs-xapi-sync > /dev/null 2>&1 +if [ $? -eq 0 ]; then + killall -HUP ovs-xapi-sync +fi + diff --git a/quantum/plugins/openvswitch/ovs_db.py b/quantum/plugins/openvswitch/ovs_db.py new file mode 100644 index 0000000000..d72f9a89ce --- /dev/null +++ b/quantum/plugins/openvswitch/ovs_db.py @@ -0,0 +1,76 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# Copyright 2011 Nicira Networks, Inc. +# 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: Somik Behera, Nicira Networks, Inc. +# @author: Brad Hall, Nicira Networks, Inc. +# @author: Dan Wendlandt, Nicira Networks, Inc. + + +from sqlalchemy.orm import exc + +import quantum.db.api as db +import quantum.db.models as models +import ovs_models + + +def get_vlans(): + session = db.get_session() + try: + bindings = session.query(ovs_models.VlanBinding).\ + all() + except exc.NoResultFound: + return [] + res = [] + for x in bindings: + res.append((x.vlan_id, x.network_id)) + return res + + +def add_vlan_binding(vlanid, netid): + session = db.get_session() + binding = ovs_models.VlanBinding(vlanid, netid) + session.add(binding) + session.flush() + return binding.vlan_id + + +def remove_vlan_binding(netid): + session = db.get_session() + try: + binding = session.query(ovs_models.VlanBinding).\ + filter_by(network_id=netid).\ + one() + session.delete(binding) + except exc.NoResultFound: + pass + session.flush() + + +def update_network_binding(netid, ifaceid): + session = db.get_session() + # Add to or delete from the bindings table + if ifaceid == None: + try: + binding = session.query(ovs_models.NetworkBinding).\ + filter_by(network_id=netid).\ + one() + session.delete(binding) + except exc.NoResultFound: + raise Exception("No binding found with network_id = %s" % netid) + else: + binding = ovs_models.NetworkBinding(netid, ifaceid) + session.add(binding) + + session.flush() diff --git a/quantum/plugins/openvswitch/ovs_models.py b/quantum/plugins/openvswitch/ovs_models.py new file mode 100644 index 0000000000..5f529e55a3 --- /dev/null +++ b/quantum/plugins/openvswitch/ovs_models.py @@ -0,0 +1,59 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# Copyright 2011 Nicira Networks, Inc. +# 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: Somik Behera, Nicira Networks, Inc. +# @author: Brad Hall, Nicira Networks, Inc. +# @author: Dan Wendlandt, Nicira Networks, Inc. + + +import uuid + +from sqlalchemy import Column, Integer, String, ForeignKey +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import relation +from quantum.db.models import BASE + + +class NetworkBinding(BASE): + """Represents a binding of network_id, vif_id""" + __tablename__ = 'network_bindings' + + id = Column(Integer, primary_key=True, autoincrement=True) + network_id = Column(String(255)) + vif_id = Column(String(255)) + + def __init__(self, network_id, vif_id): + self.network_id = network_id + self.vif_id = vif_id + + def __repr__(self): + return "" % \ + (self.network_id, self.vif_id) + + +class VlanBinding(BASE): + """Represents a binding of network_id, vlan_id""" + __tablename__ = 'vlan_bindings' + + vlan_id = Column(Integer, primary_key=True) + network_id = Column(String(255)) + + def __init__(self, vlan_id, network_id): + self.network_id = network_id + self.vlan_id = vlan_id + + def __repr__(self): + return "" % \ + (self.vlan_id, self.network_id) diff --git a/quantum/plugins/openvswitch/ovs_quantum_plugin.ini b/quantum/plugins/openvswitch/ovs_quantum_plugin.ini new file mode 100644 index 0000000000..0c75b3fc48 --- /dev/null +++ b/quantum/plugins/openvswitch/ovs_quantum_plugin.ini @@ -0,0 +1,9 @@ +[DATABASE] +name = ovs_naas +user = root +pass = foobar +host = 127.0.0.1 +port = 3306 + +[OVS] +integration-bridge = xapi1 diff --git a/quantum/plugins/openvswitch/ovs_quantum_plugin.py b/quantum/plugins/openvswitch/ovs_quantum_plugin.py new file mode 100644 index 0000000000..b5d1bc68d1 --- /dev/null +++ b/quantum/plugins/openvswitch/ovs_quantum_plugin.py @@ -0,0 +1,353 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# Copyright 2011 Nicira Networks, Inc. +# 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: Somik Behera, Nicira Networks, Inc. +# @author: Brad Hall, Nicira Networks, Inc. +# @author: Dan Wendlandt, Nicira Networks, Inc. + +import ConfigParser +import logging as LOG +import os +import sys +import unittest + +from quantum.quantum_plugin_base import QuantumPluginBase +from optparse import OptionParser + +import quantum.db.api as db +import ovs_db + +CONF_FILE = "ovs_quantum_plugin.ini" + +LOG.basicConfig(level=LOG.WARN) +LOG.getLogger("ovs_quantum_plugin") + + +def find_config(basepath): + for root, dirs, files in os.walk(basepath): + if CONF_FILE in files: + return os.path.join(root, CONF_FILE) + return None + + +class VlanMap(object): + vlans = {} + + def __init__(self): + for x in xrange(2, 4094): + self.vlans[x] = None + + def set(self, vlan_id, network_id): + self.vlans[vlan_id] = network_id + + def acquire(self, network_id): + for x in xrange(2, 4094): + if self.vlans[x] == None: + self.vlans[x] = network_id + # LOG.debug("VlanMap::acquire %s -> %s" % (x, network_id)) + return x + raise Exception("No free vlans..") + + def get(self, vlan_id): + return self.vlans[vlan_id] + + def release(self, network_id): + for x in self.vlans.keys(): + if self.vlans[x] == network_id: + self.vlans[x] = None + # LOG.debug("VlanMap::release %s" % (x)) + return + LOG.error("No vlan found with network \"%s\"" % network_id) + + +class OVSQuantumPlugin(QuantumPluginBase): + + def __init__(self, configfile=None): + config = ConfigParser.ConfigParser() + if configfile == None: + if os.path.exists(CONF_FILE): + configfile = CONF_FILE + else: + configfile = find_config(os.path.abspath( + os.path.dirname(__file__))) + if configfile == None: + raise Exception("Configuration file \"%s\" doesn't exist" % + (configfile)) + LOG.debug("Using configuration file: %s" % configfile) + config.read(configfile) + LOG.debug("Config: %s" % config) + + DB_NAME = config.get("DATABASE", "name") + DB_USER = config.get("DATABASE", "user") + DB_PASS = config.get("DATABASE", "pass") + DB_HOST = config.get("DATABASE", "host") + options = {"sql_connection": "mysql://%s:%s@%s/%s" % (DB_USER, + DB_PASS, DB_HOST, DB_NAME)} + db.configure_db(options) + + self.vmap = VlanMap() + # Populate the map with anything that is already present in the + # database + vlans = ovs_db.get_vlans() + for x in vlans: + vlan_id, network_id = x + # LOG.debug("Adding already populated vlan %s -> %s" + # % (vlan_id, network_id)) + self.vmap.set(vlan_id, network_id) + + def get_all_networks(self, tenant_id): + nets = [] + for x in db.network_list(tenant_id): + LOG.debug("Adding network: %s" % x.uuid) + d = {} + d["net-id"] = str(x.uuid) + d["net-name"] = x.name + nets.append(d) + return nets + + def create_network(self, tenant_id, net_name): + d = {} + try: + res = db.network_create(tenant_id, net_name) + LOG.debug("Created newtork: %s" % res) + except Exception, e: + LOG.error("Error: %s" % str(e)) + return d + d["net-id"] = str(res.uuid) + d["net-name"] = res.name + vlan_id = self.vmap.acquire(str(res.uuid)) + ovs_db.add_vlan_binding(vlan_id, str(res.uuid)) + return d + + def delete_network(self, tenant_id, net_id): + net = db.network_destroy(net_id) + d = {} + d["net-id"] = str(net.uuid) + ovs_db.remove_vlan_binding(net_id) + self.vmap.release(net_id) + return d + + def get_network_details(self, tenant_id, net_id): + ports = db.port_list(net_id) + ifaces = [] + for p in ports: + ifaces.append(p.interface_id) + return ifaces + + def rename_network(self, tenant_id, net_id, new_name): + try: + net = db.network_rename(net_id, tenant_id, new_name) + except Exception, e: + raise Exception("Failed to rename network: %s" % str(e)) + d = {} + d["net-id"] = str(net.uuid) + d["net-name"] = net.name + return d + + def get_all_ports(self, tenant_id, net_id): + ids = [] + ports = db.port_list(net_id) + for x in ports: + LOG.debug("Appending port: %s" % x.uuid) + d = {} + d["port-id"] = str(x.uuid) + ids.append(d) + return ids + + def create_port(self, tenant_id, net_id, port_state=None): + LOG.debug("Creating port with network_id: %s" % net_id) + port = db.port_create(net_id) + d = {} + d["port-id"] = str(port.uuid) + LOG.debug("-> %s" % (port.uuid)) + return d + + def delete_port(self, tenant_id, net_id, port_id): + try: + port = db.port_destroy(port_id) + except Exception, e: + raise Exception("Failed to delete port: %s" % str(e)) + d = {} + d["port-id"] = str(port.uuid) + return d + + def update_port(self, tenant_id, net_id, port_id, port_state): + """ + Updates the state of a port on the specified Virtual Network. + """ + LOG.debug("update_port() called\n") + port = db.port_get(port_id) + port['port-state'] = port_state + return port + + def get_port_details(self, tenant_id, net_id, port_id): + port = db.port_get(port_id) + rv = {"port-id": port.uuid, "attachment": port.interface_id, + "net-id": port.network_id, "port-state": "UP"} + return rv + + def plug_interface(self, tenant_id, net_id, port_id, remote_iface_id): + db.port_set_attachment(port_id, remote_iface_id) + ovs_db.update_network_binding(net_id, remote_iface_id) + + def unplug_interface(self, tenant_id, net_id, port_id): + db.port_set_attachment(port_id, "") + ovs_db.update_network_binding(net_id, None) + + def get_interface_details(self, tenant_id, net_id, port_id): + res = db.port_get(port_id) + return res.interface_id + + +class VlanMapTest(unittest.TestCase): + + def setUp(self): + self.vmap = VlanMap() + + def tearDown(self): + pass + + def testAddVlan(self): + vlan_id = self.vmap.acquire("foobar") + self.assertTrue(vlan_id == 2) + + def testReleaseVlan(self): + vlan_id = self.vmap.acquire("foobar") + self.vmap.release("foobar") + self.assertTrue(self.vmap.get(vlan_id) == None) + + +# TODO(bgh): Make the tests use a sqlite database instead of mysql +class OVSPluginTest(unittest.TestCase): + + def setUp(self): + self.quantum = OVSQuantumPlugin() + self.tenant_id = "testtenant" + + def testCreateNetwork(self): + net1 = self.quantum.create_network(self.tenant_id, "plugin_test1") + self.assertTrue(net1["net-name"] == "plugin_test1") + + def testGetNetworks(self): + net1 = self.quantum.create_network(self.tenant_id, "plugin_test1") + net2 = self.quantum.create_network(self.tenant_id, "plugin_test2") + nets = self.quantum.get_all_networks(self.tenant_id) + count = 0 + for x in nets: + if "plugin_test" in x["net-name"]: + count += 1 + self.assertTrue(count == 2) + + def testDeleteNetwork(self): + net = self.quantum.create_network(self.tenant_id, "plugin_test1") + self.quantum.delete_network(self.tenant_id, net["net-id"]) + nets = self.quantum.get_all_networks(self.tenant_id) + count = 0 + for x in nets: + if "plugin_test" in x["net-name"]: + count += 1 + self.assertTrue(count == 0) + + def testRenameNetwork(self): + net = self.quantum.create_network(self.tenant_id, "plugin_test1") + net = self.quantum.rename_network(self.tenant_id, net["net-id"], + "plugin_test_renamed") + self.assertTrue(net["net-name"] == "plugin_test_renamed") + + def testCreatePort(self): + net1 = self.quantum.create_network(self.tenant_id, "plugin_test1") + port = self.quantum.create_port(self.tenant_id, net1["net-id"]) + ports = self.quantum.get_all_ports(self.tenant_id, net1["net-id"]) + count = 0 + for p in ports: + count += 1 + self.assertTrue(count == 1) + + def testDeletePort(self): + net1 = self.quantum.create_network(self.tenant_id, "plugin_test1") + port = self.quantum.create_port(self.tenant_id, net1["net-id"]) + ports = self.quantum.get_all_ports(self.tenant_id, net1["net-id"]) + count = 0 + for p in ports: + count += 1 + self.assertTrue(count == 1) + for p in ports: + self.quantum.delete_port(self.tenant_id, id, p["port-id"]) + ports = self.quantum.get_all_ports(self.tenant_id, net1["net-id"]) + count = 0 + for p in ports: + count += 1 + self.assertTrue(count == 0) + + def testGetPorts(self): + pass + + def testPlugInterface(self): + net1 = self.quantum.create_network(self.tenant_id, "plugin_test1") + port = self.quantum.create_port(self.tenant_id, net1["net-id"]) + self.quantum.plug_interface(self.tenant_id, net1["net-id"], + port["port-id"], "vif1.1") + port = self.quantum.get_port_details(self.tenant_id, net1["net-id"], + port["port-id"]) + self.assertTrue(port["attachment"] == "vif1.1") + + def testUnPlugInterface(self): + net1 = self.quantum.create_network(self.tenant_id, "plugin_test1") + port = self.quantum.create_port(self.tenant_id, net1["net-id"]) + self.quantum.plug_interface(self.tenant_id, net1["net-id"], + port["port-id"], "vif1.1") + port = self.quantum.get_port_details(self.tenant_id, net1["net-id"], + port["port-id"]) + self.assertTrue(port["attachment"] == "vif1.1") + self.quantum.unplug_interface(self.tenant_id, net1["net-id"], + port["port-id"]) + port = self.quantum.get_port_details(self.tenant_id, net1["net-id"], + port["port-id"]) + self.assertTrue(port["attachment"] == "") + + def tearDown(self): + networks = self.quantum.get_all_networks(self.tenant_id) + # Clean up any test networks lying around + for net in networks: + id = net["net-id"] + name = net["net-name"] + if "plugin_test" in name: + # Clean up any test ports lying around + ports = self.quantum.get_all_ports(self.tenant_id, id) + for p in ports: + self.quantum.delete_port(self.tenant_id, id, p["port-id"]) + self.quantum.delete_network(self.tenant_id, id) + + +if __name__ == "__main__": + usagestr = "Usage: %prog [OPTIONS] [args]" + parser = OptionParser(usage=usagestr) + 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) + + # Make sqlalchemy quieter + LOG.getLogger('sqlalchemy.engine').setLevel(LOG.WARN) + # Run the tests + suite = unittest.TestLoader().loadTestsFromTestCase(OVSPluginTest) + unittest.TextTestRunner(verbosity=2).run(suite) + suite = unittest.TestLoader().loadTestsFromTestCase(VlanMapTest) + unittest.TextTestRunner(verbosity=2).run(suite) diff --git a/quantum/quantum_plugin_base.py b/quantum/quantum_plugin_base.py index 3d79d3a19f..8e3f1f8e15 100644 --- a/quantum/quantum_plugin_base.py +++ b/quantum/quantum_plugin_base.py @@ -35,6 +35,20 @@ class QuantumPluginBase(object): Returns a dictionary containing all for the specified tenant. + :returns: a list of mapping sequences with the following signature: + [ {'net-id': uuid that uniquely identifies + the particular quantum network, + 'net-name': a human-readable name associated + with network referenced by net-id + }, + .... + {'net-id': uuid that uniquely identifies the + particular quantum network, + 'net-name': a human-readable name associated + with network referenced by net-id + } + ] + :raises: None """ pass @@ -43,6 +57,14 @@ class QuantumPluginBase(object): """ Creates a new Virtual Network, and assigns it a symbolic name. + + :returns: a sequence of mappings with the following signature: + {'net-id': uuid that uniquely identifies the + particular quantum network, + 'net-name': a human-readable name associated + with network referenced by net-id + } + :raises: """ pass @@ -51,14 +73,31 @@ class QuantumPluginBase(object): """ Deletes the network with the specified network identifier belonging to the specified tenant. + + :returns: a sequence of mappings with the following signature: + {'net-id': uuid that uniquely identifies the + particular quantum network + } + :raises: exception.NetworkInUse + :raises: exception.NetworkNotFound """ pass @abstractmethod def get_network_details(self, tenant_id, net_id): """ - retrieved a list of all the remote vifs that - are attached to the network + Retrieves a list of all the remote vifs that + are attached to the network. + + :returns: a sequence of mappings with the following signature: + {'net-id': uuid that uniquely identifies the + particular quantum network + 'net-name': a human-readable name associated + with network referenced by net-id + 'net-ifaces': ['vif1_on_network_uuid', + 'vif2_on_network_uuid',...,'vifn_uuid'] + } + :raises: exception.NetworkNotFound """ pass @@ -67,6 +106,15 @@ class QuantumPluginBase(object): """ Updates the symbolic name belonging to a particular Virtual Network. + + :returns: a sequence of mappings representing the new network + attributes, with the following signature: + {'net-id': uuid that uniquely identifies the + particular quantum network + 'net-name': the new human-readable name + associated with network referenced by net-id + } + :raises: exception.NetworkNotFound """ pass @@ -75,6 +123,17 @@ class QuantumPluginBase(object): """ Retrieves all port identifiers belonging to the specified Virtual Network. + + :returns: a list of mapping sequences with the following signature: + [ {'port-id': uuid representing a particular port + on the specified quantum network + }, + .... + {'port-id': uuid representing a particular port + on the specified quantum network + } + ] + :raises: exception.NetworkNotFound """ pass @@ -82,6 +141,13 @@ class QuantumPluginBase(object): def create_port(self, tenant_id, net_id, port_state=None): """ Creates a port on the specified Virtual Network. + + :returns: a mapping sequence with the following signature: + {'port-id': uuid representing the created port + on specified quantum network + } + :raises: exception.NetworkNotFound + :raises: exception.StateInvalid """ pass @@ -89,7 +155,15 @@ class QuantumPluginBase(object): def update_port(self, tenant_id, net_id, port_id, port_state): """ Updates the state of a specific port on the - specified Virtual Network + specified Virtual Network. + + :returns: a mapping sequence with the following signature: + {'port-id': uuid representing the + updated port on specified quantum network + 'port-state': update port state( UP or DOWN) + } + :raises: exception.StateInvalid + :raises: exception.PortNotFound """ pass @@ -100,6 +174,14 @@ class QuantumPluginBase(object): if the port contains a remote interface attachment, the remote interface is first un-plugged and then the port is deleted. + + :returns: a mapping sequence with the following signature: + {'port-id': uuid representing the deleted port + on specified quantum network + } + :raises: exception.PortInUse + :raises: exception.PortNotFound + :raises: exception.NetworkNotFound """ pass @@ -108,6 +190,17 @@ class QuantumPluginBase(object): """ This method allows the user to retrieve a remote interface that is attached to this particular port. + + :returns: a mapping sequence with the following signature: + {'port-id': uuid representing the port on + specified quantum network + 'net-id': uuid representing the particular + quantum network + 'attachment': uuid of the virtual interface + bound to the port, None otherwise + } + :raises: exception.PortNotFound + :raises: exception.NetworkNotFound """ pass @@ -116,6 +209,12 @@ class QuantumPluginBase(object): """ Attaches a remote interface to the specified port on the specified Virtual Network. + + :returns: None + :raises: exception.NetworkNotFound + :raises: exception.PortNotFound + :raises: exception.AlreadyAttached + (? should the network automatically unplug/replug) """ pass @@ -124,22 +223,10 @@ class QuantumPluginBase(object): """ Detaches a remote interface from the specified port on the specified Virtual Network. - """ - pass - @abstractmethod - def get_interface_details(self, tenant_id, net_id, port_id): - """ - Retrieves the remote interface that is attached at this - particular port. - """ - pass - - @abstractmethod - def get_all_attached_interfaces(self, tenant_id, net_id): - """ - Retrieves all remote interfaces that are attached to - a particular Virtual Network. + :returns: None + :raises: exception.NetworkNotFound + :raises: exception.PortNotFound """ pass diff --git a/quantum/service.py b/quantum/service.py index 1406462b18..0c27bcc5b7 100644 --- a/quantum/service.py +++ b/quantum/service.py @@ -102,7 +102,9 @@ def serve_wsgi(cls, conf=None, options=None, args=None): def _run_wsgi(app_name, paste_conf, paste_config_file): LOG.info(_('Using paste.deploy config at: %s'), paste_config_file) - app = config.load_paste_app(paste_config_file, app_name) + conf, app = config.load_paste_app(app_name, + {'config_file': paste_config_file}, + None) if not app: LOG.error(_('No known API applications configured in %s.'), paste_config_file) diff --git a/run_tests.py b/run_tests.py new file mode 100644 index 0000000000..acc8260b81 --- /dev/null +++ b/run_tests.py @@ -0,0 +1,293 @@ +#!/usr/bin/env python +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 OpenStack, LLC +# 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. + +# Colorizer Code is borrowed from Twisted: +# Copyright (c) 2001-2010 Twisted Matrix Laboratories. +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +"""Unittest runner for quantum + +To run all test:: + python run_tests.py + +To run all unit tests:: + python run_tests.py unit + +To run all functional tests:: + python run_tests.py functional + +To run a single unit test:: + python run_tests.py unit.test_stores:TestSwiftBackend.test_get + +To run a single functional test:: + python run_tests.py functional.test_service:TestController.test_create + +To run a single unit test module:: + python run_tests.py unit.test_stores + +To run a single functional test module:: + python run_tests.py functional.test_stores +""" + +import gettext +import os +import unittest +import sys + +from nose import config +from nose import result +from nose import core + + +class _AnsiColorizer(object): + """ + A colorizer is an object that loosely wraps around a stream, allowing + callers to write text to the stream in a particular color. + + Colorizer classes must implement C{supported()} and C{write(text, color)}. + """ + _colors = dict(black=30, red=31, green=32, yellow=33, + blue=34, magenta=35, cyan=36, white=37) + + def __init__(self, stream): + self.stream = stream + + def supported(cls, stream=sys.stdout): + """ + A class method that returns True if the current platform supports + coloring terminal output using this method. Returns False otherwise. + """ + if not stream.isatty(): + return False # auto color only on TTYs + try: + import curses + except ImportError: + return False + else: + try: + try: + return curses.tigetnum("colors") > 2 + except curses.error: + curses.setupterm() + return curses.tigetnum("colors") > 2 + except: + raise + # guess false in case of error + return False + supported = classmethod(supported) + + def write(self, text, color): + """ + Write the given text to the stream in the given color. + + @param text: Text to be written to the stream. + + @param color: A string label for a color. e.g. 'red', 'white'. + """ + color = self._colors[color] + self.stream.write('\x1b[%s;1m%s\x1b[0m' % (color, text)) + + +class _Win32Colorizer(object): + """ + See _AnsiColorizer docstring. + """ + def __init__(self, stream): + from win32console import GetStdHandle, STD_OUT_HANDLE, \ + FOREGROUND_RED, FOREGROUND_BLUE, FOREGROUND_GREEN, \ + FOREGROUND_INTENSITY + red, green, blue, bold = (FOREGROUND_RED, FOREGROUND_GREEN, + FOREGROUND_BLUE, FOREGROUND_INTENSITY) + self.stream = stream + self.screenBuffer = GetStdHandle(STD_OUT_HANDLE) + self._colors = { + 'normal': red | green | blue, + 'red': red | bold, + 'green': green | bold, + 'blue': blue | bold, + 'yellow': red | green | bold, + 'magenta': red | blue | bold, + 'cyan': green | blue | bold, + 'white': red | green | blue | bold} + + def supported(cls, stream=sys.stdout): + try: + import win32console + screenBuffer = win32console.GetStdHandle( + win32console.STD_OUT_HANDLE) + except ImportError: + return False + import pywintypes + try: + screenBuffer.SetConsoleTextAttribute( + win32console.FOREGROUND_RED | + win32console.FOREGROUND_GREEN | + win32console.FOREGROUND_BLUE) + except pywintypes.error: + return False + else: + return True + supported = classmethod(supported) + + def write(self, text, color): + color = self._colors[color] + self.screenBuffer.SetConsoleTextAttribute(color) + self.stream.write(text) + self.screenBuffer.SetConsoleTextAttribute(self._colors['normal']) + + +class _NullColorizer(object): + """ + See _AnsiColorizer docstring. + """ + def __init__(self, stream): + self.stream = stream + + def supported(cls, stream=sys.stdout): + return True + supported = classmethod(supported) + + def write(self, text, color): + self.stream.write(text) + + +class QuantumTestResult(result.TextTestResult): + def __init__(self, *args, **kw): + result.TextTestResult.__init__(self, *args, **kw) + self._last_case = None + self.colorizer = None + # NOTE(vish, tfukushima): reset stdout for the terminal check + stdout = sys.__stdout__ + for colorizer in [_Win32Colorizer, _AnsiColorizer, _NullColorizer]: + if colorizer.supported(): + self.colorizer = colorizer(self.stream) + break + sys.stdout = stdout + + def getDescription(self, test): + return str(test) + + # NOTE(vish, tfukushima): copied from unittest with edit to add color + def addSuccess(self, test): + unittest.TestResult.addSuccess(self, test) + if self.showAll: + self.colorizer.write("OK", 'green') + self.stream.writeln() + elif self.dots: + self.stream.write('.') + self.stream.flush() + + # NOTE(vish, tfukushima): copied from unittest with edit to add color + def addFailure(self, test, err): + unittest.TestResult.addFailure(self, test, err) + if self.showAll: + self.colorizer.write("FAIL", 'red') + self.stream.writeln() + elif self.dots: + self.stream.write('F') + self.stream.flush() + + # NOTE(vish, tfukushima): copied from unittest with edit to add color + def addError(self, test, err): + """Overrides normal addError to add support for errorClasses. + If the exception is a registered class, the error will be added + to the list for that class, not errors. + """ + stream = getattr(self, 'stream', None) + ec, ev, tb = err + try: + exc_info = self._exc_info_to_string(err, test) + except TypeError: + # This is for compatibility with Python 2.3. + exc_info = self._exc_info_to_string(err) + for cls, (storage, label, isfail) in self.errorClasses.items(): + if result.isclass(ec) and issubclass(ec, cls): + if isfail: + test.passwd = False + storage.append((test, exc_info)) + # Might get patched into a streamless result + if stream is not None: + if self.showAll: + message = [label] + detail = result._exception_details(err[1]) + if detail: + message.append(detail) + stream.writeln(": ".join(message)) + elif self.dots: + stream.write(label[:1]) + return + self.errors.append((test, exc_info)) + test.passed = False + if stream is not None: + if self.showAll: + self.colorizer.write("ERROR", 'red') + self.stream.writeln() + elif self.dots: + stream.write('E') + + def startTest(self, test): + unittest.TestResult.startTest(self, test) + current_case = test.test.__class__.__name__ + + if self.showAll: + if current_case != self._last_case: + self.stream.writeln(current_case) + self._last_case = current_case + + self.stream.write( + ' %s' % str(test.test._testMethodName).ljust(60)) + self.stream.flush() + + +class QuantumTestRunner(core.TextTestRunner): + def _makeResult(self): + return QuantumTestResult(self.stream, + self.descriptions, + self.verbosity, + self.config) + + +if __name__ == '__main__': + working_dir = os.path.abspath("tests") + c = config.Config(stream=sys.stdout, + env=os.environ, + verbosity=3, + workingDir=working_dir) + + runner = QuantumTestRunner(stream=c.stream, + verbosity=c.verbosity, + config=c) + sys.exit(not core.run(config=c, testRunner=runner)) diff --git a/run_tests.sh b/run_tests.sh new file mode 100755 index 0000000000..aa72cbe22d --- /dev/null +++ b/run_tests.sh @@ -0,0 +1,83 @@ +#!/bin/bash + +function usage { + echo "Usage: $0 [OPTION]..." + echo "Run Melange's test suite(s)" + echo "" + echo " -V, --virtual-env Always use virtualenv. Install automatically if not present" + echo " -N, --no-virtual-env Don't use virtualenv. Run tests in local environment" + echo " -f, --force Force a clean re-build of the virtual environment. Useful when dependencies have been added." + echo " -h, --help Print this usage message" + echo "" + echo "Note: with no options specified, the script will try to run the tests in a virtual environment," + echo " If no virtualenv is found, the script will ask if you would like to create one. If you " + echo " prefer to run tests NOT in a virtual environment, simply pass the -N option." + exit +} + +function process_option { + case "$1" in + -h|--help) usage;; + -V|--virtual-env) let always_venv=1; let never_venv=0;; + -N|--no-virtual-env) let always_venv=0; let never_venv=1;; + -f|--force) let force=1;; + *) noseargs="$noseargs $1" + esac +} + +venv=.quantum-venv +with_venv=tools/with_venv.sh +always_venv=0 +never_venv=0 +force=0 +noseargs= +wrapper="" + +for arg in "$@"; do + process_option $arg +done + +function run_tests { + # Just run the test suites in current environment + ${wrapper} rm -f tests.sqlite + ${wrapper} $NOSETESTS 2> run_tests.err.log +} + +NOSETESTS="python run_tests.py $noseargs" + +if [ $never_venv -eq 0 ] +then + # Remove the virtual environment if --force used + if [ $force -eq 1 ]; then + echo "Cleaning virtualenv..." + rm -rf ${venv} + fi + if [ -e ${venv} ]; then + wrapper="${with_venv}" + else + if [ $always_venv -eq 1 ]; then + # Automatically install the virtualenv + python tools/install_venv.py + wrapper="${with_venv}" + else + echo -e "No virtual environment found...create one? (Y/n) \c" + read use_ve + if [ "x$use_ve" = "xY" -o "x$use_ve" = "x" -o "x$use_ve" = "xy" ]; then + # Install the virtualenv and run the test suite in it + python tools/install_venv.py + wrapper=${with_venv} + fi + fi + fi +fi + +# FIXME(sirp): bzr version-info is not currently pep-8. This was fixed with +# lp701898 [1], however, until that version of bzr becomes standard, I'm just +# excluding the vcsversion.py file +# +# [1] https://bugs.launchpad.net/bzr/+bug/701898 +# +PEP8_EXCLUDE=vcsversion.py +PEP8_OPTIONS="--exclude=$PEP8_EXCLUDE --repeat --show-source" +PEP8_INCLUDE="bin/* quantum tests tools run_tests.py" +run_tests && pep8 $PEP8_OPTIONS $PEP8_INCLUDE || exit 1 diff --git a/test_scripts/tests.py b/test_scripts/tests.py deleted file mode 100644 index 589d9da224..0000000000 --- a/test_scripts/tests.py +++ /dev/null @@ -1,150 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 Citrix Systems -# 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. - -import gettext - -gettext.install('quantum', unicode=1) - -from miniclient import MiniClient -from quantum.common.wsgi import Serializer - -HOST = '127.0.0.1' -PORT = 9696 -USE_SSL = False -TENANT_ID = 'totore' - -test_network_data = \ - {'network': {'network-name': 'test' }} - -def print_response(res): - content = res.read() - print "Status: %s" %res.status - print "Content: %s" %content - return content - -def test_list_networks_and_ports(format = 'xml'): - client = MiniClient(HOST, PORT, USE_SSL) - print "TEST LIST NETWORKS AND PORTS -- FORMAT:%s" %format - print "----------------------------" - print "--> Step 1 - List All Networks" - res = client.do_request(TENANT_ID,'GET', "/networks." + format) - print_response(res) - print "--> Step 2 - Details for Network 001" - res = client.do_request(TENANT_ID,'GET', "/networks/001." + format) - print_response(res) - print "--> Step 3 - Ports for Network 001" - res = client.do_request(TENANT_ID,'GET', "/networks/001/ports." + format) - print_response(res) - print "--> Step 4 - Details for Port 1" - res = client.do_request(TENANT_ID,'GET', "/networks/001/ports/1." + format) - print_response(res) - print "COMPLETED" - print "----------------------------" - -def test_create_network(format = 'xml'): - client = MiniClient(HOST, PORT, USE_SSL) - print "TEST CREATE NETWORK -- FORMAT:%s" %format - print "----------------------------" - print "--> Step 1 - Create Network" - content_type = "application/" + format - body = Serializer().serialize(test_network_data, content_type) - res = client.do_request(TENANT_ID,'POST', "/networks." + format, body=body) - print_response(res) - print "--> Step 2 - List All Networks" - res = client.do_request(TENANT_ID,'GET', "/networks." + format) - print_response(res) - print "COMPLETED" - print "----------------------------" - -def test_rename_network(format = 'xml'): - client = MiniClient(HOST, PORT, USE_SSL) - content_type = "application/" + format - print "TEST RENAME NETWORK -- FORMAT:%s" %format - print "----------------------------" - print "--> Step 1 - Retrieve network" - res = client.do_request(TENANT_ID,'GET', "/networks/001." + format) - print_response(res) - print "--> Step 2 - Rename network to 'test_renamed'" - test_network_data['network']['network-name'] = 'test_renamed' - body = Serializer().serialize(test_network_data, content_type) - res = client.do_request(TENANT_ID,'PUT', "/networks/001." + format, body=body) - print_response(res) - print "--> Step 2 - Retrieve network (again)" - res = client.do_request(TENANT_ID,'GET', "/networks/001." + format) - print_response(res) - print "COMPLETED" - print "----------------------------" - -def test_delete_network(format = 'xml'): - client = MiniClient(HOST, PORT, USE_SSL) - content_type = "application/" + format - print "TEST DELETE NETWORK -- FORMAT:%s" %format - print "----------------------------" - print "--> Step 1 - List All Networks" - res = client.do_request(TENANT_ID,'GET', "/networks." + format) - content = print_response(res) - network_data = Serializer().deserialize(content, content_type) - print network_data - net_id = network_data['networks'][0]['id'] - print "--> Step 2 - Delete network %s" %net_id - res = client.do_request(TENANT_ID,'DELETE', - "/networks/" + net_id + "." + format) - print_response(res) - print "--> Step 3 - List All Networks (Again)" - res = client.do_request(TENANT_ID,'GET', "/networks." + format) - print_response(res) - print "COMPLETED" - print "----------------------------" - - -def test_create_port(format = 'xml'): - client = MiniClient(HOST, PORT, USE_SSL) - print "TEST CREATE PORT -- FORMAT:%s" %format - print "----------------------------" - print "--> Step 1 - List Ports for network 001" - res = client.do_request(TENANT_ID,'GET', "/networks/001/ports." + format) - print_response(res) - print "--> Step 2 - Create Port for network 001" - res = client.do_request(TENANT_ID,'POST', "/networks/001/ports." + format) - print_response(res) - print "--> Step 3 - List Ports for network 001 (again)" - res = client.do_request(TENANT_ID,'GET', "/networks/001/ports." + format) - print_response(res) - print "COMPLETED" - print "----------------------------" - - -def main(): - test_list_networks_and_ports('xml') - test_list_networks_and_ports('json') - test_create_network('xml') - test_create_network('json') - test_rename_network('xml') - test_rename_network('json') - # NOTE: XML deserializer does not work properly - # disabling XML test - this is NOT a server-side issue - #test_delete_network('xml') - test_delete_network('json') - test_create_port('xml') - test_create_port('json') - - pass - - -# Standard boilerplate to call the main() function. -if __name__ == '__main__': - main() \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/functional/__init__.py b/tests/functional/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test_scripts/miniclient.py b/tests/functional/miniclient.py similarity index 95% rename from test_scripts/miniclient.py rename to tests/functional/miniclient.py index fb1ebc8fec..be49867355 100644 --- a/test_scripts/miniclient.py +++ b/tests/functional/miniclient.py @@ -19,6 +19,7 @@ import httplib import socket import urllib + class MiniClient(object): """A base client class - derived from Glance.BaseClient""" @@ -50,7 +51,7 @@ class MiniClient(object): def do_request(self, tenant, method, action, body=None, headers=None, params=None): """ - Connects to the server and issues a request. + Connects to the server and issues a request. Returns the result data, or raises an appropriate exception if HTTP status code is not 2xx @@ -62,14 +63,14 @@ class MiniClient(object): """ action = MiniClient.action_prefix + action - action = action.replace('{tenant_id}',tenant) + action = action.replace('{tenant_id}', tenant) if type(params) is dict: action += '?' + urllib.urlencode(params) try: connection_type = self.get_connection_type() headers = headers or {} - + # Open connection and send request c = connection_type(self.host, self.port) c.request(method, action, body, headers) @@ -95,4 +96,4 @@ class MiniClient(object): if hasattr(response, 'status_int'): return response.status_int else: - return response.status \ No newline at end of file + return response.status diff --git a/tests/functional/test_service.py b/tests/functional/test_service.py new file mode 100644 index 0000000000..6ed728394a --- /dev/null +++ b/tests/functional/test_service.py @@ -0,0 +1,136 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 Citrix Systems +# Copyright 2011 Nicira Networks +# 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. + +import gettext +import simplejson +import sys +import unittest + +gettext.install('quantum', unicode=1) + +from miniclient import MiniClient +from quantum.common.wsgi import Serializer + +HOST = '127.0.0.1' +PORT = 9696 +USE_SSL = False + +TENANT_ID = 'totore' +FORMAT = "json" + +test_network1_data = \ + {'network': {'network-name': 'test1'}} +test_network2_data = \ + {'network': {'network-name': 'test2'}} + + +def print_response(res): + content = res.read() + print "Status: %s" % res.status + print "Content: %s" % content + return content + + +class QuantumTest(unittest.TestCase): + def setUp(self): + self.client = MiniClient(HOST, PORT, USE_SSL) + + def create_network(self, data): + content_type = "application/" + FORMAT + body = Serializer().serialize(data, content_type) + res = self.client.do_request(TENANT_ID, 'POST', "/networks." + FORMAT, + body=body) + self.assertEqual(res.status, 200, "bad response: %s" % res.read()) + + def test_listNetworks(self): + self.create_network(test_network1_data) + self.create_network(test_network2_data) + res = self.client.do_request(TENANT_ID, 'GET', "/networks." + FORMAT) + self.assertEqual(res.status, 200, "bad response: %s" % res.read()) + + def test_createNetwork(self): + self.create_network(test_network1_data) + + def test_createPort(self): + self.create_network(test_network1_data) + res = self.client.do_request(TENANT_ID, 'GET', "/networks." + FORMAT) + resdict = simplejson.loads(res.read()) + for n in resdict["networks"]: + net_id = n["id"] + + # Step 1 - List Ports for network (should not find any) + res = self.client.do_request(TENANT_ID, 'GET', + "/networks/%s/ports.%s" % (net_id, FORMAT)) + self.assertEqual(res.status, 200, "Bad response: %s" % res.read()) + output = res.read() + self.assertTrue(len(output) == 0, + "Found unexpected ports: %s" % output) + + # Step 2 - Create Port for network + res = self.client.do_request(TENANT_ID, 'POST', + "/networks/%s/ports.%s" % (net_id, FORMAT)) + self.assertEqual(res.status, 200, "Bad response: %s" % output) + + # Step 3 - List Ports for network (again); should find one + res = self.client.do_request(TENANT_ID, 'GET', + "/networks/%s/ports.%s" % (net_id, FORMAT)) + output = res.read() + self.assertEqual(res.status, 200, "Bad response: %s" % output) + resdict = simplejson.loads(output) + ids = [] + for p in resdict["ports"]: + ids.append(p["id"]) + self.assertTrue(len(ids) == 1, + "Didn't find expected # of ports (1): %s" % ids) + + def test_renameNetwork(self): + self.create_network(test_network1_data) + res = self.client.do_request(TENANT_ID, 'GET', "/networks." + FORMAT) + resdict = simplejson.loads(res.read()) + net_id = resdict["networks"][0]["id"] + + data = test_network1_data.copy() + data['network']['network-name'] = 'test_renamed' + content_type = "application/" + FORMAT + body = Serializer().serialize(data, content_type) + res = self.client.do_request(TENANT_ID, 'PUT', + "/networks/%s.%s" % (net_id, FORMAT), body=body) + resdict = simplejson.loads(res.read()) + self.assertTrue(resdict["networks"]["network"]["id"] == net_id, + "Network_rename: renamed network has a different uuid") + self.assertTrue( + resdict["networks"]["network"]["name"] == "test_renamed", + "Network rename didn't take effect") + + def delete_networks(self): + # Remove all the networks created on the tenant + res = self.client.do_request(TENANT_ID, 'GET', "/networks." + FORMAT) + resdict = simplejson.loads(res.read()) + for n in resdict["networks"]: + net_id = n["id"] + res = self.client.do_request(TENANT_ID, 'DELETE', + "/networks/" + net_id + "." + FORMAT) + self.assertEqual(res.status, 202) + + def tearDown(self): + self.delete_networks() + +# Standard boilerplate to call the main() function. +if __name__ == '__main__': + suite = unittest.TestLoader().loadTestsFromTestCase(QuantumTest) + unittest.TextTestRunner(verbosity=2).run(suite) diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 0000000000..5910e35494 --- /dev/null +++ b/tests/unit/__init__.py @@ -0,0 +1,32 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC. +# 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. + +# See http://code.google.com/p/python-nose/issues/detail?id=373 +# The code below enables nosetests to work with i18n _() blocks +import __builtin__ +import unittest +setattr(__builtin__, '_', lambda x: x) + + +class BaseTest(unittest.TestCase): + + def setUp(self): + pass + + +def setUp(): + pass diff --git a/tools/install_venv.py b/tools/install_venv.py new file mode 100644 index 0000000000..5c3ef374b6 --- /dev/null +++ b/tools/install_venv.py @@ -0,0 +1,137 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2010 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. + +""" +Installation script for Quantum's development virtualenv +""" + +import os +import subprocess +import sys + + +ROOT = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) +VENV = os.path.join(ROOT, '.quantum-venv') +PIP_REQUIRES = os.path.join(ROOT, 'tools', 'pip-requires') + + +def die(message, *args): + print >> sys.stderr, message % args + sys.exit(1) + + +def run_command(cmd, redirect_output=True, check_exit_code=True): + """ + Runs a command in an out-of-process shell, returning the + output of that command. Working directory is ROOT. + """ + if redirect_output: + stdout = subprocess.PIPE + else: + stdout = None + + proc = subprocess.Popen(cmd, cwd=ROOT, stdout=stdout) + output = proc.communicate()[0] + if check_exit_code and proc.returncode != 0: + die('Command "%s" failed.\n%s', ' '.join(cmd), output) + return output + + +HAS_EASY_INSTALL = bool(run_command(['which', 'easy_install'], + check_exit_code=False).strip()) +HAS_VIRTUALENV = bool(run_command(['which', 'virtualenv'], + check_exit_code=False).strip()) + + +def check_dependencies(): + """Make sure virtualenv is in the path.""" + + if not HAS_VIRTUALENV: + print 'not found.' + # Try installing it via easy_install... + if HAS_EASY_INSTALL: + print 'Installing virtualenv via easy_install...', + if not run_command(['which', 'easy_install']): + die('ERROR: virtualenv not found.\n\n' + 'Quantum requires virtualenv, please install' + ' it using your favorite package management tool') + print 'done.' + print 'done.' + + +def create_virtualenv(venv=VENV): + """Creates the virtual environment and installs PIP only into the + virtual environment + """ + print 'Creating venv...', + run_command(['virtualenv', '-q', '--no-site-packages', VENV]) + print 'done.' + print 'Installing pip in virtualenv...', + if not run_command(['tools/with_venv.sh', 'easy_install', 'pip']).strip(): + die("Failed to install pip.") + print 'done.' + + +def install_dependencies(venv=VENV): + print 'Installing dependencies with pip (this can take a while)...' + + # Install greenlet by hand - just listing it in the requires file does not + # get it in stalled in the right order + venv_tool = 'tools/with_venv.sh' + run_command([venv_tool, 'pip', 'install', '-E', venv, '-r', PIP_REQUIRES], + redirect_output=False) + + # Tell the virtual env how to "import quantum" + pthfile = os.path.join(venv, "lib", "python2.6", "site-packages", + "quantum.pth") + f = open(pthfile, 'w') + f.write("%s\n" % ROOT) + + +def print_help(): + help = """ + Quantum development environment setup is complete. + + Quantum development uses virtualenv to track and manage Python dependencies + while in development and testing. + + To activate the Quantum virtualenv for the extent of your current shell + session you can run: + + $ source .quantum-venv/bin/activate + + Or, if you prefer, you can run commands in the virtualenv on a case by case + basis by running: + + $ tools/with_venv.sh + + Also, make test will automatically use the virtualenv. + """ + print help + + +def main(argv): + check_dependencies() + create_virtualenv() + install_dependencies() + print_help() + +if __name__ == '__main__': + main(sys.argv) diff --git a/tools/pip-requires b/tools/pip-requires new file mode 100644 index 0000000000..8dbfd85d31 --- /dev/null +++ b/tools/pip-requires @@ -0,0 +1,10 @@ +eventlet>=0.9.12 +nose +Paste +PasteDeploy +pep8==0.5.0 +python-gflags +routes +simplejson +webob +webtest diff --git a/tools/with_venv.sh b/tools/with_venv.sh new file mode 100755 index 0000000000..83149462c1 --- /dev/null +++ b/tools/with_venv.sh @@ -0,0 +1,21 @@ +#!/bin/bash +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC. +# 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. + +TOOLS=`dirname $0` +VENV=$TOOLS/../.quantum-venv +source $VENV/bin/activate && $@