From 804c936667151cdea9b499dd768adcfd7ae20c1f Mon Sep 17 00:00:00 2001 From: Aaron Rosen Date: Thu, 28 Jun 2012 18:17:16 -0700 Subject: [PATCH] OVS plugin support for v2 Quantum API blueprint: ovs-api-v2-support This commit allows the ovs_quantum_plugin to work with the v2 api. change-Id: I9e332a799f6bee8a90755f961fbb9711a1ecdaca --- .../openvswitch/ovs_quantum_plugin.ini | 4 + quantum/common/utils.py | 6 ++ quantum/db/model_base.py | 7 -- quantum/db/models_v2.py | 14 ++- .../openvswitch/agent/ovs_quantum_agent.py | 96 ++++++++++++++----- quantum/plugins/openvswitch/common/config.py | 1 + quantum/plugins/openvswitch/ovs_db_v2.py | 51 ++++++++++ quantum/plugins/openvswitch/ovs_models_v2.py | 49 ++++++++++ .../plugins/openvswitch/ovs_quantum_plugin.py | 88 +++++++++++------ 9 files changed, 250 insertions(+), 66 deletions(-) create mode 100644 quantum/plugins/openvswitch/ovs_db_v2.py create mode 100644 quantum/plugins/openvswitch/ovs_models_v2.py diff --git a/etc/quantum/plugins/openvswitch/ovs_quantum_plugin.ini b/etc/quantum/plugins/openvswitch/ovs_quantum_plugin.ini index 3262571206..44f3fca58d 100644 --- a/etc/quantum/plugins/openvswitch/ovs_quantum_plugin.ini +++ b/etc/quantum/plugins/openvswitch/ovs_quantum_plugin.ini @@ -38,6 +38,8 @@ polling_interval = 2 # Change to "sudo quantum-rootwrap" to limit commands that can be run # as root. root_helper = sudo +# Use Quantumv2 API +target_v2_api = False #----------------------------------------------------------------------------- # Sample Configurations. @@ -53,6 +55,8 @@ root_helper = sudo # root_helper = sudo # Add the following setting, if you want to log to a file # log_file = /var/log/quantum/ovs_quantum_agent.log +# Use Quantumv2 API +# target_v2_api = False # # 2. With tunneling. # [DATABASE] diff --git a/quantum/common/utils.py b/quantum/common/utils.py index 4ac2c328be..0d30d32793 100644 --- a/quantum/common/utils.py +++ b/quantum/common/utils.py @@ -26,6 +26,7 @@ import inspect import logging import os import subprocess +import uuid from quantum.common import exceptions as exception from quantum.common import flags @@ -153,3 +154,8 @@ def find_config_file(options, config_file): cfg_file = os.path.join(cfg_dir, config_file) if os.path.exists(cfg_file): return cfg_file + + +def str_uuid(): + """Return a uuid as a string""" + return str(uuid.uuid4()) diff --git a/quantum/db/model_base.py b/quantum/db/model_base.py index 54842b8157..d93d14867e 100644 --- a/quantum/db/model_base.py +++ b/quantum/db/model_base.py @@ -13,17 +13,11 @@ # See the License for the specific language governing permissions and # limitations under the License. -import uuid - import sqlalchemy as sa from sqlalchemy.ext import declarative from sqlalchemy import orm -def str_uuid(): - return str(uuid.uuid4()) - - class QuantumBase(object): """Base class for Quantum Models.""" @@ -60,7 +54,6 @@ class QuantumBase(object): class QuantumBaseV2(QuantumBase): - id = sa.Column(sa.String(36), primary_key=True, default=str_uuid) @declarative.declared_attr def __tablename__(cls): diff --git a/quantum/db/models_v2.py b/quantum/db/models_v2.py index 9425ef6d44..72c737239a 100644 --- a/quantum/db/models_v2.py +++ b/quantum/db/models_v2.py @@ -16,6 +16,7 @@ import sqlalchemy as sa from sqlalchemy import orm +from quantum.common import utils from quantum.db import model_base @@ -25,7 +26,12 @@ class HasTenant(object): tenant_id = sa.Column(sa.String(255)) -class IPAllocationRange(model_base.BASEV2): +class HasId(object): + """id mixin, add to subclasses that have an id.""" + id = sa.Column(sa.String(36), primary_key=True, default=utils.str_uuid) + + +class IPAllocationRange(model_base.BASEV2, HasId): """Internal representation of a free IP address range in a Quantum subnet. The range of available ips is [first_ip..last_ip]. The allocation retrieves the first entry from the range. If the first @@ -53,7 +59,7 @@ class IPAllocation(model_base.BASEV2): nullable=False, primary_key=True) -class Port(model_base.BASEV2, HasTenant): +class Port(model_base.BASEV2, HasId, HasTenant): """Represents a port on a quantum v2 network.""" network_id = sa.Column(sa.String(36), sa.ForeignKey("networks.id"), nullable=False) @@ -64,7 +70,7 @@ class Port(model_base.BASEV2, HasTenant): device_id = sa.Column(sa.String(255), nullable=False) -class Subnet(model_base.BASEV2): +class Subnet(model_base.BASEV2, HasId): """Represents a quantum subnet. When a subnet is created the first and last entries will be created. These @@ -81,7 +87,7 @@ class Subnet(model_base.BASEV2): # - additional_routes -class Network(model_base.BASEV2, HasTenant): +class Network(model_base.BASEV2, HasId, HasTenant): """Represents a v2 quantum network.""" name = sa.Column(sa.String(255)) ports = orm.relationship(Port, backref='networks') diff --git a/quantum/plugins/openvswitch/agent/ovs_quantum_agent.py b/quantum/plugins/openvswitch/agent/ovs_quantum_agent.py index a11f43e069..be46942d82 100755 --- a/quantum/plugins/openvswitch/agent/ovs_quantum_agent.py +++ b/quantum/plugins/openvswitch/agent/ovs_quantum_agent.py @@ -18,6 +18,7 @@ # @author: Brad Hall, Nicira Networks, Inc. # @author: Dan Wendlandt, Nicira Networks, Inc. # @author: Dave Lapsley, Nicira Networks, Inc. +# @author: Aaron Rosen, Nicira Networks, Inc. import logging from optparse import OptionParser @@ -59,21 +60,21 @@ class LocalVLANMapping: class Port(object): - '''class stores port data in an ORM-free way, - so attributes are still available even if a - row has been deleted. - ''' + """Represents a quantum port. + + Class stores port data in a ORM-free way, so attributres are + still available even if a row has been deleted. + """ def __init__(self, p): self.uuid = p.uuid self.network_id = p.network_id self.interface_id = p.interface_id self.state = p.state - self.op_status = p.op_status + self.status = p.op_status def __eq__(self, other): - '''compare only fields that will cause us to re-wire - ''' + '''Compare only fields that will cause us to re-wire.''' try: return (self and other and self.interface_id == other.interface_id @@ -88,14 +89,45 @@ class Port(object): return hash(self.uuid) +class Portv2(object): + """Represents a quantumv2 port. + + Class stores port data in a ORM-free way, so attributres are + still available even if a row has been deleted. + """ + + def __init__(self, p): + self.id = p.id + self.network_id = p.network_id + self.device_id = p.device_id + self.admin_state_up = p.admin_state_up + self.status = p.status + + def __eq__(self, other): + '''Compare only fields that will cause us to re-wire.''' + try: + return (self and other + and self.id == other.id + and self.admin_state_up == other.admin_state_up) + except: + return False + + def __ne__(self, other): + return not self.__eq__(other) + + def __hash__(self): + return hash(self.id) + + class OVSQuantumAgent(object): - def __init__(self, integ_br, root_helper, - polling_interval, reconnect_interval): + def __init__(self, integ_br, root_helper, polling_interval, + reconnect_interval, target_v2_api=False): self.root_helper = root_helper self.setup_integration_br(integ_br) self.polling_interval = polling_interval self.reconnect_interval = reconnect_interval + self.target_v2_api = target_v2_api def port_bound(self, port, vlan_id): self.int_br.set_db_attribute("Port", port.port_name, @@ -139,7 +171,10 @@ class OVSQuantumAgent(object): continue for port in ports: - all_bindings[port.interface_id] = port + if self.target_v2_api: + all_bindings[port.id] = port + else: + all_bindings[port.interface_id] = port vlan_bindings = {} try: @@ -177,7 +212,7 @@ class OVSQuantumAgent(object): % (old_b, str(p))) self.port_unbound(p, True) if p.vif_id in all_bindings: - all_bindings[p.vif_id].op_status = OP_STATUS_DOWN + all_bindings[p.vif_id].status = OP_STATUS_DOWN if new_b is not None: # If we don't have a binding we have to stick it on # the dead vlan @@ -185,7 +220,7 @@ class OVSQuantumAgent(object): vlan_id = vlan_bindings.get(net_id, DEAD_VLAN_TAG) self.port_bound(p, vlan_id) if p.vif_id in all_bindings: - all_bindings[p.vif_id].op_status = OP_STATUS_UP + all_bindings[p.vif_id].status = OP_STATUS_UP LOG.info(("Adding binding to net-id = %s " "for %s on vlan %s") % (new_b, str(p), vlan_id)) @@ -197,7 +232,7 @@ class OVSQuantumAgent(object): old_b = old_local_bindings[vif_id] self.port_unbound(old_vif_ports[vif_id], False) if vif_id in all_bindings: - all_bindings[vif_id].op_status = OP_STATUS_DOWN + all_bindings[vif_id].status = OP_STATUS_DOWN old_vif_ports = new_vif_ports old_local_bindings = new_local_bindings @@ -237,7 +272,7 @@ class OVSQuantumTunnelAgent(object): MAX_VLAN_TAG = 4094 def __init__(self, integ_br, tun_br, local_ip, root_helper, - polling_interval, reconnect_interval): + polling_interval, reconnect_interval, target_v2_api=False): '''Constructor. :param integ_br: name of the integration bridge. @@ -245,7 +280,9 @@ class OVSQuantumTunnelAgent(object): :param local_ip: local IP address of this hypervisor. :param root_helper: utility to use when running shell cmds. :param polling_interval: interval (secs) to poll DB. - :param reconnect_internal: retry interval (secs) on DB error.''' + :param reconnect_internal: retry interval (secs) on DB error. + :param target_v2_api: if True use v2 api. + ''' self.root_helper = root_helper self.available_local_vlans = set( xrange(OVSQuantumTunnelAgent.MIN_VLAN_TAG, @@ -259,6 +296,7 @@ class OVSQuantumTunnelAgent(object): self.local_ip = local_ip self.tunnel_count = 0 self.setup_tunnel_br(tun_br) + self.target_v2_api = target_v2_api def provision_local_vlan(self, net_uuid, lsw_id): '''Provisions a local VLAN. @@ -294,7 +332,8 @@ class OVSQuantumTunnelAgent(object): self.available_local_vlans.add(lvm.vlan) def port_bound(self, port, net_uuid, lsw_id): - '''Bind port to net_uuid/lsw_id. + '''Bind port to net_uuid/lsw_id and install flow for inbound traffic + to vm. :param port: a ovslib.VifPort object. :param net_uuid: the net_uuid this port is to be associated with. @@ -405,8 +444,12 @@ class OVSQuantumTunnelAgent(object): while True: try: - all_bindings = dict((p.interface_id, Port(p)) - for p in db.ports.all()) + if self.target_v2_api: + all_bindings = dict((p.id, Portv2(p)) + for p in db.ports.all()) + else: + all_bindings = dict((p.interface_id, Port(p)) + for p in db.ports.all()) all_bindings_vif_port_ids = set(all_bindings) lsw_id_bindings = dict((bind.network_id, bind.vlan_id) for bind in db.vlan_bindings.all()) @@ -461,7 +504,8 @@ class OVSQuantumTunnelAgent(object): old_net_uuid + " for " + str(p) + " added to dead vlan") self.port_unbound(p, old_net_uuid) - all_bindings[p.vif_id].op_status = OP_STATUS_DOWN + if p.vif_id in all_bindings: + all_bindings[p.vif_id].status = OP_STATUS_DOWN if not new_port: self.port_dead(p) @@ -474,7 +518,7 @@ class OVSQuantumTunnelAgent(object): lsw_id = lsw_id_bindings[new_net_uuid] self.port_bound(p, new_net_uuid, lsw_id) - all_bindings[p.vif_id].op_status = OP_STATUS_UP + all_bindings[p.vif_id].status = OP_STATUS_UP LOG.info("Port %s on net-id = %s bound to %s " % ( str(p), new_net_uuid, str(self.local_vlan_map[new_net_uuid]))) @@ -482,7 +526,7 @@ class OVSQuantumTunnelAgent(object): for vif_id in disappeared_vif_ports_ids: LOG.info("Port Disappeared: " + vif_id) if vif_id in all_bindings: - all_bindings[vif_id].op_status = OP_STATUS_DOWN + all_bindings[vif_id].status = OP_STATUS_DOWN old_port = old_local_bindings.get(vif_id) if old_port: self.port_unbound(old_vif_ports[vif_id], @@ -540,17 +584,21 @@ def main(): reconnect_interval = conf.DATABASE.reconnect_interval root_helper = conf.AGENT.root_helper + # Determine API Version to use + target_v2_api = conf.AGENT.target_v2_api + if enable_tunneling: # Get parameters for OVSQuantumTunnelAgent tun_br = conf.OVS.tunnel_bridge # Mandatory parameter. local_ip = conf.OVS.local_ip plugin = OVSQuantumTunnelAgent(integ_br, tun_br, local_ip, root_helper, - polling_interval, reconnect_interval) + polling_interval, reconnect_interval, + target_v2_api) else: # Get parameters for OVSQuantumAgent. - plugin = OVSQuantumAgent(integ_br, root_helper, - polling_interval, reconnect_interval) + plugin = OVSQuantumAgent(integ_br, root_helper, polling_interval, + reconnect_interval, target_v2_api) # Start everything. plugin.daemon_loop(db_connection_url) diff --git a/quantum/plugins/openvswitch/common/config.py b/quantum/plugins/openvswitch/common/config.py index 88a5798807..9774b82de4 100644 --- a/quantum/plugins/openvswitch/common/config.py +++ b/quantum/plugins/openvswitch/common/config.py @@ -32,6 +32,7 @@ ovs_opts = [ ] agent_opts = [ + cfg.BoolOpt('target_v2_api', default=True), cfg.IntOpt('polling_interval', default=2), cfg.StrOpt('root_helper', default='sudo'), cfg.StrOpt('log_file', default=None), diff --git a/quantum/plugins/openvswitch/ovs_db_v2.py b/quantum/plugins/openvswitch/ovs_db_v2.py new file mode 100644 index 0000000000..9cdaff6666 --- /dev/null +++ b/quantum/plugins/openvswitch/ovs_db_v2.py @@ -0,0 +1,51 @@ +# 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: Aaron Rosen, Nicira Networks, Inc. + +from sqlalchemy.orm import exc + +import quantum.db.api as db +from quantum.plugins.openvswitch import ovs_models_v2 + + +def get_vlans(): + session = db.get_session() + try: + bindings = (session.query(ovs_models_v2.VlanBinding). + all()) + except exc.NoResultFound: + return [] + return [(binding.vlan_id, binding.network_id) for binding in bindings] + + +def add_vlan_binding(vlan_id, net_id): + session = db.get_session() + binding = ovs_models_v2.VlanBinding(vlan_id, net_id) + session.add(binding) + session.flush() + return binding + + +def remove_vlan_binding(net_id): + session = db.get_session() + try: + binding = (session.query(ovs_models_v2.VlanBinding). + filter_by(network_id=net_id). + one()) + session.delete(binding) + except exc.NoResultFound: + pass + session.flush() diff --git a/quantum/plugins/openvswitch/ovs_models_v2.py b/quantum/plugins/openvswitch/ovs_models_v2.py new file mode 100644 index 0000000000..cbdabdd4e3 --- /dev/null +++ b/quantum/plugins/openvswitch/ovs_models_v2.py @@ -0,0 +1,49 @@ +# 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: Aaron Rosen, Nicira Networks, Inc. + + +from sqlalchemy import Column, Integer, String + +from quantum.db import models_v2 + + +class VlanBinding(models_v2.model_base.BASEV2): + """Represents a binding of network_id to 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) + + +class TunnelIP(models_v2.model_base.BASEV2): + """Represents a remote IP in tunnel mode.""" + __tablename__ = 'tunnel_ips' + + ip_address = Column(String(255), primary_key=True) + + def __init__(self, ip_address): + self.ip_address = ip_address + + def __repr__(self): + return "" % (self.ip_address) diff --git a/quantum/plugins/openvswitch/ovs_quantum_plugin.py b/quantum/plugins/openvswitch/ovs_quantum_plugin.py index 231584f747..31ce5c4fb8 100644 --- a/quantum/plugins/openvswitch/ovs_quantum_plugin.py +++ b/quantum/plugins/openvswitch/ovs_quantum_plugin.py @@ -17,6 +17,7 @@ # @author: Brad Hall, Nicira Networks, Inc. # @author: Dan Wendlandt, Nicira Networks, Inc. # @author: Dave Lapsley, Nicira Networks, Inc. +# @author: Aaron Rosen, Nicira Networks, Inc. import logging import os @@ -24,14 +25,16 @@ import os from quantum.api.api_common import OperationalStatus from quantum.common import exceptions as q_exc from quantum.common.utils import find_config_file -import quantum.db.api as db +from quantum.db import api as db +from quantum.db import db_base_plugin_v2 +from quantum.db import models_v2 from quantum.plugins.openvswitch.common import config from quantum.plugins.openvswitch import ovs_db +from quantum.plugins.openvswitch import ovs_db_v2 from quantum.quantum_plugin_base import QuantumPluginBase + LOG = logging.getLogger("ovs_quantum_plugin") - - CONF_FILE = find_config_file({"plugin": "openvswitch"}, "ovs_quantum_plugin.ini") @@ -47,6 +50,12 @@ class VlanMap(object): free_vlans = set() def __init__(self, vlan_min=1, vlan_max=4094): + if vlan_min > vlan_max: + LOG.warn("Using default VLAN values! vlan_min = %s is larger" + " than vlan_max = %s!" % (vlan_min, vlan_max)) + vlan_min = 1 + vlan_max = 4094 + self.vlan_min = vlan_min self.vlan_max = vlan_max self.vlans.clear() @@ -82,44 +91,26 @@ class VlanMap(object): else: LOG.error("No vlan found with network \"%s\"", network_id) + def populate_already_used(self, vlans): + for vlan_id, network_id in vlans: + LOG.debug("Adding already populated vlan %s -> %s" % + (vlan_id, network_id)) + self.already_used(vlan_id, network_id) + class OVSQuantumPlugin(QuantumPluginBase): def __init__(self, configfile=None): - if configfile is None: - if os.path.exists(CONF_FILE): - configfile = CONF_FILE - else: - configfile = find_config(os.path.abspath( - os.path.dirname(__file__))) - if configfile is None: - raise Exception("Configuration file \"%s\" doesn't exist" % - (configfile)) - LOG.debug("Using configuration file: %s" % configfile) - conf = config.parse(configfile) + conf = config.parse(CONF_FILE) options = {"sql_connection": conf.DATABASE.sql_connection} reconnect_interval = conf.DATABASE.reconnect_interval options.update({"reconnect_interval": reconnect_interval}) db.configure_db(options) - vlan_min = conf.OVS.vlan_min - vlan_max = conf.OVS.vlan_max - - if vlan_min > vlan_max: - LOG.warn("Using default VLAN values! vlan_min = %s is larger" - " than vlan_max = %s!" % (vlan_min, vlan_max)) - vlan_min = 1 - vlan_max = 4094 - - self.vmap = VlanMap(vlan_min, vlan_max) + self.vmap = VlanMap(conf.OVS.vlan_min, conf.OVS.vlan_max) # 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.already_used(vlan_id, network_id) + self.vmap.populate_already_used(ovs_db.get_vlans()) def get_all_networks(self, tenant_id, **kwargs): nets = [] @@ -142,8 +133,13 @@ class OVSQuantumPlugin(QuantumPluginBase): def create_network(self, tenant_id, net_name, **kwargs): net = db.network_create(tenant_id, net_name, op_status=OperationalStatus.UP) + try: + vlan_id = self.vmap.acquire(str(net.uuid)) + except NoFreeVLANException: + db.network_destroy(net.uuid) + raise + LOG.debug("Created network: %s" % net) - vlan_id = self.vmap.acquire(str(net.uuid)) ovs_db.add_vlan_binding(vlan_id, str(net.uuid)) return self._make_net_dict(str(net.uuid), net.name, [], net.op_status) @@ -233,3 +229,33 @@ class OVSQuantumPlugin(QuantumPluginBase): db.validate_port_ownership(tenant_id, net_id, port_id) res = db.port_get(port_id, net_id) return res.interface_id + + +class OVSQuantumPluginV2(db_base_plugin_v2.QuantumDbPluginV2): + def __init__(self, configfile=None): + conf = config.parse(CONF_FILE) + options = {"sql_connection": conf.DATABASE.sql_connection} + options.update({'base': models_v2.model_base.BASEV2}) + reconnect_interval = conf.DATABASE.reconnect_interval + options.update({"reconnect_interval": reconnect_interval}) + db.configure_db(options) + + self.vmap = VlanMap(conf.OVS.vlan_min, conf.OVS.vlan_max) + self.vmap.populate_already_used(ovs_db_v2.get_vlans()) + + def create_network(self, context, network): + net = super(OVSQuantumPluginV2, self).create_network(context, network) + try: + vlan_id = self.vmap.acquire(str(net['id'])) + except NoFreeVLANException: + super(OVSQuantumPluginV2, self).delete_network(context, net['id']) + raise + + LOG.debug("Created network: %s" % net['id']) + ovs_db_v2.add_vlan_binding(vlan_id, str(net['id'])) + return net + + def delete_network(self, context, id): + ovs_db_v2.remove_vlan_binding(id) + self.vmap.release(id) + return super(OVSQuantumPluginV2, self).delete_network(context, id)