From 5cf7dc6cb262f4b9435a052b9244d6cb5b023dc5 Mon Sep 17 00:00:00 2001 From: Brad Hall Date: Thu, 2 Jun 2011 22:30:37 -0700 Subject: [PATCH 01/17] 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 2d93a35f60b..6077214b68f 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 78f1a6b4811..faa92f1b168 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 02/17] 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 faa92f1b168..ad37f007809 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 03/17] 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 25b66f55730..3665464c921 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 2b93cdec723..8e010063463 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 04/17] 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 ad37f007809..2fa2b02813d 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 05/17] 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 00000000000..620282b5831 --- /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 00000000000..7ae5a26256e --- /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 00000000000..28bc139b919 --- /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 06/17] 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 00000000000..7cd5e620ed7 --- /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 00000000000..ee2248a1736 --- /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 00000000000..e69de29bb2d diff --git a/quantum/plugins/openvswitch/agent/install.sh b/quantum/plugins/openvswitch/agent/install.sh new file mode 100644 index 00000000000..2f2b0381f31 --- /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 00000000000..9769ab93f97 --- /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 00000000000..f56dc4bd91c --- /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 00000000000..8b4106215da --- /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 00000000000..7e8b2f54b6c --- /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 00000000000..0c75b3fc48a --- /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 00000000000..e8111c65983 --- /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 07/17] 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 9769ab93f97..34f28fbdd5c 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 f56dc4bd91c..2fae05f0a13 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 8b4106215da..a2a72ec8ceb 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 7e8b2f54b6c..610902a7caa 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 e8111c65983..50053df7a72 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 08/17] 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 7ae5a26256e..8a6ba305ccb 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 50053df7a72..036c3c83061 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 09/17] 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 ee2248a1736..77f053a38db 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 10/17] 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 2fa2b02813d..7daa5de6971 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 036c3c83061..75619cae627 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 11/17] 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 7daa5de6971..4ea93002f01 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 12/17] 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 79a5139d8be..dc882bbb06c 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 13/17] 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 dc882bbb06c..9932f17af01 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 14/17] 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 77f053a38db..6fcfb40056b 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 2f2b0381f31..1e71c09b4b7 --- 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 15/17] 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 4ea93002f01..539d1417365 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 16/17] 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 6fcfb40056b..d48b793f3a3 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 17/17] 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 d48b793f3a3..689624f1b98 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.