From 6d97d94509d14e237c01cf6b25cdd2ad1cf2ed57 Mon Sep 17 00:00:00 2001 From: Brad Hall Date: Wed, 1 Jun 2011 11:00:15 -0700 Subject: [PATCH 01/33] Copy over miniclient from testscripts and port tests.py to use unittest --- smoketests/__init__.py | 0 smoketests/miniclient.py | 98 +++++++++++++++++++++++++++++ smoketests/tests.py | 133 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 231 insertions(+) create mode 100644 smoketests/__init__.py create mode 100644 smoketests/miniclient.py create mode 100644 smoketests/tests.py diff --git a/smoketests/__init__.py b/smoketests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/smoketests/miniclient.py b/smoketests/miniclient.py new file mode 100644 index 000000000..fb1ebc8fe --- /dev/null +++ b/smoketests/miniclient.py @@ -0,0 +1,98 @@ +# 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 httplib +import socket +import urllib + +class MiniClient(object): + + """A base client class - derived from Glance.BaseClient""" + + action_prefix = '/v0.1/tenants/{tenant_id}' + + def __init__(self, host, port, use_ssl): + """ + Creates a new client to some service. + + :param host: The host where service resides + :param port: The port where service resides + :param use_ssl: Should we use HTTPS? + """ + self.host = host + self.port = port + self.use_ssl = use_ssl + self.connection = None + + def get_connection_type(self): + """ + Returns the proper connection type + """ + if self.use_ssl: + return httplib.HTTPSConnection + else: + return httplib.HTTPConnection + + def do_request(self, tenant, method, action, body=None, + headers=None, params=None): + """ + Connects to the server and issues a request. + Returns the result data, or raises an appropriate exception if + HTTP status code is not 2xx + + :param method: HTTP method ("GET", "POST", "PUT", etc...) + :param body: string of data to send, or None (default) + :param headers: mapping of key/value pairs to add as headers + :param params: dictionary of key/value pairs to add to append + to action + + """ + 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): + """ + Returns the integer status code from the response, which + can be either a Webob.Response (used in testing) or httplib.Response + """ + if hasattr(response, 'status_int'): + return response.status_int + else: + return response.status \ No newline at end of file diff --git a/smoketests/tests.py b/smoketests/tests.py new file mode 100644 index 000000000..f9cd77418 --- /dev/null +++ b/smoketests/tests.py @@ -0,0 +1,133 @@ +# 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) From 5cf7dc6cb262f4b9435a052b9244d6cb5b023dc5 Mon Sep 17 00:00:00 2001 From: Brad Hall Date: Thu, 2 Jun 2011 22:30:37 -0700 Subject: [PATCH 02/33] Initial rework of cli to use the WS api - Still need to implement the interface commands and also address TODO's in the code. --- quantum/api/views/ports.py | 7 +- quantum/cli.py | 469 ++++++++++++++++++++++++++++++------- 2 files changed, 389 insertions(+), 87 deletions(-) diff --git a/quantum/api/views/ports.py b/quantum/api/views/ports.py index 2d93a35f6..6077214b6 100644 --- a/quantum/api/views/ports.py +++ b/quantum/api/views/ports.py @@ -37,12 +37,13 @@ class ViewBuilder(object): else: port = self._build_simple(port_data) return port - + def _build_simple(self, port_data): """Return a simple model of a server.""" return dict(port=dict(id=port_data['port-id'])) - + def _build_detail(self, port_data): """Return a simple model of a server.""" return dict(port=dict(id=port_data['port-id'], - state=port_data['port-state'])) + attachment=port_data['attachment'], + state=port_data['port-state'])) diff --git a/quantum/cli.py b/quantum/cli.py index 78f1a6b48..faa92f1b1 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,97 +15,397 @@ # 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 simplejson +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}' + def __init__(self, host, port, use_ssl): + self.host = host + self.port = port + self.use_ssl = use_ssl + self.connection = None + def get_connection_type(self): + if self.use_ssl: + return httplib.HTTPSConnection + else: + return httplib.HTTPConnection + 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 -if len(sys.argv) < 2 or len(sys.argv) > 6: - usage() - exit(1) +### -- Core CLI functions -quantum = QuantumManager() -manager = quantum.get_manager() +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) -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 api_list_nets(client, *args): + tenant_id = args[0] + res = client.do_request(tenant_id, 'GET', "/networks." + FORMAT) + resdict = simplejson.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] - 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() +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 = simplejson.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 + network = manager.get_network_details(tid, nid) + network_id = network["net-id"] + network_name = network["net-name"] + print "\tNetwork id:%s\n\tNetwork name:%s\n" % (network_id, network_name) + +def api_detail_net(client, *args): + tid, nid = args + res = client.do_request(tid, 'GET', "/networks/" + nid + "." + FORMAT) + output = res.read() + rd = simplejson.loads(output) + LOG.debug(rd) + network_id = rd["networks"]["network"]["id"] + network_name = rd["networks"]["network"]["name"] + print "\tNetwork id:%s\n\tNetwork name:%s\n" % (network_id, network_name) + +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 = simplejson.loads(res.read()) + LOG.debug(resdict) + print "Renamed Virtual Network with ID:%s" % nid + +# TODO(bgh): fix this command +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: + "\tVirtual Port:%s" % port + +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 = simplejson.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 = simplejson.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 = simplejson.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) + +# TODO(bgh): still need to implement the iface commands +def plug_iface(manager, *args): + tid, nid, pid, vid = args + manager.plug_interface(tid, nid, pid, vid) + LOG.info("Plugged remote interface:%s " \ + "into Virtual Network:%s" % (vid, nid)) + +def unplug_iface(manager, *args): + tid, nid, pid = args + manager.unplug_interface(tid, nid, pid) + LOG.info("UnPlugged remote interface " \ + "from Virtual Port:%s Virtual Network:%s" % (pid, nid)) + +def detail_iface(manager, *args): + tid, nid, pid = args + remote_iface = manager.get_interface_details(tid, nid, pid) + LOG.info("Remote interface on Virtual Port:%s " \ + "Virtual Network:%s is %s" % (pid, nid, remote_iface)) + +def list_iface(manager, *args): + tid, nid = args + iface_list = manager.get_all_attached_interfaces(tid, nid) + LOG.info("Remote Interfaces on Virtual Network:%s\n" % nid) + for iface in iface_list: + LOG.info("\tRemote interface :%s" % iface) + +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, + "args": ["tenant-id", "net-id", "port-id", "iface-id"] + }, + "unplug_iface": { + "func": unplug_iface, + "args": ["tenant-id", "net-id", "port-id"] + }, + "detail_iface": { + "func": detail_iface, + "args": ["tenant-id", "net-id", "port-id"] + }, + "list_iface": { + "func": list_iface, + "args": ["tenant-id", "net-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("-a", "--use-api", dest="use_api", + action="store_true", default=False, help="Use 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() + 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 options.use_api: + client = MiniClient(options.host, options.port, options.ssl) + if not commands[cmd].has_key("api_func"): + 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) From bee2ea21aa7b8210e4a2ee69a90e172923cdb9ea Mon Sep 17 00:00:00 2001 From: Brad Hall Date: Fri, 3 Jun 2011 10:25:43 -0700 Subject: [PATCH 03/33] Added api functions for the interface commands --- quantum/cli.py | 83 ++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 74 insertions(+), 9 deletions(-) diff --git a/quantum/cli.py b/quantum/cli.py index faa92f1b1..ad37f0078 100644 --- a/quantum/cli.py +++ b/quantum/cli.py @@ -246,31 +246,92 @@ def api_detail_port(client, *args): print "Virtual Port:%s on Virtual Network:%s " \ "contains remote interface:%s" % (pid, nid, attachment) -# TODO(bgh): still need to implement the iface commands def plug_iface(manager, *args): tid, nid, pid, vid = args manager.plug_interface(tid, nid, pid, vid) - LOG.info("Plugged remote interface:%s " \ - "into Virtual Network:%s" % (vid, nid)) + 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) - LOG.info("UnPlugged remote interface " \ - "from Virtual Port:%s Virtual Network:%s" % (pid, nid)) + 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) def detail_iface(manager, *args): tid, nid, pid = args remote_iface = manager.get_interface_details(tid, nid, pid) - LOG.info("Remote interface on Virtual Port:%s " \ - "Virtual Network:%s is %s" % (pid, nid, remote_iface)) + print "Remote interface on Virtual Port:%s " \ + "Virtual Network:%s is %s" % (pid, nid, remote_iface) + +def api_detail_iface(manager, *args): + tid, nid, pid = args + res = client.do_request(tid, 'GET', + "/networks/%s/ports/%s/attachment.%s" % (nid, pid, FORMAT)) + output = res.read() + rd = simplejson.loads(output) + LOG.debug(rd) + remote_iface = rd["attachment"] + print "Remote interface on Virtual Port:%s " \ + "Virtual Network:%s is %s" % (pid, nid, remote_iface) def list_iface(manager, *args): tid, nid = args iface_list = manager.get_all_attached_interfaces(tid, nid) - LOG.info("Remote Interfaces on Virtual Network:%s\n" % nid) + print "Remote Interfaces on Virtual Network:%s\n" % nid for iface in iface_list: - LOG.info("\tRemote interface :%s" % iface) + print "\tRemote interface:%s" % iface + +# TODO(bgh): I'm not sure how the api maps to manager.get_all_interfaces so +# I'm just doing this the manual way for now. +def api_list_iface(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 = simplejson.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 = simplejson.loads(output) + LOG.debug(rd) + remote_iface = rd["attachment"] + print "\tRemote interface:%s" % remote_iface commands = { "list_nets": { @@ -320,18 +381,22 @@ commands = { }, "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"] }, "detail_iface": { "func": detail_iface, + "api_func": api_detail_iface, "args": ["tenant-id", "net-id", "port-id"] }, "list_iface": { "func": list_iface, + "api_func": api_list_iface, "args": ["tenant-id", "net-id"] }, } From 90a78e410cdcc6aa484b4e5e7ee0d4c0a6018056 Mon Sep 17 00:00:00 2001 From: Brad Hall Date: Fri, 3 Jun 2011 10:26:36 -0700 Subject: [PATCH 04/33] Whitespace fixes --- quantum/api/__init__.py | 3 +-- quantum/api/ports.py | 39 +++++++++++++++++---------------------- 2 files changed, 18 insertions(+), 24 deletions(-) diff --git a/quantum/api/__init__.py b/quantum/api/__init__.py index 25b66f557..3665464c9 100644 --- a/quantum/api/__init__.py +++ b/quantum/api/__init__.py @@ -51,12 +51,11 @@ class APIRouterV01(wsgi.Router): mapper.resource('network', 'networks', controller=networks.Controller(), path_prefix=uri_prefix) - mapper.resource('port', 'ports', + mapper.resource('port', 'ports', controller=ports.Controller(), parent_resource=dict(member_name='network', collection_name=\ uri_prefix + 'networks')) - mapper.connect("get_resource", uri_prefix + 'networks/{network_id}/' \ 'ports/{id}/attachment{.format}', diff --git a/quantum/api/ports.py b/quantum/api/ports.py index 2b93cdec7..8e0100634 100644 --- a/quantum/api/ports.py +++ b/quantum/api/ports.py @@ -29,15 +29,13 @@ class Controller(common.QuantumController): _port_ops_param_list = [{ 'param-name': 'port-state', - 'default-value': 'DOWN', + 'default-value': 'DOWN', 'required': False},] - _attachment_ops_param_list = [{ 'param-name': 'attachment-id', 'required': True},] - _serialization_metadata = { "application/xml": { "attributes": { @@ -49,7 +47,7 @@ class Controller(common.QuantumController): def __init__(self, plugin_conf_file=None): self._resource_name = 'port' super(Controller, self).__init__() - + def index(self, req, 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) @@ -64,7 +62,7 @@ class Controller(common.QuantumController): return dict(ports=result) except exception.NetworkNotFound as e: return faults.Fault(faults.NetworkNotFound(e)) - + def show(self, req, tenant_id, network_id, id): """ Returns port details for given port and network """ try: @@ -77,7 +75,7 @@ class Controller(common.QuantumController): except exception.NetworkNotFound as e: return faults.Fault(faults.NetworkNotFound(e)) except exception.PortNotFound as e: - return faults.Fault(faults.PortNotFound(e)) + return faults.Fault(faults.PortNotFound(e)) def create(self, req, tenant_id, network_id): """ Creates a new port for a given network """ @@ -87,17 +85,17 @@ class Controller(common.QuantumController): self._parse_request_params(req, self._port_ops_param_list) except exc.HTTPError as e: return faults.Fault(e) - try: + try: port = self.network_manager.create_port(tenant_id, - network_id, + network_id, req_params['port-state']) builder = ports_view.get_view_builder(req) result = builder.build(port) return dict(ports=result) except exception.NetworkNotFound as e: return faults.Fault(faults.NetworkNotFound(e)) - except exception.StateInvalid as e: - return faults.Fault(faults.RequestedStateInvalid(e)) + except exception.StateInvalid as e: + return faults.Fault(faults.RequestedStateInvalid(e)) def update(self, req, tenant_id, network_id, id): """ Updates the state of a port for a given network """ @@ -106,8 +104,8 @@ class Controller(common.QuantumController): req_params = \ self._parse_request_params(req, self._port_ops_param_list) except exc.HTTPError as e: - return faults.Fault(e) - try: + 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) @@ -117,10 +115,9 @@ class Controller(common.QuantumController): return faults.Fault(faults.NetworkNotFound(e)) except exception.PortNotFound as e: return faults.Fault(faults.PortNotFound(e)) - except exception.StateInvalid as e: + except exception.StateInvalid as e: return faults.Fault(faults.RequestedStateInvalid(e)) - def delete(self, req, tenant_id, network_id, id): """ Destroys the port with the given id """ #look for port state in request @@ -131,11 +128,10 @@ class Controller(common.QuantumController): except exception.NetworkNotFound as e: return faults.Fault(faults.NetworkNotFound(e)) except exception.PortNotFound as e: - return faults.Fault(faults.PortNotFound(e)) + return faults.Fault(faults.PortNotFound(e)) except exception.PortInUse as e: return faults.Fault(faults.PortInUse(e)) - def get_resource(self,req,tenant_id, network_id, id): try: result = self.network_manager.get_interface_details( @@ -144,9 +140,9 @@ class Controller(common.QuantumController): except exception.NetworkNotFound as e: return faults.Fault(faults.NetworkNotFound(e)) except exception.PortNotFound as e: - return faults.Fault(faults.PortNotFound(e)) + return faults.Fault(faults.PortNotFound(e)) - #TODO - Complete implementation of these APIs + #TODO - Complete implementation of these APIs def attach_resource(self,req,tenant_id, network_id, id): content_type = req.best_match_content_type() print "Content type:%s" %content_type @@ -164,14 +160,13 @@ class Controller(common.QuantumController): except exception.NetworkNotFound as e: return faults.Fault(faults.NetworkNotFound(e)) except exception.PortNotFound as e: - return faults.Fault(faults.PortNotFound(e)) + return faults.Fault(faults.PortNotFound(e)) except exception.PortInUse as e: return faults.Fault(faults.PortInUse(e)) except exception.AlreadyAttached as e: return faults.Fault(faults.AlreadyAttached(e)) - - #TODO - Complete implementation of these APIs + #TODO - Complete implementation of these APIs def detach_resource(self,req,tenant_id, network_id, id): try: self.network_manager.unplug_interface(tenant_id, @@ -180,4 +175,4 @@ class Controller(common.QuantumController): except exception.NetworkNotFound as e: return faults.Fault(faults.NetworkNotFound(e)) except exception.PortNotFound as e: - return faults.Fault(faults.PortNotFound(e)) + return faults.Fault(faults.PortNotFound(e)) From 9453bb93c455ea8e7fa026e1870a3335d7b584c7 Mon Sep 17 00:00:00 2001 From: Brad Hall Date: Fri, 3 Jun 2011 11:02:54 -0700 Subject: [PATCH 05/33] Print the command list in the help --- quantum/cli.py | 1 + 1 file changed, 1 insertion(+) diff --git a/quantum/cli.py b/quantum/cli.py index ad37f0078..2fa2b0281 100644 --- a/quantum/cli.py +++ b/quantum/cli.py @@ -451,6 +451,7 @@ if __name__ == "__main__": if len(args) < 1: parser.print_help() + help() sys.exit(1) cmd = args[0] From 84bed05e112c2d616903972769330088551e73fc Mon Sep 17 00:00:00 2001 From: Brad Hall Date: Fri, 3 Jun 2011 20:55:26 -0700 Subject: [PATCH 06/33] Add database models/functions for ports and networks --- quantum/db/__init__.py | 17 +++++ quantum/db/api.py | 166 +++++++++++++++++++++++++++++++++++++++++ quantum/db/models.py | 59 +++++++++++++++ 3 files changed, 242 insertions(+) create mode 100644 quantum/db/__init__.py create mode 100644 quantum/db/api.py create mode 100644 quantum/db/models.py diff --git a/quantum/db/__init__.py b/quantum/db/__init__.py new file mode 100644 index 000000000..620282b58 --- /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 000000000..7ae5a2625 --- /dev/null +++ b/quantum/db/api.py @@ -0,0 +1,166 @@ +# 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=True, + 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() + # TODO(bgh): Make sure another network doesn't have that name + 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() + # TODO(bgh): check to make sure new_inteface_id is + # unique if it is not None + port = port_get(port_id) + port.interface_id = new_interface_id + session.merge(port) + session.flush() + return port + +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 000000000..28bc139b9 --- /dev/null +++ b/quantum/db/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 + +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) From 46a57d2c11b99ff4309a9d92023b739138d57896 Mon Sep 17 00:00:00 2001 From: Brad Hall Date: Fri, 3 Jun 2011 20:56:32 -0700 Subject: [PATCH 07/33] Initial cut of openvswitch plugin --- quantum/plugins/openvswitch/Makefile | 30 ++ quantum/plugins/openvswitch/README | 37 +++ quantum/plugins/openvswitch/__init__.py | 0 quantum/plugins/openvswitch/agent/install.sh | 38 +++ .../openvswitch/agent/ovs_quantum_agent.py | 272 ++++++++++++++++ .../openvswitch/agent/set_external_ids.sh | 15 + quantum/plugins/openvswitch/ovs_db.py | 52 +++ quantum/plugins/openvswitch/ovs_models.py | 38 +++ .../openvswitch/ovs_quantum_plugin.ini | 9 + .../plugins/openvswitch/ovs_quantum_plugin.py | 296 ++++++++++++++++++ 10 files changed, 787 insertions(+) create mode 100644 quantum/plugins/openvswitch/Makefile create mode 100644 quantum/plugins/openvswitch/README create mode 100644 quantum/plugins/openvswitch/__init__.py create mode 100644 quantum/plugins/openvswitch/agent/install.sh create mode 100755 quantum/plugins/openvswitch/agent/ovs_quantum_agent.py create mode 100755 quantum/plugins/openvswitch/agent/set_external_ids.sh create mode 100644 quantum/plugins/openvswitch/ovs_db.py create mode 100644 quantum/plugins/openvswitch/ovs_models.py create mode 100644 quantum/plugins/openvswitch/ovs_quantum_plugin.ini create mode 100644 quantum/plugins/openvswitch/ovs_quantum_plugin.py diff --git a/quantum/plugins/openvswitch/Makefile b/quantum/plugins/openvswitch/Makefile new file mode 100644 index 000000000..7cd5e620e --- /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 000000000..ee2248a17 --- /dev/null +++ b/quantum/plugins/openvswitch/README @@ -0,0 +1,37 @@ +To Run: + +1) On the "Openstack Controller" host: + +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_naas" + +2) Edit the configuration file (src/ovs/plugins/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. + +3) Create the agent distribution tarball + +$ make agent-dist + +4) Copy the resulting tarball to your xenserver + +5) Unpack the tarball and run install.sh. This will install all of the +necessary pieces into /etc/xapi.d/plugins. + +6) Run the agent (example below): + +# /etc/xapi.d/plugins/ovs_quantum_agent.py /etc/xapi.d/plugins/ovs_quantum_plugin.ini + +7) Run ovs_quantum_plugin.py via the quantum plugin framework cli. + +- Edit quantum/plugins.ini to point to where the plugin and configuration + files live + +$ PYTHONPATH=$HOME/src/quantum-framework/quantum:$PYTHONPATH python quantum/cli.py + +This will show all of the available commands. diff --git a/quantum/plugins/openvswitch/__init__.py b/quantum/plugins/openvswitch/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/quantum/plugins/openvswitch/agent/install.sh b/quantum/plugins/openvswitch/agent/install.sh new file mode 100644 index 000000000..2f2b0381f --- /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 MYyQL-python >/dev/null 2>&1 +if [ $? -ne 0 ]; then + echo "MySQL-python not found; installing." + yum -y install MySQL-python + if [ $? -ne 0 ]; then + echo "Failed to install MYSQL-python; agent will not work." + exit 1 + fi +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 "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 000000000..9769ab93f --- /dev/null +++ b/quantum/plugins/openvswitch/agent/ovs_quantum_agent.py @@ -0,0 +1,272 @@ +#!/usr/bin/env python + +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 000000000..f56dc4bd9 --- /dev/null +++ b/quantum/plugins/openvswitch/agent/set_external_ids.sh @@ -0,0 +1,15 @@ + +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 000000000..8b4106215 --- /dev/null +++ b/quantum/plugins/openvswitch/ovs_db.py @@ -0,0 +1,52 @@ +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 000000000..7e8b2f54b --- /dev/null +++ b/quantum/plugins/openvswitch/ovs_models.py @@ -0,0 +1,38 @@ +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 000000000..0c75b3fc4 --- /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 000000000..e8111c659 --- /dev/null +++ b/quantum/plugins/openvswitch/ovs_quantum_plugin.py @@ -0,0 +1,296 @@ +import ConfigParser +import logging as LOG +import os +import sys +import unittest + +from quantum.quantum_plugin_base import QuantumPluginBase +import quantum.db.api as db +import ovs_db + +# TODO(bgh): Make sure we delete from network bindings when deleting a port, +# network, etc. + +CONF_FILE="ovs_quantum_plugin.ini" + +LOG.basicConfig(level=LOG.DEBUG) +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 + raise Exception("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.info("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"] = 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): + network = db.network_get(net_id) + d = {} + d["net-id"] = str(network.uuid) + d["net-name"] = network.name + d["net-ports"] = self.get_all_ports(tenant_id, net_id) + return d + + 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 get_all_attached_interfaces(self, tenant_id, net_id): + ports = db.port_list(net_id) + ifaces = [] + for p in ports: + ifaces.append(p.interface_id) + return ifaces + + 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, "None") + ovs_db.update_network_binding(net_id, remote_iface_id) + + 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: + print x + 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: + print x + 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): + pass + + def testGetPorts(self): + pass + + def testPlugInterface(self): + pass + + def testUnPlugInterface(self): + pass + + def tearDown(self): + networks = self.quantum.get_all_networks(self.tenant_id) + print networks + # 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) + print ports + 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__": + suite = unittest.TestLoader().loadTestsFromTestCase(OVSPluginTest) + unittest.TextTestRunner(verbosity=2).run(suite) + suite = unittest.TestLoader().loadTestsFromTestCase(VlanMapTest) + unittest.TextTestRunner(verbosity=2).run(suite) + + # TODO(bgh) move to unit tets + if False: + quantum.plug_interface(tenant_id, net1, port, "vif1.1") + portdetails = quantum.get_port_details(tenant_id, net1, port) + LOG.DEBUG(portdetails) + LOG.info("=== PORT: %s" % quantum.get_port_details(tenant_id, net1, port)) + assert(portdetails["interface_id"] == "vif1.1") + networks = quantum.get_all_networks(tenant_id) + LOG.debug(networks) + for nid, name in networks.iteritems(): + ports = quantum.get_all_ports(tenant_id, nid) + LOG.debug(ports) From 1c37bde8cb513e531f32424bcbf86deb4b50fa8b Mon Sep 17 00:00:00 2001 From: Brad Hall Date: Fri, 3 Jun 2011 20:59:49 -0700 Subject: [PATCH 08/33] Add headers --- .../openvswitch/agent/ovs_quantum_agent.py | 18 +++++++++++++++++ .../openvswitch/agent/set_external_ids.sh | 2 +- quantum/plugins/openvswitch/ovs_db.py | 20 +++++++++++++++++++ quantum/plugins/openvswitch/ovs_models.py | 20 +++++++++++++++++++ .../plugins/openvswitch/ovs_quantum_plugin.py | 19 ++++++++++++++++++ 5 files changed, 78 insertions(+), 1 deletion(-) diff --git a/quantum/plugins/openvswitch/agent/ovs_quantum_agent.py b/quantum/plugins/openvswitch/agent/ovs_quantum_agent.py index 9769ab93f..34f28fbdd 100755 --- a/quantum/plugins/openvswitch/agent/ovs_quantum_agent.py +++ b/quantum/plugins/openvswitch/agent/ovs_quantum_agent.py @@ -1,4 +1,22 @@ #!/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 diff --git a/quantum/plugins/openvswitch/agent/set_external_ids.sh b/quantum/plugins/openvswitch/agent/set_external_ids.sh index f56dc4bd9..2fae05f0a 100755 --- a/quantum/plugins/openvswitch/agent/set_external_ids.sh +++ b/quantum/plugins/openvswitch/agent/set_external_ids.sh @@ -1,4 +1,4 @@ - +#!/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` diff --git a/quantum/plugins/openvswitch/ovs_db.py b/quantum/plugins/openvswitch/ovs_db.py index 8b4106215..a2a72ec8c 100644 --- a/quantum/plugins/openvswitch/ovs_db.py +++ b/quantum/plugins/openvswitch/ovs_db.py @@ -1,3 +1,23 @@ +# 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 diff --git a/quantum/plugins/openvswitch/ovs_models.py b/quantum/plugins/openvswitch/ovs_models.py index 7e8b2f54b..610902a7c 100644 --- a/quantum/plugins/openvswitch/ovs_models.py +++ b/quantum/plugins/openvswitch/ovs_models.py @@ -1,3 +1,23 @@ +# 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 diff --git a/quantum/plugins/openvswitch/ovs_quantum_plugin.py b/quantum/plugins/openvswitch/ovs_quantum_plugin.py index e8111c659..50053df7a 100644 --- a/quantum/plugins/openvswitch/ovs_quantum_plugin.py +++ b/quantum/plugins/openvswitch/ovs_quantum_plugin.py @@ -1,3 +1,22 @@ +# 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 From 1e441b67d1e87ddecd570c0d5cf1c590d0a03ee8 Mon Sep 17 00:00:00 2001 From: Brad Hall Date: Sat, 4 Jun 2011 13:17:32 -0700 Subject: [PATCH 09/33] Address some of the remaining TODOs and general cleanup --- quantum/db/api.py | 25 ++++-- .../plugins/openvswitch/ovs_quantum_plugin.py | 85 ++++++++++++------- 2 files changed, 72 insertions(+), 38 deletions(-) diff --git a/quantum/db/api.py b/quantum/db/api.py index 7ae5a2625..8a6ba305c 100644 --- a/quantum/db/api.py +++ b/quantum/db/api.py @@ -35,7 +35,7 @@ def configure_db(options): global _ENGINE if not _ENGINE: _ENGINE = create_engine(options['sql_connection'], - echo=True, + echo=False, echo_pool=True, pool_recycle=3600) register_models() @@ -94,7 +94,6 @@ def network_get(net_id): def network_rename(net_id, tenant_id, new_name): session = get_session() - # TODO(bgh): Make sure another network doesn't have that name try: res = session.query(models.Network).\ filter_by(name=new_name).\ @@ -144,13 +143,21 @@ def port_get(port_id): def port_set_attachment(port_id, new_interface_id): session = get_session() - # TODO(bgh): check to make sure new_inteface_id is - # unique if it is not None - port = port_get(port_id) - port.interface_id = new_interface_id - session.merge(port) - session.flush() - return port + 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() diff --git a/quantum/plugins/openvswitch/ovs_quantum_plugin.py b/quantum/plugins/openvswitch/ovs_quantum_plugin.py index 50053df7a..036c3c830 100644 --- a/quantum/plugins/openvswitch/ovs_quantum_plugin.py +++ b/quantum/plugins/openvswitch/ovs_quantum_plugin.py @@ -24,15 +24,14 @@ import sys import unittest from quantum.quantum_plugin_base import QuantumPluginBase +from optparse import OptionParser + import quantum.db.api as db import ovs_db -# TODO(bgh): Make sure we delete from network bindings when deleting a port, -# network, etc. - CONF_FILE="ovs_quantum_plugin.ini" -LOG.basicConfig(level=LOG.DEBUG) +LOG.basicConfig(level=LOG.WARN) LOG.getLogger("ovs_quantum_plugin") def find_config(basepath): @@ -63,7 +62,7 @@ class VlanMap(object): self.vlans[x] = None # LOG.debug("VlanMap::release %s" % (x)) return - raise Exception("No vlan found with network \"%s\"" % network_id) + LOG.error("No vlan found with network \"%s\"" % network_id) class OVSQuantumPlugin(QuantumPluginBase): def __init__(self, configfile=None): @@ -76,7 +75,7 @@ class OVSQuantumPlugin(QuantumPluginBase): if configfile == None: raise Exception("Configuration file \"%s\" doesn't exist" % (configfile)) - LOG.info("Using configuration file: %s" % configfile) + LOG.debug("Using configuration file: %s" % configfile) config.read(configfile) LOG.debug("Config: %s" % config) @@ -124,7 +123,7 @@ class OVSQuantumPlugin(QuantumPluginBase): def delete_network(self, tenant_id, net_id): net = db.network_destroy(net_id) d = {} - d["net-id"] = net.uuid + d["net-id"] = str(net.uuid) ovs_db.remove_vlan_binding(net_id) self.vmap.release(net_id) return d @@ -201,8 +200,8 @@ class OVSQuantumPlugin(QuantumPluginBase): 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, "None") - ovs_db.update_network_binding(net_id, remote_iface_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) @@ -237,7 +236,6 @@ class OVSPluginTest(unittest.TestCase): nets = self.quantum.get_all_networks(self.tenant_id) count = 0 for x in nets: - print x if "plugin_test" in x["net-name"]: count += 1 self.assertTrue(count == 2) @@ -248,7 +246,6 @@ class OVSPluginTest(unittest.TestCase): nets = self.quantum.get_all_networks(self.tenant_id) count = 0 for x in nets: - print x if "plugin_test" in x["net-name"]: count += 1 self.assertTrue(count == 0) @@ -269,20 +266,49 @@ class OVSPluginTest(unittest.TestCase): self.assertTrue(count == 1) def testDeletePort(self): - pass + 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): - pass + 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): - pass + 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) - print networks # Clean up any test networks lying around for net in networks: id = net["net-id"] @@ -290,26 +316,27 @@ class OVSPluginTest(unittest.TestCase): if "plugin_test" in name: # Clean up any test ports lying around ports = self.quantum.get_all_ports(self.tenant_id, id) - print ports 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) - - # TODO(bgh) move to unit tets - if False: - quantum.plug_interface(tenant_id, net1, port, "vif1.1") - portdetails = quantum.get_port_details(tenant_id, net1, port) - LOG.DEBUG(portdetails) - LOG.info("=== PORT: %s" % quantum.get_port_details(tenant_id, net1, port)) - assert(portdetails["interface_id"] == "vif1.1") - networks = quantum.get_all_networks(tenant_id) - LOG.debug(networks) - for nid, name in networks.iteritems(): - ports = quantum.get_all_ports(tenant_id, nid) - LOG.debug(ports) From 5050560418545a2ac94c87f73685bf529f7dbcd6 Mon Sep 17 00:00:00 2001 From: Brad Hall Date: Sat, 4 Jun 2011 13:23:29 -0700 Subject: [PATCH 10/33] Update readme with quantum specific instructions --- quantum/plugins/openvswitch/README | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/quantum/plugins/openvswitch/README b/quantum/plugins/openvswitch/README index ee2248a17..77f053a38 100644 --- a/quantum/plugins/openvswitch/README +++ b/quantum/plugins/openvswitch/README @@ -1,5 +1,10 @@ To Run: +0) Make it the current quantum plugin + +edit ../../plugins.ini and change the provider line to be: +provider = quantum.plugins.openvswitch.ovs_quantum_plugin.OVSQuantumPlugin + 1) On the "Openstack Controller" host: MySQL should be installed on the host, and all plugins and clients must be @@ -18,7 +23,7 @@ mysql -u root -p -e "create database ovs_naas" $ make agent-dist -4) Copy the resulting tarball to your xenserver +4) Copy the resulting tarball to your xenserver(s) 5) Unpack the tarball and run install.sh. This will install all of the necessary pieces into /etc/xapi.d/plugins. @@ -27,11 +32,15 @@ necessary pieces into /etc/xapi.d/plugins. # /etc/xapi.d/plugins/ovs_quantum_agent.py /etc/xapi.d/plugins/ovs_quantum_plugin.ini -7) Run ovs_quantum_plugin.py via the quantum plugin framework cli. +7) Start quantum + +~/src/quantum-framework$ PYTHONPATH=.:$PYTHONPATH python bin/quantum etc/quantum.conf + +8) Run ovs_quantum_plugin.py via the quantum plugin framework cli. - Edit quantum/plugins.ini to point to where the plugin and configuration files live -$ PYTHONPATH=$HOME/src/quantum-framework/quantum:$PYTHONPATH python quantum/cli.py +~/src/quantum-framework$ PYTHONPATH=.:$PYTHONPATH python quantum/cli.py This will show all of the available commands. From 2265c84a30644160a983bc2aaf48fae020f373e3 Mon Sep 17 00:00:00 2001 From: Brad Hall Date: Sat, 4 Jun 2011 18:46:44 -0700 Subject: [PATCH 11/33] Remove get_all_interfaces and fix detail_network commands --- quantum/cli.py | 85 +++++-------------- .../plugins/openvswitch/ovs_quantum_plugin.py | 18 ++-- 2 files changed, 25 insertions(+), 78 deletions(-) diff --git a/quantum/cli.py b/quantum/cli.py index 2fa2b0281..7daa5de69 100644 --- a/quantum/cli.py +++ b/quantum/cli.py @@ -137,20 +137,32 @@ def api_delete_net(client, *args): def detail_net(manager, *args): tid, nid = args - network = manager.get_network_details(tid, nid) - network_id = network["net-id"] - network_name = network["net-name"] - print "\tNetwork id:%s\n\tNetwork name:%s\n" % (network_id, network_name) + iface_list = manager.get_all_attached_interfaces(tid, nid) + print "Remote Interfaces on Virtual Network:%s\n" % nid + for iface in iface_list: + print "\tRemote interface:%s" % iface + def api_detail_net(client, *args): tid, nid = args - res = client.do_request(tid, 'GET', "/networks/" + nid + "." + FORMAT) + 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 = simplejson.loads(output) LOG.debug(rd) - network_id = rd["networks"]["network"]["id"] - network_name = rd["networks"]["network"]["name"] - print "\tNetwork id:%s\n\tNetwork name:%s\n" % (network_id, network_name) + 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 = simplejson.loads(output) + LOG.debug(rd) + remote_iface = rd["attachment"] + print "\tRemote interface:%s" % remote_iface def rename_net(manager, *args): tid, nid, name = args @@ -286,53 +298,6 @@ def api_unplug_iface(client, *args): return print "Unplugged interface from port:%s on network:%s" % (pid, nid) -def detail_iface(manager, *args): - tid, nid, pid = args - remote_iface = manager.get_interface_details(tid, nid, pid) - print "Remote interface on Virtual Port:%s " \ - "Virtual Network:%s is %s" % (pid, nid, remote_iface) - -def api_detail_iface(manager, *args): - tid, nid, pid = args - res = client.do_request(tid, 'GET', - "/networks/%s/ports/%s/attachment.%s" % (nid, pid, FORMAT)) - output = res.read() - rd = simplejson.loads(output) - LOG.debug(rd) - remote_iface = rd["attachment"] - print "Remote interface on Virtual Port:%s " \ - "Virtual Network:%s is %s" % (pid, nid, remote_iface) - -def list_iface(manager, *args): - tid, nid = args - iface_list = manager.get_all_attached_interfaces(tid, nid) - print "Remote Interfaces on Virtual Network:%s\n" % nid - for iface in iface_list: - print "\tRemote interface:%s" % iface - -# TODO(bgh): I'm not sure how the api maps to manager.get_all_interfaces so -# I'm just doing this the manual way for now. -def api_list_iface(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 = simplejson.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 = simplejson.loads(output) - LOG.debug(rd) - remote_iface = rd["attachment"] - print "\tRemote interface:%s" % remote_iface - commands = { "list_nets": { "func": list_nets, @@ -389,16 +354,6 @@ commands = { "api_func": api_unplug_iface, "args": ["tenant-id", "net-id", "port-id"] }, - "detail_iface": { - "func": detail_iface, - "api_func": api_detail_iface, - "args": ["tenant-id", "net-id", "port-id"] - }, - "list_iface": { - "func": list_iface, - "api_func": api_list_iface, - "args": ["tenant-id", "net-id"] - }, } def help(): diff --git a/quantum/plugins/openvswitch/ovs_quantum_plugin.py b/quantum/plugins/openvswitch/ovs_quantum_plugin.py index 036c3c830..75619cae6 100644 --- a/quantum/plugins/openvswitch/ovs_quantum_plugin.py +++ b/quantum/plugins/openvswitch/ovs_quantum_plugin.py @@ -129,12 +129,11 @@ class OVSQuantumPlugin(QuantumPluginBase): return d def get_network_details(self, tenant_id, net_id): - network = db.network_get(net_id) - d = {} - d["net-id"] = str(network.uuid) - d["net-name"] = network.name - d["net-ports"] = self.get_all_ports(tenant_id, net_id) - return d + 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: @@ -188,13 +187,6 @@ class OVSQuantumPlugin(QuantumPluginBase): "net-id": port.network_id, "port-state": "UP"} return rv - def get_all_attached_interfaces(self, tenant_id, net_id): - ports = db.port_list(net_id) - ifaces = [] - for p in ports: - ifaces.append(p.interface_id) - return ifaces - 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) From e1475fcc0707ad3a23262c8db36af2941e2d5fc9 Mon Sep 17 00:00:00 2001 From: Brad Hall Date: Sat, 4 Jun 2011 21:58:27 -0700 Subject: [PATCH 12/33] Fix detail_net and list_ports commands --- quantum/cli.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/quantum/cli.py b/quantum/cli.py index 7daa5de69..4ea93002f 100644 --- a/quantum/cli.py +++ b/quantum/cli.py @@ -137,12 +137,11 @@ def api_delete_net(client, *args): def detail_net(manager, *args): tid, nid = args - iface_list = manager.get_all_attached_interfaces(tid, nid) + iface_list = manager.get_network_details(tid, nid) print "Remote Interfaces on Virtual Network:%s\n" % nid for iface in iface_list: print "\tRemote interface:%s" % iface - def api_detail_net(client, *args): tid, nid = args res = client.do_request(tid, 'GET', @@ -179,13 +178,12 @@ def api_rename_net(client, *args): LOG.debug(resdict) print "Renamed Virtual Network with ID:%s" % nid -# TODO(bgh): fix this command 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: - "\tVirtual Port:%s" % port + print "\tVirtual Port:%s" % port["port-id"] def api_list_ports(client, *args): tid, nid = args From f68ba1f58a80b1fe571891c6f7c17f2ffbf920f4 Mon Sep 17 00:00:00 2001 From: Brad Hall Date: Sat, 4 Jun 2011 22:00:50 -0700 Subject: [PATCH 13/33] Fix another TODO: remove main function from manager --- quantum/manager.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/quantum/manager.py b/quantum/manager.py index 79a5139d8..dc882bbb0 100644 --- a/quantum/manager.py +++ b/quantum/manager.py @@ -50,15 +50,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() From c93cbb7d1b179a58622356276afcc34f1579e15f Mon Sep 17 00:00:00 2001 From: Brad Hall Date: Sun, 5 Jun 2011 21:52:09 -0700 Subject: [PATCH 14/33] Make the manager a little smarter about finding its config file --- quantum/manager.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/quantum/manager.py b/quantum/manager.py index dc882bbb0..9932f17af 100644 --- a/quantum/manager.py +++ b/quantum/manager.py @@ -26,17 +26,26 @@ The caller should make sure that QuantumManager is a singleton. import gettext gettext.install('quantum', unicode=1) +import os + from common import utils from quantum_plugin_base import QuantumPluginBase 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) + 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) print "PLUGIN LOCATION:%s" % plugin_location plugin_klass = utils.import_class(plugin_location) if not issubclass(plugin_klass, QuantumPluginBase): From e33ad93cdbf7b4b279f48917fb6d0ad051802386 Mon Sep 17 00:00:00 2001 From: Brad Hall Date: Sun, 5 Jun 2011 22:38:04 -0700 Subject: [PATCH 15/33] Address Dan's review comments --- quantum/plugins/openvswitch/README | 107 ++++++++++++++----- quantum/plugins/openvswitch/agent/install.sh | 12 +-- 2 files changed, 89 insertions(+), 30 deletions(-) mode change 100644 => 100755 quantum/plugins/openvswitch/agent/install.sh diff --git a/quantum/plugins/openvswitch/README b/quantum/plugins/openvswitch/README index 77f053a38..6fcfb4005 100644 --- a/quantum/plugins/openvswitch/README +++ b/quantum/plugins/openvswitch/README @@ -1,46 +1,105 @@ -To Run: +# -- Background -0) Make it the current quantum plugin +The quantum openvswitch plugin is a simple plugin that allows you to manage +connectivity between VMs on hypervisors running openvswitch. -edit ../../plugins.ini and change the provider line to be: +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 will be used later by the agent. + +2) An agent which runs on the hypervisor (dom0) and communicates with + openvswitch. + +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 -1) On the "Openstack Controller" host: +# -- 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_naas" +$ mysql -u root -p -e "create database ovs_quantum" -2) Edit the configuration file (src/ovs/plugins/ovs_quantum_plugin.ini) +Make sure any xenserver running the ovs quantum agent will be able to communicate with the host running the quantum service: -- Make sure it matches your mysql configuration. This file must be updated - with the addresses and credentials to access the database. +//log in to mysql service +$ mysql -u root -p +//grant access to user-remote host combination +mysql> GRANT USAGE ON *.* to root@'yourremotehost' IDENTIFIED BY 'newpassword'; +//force update of authorization changes +mysql> FLUSH PRIVILEGES; -3) Create the agent distribution tarball +# -- 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. +- Run the agent [on your hypervisor (dom0)]: +$ /etc/xapi.d/plugins/ovs_quantum_agent.py /etc/xapi.d/plugins/ovs_quantum_plugin.ini -4) Copy the resulting tarball to your xenserver(s) - -5) Unpack the tarball and run install.sh. This will install all of the -necessary pieces into /etc/xapi.d/plugins. - -6) Run the agent (example below): - -# /etc/xapi.d/plugins/ovs_quantum_agent.py /etc/xapi.d/plugins/ovs_quantum_plugin.ini - -7) Start quantum +# -- Getting quantum up and running +- Start quantum [on the quantum service host]: ~/src/quantum-framework$ 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 -a quantum/cli.py -8) Run ovs_quantum_plugin.py via the quantum plugin framework cli. +This will show help all of the available commands. -- Edit quantum/plugins.ini to point to where the plugin and configuration - files live +An example session looks like this: -~/src/quantum-framework$ PYTHONPATH=.:$PYTHONPATH python quantum/cli.py +$ export TENANT=t1 +$ PYTHONPATH=. python quantum/cli.py -v -a 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 -v -a 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 -v -a 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 +$ PYTHONPATH=. python quantum/cli.py -v -a plug_iface $TENANT $NETWORK $PORT ubuntu2-eth1 +Plugged interface "ubuntu2-eth1" to port:5a1e121b-ccc8-471d-9445-24f15f9f854c on network:e754e7c0-a8eb-40e5-861a-b182d30c3441 -This will show all of the available commands. +Now you should have connectivity between ubuntu1-eth1 and ubuntu2-eth1.. + +# -- Other items + +- To get a listing of the vif names 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/quantum/plugins/openvswitch/agent/install.sh b/quantum/plugins/openvswitch/agent/install.sh old mode 100644 new mode 100755 index 2f2b0381f..1e71c09b4 --- a/quantum/plugins/openvswitch/agent/install.sh +++ b/quantum/plugins/openvswitch/agent/install.sh @@ -10,12 +10,10 @@ fi # Make sure we have mysql-python rpm -qa | grep MYyQL-python >/dev/null 2>&1 if [ $? -ne 0 ]; then - echo "MySQL-python not found; installing." - yum -y install MySQL-python - if [ $? -ne 0 ]; then - echo "Failed to install MYSQL-python; agent will not work." - exit 1 - fi + 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 @@ -35,4 +33,6 @@ if [ "X$BR" != "X$CONF_BR" ]; then 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" From 1a0c3722eecea8709d12e16ddfe0a0a0eb7094ba Mon Sep 17 00:00:00 2001 From: Brad Hall Date: Sun, 5 Jun 2011 22:41:46 -0700 Subject: [PATCH 16/33] Make the API the default --- quantum/cli.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/quantum/cli.py b/quantum/cli.py index 4ea93002f..539d14173 100644 --- a/quantum/cli.py +++ b/quantum/cli.py @@ -384,8 +384,9 @@ def build_args(cmd, cmdargs, arglist): if __name__ == "__main__": usagestr = "Usage: %prog [OPTIONS] [args]" parser = OptionParser(usage=usagestr) - parser.add_option("-a", "--use-api", dest="use_api", - action="store_true", default=False, help="Use WS API") + 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", @@ -417,7 +418,7 @@ if __name__ == "__main__": if not args: sys.exit(1) LOG.debug("Executing command \"%s\" with args: %s" % (cmd, args)) - if options.use_api: + if not options.load_plugin: client = MiniClient(options.host, options.port, options.ssl) if not commands[cmd].has_key("api_func"): LOG.error("API version of \"%s\" is not yet implemented" % cmd) From 8fe528d4f70589300aa9ff7801ba05ea43771a04 Mon Sep 17 00:00:00 2001 From: Brad Hall Date: Sun, 5 Jun 2011 22:42:46 -0700 Subject: [PATCH 17/33] Remove -a option from examples (it no longer exists) --- quantum/plugins/openvswitch/README | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/quantum/plugins/openvswitch/README b/quantum/plugins/openvswitch/README index 6fcfb4005..d48b793f3 100644 --- a/quantum/plugins/openvswitch/README +++ b/quantum/plugins/openvswitch/README @@ -78,22 +78,22 @@ $ /etc/xapi.d/plugins/ovs_quantum_agent.py /etc/xapi.d/plugins/ovs_quantum_plugi ~/src/quantum-framework$ 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 -a quantum/cli.py +~/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 -v -a create_net $TENANT network1 +$ PYTHONPATH=. python quantum/cli.py -v 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 -v -a create_port $TENANT $NETWORK +$ PYTHONPATH=. python quantum/cli.py -v 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 -v -a plug_iface $TENANT $NETWORK $PORT ubuntu1-eth1 +$ PYTHONPATH=. python quantum/cli.py -v 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 -$ PYTHONPATH=. python quantum/cli.py -v -a plug_iface $TENANT $NETWORK $PORT ubuntu2-eth1 +$ PYTHONPATH=. python quantum/cli.py -v plug_iface $TENANT $NETWORK $PORT ubuntu2-eth1 Plugged interface "ubuntu2-eth1" to port:5a1e121b-ccc8-471d-9445-24f15f9f854c on network:e754e7c0-a8eb-40e5-861a-b182d30c3441 Now you should have connectivity between ubuntu1-eth1 and ubuntu2-eth1.. From ccf5aa87c875d4fab3667736483bc90e470b3180 Mon Sep 17 00:00:00 2001 From: Brad Hall Date: Sun, 5 Jun 2011 22:54:44 -0700 Subject: [PATCH 18/33] Make the wording a little clearer --- quantum/plugins/openvswitch/README | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/quantum/plugins/openvswitch/README b/quantum/plugins/openvswitch/README index d48b793f3..689624f1b 100644 --- a/quantum/plugins/openvswitch/README +++ b/quantum/plugins/openvswitch/README @@ -6,10 +6,11 @@ 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 will be used later by the agent. + store configuration and mappings that are used by the agent. 2) An agent which runs on the hypervisor (dom0) and communicates with - openvswitch. + 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. From 0dfdf96311b05a9ae182091c2c7c3a4da56d75a7 Mon Sep 17 00:00:00 2001 From: Somik Behera Date: Mon, 6 Jun 2011 09:22:05 -0700 Subject: [PATCH 19/33] Initial commit of exceptions that are raised by a quantum plugin. This list of exceptions is consistent with exceptions being expected by the API service. Please note that the exception list is still evolving and will soon be concrete. --- quantum/quantum_plugin_base.py | 49 ++++++++++++++++++++++++++++++++-- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/quantum/quantum_plugin_base.py b/quantum/quantum_plugin_base.py index 0bc156d49..bf2976ec6 100644 --- a/quantum/quantum_plugin_base.py +++ b/quantum/quantum_plugin_base.py @@ -35,6 +35,9 @@ class QuantumPluginBase(object): Returns a dictionary containing all for the specified tenant. + + :returns: + :raises: """ pass @@ -43,6 +46,9 @@ class QuantumPluginBase(object): """ Creates a new Virtual Network, and assigns it a symbolic name. + + :returns: + :raises: """ pass @@ -51,6 +57,10 @@ class QuantumPluginBase(object): """ Deletes the network with the specified network identifier belonging to the specified tenant. + + :returns: + :raises: exception.NetworkInUse + :raises: exception.NetworkNotFound """ pass @@ -58,7 +68,10 @@ class QuantumPluginBase(object): def get_network_details(self, tenant_id, net_id): """ retrieved a list of all the remote vifs that - are attached to the network + are attached to the network. + + :returns: + :raises: exception.NetworkNotFound """ pass @@ -67,6 +80,9 @@ class QuantumPluginBase(object): """ Updates the symbolic name belonging to a particular Virtual Network. + + :returns: + :raises: exception.NetworkNotFound """ pass @@ -75,6 +91,9 @@ class QuantumPluginBase(object): """ Retrieves all port identifiers belonging to the specified Virtual Network. + + :returns: + :raises: exception.NetworkNotFound """ pass @@ -82,6 +101,10 @@ class QuantumPluginBase(object): def create_port(self, tenant_id, net_id, port_state=None): """ Creates a port on the specified Virtual Network. + + :returns: + :raises: exception.NetworkNotFound + :raises: exception.StateInvalid """ pass @@ -89,7 +112,11 @@ 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: + :raises: exception.StateInvalid + :raises: exception.PortNotFound """ pass @@ -100,6 +127,11 @@ 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: + :raises: exception.PortInUse + :raises: exception.PortNotFound + :raises: exception.NetworkNotFound """ pass @@ -108,6 +140,10 @@ class QuantumPluginBase(object): """ This method allows the user to retrieve a remote interface that is attached to this particular port. + + :returns: + :raises: exception.PortNotFound + :raises: exception.NetworkNotFound """ pass @@ -116,6 +152,11 @@ class QuantumPluginBase(object): """ Attaches a remote interface to the specified port on the specified Virtual Network. + + :returns: + :raises: exception.NetworkNotFound + :raises: exception.PortNotFound + :raises: exception.AlreadyAttached (? should the network automatically unplug/replug) """ pass @@ -124,6 +165,10 @@ class QuantumPluginBase(object): """ Detaches a remote interface from the specified port on the specified Virtual Network. + + :returns: + :raises: exception.NetworkNotFound + :raises: exception.PortNotFound """ pass From d769196098d55f39ba1bdc08c0422c711a2b1bbd Mon Sep 17 00:00:00 2001 From: Somik Behera Date: Mon, 6 Jun 2011 10:34:43 -0700 Subject: [PATCH 20/33] Added a basic README file and updated Quantum plugin base class with appropriate exceptions. --- README | 29 +++++++++++++++++++++++++++++ quantum/quantum_plugin_base.py | 2 +- 2 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 README diff --git a/README b/README new file mode 100644 index 000000000..e09f1ab8a --- /dev/null +++ b/README @@ -0,0 +1,29 @@ +# -- 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 + +# -- Configuring Quantum plug-in + +# -- Launching the Quantum Service + +# -- Making requests against the Quantum Service + +# -- CLI tools to program the Quantum-managed network fabric + +# -- Writing your own Quantum plug-in + \ No newline at end of file diff --git a/quantum/quantum_plugin_base.py b/quantum/quantum_plugin_base.py index bf2976ec6..843bc85c5 100644 --- a/quantum/quantum_plugin_base.py +++ b/quantum/quantum_plugin_base.py @@ -37,7 +37,7 @@ class QuantumPluginBase(object): the specified tenant. :returns: - :raises: + :raises: None """ pass From b0e7265fa6c68df6bf6909361c3873a81793d870 Mon Sep 17 00:00:00 2001 From: Somik Behera Date: Mon, 6 Jun 2011 11:49:12 -0700 Subject: [PATCH 21/33] Updated quantum_plugin_base with with return type dataformats as well as exceptions. --- quantum/quantum_plugin_base.py | 65 +++++++++++++++++++++++++++------- 1 file changed, 52 insertions(+), 13 deletions(-) diff --git a/quantum/quantum_plugin_base.py b/quantum/quantum_plugin_base.py index 843bc85c5..8f7b55869 100644 --- a/quantum/quantum_plugin_base.py +++ b/quantum/quantum_plugin_base.py @@ -36,18 +36,31 @@ class QuantumPluginBase(object): for the specified tenant. - :returns: + :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 + + @abstractmethod def create_network(self, tenant_id, net_name): """ Creates a new Virtual Network, and assigns it a symbolic name. - :returns: + :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 @@ -58,7 +71,9 @@ class QuantumPluginBase(object): Deletes the network with the specified network identifier belonging to the specified tenant. - :returns: + :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 """ @@ -67,10 +82,14 @@ class QuantumPluginBase(object): @abstractmethod def get_network_details(self, tenant_id, net_id): """ - retrieved a list of all the remote vifs that + Retrieves a list of all the remote vifs that are attached to the network. - :returns: + :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 @@ -81,7 +100,10 @@ class QuantumPluginBase(object): Updates the symbolic name belonging to a particular Virtual Network. - :returns: + :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 @@ -92,7 +114,13 @@ class QuantumPluginBase(object): Retrieves all port identifiers belonging to the specified Virtual Network. - :returns: + :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 @@ -102,7 +130,9 @@ class QuantumPluginBase(object): """ Creates a port on the specified Virtual Network. - :returns: + :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 """ @@ -114,7 +144,10 @@ class QuantumPluginBase(object): Updates the state of a specific port on the specified Virtual Network. - :returns: + :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 """ @@ -128,7 +161,9 @@ class QuantumPluginBase(object): the remote interface is first un-plugged and then the port is deleted. - :returns: + :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 @@ -141,7 +176,11 @@ class QuantumPluginBase(object): This method allows the user to retrieve a remote interface that is attached to this particular port. - :returns: + :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 """ @@ -153,7 +192,7 @@ class QuantumPluginBase(object): Attaches a remote interface to the specified port on the specified Virtual Network. - :returns: + :returns: None :raises: exception.NetworkNotFound :raises: exception.PortNotFound :raises: exception.AlreadyAttached (? should the network automatically unplug/replug) @@ -166,7 +205,7 @@ class QuantumPluginBase(object): Detaches a remote interface from the specified port on the specified Virtual Network. - :returns: + :returns: None :raises: exception.NetworkNotFound :raises: exception.PortNotFound """ From 87a984d3f3c4430b1174ef4b08aaa0544d822922 Mon Sep 17 00:00:00 2001 From: Somik Behera Date: Mon, 6 Jun 2011 16:35:47 -0700 Subject: [PATCH 22/33] Update Quantum README file with instructions to launch the service and get going. --- README | 52 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/README b/README index e09f1ab8a..799d05f30 100644 --- a/README +++ b/README @@ -17,13 +17,63 @@ 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 + # -- Configuring Quantum plug-in +1) Explore sample and real Quantum plug-ins in the quantum.plugins module. + +2) Or copy another Quantum plug-in into the quantum.plugins module. + +3) Update plug-in configuration by editing plugins.ini file and modify + "provider" property to point to the location of the Quantum plug-in. + +4) 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 -# -- CLI tools to program the Quantum-managed network fabric +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 +../quantum/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. \ No newline at end of file From 5d3d03aee97eef4e822cee83ac35ea66beafe399 Mon Sep 17 00:00:00 2001 From: Somik Behera Date: Mon, 6 Jun 2011 22:48:57 -0700 Subject: [PATCH 23/33] pep8 changes for quantum-framework code pieces. --- quantum/api/__init__.py | 2 +- quantum/cli.py | 9 ++-- quantum/manager.py | 7 +-- quantum/plugins/SamplePlugin.py | 2 +- quantum/quantum_plugin_base.py | 96 +++++++++++++++++++-------------- quantum/service.py | 4 +- 6 files changed, 69 insertions(+), 51 deletions(-) diff --git a/quantum/api/__init__.py b/quantum/api/__init__.py index 25b66f557..bf8edeb1e 100644 --- a/quantum/api/__init__.py +++ b/quantum/api/__init__.py @@ -51,7 +51,7 @@ class APIRouterV01(wsgi.Router): mapper.resource('network', 'networks', controller=networks.Controller(), path_prefix=uri_prefix) - mapper.resource('port', 'ports', + mapper.resource('port', 'ports', controller=ports.Controller(), parent_resource=dict(member_name='network', collection_name=\ diff --git a/quantum/cli.py b/quantum/cli.py index 78f1a6b48..dc6cf8240 100644 --- a/quantum/cli.py +++ b/quantum/cli.py @@ -36,7 +36,7 @@ def usage(): print "detail_iface " print "list_iface \n" -if len(sys.argv) < 2 or len(sys.argv) > 6: +if len(sys.argv) < 2 or len(sys.argv) > 6: usage() exit(1) @@ -89,13 +89,13 @@ elif sys.argv[1] == "plug_iface" and len(sys.argv) == 6: 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], + "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], + 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], + "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]) @@ -107,4 +107,3 @@ elif sys.argv[1] == "all" and len(sys.argv) == 2: else: print "invalid arguments: %s" % str(sys.argv) usage() - diff --git a/quantum/manager.py b/quantum/manager.py index 79a5139d8..28daef176 100644 --- a/quantum/manager.py +++ b/quantum/manager.py @@ -18,8 +18,9 @@ """ -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. """ @@ -34,7 +35,7 @@ CONFIG_FILE = "plugins.ini" class QuantumManager(object): - def __init__(self,config=CONFIG_FILE): + def __init__(self, config=CONFIG_FILE): self.configuration_file = CONFIG_FILE plugin_location = utils.getPluginFromConfig(CONFIG_FILE) print "PLUGIN LOCATION:%s" % plugin_location diff --git a/quantum/plugins/SamplePlugin.py b/quantum/plugins/SamplePlugin.py index 41dd3271d..f82397ebb 100644 --- a/quantum/plugins/SamplePlugin.py +++ b/quantum/plugins/SamplePlugin.py @@ -166,7 +166,7 @@ class DummyDataPlugin(object): 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 diff --git a/quantum/quantum_plugin_base.py b/quantum/quantum_plugin_base.py index 8f7b55869..8e3f1f8e1 100644 --- a/quantum/quantum_plugin_base.py +++ b/quantum/quantum_plugin_base.py @@ -35,31 +35,34 @@ 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 }, .... - {'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 - - @abstractmethod def create_network(self, tenant_id, net_name): """ 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 + {'net-id': uuid that uniquely identifies the + particular quantum network, + 'net-name': a human-readable name associated + with network referenced by net-id } :raises: """ @@ -70,9 +73,10 @@ 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 + {'net-id': uuid that uniquely identifies the + particular quantum network } :raises: exception.NetworkInUse :raises: exception.NetworkNotFound @@ -84,11 +88,14 @@ class QuantumPluginBase(object): """ 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'] + {'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 """ @@ -99,10 +106,13 @@ 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 + + :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 """ @@ -113,14 +123,16 @@ 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 }, .... - {'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 @@ -129,9 +141,10 @@ 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 + {'port-id': uuid representing the created port + on specified quantum network } :raises: exception.NetworkNotFound :raises: exception.StateInvalid @@ -143,9 +156,10 @@ class QuantumPluginBase(object): """ Updates the state of a specific port on the specified Virtual Network. - + :returns: a mapping sequence with the following signature: - {'port-id': uuid representing the updated port on specified quantum network + {'port-id': uuid representing the + updated port on specified quantum network 'port-state': update port state( UP or DOWN) } :raises: exception.StateInvalid @@ -160,9 +174,10 @@ 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 + {'port-id': uuid representing the deleted port + on specified quantum network } :raises: exception.PortInUse :raises: exception.PortNotFound @@ -175,11 +190,14 @@ 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 + {'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 @@ -191,11 +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) + :raises: exception.AlreadyAttached + (? should the network automatically unplug/replug) """ pass @@ -204,14 +223,13 @@ class QuantumPluginBase(object): """ Detaches a remote interface from the specified port on the specified Virtual Network. - + :returns: None :raises: exception.NetworkNotFound :raises: exception.PortNotFound """ pass - @classmethod def __subclasshook__(cls, klass): """ diff --git a/quantum/service.py b/quantum/service.py index 193725ef3..1406462b1 100644 --- a/quantum/service.py +++ b/quantum/service.py @@ -88,7 +88,7 @@ class QuantumApiService(WsgiService): return service -def serve_wsgi(cls, conf=None, options = None, args=None): +def serve_wsgi(cls, conf=None, options=None, args=None): try: service = cls.create(conf, options, args) except Exception: @@ -99,7 +99,7 @@ def serve_wsgi(cls, conf=None, options = None, args=None): return service - + 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) From cf4e1eeff4818d4cafe9461a263e8f964a150bd2 Mon Sep 17 00:00:00 2001 From: Somik Behera Date: Mon, 6 Jun 2011 23:09:53 -0700 Subject: [PATCH 24/33] Fix merge indentation errors --- quantum/manager.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/quantum/manager.py b/quantum/manager.py index 2b62603fb..9932f17af 100644 --- a/quantum/manager.py +++ b/quantum/manager.py @@ -18,9 +18,8 @@ """ -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. """ @@ -41,19 +40,12 @@ def find_config(basepath): return None class QuantumManager(object): -<<<<<<< TREE - - def __init__(self, config=CONFIG_FILE): - self.configuration_file = CONFIG_FILE - plugin_location = utils.getPluginFromConfig(CONFIG_FILE) -======= 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) ->>>>>>> MERGE-SOURCE print "PLUGIN LOCATION:%s" % plugin_location plugin_klass = utils.import_class(plugin_location) if not issubclass(plugin_klass, QuantumPluginBase): From 645c863ba3f8829d5327ab194f5dc79729f7af1f Mon Sep 17 00:00:00 2001 From: Brad Hall Date: Tue, 7 Jun 2011 10:17:56 -0700 Subject: [PATCH 25/33] Add dependencies to README and fix whitespace --- README | 37 +++++++++++++++++++++++++++++-------- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/README b/README index 799d05f30..19d15d095 100644 --- a/README +++ b/README @@ -2,7 +2,7 @@ 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 @@ -11,12 +11,32 @@ 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 @@ -26,10 +46,10 @@ 3) Update plug-in configuration by editing plugins.ini file and modify "provider" property to point to the location of the Quantum plug-in. - -4) Read the plugin specific README, this is usually found in the same + +4) 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]: @@ -65,7 +85,7 @@ well as sample plugins available in: There are a few requirements to writing your own plugin: -1) Your plugin should implement all methods defined in +1) Your plugin should implement all methods defined in ../quantum/quantum/quantum_plugin_base.QuantumPluginBase class 2) Copy your Quantum plug-in over to the ../quantum/quantum/plugins/.. directory @@ -76,4 +96,5 @@ There are a few requirements to writing your own plugin: 4) Launch the Quantum Service, and your plug-in is configured and ready to manage a Cloud Networking Fabric. - \ No newline at end of file + + From 5ece989e47d1281fb44be5d14bb5c3ed685207b7 Mon Sep 17 00:00:00 2001 From: Somik Behera Date: Tue, 7 Jun 2011 16:56:53 -0700 Subject: [PATCH 26/33] Fix cli.py from last merge when it got overwritten --- quantum/cli.py | 446 ++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 408 insertions(+), 38 deletions(-) diff --git a/quantum/cli.py b/quantum/cli.py index 9932f17af..4c0ba4eee 100644 --- a/quantum/cli.py +++ b/quantum/cli.py @@ -1,7 +1,7 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 -# Copyright 2011 Nicira Networks, Inc -# All Rights Reserved. +# 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 @@ -15,47 +15,417 @@ # 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 -""" -Quantum's Manager class is responsible for parsing a config file and instantiating the correct -plugin that concretely implement quantum_plugin_base class +from manager import QuantumManager +from optparse import OptionParser +from quantum.common.wsgi import Serializer -The caller should make sure that QuantumManager is a singleton. -""" -import gettext -gettext.install('quantum', unicode=1) +FORMAT = "json" +CONTENT_TYPE = "application/" + FORMAT -import os - -from common import utils -from quantum_plugin_base import QuantumPluginBase - -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=None): - if config == None: - self.configuration_file = find_config(os.path.abspath(os.path.dirname(__file__))) +### --- 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}' + def __init__(self, host, port, use_ssl): + self.host = host + self.port = port + self.use_ssl = use_ssl + self.connection = None + def get_connection_type(self): + if self.use_ssl: + return httplib.HTTPSConnection else: - self.configuration_file = config - plugin_location = utils.getPluginFromConfig(self.configuration_file) - print "PLUGIN LOCATION:%s" % plugin_location - plugin_klass = utils.import_class(plugin_location) - if not issubclass(plugin_klass, QuantumPluginBase): - raise Exception("Configured Quantum plug-in " \ - "didn't pass compatibility test") + return httplib.HTTPConnection + 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: - print("Successfully imported Quantum plug-in." \ - "All compatibility tests passed\n") - self.plugin = plugin_klass() + return response.status +### -- end of miniclient - def get_manager(self): - return self.plugin +### -- 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 + +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 + +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 not commands[cmd].has_key("api_func"): + 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) From 6458fa7cff6bda9a82aa598bcf9d03c76136ccd0 Mon Sep 17 00:00:00 2001 From: Santhosh Date: Wed, 8 Jun 2011 14:22:51 +0530 Subject: [PATCH 27/33] Santhosh/Vinkesh | Added the testing framework. Moved the smoketest to tests/functional --- run_tests.py | 293 ++++++++++++++++++ run_tests.sh | 83 +++++ test_scripts/miniclient.py | 98 ------ test_scripts/tests.py | 150 --------- {smoketests => tests}/__init__.py | 0 .../functional}/__init__.py | 0 .../functional}/miniclient.py | 0 .../functional/test_service.py | 0 tests/unit/__init__.py | 32 ++ tools/install_venv.py | 137 ++++++++ tools/pip-requires | 10 + tools/with_venv.sh | 21 ++ 12 files changed, 576 insertions(+), 248 deletions(-) create mode 100644 run_tests.py create mode 100755 run_tests.sh delete mode 100644 test_scripts/miniclient.py delete mode 100644 test_scripts/tests.py rename {smoketests => tests}/__init__.py (100%) rename {test_scripts => tests/functional}/__init__.py (100%) rename {smoketests => tests/functional}/miniclient.py (100%) rename smoketests/tests.py => tests/functional/test_service.py (100%) create mode 100644 tests/unit/__init__.py create mode 100644 tools/install_venv.py create mode 100644 tools/pip-requires create mode 100755 tools/with_venv.sh diff --git a/run_tests.py b/run_tests.py new file mode 100644 index 000000000..acc8260b8 --- /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 000000000..aa72cbe22 --- /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/miniclient.py b/test_scripts/miniclient.py deleted file mode 100644 index fb1ebc8fe..000000000 --- a/test_scripts/miniclient.py +++ /dev/null @@ -1,98 +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 httplib -import socket -import urllib - -class MiniClient(object): - - """A base client class - derived from Glance.BaseClient""" - - action_prefix = '/v0.1/tenants/{tenant_id}' - - def __init__(self, host, port, use_ssl): - """ - Creates a new client to some service. - - :param host: The host where service resides - :param port: The port where service resides - :param use_ssl: Should we use HTTPS? - """ - self.host = host - self.port = port - self.use_ssl = use_ssl - self.connection = None - - def get_connection_type(self): - """ - Returns the proper connection type - """ - if self.use_ssl: - return httplib.HTTPSConnection - else: - return httplib.HTTPConnection - - def do_request(self, tenant, method, action, body=None, - headers=None, params=None): - """ - Connects to the server and issues a request. - Returns the result data, or raises an appropriate exception if - HTTP status code is not 2xx - - :param method: HTTP method ("GET", "POST", "PUT", etc...) - :param body: string of data to send, or None (default) - :param headers: mapping of key/value pairs to add as headers - :param params: dictionary of key/value pairs to add to append - to action - - """ - 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): - """ - Returns the integer status code from the response, which - can be either a Webob.Response (used in testing) or httplib.Response - """ - if hasattr(response, 'status_int'): - return response.status_int - else: - return response.status \ No newline at end of file diff --git a/test_scripts/tests.py b/test_scripts/tests.py deleted file mode 100644 index 589d9da22..000000000 --- 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/smoketests/__init__.py b/tests/__init__.py similarity index 100% rename from smoketests/__init__.py rename to tests/__init__.py diff --git a/test_scripts/__init__.py b/tests/functional/__init__.py similarity index 100% rename from test_scripts/__init__.py rename to tests/functional/__init__.py diff --git a/smoketests/miniclient.py b/tests/functional/miniclient.py similarity index 100% rename from smoketests/miniclient.py rename to tests/functional/miniclient.py diff --git a/smoketests/tests.py b/tests/functional/test_service.py similarity index 100% rename from smoketests/tests.py rename to tests/functional/test_service.py diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 000000000..5910e3549 --- /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 000000000..5c3ef374b --- /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 000000000..8dbfd85d3 --- /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 000000000..83149462c --- /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 && $@ From fed70a5272a03b9d1a1a0f27df4236091b1ac882 Mon Sep 17 00:00:00 2001 From: Santhosh Date: Wed, 8 Jun 2011 15:51:47 +0530 Subject: [PATCH 28/33] Santhosh/Vinkesh | Fixed all the pep8 violations. Modified the 'req' to 'request' across all the services and wsgi so that it's consistent with other projects --- bin/quantum | 4 +- etc/quantum.conf.test | 2 +- quantum/api/__init__.py | 5 +- quantum/api/faults.py | 5 +- quantum/api/networks.py | 37 ++-- quantum/api/ports.py | 70 ++++--- quantum/api/views/__init__.py | 2 +- quantum/api/views/networks.py | 6 +- quantum/api/views/ports.py | 2 +- quantum/cli.py | 71 +++++--- quantum/common/config.py | 12 +- quantum/common/exceptions.py | 11 +- quantum/common/flags.py | 3 +- quantum/common/utils.py | 2 + quantum/common/wsgi.py | 22 +-- quantum/db/api.py | 18 +- quantum/db/models.py | 10 +- quantum/manager.py | 12 +- quantum/plugins/SamplePlugin.py | 172 ++++++++---------- quantum/plugins/__init__.py | 2 +- .../openvswitch/agent/ovs_quantum_agent.py | 66 ++++--- quantum/plugins/openvswitch/ovs_db.py | 6 +- quantum/plugins/openvswitch/ovs_models.py | 3 +- .../plugins/openvswitch/ovs_quantum_plugin.py | 29 ++- tests/functional/miniclient.py | 9 +- tests/functional/test_service.py | 17 +- 26 files changed, 326 insertions(+), 272 deletions(-) diff --git a/bin/quantum b/bin/quantum index 0913c31c0..48abd18ae 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 3e532b04d..b1c266246 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 3665464c9..3bf7f113a 100644 --- a/quantum/api/__init__.py +++ b/quantum/api/__init__.py @@ -54,8 +54,9 @@ class APIRouterV01(wsgi.Router): mapper.resource('port', 'ports', controller=ports.Controller(), parent_resource=dict(member_name='network', - collection_name=\ - uri_prefix + 'networks')) + collection_name=uri_prefix +\ + 'networks')) + mapper.connect("get_resource", uri_prefix + 'networks/{network_id}/' \ 'ports/{id}/attachment{.format}', diff --git a/quantum/api/faults.py b/quantum/api/faults.py index a10364df1..35f6c1073 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.""" @@ -52,7 +51,7 @@ class Fault(webob.exc.HTTPException): fault_data = { fault_name: { 'code': code, - 'message': self.wrapped_exc.explanation, + 'message': self.wrapped_exc.explanation, 'detail': self.wrapped_exc.detail}} # 'code' is an attribute on the fault tag itself metadata = {'application/xml': {'attributes': {fault_name: 'code'}}} diff --git a/quantum/api/networks.py b/quantum/api/networks.py index 98dd5e305..a24cf09ab 100644 --- a/quantum/api/networks.py +++ b/quantum/api/networks.py @@ -44,63 +44,66 @@ 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, is_detail=False) + return self._items(request, tenant_id, is_detail=False) - def _items(self, req, tenant_id, is_detail): + def _items(self, request, tenant_id, is_detail): """ Returns a list of networks. """ networks = self.network_manager.get_all_networks(tenant_id) - builder = networks_view.get_view_builder(req) + builder = networks_view.get_view_builder(request) result = [builder.build(network, is_detail)['network'] 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: network = self.network_manager.get_network_details( tenant_id, id) - builder = networks_view.get_view_builder(req) + builder = networks_view.get_view_builder(request) #build response with details result = builder.build(network, True) return dict(networks=result) 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 8e0100634..c2de0d75f 100644 --- a/quantum/api/ports.py +++ b/quantum/api/ports.py @@ -24,51 +24,49 @@ from quantum.common import exceptions as exception LOG = logging.getLogger('quantum.api.ports') + class Controller(common.QuantumController): """ Port API controller for Quantum API """ _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 : + 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) @@ -77,19 +75,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: @@ -97,18 +95,18 @@ 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: @@ -118,7 +116,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: @@ -132,7 +130,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) @@ -143,19 +141,19 @@ class Controller(common.QuantumController): return faults.Fault(faults.PortNotFound(e)) #TODO - Complete implementation of these APIs - def attach_resource(self,req,tenant_id, network_id, id): - content_type = req.best_match_content_type() - print "Content type:%s" %content_type + 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)) @@ -167,10 +165,10 @@ class Controller(common.QuantumController): return faults.Fault(faults.AlreadyAttached(e)) #TODO - Complete implementation of these APIs - 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) + network_id, id) return exc.HTTPAccepted() except exception.NetworkNotFound as e: return faults.Fault(faults.NetworkNotFound(e)) diff --git a/quantum/api/views/__init__.py b/quantum/api/views/__init__.py index ea9103400..cd6a1d3e2 100644 --- a/quantum/api/views/__init__.py +++ b/quantum/api/views/__init__.py @@ -13,4 +13,4 @@ # 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. \ No newline at end of file +# @author: Somik Behera, Nicira Networks, Inc. diff --git a/quantum/api/views/networks.py b/quantum/api/views/networks.py index 2a64d87f0..6630c6c39 100644 --- a/quantum/api/views/networks.py +++ b/quantum/api/views/networks.py @@ -33,17 +33,17 @@ class ViewBuilder(object): def build(self, network_data, is_detail=False): """Generic method used to generate a network entity.""" - print "NETWORK-DATA:%s" %network_data + print "NETWORK-DATA:%s" % network_data if is_detail: network = self._build_detail(network_data) else: network = self._build_simple(network_data) return network - + def _build_simple(self, network_data): """Return a simple model of a server.""" return dict(network=dict(id=network_data['net-id'])) - + def _build_detail(self, network_data): """Return a simple model of a server.""" return dict(network=dict(id=network_data['net-id'], diff --git a/quantum/api/views/ports.py b/quantum/api/views/ports.py index 6077214b6..dabc7cf0f 100644 --- a/quantum/api/views/ports.py +++ b/quantum/api/views/ports.py @@ -31,7 +31,7 @@ class ViewBuilder(object): def build(self, port_data, is_detail=False): """Generic method used to generate a port entity.""" - print "PORT-DATA:%s" %port_data + print "PORT-DATA:%s" % port_data if is_detail: port = self._build_detail(port_data) else: diff --git a/quantum/cli.py b/quantum/cli.py index 4c0ba4eee..8663d548b 100644 --- a/quantum/cli.py +++ b/quantum/cli.py @@ -31,25 +31,29 @@ from quantum.common.wsgi import Serializer FORMAT = "json" CONTENT_TYPE = "application/" + FORMAT + ### --- 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}' + def __init__(self, host, port, use_ssl): self.host = host self.port = port self.use_ssl = use_ssl self.connection = None + def get_connection_type(self): if self.use_ssl: return httplib.HTTPSConnection else: return httplib.HTTPConnection + def do_request(self, tenant, method, action, body=None, headers=None, params=None): 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: @@ -67,6 +71,7 @@ class MiniClient(object): 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 @@ -76,6 +81,7 @@ class MiniClient(object): ### -- Core CLI functions + def list_nets(manager, *args): tenant_id = args[0] networks = manager.get_all_networks(tenant_id) @@ -85,6 +91,7 @@ def list_nets(manager, *args): 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) @@ -98,11 +105,13 @@ def api_list_nets(client, *args): # name = n["net-name"] # LOG.info("\tNetwork ID:%s \n\tNetwork Name:%s \n" % (id, name)) + def create_net(manager, *args): tid, name = args new_net_id = manager.create_network(tid, name) print "Created a new Virtual Network with ID:%s\n" % new_net_id + def api_create_net(client, *args): tid, name = args data = {'network': {'network-name': '%s' % name}} @@ -119,11 +128,13 @@ def api_create_net(client, *args): 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) @@ -135,6 +146,7 @@ def api_delete_net(client, *args): 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) @@ -142,6 +154,7 @@ def detail_net(manager, *args): for iface in iface_list: print "\tRemote interface:%s" % iface + def api_detail_net(client, *args): tid, nid = args res = client.do_request(tid, 'GET', @@ -163,11 +176,13 @@ def api_detail_net(client, *args): 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}} @@ -178,6 +193,7 @@ def api_rename_net(client, *args): 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) @@ -185,6 +201,7 @@ def list_ports(manager, *args): 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', @@ -199,12 +216,14 @@ def api_list_ports(client, *args): 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', @@ -218,11 +237,13 @@ def api_create_port(client, *args): 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', @@ -234,12 +255,14 @@ def api_delete_port(client, *args): 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', @@ -256,12 +279,14 @@ def api_detail_port(client, *args): 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}} @@ -276,12 +301,14 @@ def api_plug_iface(client, *args): return print "Plugged interface \"%s\" to port:%s on network:%s" % (vid, pid, nid) -def unplug_iface(manager, *args): + +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': ''}} @@ -296,63 +323,53 @@ def api_unplug_iface(client, *args): 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"] - }, + "args": ["tenant-id"]}, "create_net": { "func": create_net, "api_func": api_create_net, - "args": ["tenant-id", "net-name"] - }, + "args": ["tenant-id", "net-name"]}, "delete_net": { "func": delete_net, "api_func": api_delete_net, - "args": ["tenant-id", "net-id"] - }, + "args": ["tenant-id", "net-id"]}, "detail_net": { "func": detail_net, "api_func": api_detail_net, - "args": ["tenant-id", "net-id"] - }, + "args": ["tenant-id", "net-id"]}, "rename_net": { "func": rename_net, "api_func": api_rename_net, - "args": ["tenant-id", "net-id", "new-name"] - }, + "args": ["tenant-id", "net-id", "new-name"]}, "list_ports": { "func": list_ports, "api_func": api_list_ports, - "args": ["tenant-id", "net-id"] - }, + "args": ["tenant-id", "net-id"]}, "create_port": { "func": create_port, "api_func": api_create_port, - "args": ["tenant-id", "net-id"] - }, + "args": ["tenant-id", "net-id"]}, "delete_port": { "func": delete_port, "api_func": api_delete_port, - "args": ["tenant-id", "net-id", "port-id"] - }, + "args": ["tenant-id", "net-id", "port-id"]}, "detail_port": { "func": detail_port, "api_func": api_detail_port, - "args": ["tenant-id", "net-id", "port-id"] - }, + "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"] - }, + "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"] - }, - } + "args": ["tenant-id", "net-id", "port-id"]}, } + def help(): print "\nCommands:" @@ -360,6 +377,7 @@ def help(): print " %s %s" % (k, " ".join(["<%s>" % y for y in commands[k]["args"]])) + def build_args(cmd, cmdargs, arglist): args = [] orig_arglist = arglist[:] @@ -381,6 +399,7 @@ def build_args(cmd, cmdargs, arglist): return None return args + if __name__ == "__main__": usagestr = "Usage: %prog [OPTIONS] [args]" parser = OptionParser(usage=usagestr) @@ -420,7 +439,7 @@ if __name__ == "__main__": LOG.debug("Executing command \"%s\" with args: %s" % (cmd, args)) if not options.load_plugin: client = MiniClient(options.host, options.port, options.ssl) - if not commands[cmd].has_key("api_func"): + 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) diff --git a/quantum/common/config.py b/quantum/common/config.py index cda765078..f2daa417b 100644 --- a/quantum/common/config.py +++ b/quantum/common/config.py @@ -33,6 +33,7 @@ from paste import deploy from quantum.common import flags from quantum.common import exceptions as exception +from quantum.common import extensions DEFAULT_LOG_FORMAT = "%(asctime)s %(levelname)8s [%(name)s] %(message)s" DEFAULT_LOG_DATE_FORMAT = "%Y-%m-%d %H:%M:%S" @@ -209,7 +210,7 @@ def find_config_file(options, args): fix_path(os.path.join('~', '.quantum')), fix_path('~'), os.path.join(FLAGS.state_path, 'etc'), - os.path.join(FLAGS.state_path, 'etc','quantum'), + os.path.join(FLAGS.state_path, 'etc', 'quantum'), '/etc/quantum/', '/etc'] for cfg_dir in config_file_dirs: @@ -244,12 +245,10 @@ def load_paste_config(app_name, options, args): problem loading the configuration file. """ conf_file = find_config_file(options, args) - print "Conf_file:%s" %conf_file if not conf_file: raise RuntimeError("Unable to locate any configuration file. " "Cannot load application %s" % app_name) try: - print "App_name:%s" %app_name conf = deploy.appconfig("config:%s" % conf_file, name=app_name) return conf_file, conf except Exception, e: @@ -257,7 +256,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. @@ -278,16 +277,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/exceptions.py b/quantum/common/exceptions.py index 7b9784b92..4daf31762 100644 --- a/quantum/common/exceptions.py +++ b/quantum/common/exceptions.py @@ -25,7 +25,7 @@ import logging class QuantumException(Exception): """Base Quantum Exception - + Taken from nova.exception.NovaException To correctly use this class, inherit from it and define a 'message' property. That message will get printf'd @@ -45,6 +45,7 @@ class QuantumException(Exception): def __str__(self): return self._error_string + class ProcessExecutionError(IOError): def __init__(self, stdout=None, stderr=None, exit_code=None, cmd=None, description=None): @@ -84,11 +85,11 @@ class NetworkNotFound(NotFound): class PortNotFound(NotFound): message = _("Port %(port_id)s could not be found " \ "on network %(net_id)s") - + class StateInvalid(QuantumException): message = _("Unsupported port state: %(port_state)s") - + class NetworkInUse(QuantumException): message = _("Unable to complete operation on network %(net_id)s. " \ @@ -100,11 +101,13 @@ class PortInUse(QuantumException): "for network %(net_id)s. The attachment '%(att_id)s" \ "is plugged into the logical port.") + class AlreadyAttached(QuantumException): message = _("Unable to plug the attachment %(att_id)s into port " \ "%(port_id)s for network %(net_id)s. The attachment is " \ "already plugged into port %(att_port_id)s") - + + class Duplicate(Error): pass diff --git a/quantum/common/flags.py b/quantum/common/flags.py index 947999d0f..5adf4c385 100644 --- a/quantum/common/flags.py +++ b/quantum/common/flags.py @@ -23,7 +23,7 @@ Global flags should be defined here, the rest are defined where they're used. """ import getopt -import os +import os import string import sys @@ -249,4 +249,3 @@ def DECLARE(name, module_string, flag_values=FLAGS): DEFINE_string('state_path', os.path.join(os.path.dirname(__file__), '../../'), "Top-level directory for maintaining quantum's state") - diff --git a/quantum/common/utils.py b/quantum/common/utils.py index 7ab4615b2..3a2455963 100644 --- a/quantum/common/utils.py +++ b/quantum/common/utils.py @@ -37,6 +37,7 @@ from exceptions import ProcessExecutionError TIME_FORMAT = "%Y-%m-%dT%H:%M:%SZ" FLAGS = flags.FLAGS + def int_from_bool_as_string(subject): """ Interpret a string as a boolean and return either 1 or 0. @@ -188,6 +189,7 @@ def isotime(at=None): def parse_isotime(timestr): return datetime.datetime.strptime(timestr, TIME_FORMAT) + def getPluginFromConfig(file="config.ini"): Config = ConfigParser.ConfigParser() Config.read(file) diff --git a/quantum/common/wsgi.py b/quantum/common/wsgi.py index 4caeab9ab..23e7c1c3d 100644 --- a/quantum/common/wsgi.py +++ b/quantum/common/wsgi.py @@ -40,6 +40,7 @@ from quantum.common import exceptions as exception LOG = logging.getLogger('quantum.common.wsgi') + class WritableLogger(object): """A thin wrapper that responds to `write` and logs.""" @@ -126,7 +127,7 @@ class Request(webob.Request): """ parts = self.path.rsplit('.', 1) - LOG.debug("Request parts:%s",parts) + LOG.debug("Request parts:%s", parts) if len(parts) > 1: format = parts[1] if format in ['json', 'xml']: @@ -134,7 +135,7 @@ class Request(webob.Request): ctypes = ['application/json', 'application/xml'] bm = self.accept.best_match(ctypes) - LOG.debug("BM:%s",bm) + LOG.debug("BM:%s", bm) return bm or 'application/json' def get_content_type(self): @@ -336,21 +337,21 @@ class Controller(object): arg_dict = req.environ['wsgiorg.routing_args'][1] action = arg_dict['action'] method = getattr(self, action) - LOG.debug("ARG_DICT:%s",arg_dict) - LOG.debug("Action:%s",action) - LOG.debug("Method:%s",method) + LOG.debug("ARG_DICT:%s", arg_dict) + LOG.debug("Action:%s", action) + LOG.debug("Method:%s", method) LOG.debug("%s %s" % (req.method, req.url)) del arg_dict['controller'] 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: content_type = req.best_match_content_type() - LOG.debug("Content type:%s",content_type) - LOG.debug("Result:%s",result) + LOG.debug("Content type:%s", content_type) + LOG.debug("Result:%s", result) default_xmlns = self.get_default_xmlns(req) body = self._serialize(result, content_type, default_xmlns) @@ -497,7 +498,7 @@ class Serializer(object): xmlns = metadata.get('xmlns', None) if xmlns: result.setAttribute('xmlns', xmlns) - LOG.debug("DATA:%s",data) + LOG.debug("DATA:%s", data) if type(data) is list: LOG.debug("TYPE IS LIST") collections = metadata.get('list_collections', {}) @@ -538,8 +539,7 @@ class Serializer(object): result.appendChild(node) else: # Type is atom - LOG.debug("TYPE IS ATOM:%s",data) + LOG.debug("TYPE IS ATOM:%s", data) node = doc.createTextNode(str(data)) result.appendChild(node) return result - diff --git a/quantum/db/api.py b/quantum/db/api.py index 8a6ba305c..2a296f2f0 100644 --- a/quantum/db/api.py +++ b/quantum/db/api.py @@ -25,6 +25,7 @@ _ENGINE = None _MAKER = None BASE = models.BASE + def configure_db(options): """ Establish the database, create an engine if needed, and @@ -40,6 +41,7 @@ def configure_db(options): pool_recycle=3600) register_models() + def get_session(autocommit=True, expire_on_commit=False): """Helper method to grab session""" global _MAKER, _ENGINE @@ -50,18 +52,21 @@ def get_session(autocommit=True, expire_on_commit=False): 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 @@ -77,12 +82,14 @@ def network_create(tenant_id, name): 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: @@ -92,6 +99,7 @@ def network_get(net_id): 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: @@ -106,6 +114,7 @@ def network_rename(net_id, tenant_id, new_name): return net raise Exception("A network with name \"%s\" already exists" % new_name) + def network_destroy(net_id): session = get_session() try: @@ -118,6 +127,7 @@ def network_destroy(net_id): except exc.NoResultFound: raise Exception("No network found with id = %s" % net_id) + def port_create(net_id): session = get_session() with session.begin(): @@ -126,12 +136,14 @@ def port_create(net_id): 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: @@ -141,6 +153,7 @@ def port_get(port_id): 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 @@ -157,7 +170,9 @@ def port_set_attachment(port_id, new_interface_id): session.flush() return port else: - raise Exception("Port with attachment \"%s\" already exists" % (new_interface_id)) + raise Exception("Port with attachment \"%s\" already exists" + % (new_interface_id)) + def port_destroy(port_id): session = get_session() @@ -170,4 +185,3 @@ def port_destroy(port_id): 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 index 28bc139b9..115ab282d 100644 --- a/quantum/db/models.py +++ b/quantum/db/models.py @@ -25,12 +25,14 @@ 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) + network_id = Column(String(255), ForeignKey("networks.uuid"), + nullable=False) interface_id = Column(String(255)) def __init__(self, network_id): @@ -38,7 +40,9 @@ class Port(BASE): self.network_id = network_id def __repr__(self): - return "" % (self.uuid, self.network_id, self.interface_id) + return "" % (self.uuid, self.network_id, + self.interface_id) + class Network(BASE): """Represents a quantum network""" @@ -56,4 +60,4 @@ class Network(BASE): def __repr__(self): return "" % \ - (self.uuid,self.name,self.tenant_id) + (self.uuid, self.name, self.tenant_id) diff --git a/quantum/manager.py b/quantum/manager.py index 9932f17af..a9662d8eb 100644 --- a/quantum/manager.py +++ b/quantum/manager.py @@ -18,12 +18,14 @@ """ -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 @@ -33,16 +35,19 @@ from quantum_plugin_base import QuantumPluginBase 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=None): if config == None: - self.configuration_file = find_config(os.path.abspath(os.path.dirname(__file__))) + self.configuration_file = find_config( + os.path.abspath(os.path.dirname(__file__))) else: self.configuration_file = config plugin_location = utils.getPluginFromConfig(self.configuration_file) @@ -58,4 +63,3 @@ class QuantumManager(object): def get_manager(self): return self.plugin - diff --git a/quantum/plugins/SamplePlugin.py b/quantum/plugins/SamplePlugin.py index f82397ebb..376456a6c 100644 --- a/quantum/plugins/SamplePlugin.py +++ b/quantum/plugins/SamplePlugin.py @@ -17,33 +17,32 @@ from quantum.common import exceptions as exc + class QuantumEchoPlugin(object): """ QuantumEchoPlugin is a demo plugin that doesn't do anything but demonstrated the concept of a concrete Quantum Plugin. Any call to this plugin - will result in just a "print" to std. out with + will result in just a "print" to std. out with the name of the method that was called. """ - + def get_all_networks(self, tenant_id): """ Returns a dictionary containing all for - the specified tenant. + the specified tenant. """ print("get_all_networks() called\n") - - + def create_network(self, tenant_id, net_name): """ Creates a new Virtual Network, and assigns it a symbolic name. """ print("create_network() called\n") - - + def delete_network(self, tenant_id, net_id): """ Deletes the network with the specified network identifier @@ -51,38 +50,33 @@ class QuantumEchoPlugin(object): """ print("delete_network() called\n") - def get_network_details(self, tenant_id, net_id): """ Deletes the Virtual Network belonging to a the spec """ print("get_network_details() called\n") - - + def rename_network(self, tenant_id, net_id, new_name): """ Updates the symbolic name belonging to a particular Virtual Network. """ print("rename_network() called\n") - - + def get_all_ports(self, tenant_id, net_id): """ Retrieves all port identifiers belonging to the specified Virtual Network. """ print("get_all_ports() called\n") - - + def create_port(self, tenant_id, net_id): """ Creates a port on the specified Virtual Network. """ print("create_port() called\n") - - + def delete_port(self, tenant_id, net_id, port_id): """ Deletes a port on a specified Virtual Network, @@ -97,24 +91,21 @@ class QuantumEchoPlugin(object): 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 that is attached to this particular port. """ print("get_port_details() called\n") - - + def plug_interface(self, tenant_id, net_id, port_id, remote_interface_id): """ Attaches a remote interface to the specified port on the specified Virtual Network. """ print("plug_interface() called\n") - - + def unplug_interface(self, tenant_id, net_id, port_id): """ Detaches a remote interface from the specified port on the @@ -130,18 +121,17 @@ class DummyDataPlugin(object): hard-coded data structures to aid in quantum client/cli development """ - + def get_all_networks(self, tenant_id): """ Returns a dictionary containing all for - the specified tenant. + the specified tenant. """ - nets = {"001": "lNet1", "002": "lNet2" , "003": "lNet3"} + nets = {"001": "lNet1", "002": "lNet2", "003": "lNet3"} print("get_all_networks() called\n") return nets - - + def create_network(self, tenant_id, net_name): """ Creates a new Virtual Network, and assigns it @@ -150,8 +140,7 @@ class DummyDataPlugin(object): print("create_network() called\n") # return network_id of the created network return 101 - - + def delete_network(self, tenant_id, net_id): """ Deletes the network with the specified network identifier @@ -159,7 +148,6 @@ class DummyDataPlugin(object): """ print("delete_network() called\n") - def get_network_details(self, tenant_id, net_id): """ retrieved a list of all the remote vifs that @@ -168,16 +156,14 @@ class DummyDataPlugin(object): print("get_network_details() called\n") vifs_on_net = ["/tenant1/networks/net_id/portid/vif2.0"] return vifs_on_net - - + def rename_network(self, tenant_id, net_id, new_name): """ Updates the symbolic name belonging to a particular Virtual Network. """ print("rename_network() called\n") - - + def get_all_ports(self, tenant_id, net_id): """ Retrieves all port identifiers belonging to the @@ -186,8 +172,7 @@ class DummyDataPlugin(object): print("get_all_ports() called\n") port_ids_on_net = ["2", "3", "4"] return port_ids_on_net - - + def create_port(self, tenant_id, net_id): """ Creates a port on the specified Virtual Network. @@ -201,8 +186,7 @@ class DummyDataPlugin(object): Updates the state of a port on the specified Virtual Network. """ print("update_port() called\n") - - + def delete_port(self, tenant_id, net_id, port_id): """ Deletes a port on a specified Virtual Network, @@ -211,8 +195,7 @@ class DummyDataPlugin(object): is deleted. """ print("delete_port() called\n") - - + def get_port_details(self, tenant_id, net_id, port_id): """ This method allows the user to retrieve a remote interface @@ -221,24 +204,22 @@ class DummyDataPlugin(object): print("get_port_details() called\n") #returns the remote interface UUID return "/tenant1/networks/net_id/portid/vif2.1" - - + def plug_interface(self, tenant_id, net_id, port_id, remote_interface_id): """ Attaches a remote interface to the specified port on the specified Virtual Network. """ print("plug_interface() called\n") - - + def unplug_interface(self, tenant_id, net_id, port_id): """ Detaches a remote interface from the specified port on the specified Virtual Network. """ print("unplug_interface() called\n") - - + + class FakePlugin(object): """ FakePlugin is a demo plugin that provides @@ -248,72 +229,66 @@ class FakePlugin(object): #static data for networks and ports _port_dict_1 = { - 1 : {'port-id': 1, + 1: {'port-id': 1, 'port-state': 'DOWN', 'attachment': None}, - 2 : {'port-id': 2, - 'port-state':'UP', - 'attachment': None} - } + 2: {'port-id': 2, + 'port-state': 'UP', + 'attachment': None}} _port_dict_2 = { - 1 : {'port-id': 1, + 1: {'port-id': 1, 'port-state': 'UP', 'attachment': 'SomeFormOfVIFID'}, - 2 : {'port-id': 2, - 'port-state':'DOWN', - 'attachment': None} - } - _networks={'001': + 2: {'port-id': 2, + 'port-state': 'DOWN', + 'attachment': None}} + _networks = {'001': { - 'net-id':'001', - 'net-name':'pippotest', - 'net-ports': _port_dict_1 - }, + 'net-id': '001', + 'net-name': 'pippotest', + 'net-ports': _port_dict_1}, '002': { - 'net-id':'002', - 'net-name':'cicciotest', - 'net-ports': _port_dict_2 - }} - - + 'net-id': '002', + 'net-name': 'cicciotest', + 'net-ports': _port_dict_2}} + def __init__(self): - FakePlugin._net_counter=len(FakePlugin._networks) - + FakePlugin._net_counter = len(FakePlugin._networks) + def _get_network(self, tenant_id, network_id): network = FakePlugin._networks.get(network_id) if not network: raise exc.NetworkNotFound(net_id=network_id) return network - def _get_port(self, tenant_id, network_id, port_id): net = self._get_network(tenant_id, network_id) port = net['net-ports'].get(int(port_id)) if not port: raise exc.PortNotFound(net_id=network_id, port_id=port_id) return port - + def _validate_port_state(self, port_state): - if port_state.upper() not in ('UP','DOWN'): + if port_state.upper() not in ('UP', 'DOWN'): raise exc.StateInvalid(port_state=port_state) return True - + def _validate_attachment(self, tenant_id, network_id, port_id, remote_interface_id): network = self._get_network(tenant_id, network_id) for port in network['net-ports'].values(): if port['attachment'] == remote_interface_id: - raise exc.AlreadyAttached(net_id = network_id, - port_id = port_id, - att_id = port['attachment'], - att_port_id = port['port-id']) - + raise exc.AlreadyAttached(net_id=network_id, + port_id=port_id, + att_id=port['attachment'], + att_port_id=port['port-id']) + def get_all_networks(self, tenant_id): """ Returns a dictionary containing all for - the specified tenant. + the specified tenant. """ print("get_all_networks() called\n") return FakePlugin._networks.values() @@ -333,16 +308,16 @@ class FakePlugin(object): """ print("create_network() called\n") FakePlugin._net_counter += 1 - new_net_id=("0" * (3 - len(str(FakePlugin._net_counter)))) + \ + new_net_id = ("0" * (3 - len(str(FakePlugin._net_counter)))) + \ str(FakePlugin._net_counter) print new_net_id - new_net_dict={'net-id':new_net_id, - 'net-name':net_name, + new_net_dict = {'net-id': new_net_id, + 'net-name': net_name, 'net-ports': {}} - FakePlugin._networks[new_net_id]=new_net_dict + FakePlugin._networks[new_net_id] = new_net_dict # return network_id of the created network return new_net_dict - + def delete_network(self, tenant_id, net_id): """ Deletes the network with the specified network identifier @@ -360,7 +335,7 @@ class FakePlugin(object): return net # Network not found raise exc.NetworkNotFound(net_id=net_id) - + def rename_network(self, tenant_id, net_id, new_name): """ Updates the symbolic name belonging to a particular @@ -368,7 +343,7 @@ class FakePlugin(object): """ print("rename_network() called\n") net = self._get_network(tenant_id, net_id) - net['net-name']=new_name + net['net-name'] = new_name return net def get_all_ports(self, tenant_id, net_id): @@ -388,7 +363,7 @@ class FakePlugin(object): """ print("get_port_details() called\n") return self._get_port(tenant_id, net_id, port_id) - + def create_port(self, tenant_id, net_id, port_state=None): """ Creates a port on the specified Virtual Network. @@ -396,15 +371,15 @@ class FakePlugin(object): print("create_port() called\n") net = self._get_network(tenant_id, net_id) # check port state - # TODO(salvatore-orlando): Validate port state in API? + # TODO(salvatore-orlando): Validate port state in API? self._validate_port_state(port_state) ports = net['net-ports'] - new_port_id = max(ports.keys())+1 - new_port_dict = {'port-id':new_port_id, + new_port_id = max(ports.keys()) + 1 + new_port_dict = {'port-id': new_port_id, 'port-state': port_state, 'attachment': None} ports[new_port_id] = new_port_dict - return new_port_dict + return new_port_dict def update_port(self, tenant_id, net_id, port_id, port_state): """ @@ -414,8 +389,8 @@ class FakePlugin(object): port = self._get_port(tenant_id, net_id, port_id) self._validate_port_state(port_state) port['port-state'] = port_state - return port - + return port + def delete_port(self, tenant_id, net_id, port_id): """ Deletes a port on a specified Virtual Network, @@ -427,14 +402,13 @@ class FakePlugin(object): net = self._get_network(tenant_id, net_id) port = self._get_port(tenant_id, net_id, port_id) if port['attachment']: - raise exc.PortInUse(net_id=net_id,port_id=port_id, + raise exc.PortInUse(net_id=net_id, port_id=port_id, att_id=port['attachment']) try: net['net-ports'].pop(int(port_id)) - except KeyError: + except KeyError: raise exc.PortNotFound(net_id=net_id, port_id=port_id) - def plug_interface(self, tenant_id, net_id, port_id, remote_interface_id): """ Attaches a remote interface to the specified port on the @@ -446,10 +420,10 @@ class FakePlugin(object): remote_interface_id) port = self._get_port(tenant_id, net_id, port_id) if port['attachment']: - raise exc.PortInUse(net_id=net_id,port_id=port_id, + raise exc.PortInUse(net_id=net_id, port_id=port_id, att_id=port['attachment']) port['attachment'] = remote_interface_id - + def unplug_interface(self, tenant_id, net_id, port_id): """ Detaches a remote interface from the specified port on the @@ -460,5 +434,3 @@ class FakePlugin(object): # TODO(salvatore-orlando): # Should unplug on port without attachment raise an Error? port['attachment'] = None - - \ No newline at end of file diff --git a/quantum/plugins/__init__.py b/quantum/plugins/__init__.py index df928bbf1..7e695ff08 100644 --- a/quantum/plugins/__init__.py +++ b/quantum/plugins/__init__.py @@ -13,4 +13,4 @@ # 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. \ No newline at end of file +# @author: Somik Behera, Nicira Networks, Inc. diff --git a/quantum/plugins/openvswitch/agent/ovs_quantum_agent.py b/quantum/plugins/openvswitch/agent/ovs_quantum_agent.py index 34f28fbdd..a5cd7948d 100755 --- a/quantum/plugins/openvswitch/agent/ovs_quantum_agent.py +++ b/quantum/plugins/openvswitch/agent/ovs_quantum_agent.py @@ -28,6 +28,7 @@ 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: @@ -37,11 +38,13 @@ class VifPort: 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 @@ -51,27 +54,27 @@ class OVSBridge: return Popen(args, stdout=PIPE).communicate()[0] def run_vsctl(self, args): - full_args = ["ovs-vsctl" ] + 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(["--", "--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, + 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) ] + 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 ] + 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 + full_args = ["ovs-ofctl", cmd, self.br_name] + args return self.run_cmd(full_args) def remove_all_flows(self): @@ -80,7 +83,7 @@ class OVSBridge: def get_port_ofport(self, port_name): return self.db_get_val("Interface", port_name, "ofport") - def add_flow(self,**dict): + def add_flow(self, **dict): if "actions" not in dict: raise Exception("must specify one or more actions") if "priority" not in dict: @@ -90,9 +93,9 @@ class OVSBridge: if "match" in dict: flow_str += "," + dict["match"] flow_str += ",actions=%s" % (dict["actions"]) - self.run_ofctl("add-flow", [ flow_str ] ) + self.run_ofctl("add-flow", [flow_str]) - def delete_flows(self,**dict): + def delete_flows(self, **dict): all_args = [] if "priority" in dict: all_args.append("priority=%s" % dict["priority"]) @@ -101,14 +104,14 @@ class OVSBridge: if "actions" in dict: all_args.append("actions=%s" % (dict["actions"])) flow_str = ",".join(all_args) - self.run_ofctl("del-flows", [ flow_str ] ) + 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") + 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") + return self.run_vsctl(["get", table, record, column]).rstrip("\n\r") def db_str_to_map(self, full_str): list = full_str.strip("{}").split(", ") @@ -121,7 +124,7 @@ class OVSBridge: return ret def get_port_name_list(self): - res = self.run_vsctl([ "list-ports", self.br_name]) + res = self.run_vsctl(["list-ports", self.br_name]) return res.split("\n")[0:-1] def get_port_stats(self, port_name): @@ -132,47 +135,53 @@ class OVSBridge: edge_ports = [] port_names = self.get_port_name_list() for name in port_names: - external_ids = self.db_get_map("Interface",name,"external_ids") + 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") + 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") + 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 = 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)) + 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") + "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) + 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", + 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") + self.int_br.clear_db_attribute("Port", port.port_name, "tag") def setup_integration_br(self, integ_br): self.int_br = OVSBridge(integ_br) @@ -182,7 +191,8 @@ class OVSNaaSPlugin: # 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") + #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 = {} @@ -216,9 +226,9 @@ class OVSNaaSPlugin: 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) + "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" diff --git a/quantum/plugins/openvswitch/ovs_db.py b/quantum/plugins/openvswitch/ovs_db.py index a2a72ec8c..d72f9a89c 100644 --- a/quantum/plugins/openvswitch/ovs_db.py +++ b/quantum/plugins/openvswitch/ovs_db.py @@ -24,6 +24,7 @@ import quantum.db.api as db import quantum.db.models as models import ovs_models + def get_vlans(): session = db.get_session() try: @@ -33,9 +34,10 @@ def get_vlans(): return [] res = [] for x in bindings: - res.append((x.vlan_id, x.network_id)) + 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) @@ -43,6 +45,7 @@ def add_vlan_binding(vlanid, netid): session.flush() return binding.vlan_id + def remove_vlan_binding(netid): session = db.get_session() try: @@ -54,6 +57,7 @@ def remove_vlan_binding(netid): pass session.flush() + def update_network_binding(netid, ifaceid): session = db.get_session() # Add to or delete from the bindings table diff --git a/quantum/plugins/openvswitch/ovs_models.py b/quantum/plugins/openvswitch/ovs_models.py index 610902a7c..5f529e55a 100644 --- a/quantum/plugins/openvswitch/ovs_models.py +++ b/quantum/plugins/openvswitch/ovs_models.py @@ -23,9 +23,9 @@ 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' @@ -42,6 +42,7 @@ class NetworkBinding(BASE): return "" % \ (self.network_id, self.vif_id) + class VlanBinding(BASE): """Represents a binding of network_id, vlan_id""" __tablename__ = 'vlan_bindings' diff --git a/quantum/plugins/openvswitch/ovs_quantum_plugin.py b/quantum/plugins/openvswitch/ovs_quantum_plugin.py index 75619cae6..b5d1bc68d 100644 --- a/quantum/plugins/openvswitch/ovs_quantum_plugin.py +++ b/quantum/plugins/openvswitch/ovs_quantum_plugin.py @@ -29,24 +29,29 @@ from optparse import OptionParser import quantum.db.api as db import ovs_db -CONF_FILE="ovs_quantum_plugin.ini" +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: @@ -54,8 +59,10 @@ class VlanMap(object): # 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: @@ -64,14 +71,17 @@ class VlanMap(object): 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__))) + configfile = find_config(os.path.abspath( + os.path.dirname(__file__))) if configfile == None: raise Exception("Configuration file \"%s\" doesn't exist" % (configfile)) @@ -93,7 +103,8 @@ class OVSQuantumPlugin(QuantumPluginBase): 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)) + # 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): @@ -109,8 +120,8 @@ class OVSQuantumPlugin(QuantumPluginBase): def create_network(self, tenant_id, net_name): d = {} try: - res = db.network_create(tenant_id, net_name) - LOG.debug("Created newtork: %s" % res) + 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 @@ -199,21 +210,28 @@ class OVSQuantumPlugin(QuantumPluginBase): 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" @@ -312,6 +330,7 @@ class OVSPluginTest(unittest.TestCase): 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) diff --git a/tests/functional/miniclient.py b/tests/functional/miniclient.py index fb1ebc8fe..be4986735 100644 --- a/tests/functional/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 index f9cd77418..6ed728394 100644 --- a/tests/functional/test_service.py +++ b/tests/functional/test_service.py @@ -34,16 +34,18 @@ TENANT_ID = 'totore' FORMAT = "json" test_network1_data = \ - {'network': {'network-name': 'test1' }} + {'network': {'network-name': 'test1'}} test_network2_data = \ - {'network': {'network-name': 'test2' }} + {'network': {'network-name': 'test2'}} + def print_response(res): content = res.read() - print "Status: %s" %res.status - print "Content: %s" %content + 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) @@ -58,7 +60,7 @@ class QuantumTest(unittest.TestCase): 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) + res = self.client.do_request(TENANT_ID, 'GET', "/networks." + FORMAT) self.assertEqual(res.status, 200, "bad response: %s" % res.read()) def test_createNetwork(self): @@ -111,8 +113,9 @@ class QuantumTest(unittest.TestCase): 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") + 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 From fd249e222326a99826f79f018cf565b74451a5f2 Mon Sep 17 00:00:00 2001 From: Santhosh Date: Thu, 9 Jun 2011 10:29:30 +0530 Subject: [PATCH 29/33] Santhosh/Deepak | Fixed the import issue and config.load_paste_app issue --- quantum/common/config.py | 1 - quantum/service.py | 4 +++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/quantum/common/config.py b/quantum/common/config.py index f2daa417b..320c3b2cf 100644 --- a/quantum/common/config.py +++ b/quantum/common/config.py @@ -33,7 +33,6 @@ from paste import deploy from quantum.common import flags from quantum.common import exceptions as exception -from quantum.common import extensions DEFAULT_LOG_FORMAT = "%(asctime)s %(levelname)8s [%(name)s] %(message)s" DEFAULT_LOG_DATE_FORMAT = "%Y-%m-%d %H:%M:%S" diff --git a/quantum/service.py b/quantum/service.py index 1406462b1..0c27bcc5b 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) From d9d0a6f5653d49e3f55e112b9417b32e4acd33aa Mon Sep 17 00:00:00 2001 From: Brad Hall Date: Thu, 9 Jun 2011 07:42:19 -0700 Subject: [PATCH 30/33] README fixes --- README | 58 +++++++++++++++++------------- quantum/plugins/openvswitch/README | 21 +++++------ 2 files changed, 44 insertions(+), 35 deletions(-) diff --git a/README b/README index 19d15d095..f3e973faa 100644 --- a/README +++ b/README @@ -1,25 +1,29 @@ # -- Welcome! - You have come across a cloud computing network fabric controller. It has identified - itself as "Quantum." It aims to tame your (cloud) networking! + 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. +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: +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. + 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 + 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: + The following python packages are required to run quantum. These can be + installed using pip: eventlet>=0.9.12 nose @@ -32,7 +36,9 @@ webob webtest -1) Install easy_install (there is probably a distribution specific package for this) +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: @@ -40,14 +46,16 @@ # -- Configuring Quantum plug-in -1) Explore sample and real Quantum plug-ins in the quantum.plugins module. +1) Identify your desired plug-in. Choose a plugin from one of he options in + the quantum/plugins directory. -2) Or copy another Quantum plug-in into the quantum.plugins module. +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) Update plug-in configuration by editing plugins.ini file and modify - "provider" property to point to the location of the Quantum plug-in. - -4) Read the plugin specific README, this is usually found in the same +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 @@ -63,8 +71,8 @@ Please refer to sample Web Service client code in: # -- 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: +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 @@ -85,16 +93,16 @@ well as sample plugins available in: There are a few requirements to writing your own plugin: -1) Your plugin should implement all methods defined in -../quantum/quantum/quantum_plugin_base.QuantumPluginBase class +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 +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. +4) Launch the Quantum Service, and your plug-in is configured and ready to + manage a Cloud Networking Fabric. diff --git a/quantum/plugins/openvswitch/README b/quantum/plugins/openvswitch/README index 689624f1b..090e53b73 100644 --- a/quantum/plugins/openvswitch/README +++ b/quantum/plugins/openvswitch/README @@ -43,11 +43,13 @@ 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: +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 +// 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; @@ -70,6 +72,7 @@ $ make agent-dist - 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 @@ -86,21 +89,19 @@ 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 +$ 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 -v create_port $TENANT $NETWORK +$ 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 -v plug_iface $TENANT $NETWORK $PORT ubuntu1-eth1 +$ 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 -$ PYTHONPATH=. python quantum/cli.py -v plug_iface $TENANT $NETWORK $PORT ubuntu2-eth1 -Plugged interface "ubuntu2-eth1" to port:5a1e121b-ccc8-471d-9445-24f15f9f854c on network:e754e7c0-a8eb-40e5-861a-b182d30c3441 -Now you should have connectivity between ubuntu1-eth1 and ubuntu2-eth1.. +(.. repeat for more ports and interface combinations..) # -- Other items -- To get a listing of the vif names that the ovs quantum service will expect - them in, issue the following command on the hypervisor (dom0): +- 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 From cdc4256a72cdd0c59181333392cd6be628982e5c Mon Sep 17 00:00:00 2001 From: Brad Hall Date: Thu, 16 Jun 2011 10:03:29 -0700 Subject: [PATCH 31/33] Fix typo in mysql package check issue: https://bugs.launchpad.net/bugs/798080 --- quantum/plugins/openvswitch/agent/install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quantum/plugins/openvswitch/agent/install.sh b/quantum/plugins/openvswitch/agent/install.sh index 1e71c09b4..1eebc8fa3 100755 --- a/quantum/plugins/openvswitch/agent/install.sh +++ b/quantum/plugins/openvswitch/agent/install.sh @@ -8,7 +8,7 @@ if [ ! -d /etc/xapi.d/plugins ]; then fi # Make sure we have mysql-python -rpm -qa | grep MYyQL-python >/dev/null 2>&1 +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:" From a7ca758ee046ced142184c930718e96808d1c665 Mon Sep 17 00:00:00 2001 From: Brad Hall Date: Fri, 17 Jun 2011 11:06:19 -0700 Subject: [PATCH 32/33] Fix typo in mysql package check issue: https://bugs.launchpad.net/bugs/798080 --- quantum/plugins/openvswitch/agent/install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quantum/plugins/openvswitch/agent/install.sh b/quantum/plugins/openvswitch/agent/install.sh index 1e71c09b4..1eebc8fa3 100755 --- a/quantum/plugins/openvswitch/agent/install.sh +++ b/quantum/plugins/openvswitch/agent/install.sh @@ -8,7 +8,7 @@ if [ ! -d /etc/xapi.d/plugins ]; then fi # Make sure we have mysql-python -rpm -qa | grep MYyQL-python >/dev/null 2>&1 +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:" From fdfa94c8ae23ff4c47dc9b0fd70c033f0f08b960 Mon Sep 17 00:00:00 2001 From: Salvatore Orlando Date: Fri, 24 Jun 2011 14:05:35 +0100 Subject: [PATCH 33/33] no-commit --- quantum/api/__init__.py | 1 + quantum/plugins/openvswitch/README | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/quantum/api/__init__.py b/quantum/api/__init__.py index 3bf7f113a..00ad61912 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/plugins/openvswitch/README b/quantum/plugins/openvswitch/README index 090e53b73..ef0660c08 100644 --- a/quantum/plugins/openvswitch/README +++ b/quantum/plugins/openvswitch/README @@ -79,7 +79,7 @@ $ /etc/xapi.d/plugins/ovs_quantum_agent.py /etc/xapi.d/plugins/ovs_quantum_plugi # -- Getting quantum up and running - Start quantum [on the quantum service host]: -~/src/quantum-framework$ PYTHONPATH=.:$PYTHONPATH python bin/quantum etc/quantum.conf +~/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