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
This commit is contained in:
Aaron Rosen 2012-06-28 18:17:16 -07:00
parent 6d9ddf1c37
commit 804c936667
9 changed files with 250 additions and 66 deletions

View File

@ -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]

View File

@ -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())

View File

@ -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):

View File

@ -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')

View File

@ -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,6 +171,9 @@ class OVSQuantumAgent(object):
continue
for port in ports:
if self.target_v2_api:
all_bindings[port.id] = port
else:
all_bindings[port.interface_id] = port
vlan_bindings = {}
@ -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,6 +444,10 @@ class OVSQuantumTunnelAgent(object):
while True:
try:
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)
@ -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)

View File

@ -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),

View File

@ -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()

View File

@ -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 "<VlanBinding(%s,%s)>" % (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 "<TunnelIP(%s)>" % (self.ip_address)

View File

@ -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)
LOG.debug("Created network: %s" % net)
try:
vlan_id = self.vmap.acquire(str(net.uuid))
except NoFreeVLANException:
db.network_destroy(net.uuid)
raise
LOG.debug("Created network: %s" % net)
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)