Initial implemention of MetaPlugin

This plugin supports multiple plugins at same time. This plugin is for L3 connectivility
between networks which are realized by different plugins. This plugin add new attribute 'flavor:id'.
flavor:id correspond to specific plugin. flavor-plugin mapping could be configureable by plugin_list config.
This plugin also support extensions. We can map extension to plugin by using extension_map config.

Implements blueprint metaplugin

Change-Id: Ia94d2349fb3ce9f121bbd2505324ae6f0c34247a
This commit is contained in:
Nachi Ueno 2012-07-19 07:00:05 +00:00
parent 319b05ff5f
commit 7811e2a7c3
21 changed files with 1610 additions and 2 deletions

View File

@ -0,0 +1,26 @@
[DATABASE]
# This line MUST be changed to actually run the plugin.
# Example:
# sql_connection = mysql://root:nova@127.0.0.1:3306/ovs_quantum
# Replace 127.0.0.1 above with the IP address of the database used by the
# main quantum server. (Leave it as is if the database runs on this host.)
sql_connection = mysql://root:password@localhost/quantum_metaplugin?charset=utf8
# Database reconnection retry times - in event connectivity is lost
# set to -1 implgies an infinite retry count
# sql_max_retries = 10
# Database reconnection interval in seconds - in event connectivity is lost
reconnect_interval = 2
[META]
## This is list of flavor:quantum_plugins
# extension method is used in the order of this list
plugin_list= 'openvswitch:quantum.plugins.openvswitch.ovs_quantum_plugin.OVSQuantumPluginV2,linuxbridge:quantum.plugins.linuxbridge.lb_quantum_plugin.LinuxBridgePluginV2'
# Default value of flavor
default_flavor = 'openvswitch'
# supported extentions
supported_extension_aliases = 'providernet'
# specific method map for each flavor to extensions
extension_map = 'get_port_stats:nvp'

View File

@ -23,8 +23,8 @@ import netaddr
from quantum.agent.linux import ip_lib
from quantum.agent.linux import ovs_lib
from quantum.agent.linux import utils
from quantum.common import exceptions
from quantum.openstack.common import cfg
from quantum.openstack.common import importutils
LOG = logging.getLogger(__name__)
@ -36,7 +36,9 @@ OPTS = [
help='MTU setting for device.'),
cfg.StrOpt('ryu_api_host',
default='127.0.0.1:8080',
help='Openflow Ryu REST API host:port')
help='Openflow Ryu REST API host:port'),
cfg.StrOpt('meta_flavor_driver_mappings',
help='Mapping between flavor and LinuxInterfaceDriver')
]
@ -213,3 +215,62 @@ class RyuInterfaceDriver(OVSInterfaceDriver):
datapath_id = ovs_br.get_datapath_id()
port_no = ovs_br.get_port_ofport(device_name)
self.ryu_client.create_port(network_id, datapath_id, port_no)
class MetaInterfaceDriver(LinuxInterfaceDriver):
def __init__(self, conf):
super(MetaInterfaceDriver, self).__init__(conf)
from quantumclient.v2_0 import client
self.quantum = client.Client(
username=self.conf.admin_user,
password=self.conf.admin_password,
tenant_name=self.conf.admin_tenant_name,
auth_url=self.conf.auth_url,
auth_strategy=self.conf.auth_strategy,
auth_region=self.conf.auth_region
)
self.flavor_driver_map = {}
for flavor, driver_name in [
driver_set.split(':')
for driver_set in
self.conf.meta_flavor_driver_mappings.split(',')]:
self.flavor_driver_map[flavor] =\
self._load_driver(driver_name)
def _get_driver_by_network_id(self, network_id):
network = self.quantum.show_network(network_id)
flavor = network['network']['flavor:id']
return self.flavor_driver_map[flavor]
def _get_driver_by_device_name(self, device_name):
device = ip_lib.IPDevice(device_name, self.conf.root_helper)
mac_address = device.link.address
ports = self.quantum.list_ports(mac_address=mac_address)
if not 'ports' in ports or len(ports['ports']) < 1:
raise Exception('No port for this device %s' % device_name)
return self._get_driver_by_network_id(ports['ports'][0]['network_id'])
def get_device_name(self, port):
driver = self._get_driver_by_network_id(port.network_id)
return driver.get_device_name(port)
def plug(self, network_id, port_id, device_name, mac_address):
driver = self._get_driver_by_network_id(network_id)
return driver.plug(network_id, port_id, device_name, mac_address)
def unplug(self, device_name):
driver = self._get_driver_by_device_name(device_name)
return driver.unplug(device_name)
def _load_driver(self, driver_provider):
LOG.debug("Driver location:%s", driver_provider)
# If the plugin can't be found let them know gracefully
try:
LOG.info("Loading Driver: %s" % driver_provider)
plugin_klass = importutils.import_class(driver_provider)
except ClassNotFound:
LOG.exception("Error loading driver")
raise Exception("driver_provider not found. You can install a "
"Driver with: pip install <plugin-name>\n"
"Example: pip install quantum-sample-driver")
return plugin_klass(self.conf)

View File

@ -0,0 +1,59 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
#
# Copyright 2012 Nachi Ueno, NTT MCL, 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.
#
import logging
from quantum.api.v2 import attributes
LOG = logging.getLogger(__name__)
FLAVOR_ATTRIBUTE = {
'networks': {
'flavor:id': {'allow_post': True,
'allow_put': False,
'is_visible': True,
'default': attributes.ATTR_NOT_SPECIFIED}
}
}
class Flavor(object):
@classmethod
def get_name(cls):
return "Flavor for each network"
@classmethod
def get_alias(cls):
return "flavor"
@classmethod
def get_description(cls):
return "Flavor"
@classmethod
def get_namespace(cls):
return "http://docs.openstack.org/ext/flavor/api/v1.0"
@classmethod
def get_updated(cls):
return "2012-07-20T10:00:00-00:00"
def get_extended_attributes(self, version):
if version == "2.0":
return FLAVOR_ATTRIBUTE
else:
return {}

View File

@ -0,0 +1,81 @@
# -- Background
This plugin support multiple plugin at same time. This plugin is for L3 connectivility
between networks which are realized by different plugins.This plugin add new attribute 'flavor:id'.
flavor:id correspond to specific plugin ( flavor-plugin mapping could be configureable by plugin_list config.
This plugin also support extensions. We can map extension to plugin by using extension_map config.
[DATABASE]
# This line MUST be changed to actually run the plugin.
# Example:
# sql_connection = mysql://root:nova@127.0.0.1:3306/ovs_quantum
# Replace 127.0.0.1 above with the IP address of the database used by the
# main quantum server. (Leave it as is if the database runs on this host.)
sql_connection = mysql://root:password@localhost/quantum_metaplugin?charset=utf8
# Database reconnection retry times - in event connectivity is lost
# set to -1 implgies an infinite retry count
# sql_max_retries = 10
# Database reconnection interval in seconds - in event connectivity is lost
reconnect_interval = 2
[META]
## This is list of flavor:quantum_plugins
# extension method is used in the order of this list
plugin_list= 'openvswitch:quantum.plugins.openvswitch.ovs_quantum_plugin.OVSQuantumPluginV2,linuxbridge:quantum.plugins.linuxbridge.lb_quantum_plugin.LinuxBridgePluginV2'
# Default value of flavor
default_flavor = 'openvswitch'
# supported extentions
supported_extension_aliases = 'providernet'
# specific method map for each flavor to extensions
extension_map = 'get_port_stats:nvp'
# -- BridgeDriver Configration
# In order to use metaplugin, you should use MetaDriver. Following configation is needed.
[DEFAULT]
# Meta Plugin
# Mapping between flavor and driver
meta_flavor_driver_mappings = openvswitch:quantum.agent.linux.interface.OVSInterfaceDriver, linuxbridge:quantum.agent.linux.interface.BridgeInterfaceDriver
# interface driver for MetaPlugin
interface_driver = quantum.agent.linux.interface.MetaInterfaceDriver
# -- Agent
Agents for Metaplugin are in quantum/plugins/metaplugin/agent
linuxbridge_quantum_agent and ovs_quantum_agent is available.
# -- Extensions
- flavor
MetaPlugin supports flavor and provider net extension.
Metaplugin select plugin_list using flavor.
One plugin may use multiple flavor value. If the plugin support flavor, it may provide
multiple flavor of network.
- Attribute extension
Each plugin can use attribute extension such as provider_net, if you specify that in supported_extension_aliases.
- providernet
Vlan ID range of each plugin should be different, since Metaplugin dose not manage that.
#- limitations
Basically, All plugin should inherit QuantumDBPluginV2.
Metaplugin assumes all plugin share same Database expecially for IPAM part in QuantumV2 API.
You can use another plugin if you use ProxyPluginV2, which proxies request to the another quantum server.
Example flavor configration for ProxyPluginV2
meta_flavor_driver_mappings = "openvswitch:quantum.agent.linux.interface.OVSInterfaceDriver,proxy:quantum.plugins.metaplugin.proxy_quantum_plugin.ProxyPluginV2"
[Proxy]
auth_url = http://10.0.0.1:35357/v2.0
auth_region = RegionOne
admin_tenant_name = service
admin_user = quantum
admin_password = password

View File

@ -0,0 +1,16 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
#
# Copyright 2012, Nachi Ueno, NTT MCL, 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.

View File

@ -0,0 +1,176 @@
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
#
# Copyright 2012 Cisco Systems, Inc.
# Copyright 2012 NTT MCL, 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.
#
#
# Performs per host Linux Bridge configuration for Quantum.
# Based on the structure of the OpenVSwitch agent in the
# Quantum OpenVSwitch Plugin.
# @author: Sumit Naiksatam, Cisco Systems, Inc.
import logging
import sys
import time
from sqlalchemy.ext.sqlsoup import SqlSoup
from quantum.openstack.common import cfg
from quantum.common import config as logging_config
from quantum.plugins.linuxbridge.common import config
import quantum.plugins.linuxbridge.agent.linuxbridge_quantum_agent as lb
from quantum.agent.linux import utils
logging.basicConfig()
LOG = logging.getLogger(__name__)
BRIDGE_NAME_PREFIX = "brq"
VLAN_BINDINGS = "vlan_bindings"
PORT_BINDINGS = "port_bindings"
OP_STATUS_UP = "UP"
OP_STATUS_DOWN = "DOWN"
# Default inteval values
DEFAULT_POLLING_INTERVAL = 2
DEFAULT_RECONNECT_INTERVAL = 2
class MetaLinuxBridgeQuantumAgent(lb.LinuxBridgeQuantumAgent):
def manage_networks_on_host(self, db,
old_vlan_bindings,
old_port_bindings):
vlan_bindings = {}
try:
flavor_key = db.flavors.network_id
vlan_key = db.vlan_bindings.network_id
query = db.session.query(db.vlan_bindings)
joined = query.join((db.flavors,
flavor_key == vlan_key))
where = db.flavors.flavor == 'linuxbridge'
vlan_binds = joined.filter(where).all()
except Exception as e:
LOG.info("Unable to get vlan bindings! Exception: %s" % e)
self.db_connected = False
return {VLAN_BINDINGS: {},
PORT_BINDINGS: []}
vlans_string = ""
for bind in vlan_binds:
entry = {'network_id': bind.network_id, 'vlan_id': bind.vlan_id}
vlan_bindings[bind.network_id] = entry
vlans_string = "%s %s" % (vlans_string, entry)
port_bindings = []
try:
flavor_key = db.flavors.network_id
port_key = db.ports.network_id
query = db.session.query(db.ports)
joined = query.join((db.flavors,
flavor_key == port_key))
where = db.flavors.flavor == 'linuxbridge'
port_binds = joined.filter(where).all()
except Exception as e:
LOG.info("Unable to get port bindings! Exception: %s" % e)
self.db_connected = False
return {VLAN_BINDINGS: {},
PORT_BINDINGS: []}
all_bindings = {}
for bind in port_binds:
append_entry = False
if self.target_v2_api:
all_bindings[bind.id] = bind
entry = {'network_id': bind.network_id,
'uuid': bind.id,
'status': bind.status,
'interface_id': bind.id}
append_entry = bind.admin_state_up
else:
all_bindings[bind.uuid] = bind
entry = {'network_id': bind.network_id, 'state': bind.state,
'op_status': bind.op_status, 'uuid': bind.uuid,
'interface_id': bind.interface_id}
append_entry = bind.state == 'ACTIVE'
if append_entry:
port_bindings.append(entry)
plugged_interfaces = []
ports_string = ""
for pb in port_bindings:
ports_string = "%s %s" % (ports_string, pb)
port_id = pb['uuid']
interface_id = pb['interface_id']
vlan_id = str(vlan_bindings[pb['network_id']]['vlan_id'])
if self.process_port_binding(port_id,
pb['network_id'],
interface_id,
vlan_id):
if self.target_v2_api:
all_bindings[port_id].status = OP_STATUS_UP
else:
all_bindings[port_id].op_status = OP_STATUS_UP
plugged_interfaces.append(interface_id)
if old_port_bindings != port_bindings:
LOG.debug("Port-bindings: %s" % ports_string)
self.process_unplugged_interfaces(plugged_interfaces)
if old_vlan_bindings != vlan_bindings:
LOG.debug("VLAN-bindings: %s" % vlans_string)
self.process_deleted_networks(vlan_bindings)
try:
db.commit()
except Exception as e:
LOG.info("Unable to update database! Exception: %s" % e)
db.rollback()
vlan_bindings = {}
port_bindings = []
return {VLAN_BINDINGS: vlan_bindings,
PORT_BINDINGS: port_bindings}
def main():
cfg.CONF(args=sys.argv, project='quantum')
# (TODO) - swap with common logging
logging_config.setup_logging(cfg.CONF)
br_name_prefix = BRIDGE_NAME_PREFIX
physical_interface = cfg.CONF.LINUX_BRIDGE.physical_interface
polling_interval = cfg.CONF.AGENT.polling_interval
reconnect_interval = cfg.CONF.DATABASE.reconnect_interval
root_helper = cfg.CONF.AGENT.root_helper
'Establish database connection and load models'
db_connection_url = cfg.CONF.DATABASE.sql_connection
plugin = MetaLinuxBridgeQuantumAgent(br_name_prefix, physical_interface,
polling_interval, reconnect_interval,
root_helper,
cfg.CONF.AGENT.target_v2_api)
LOG.info("Agent initialized successfully, now running... ")
plugin.daemon_loop(db_connection_url)
sys.exit(0)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,194 @@
#!/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.
# @author: Dave Lapsley, Nicira Networks, Inc.
# @author: Aaron Rosen, Nicira Networks, Inc.
import logging
import sys
import time
from sqlalchemy.ext import sqlsoup
from quantum.agent.linux import ovs_lib
from quantum.common import config as logging_config
from quantum.openstack.common import cfg
from quantum.plugins.openvswitch.common import config
from quantum.plugins.openvswitch.agent.ovs_quantum_agent import OVSQuantumAgent
logging.basicConfig()
LOG = logging.getLogger(__name__)
# Global constants.
OP_STATUS_UP = "UP"
OP_STATUS_DOWN = "DOWN"
# A placeholder for dead vlans.
DEAD_VLAN_TAG = "4095"
# Default interval values
DEFAULT_POLLING_INTERVAL = 2
DEFAULT_RECONNECT_INTERVAL = 2
class MetaOVSQuantumAgent(OVSQuantumAgent):
def daemon_loop(self, db_connection_url):
'''Main processing loop for Non-Tunneling Agent.
:param options: database information - in the event need to reconnect
'''
self.local_vlan_map = {}
old_local_bindings = {}
old_vif_ports = {}
db_connected = False
while True:
if not db_connected:
time.sleep(self.reconnect_interval)
db = sqlsoup.SqlSoup(db_connection_url)
db_connected = True
LOG.info("Connecting to database \"%s\" on %s" %
(db.engine.url.database, db.engine.url.host))
all_bindings = {}
try:
flavor_key = db.flavors.network_id
port_key = db.ports.network_id
query = db.session.query(db.ports)
joined = query.join((db.flavors,
flavor_key == port_key))
where = db.flavors.flavor == 'openvswitch'
ports = joined.filter(where).all()
except Exception, e:
LOG.info("Unable to get port bindings! Exception: %s" % e)
db_connected = False
continue
for port in ports:
if self.target_v2_api:
all_bindings[port.id] = port
else:
all_bindings[port.interface_id] = port
vlan_bindings = {}
try:
flavor_key = db.flavors.network_id
vlan_key = db.vlan_bindings.network_id
query = db.session.query(db.vlan_bindings)
joined = query.join((db.flavors,
flavor_key == vlan_key))
where = db.flavors.flavor == 'openvswitch'
vlan_binds = joined.filter(where).all()
except Exception, e:
LOG.info("Unable to get vlan bindings! Exception: %s" % e)
db_connected = False
continue
for bind in vlan_binds:
vlan_bindings[bind.network_id] = bind.vlan_id
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:
net_id = all_bindings[p.vif_id].network_id
new_local_bindings[p.vif_id] = net_id
else:
# no binding, put him on the 'dead vlan'
self.int_br.set_db_attribute("Port", p.port_name, "tag",
DEAD_VLAN_TAG)
self.int_br.add_flow(priority=2,
in_port=p.ofport,
actions="drop")
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 p.vif_id in all_bindings:
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
net_id = all_bindings[p.vif_id].network_id
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].status = OP_STATUS_UP
LOG.info(("Adding binding to net-id = %s "
"for %s on vlan %s") %
(new_b, str(p), vlan_id))
for vif_id in old_vif_ports:
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)
if vif_id in all_bindings:
all_bindings[vif_id].status = OP_STATUS_DOWN
old_vif_ports = new_vif_ports
old_local_bindings = new_local_bindings
try:
db.commit()
except Exception, e:
LOG.info("Unable to commit to database! Exception: %s" % e)
db.rollback()
old_local_bindings = {}
old_vif_ports = {}
time.sleep(self.polling_interval)
def main():
cfg.CONF(args=sys.argv, project='quantum')
# (TODO) gary - swap with common logging
logging_config.setup_logging(cfg.CONF)
# Determine which agent type to use.
enable_tunneling = cfg.CONF.OVS.enable_tunneling
integ_br = cfg.CONF.OVS.integration_bridge
db_connection_url = cfg.CONF.DATABASE.sql_connection
polling_interval = cfg.CONF.AGENT.polling_interval
reconnect_interval = cfg.CONF.DATABASE.reconnect_interval
root_helper = cfg.CONF.AGENT.root_helper
# Determine API Version to use
target_v2_api = cfg.CONF.AGENT.target_v2_api
# Get parameters for OVSQuantumAgent.
plugin = MetaOVSQuantumAgent(integ_br, root_helper, polling_interval,
reconnect_interval, target_v2_api)
# Start everything.
plugin.daemon_loop(db_connection_url)
sys.exit(0)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,16 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
#
# Copyright 2012, Nachi Ueno, NTT MCL, 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.

View File

@ -0,0 +1,45 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
#
# Copyright 2012, Nachi Ueno, NTT MCL, 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.
from quantum.openstack.common import cfg
database_opts = [
cfg.StrOpt('sql_connection', default='sqlite://'),
cfg.IntOpt('sql_max_retries', default=-1),
cfg.IntOpt('reconnect_interval', default=2),
]
meta_plugin_opts = [
cfg.StrOpt('plugin_list', default=''),
cfg.StrOpt('default_flavor', default=''),
cfg.StrOpt('supported_extension_aliases', default=''),
cfg.StrOpt('extension_map', default='')
]
proxy_plugin_opts = [
cfg.StrOpt('admin_user'),
cfg.StrOpt('admin_password'),
cfg.StrOpt('admin_tenant_name'),
cfg.StrOpt('auth_url'),
cfg.StrOpt('auth_strategy', default='keystone'),
cfg.StrOpt('auth_region'),
]
cfg.CONF.register_opts(database_opts, "DATABASE")
cfg.CONF.register_opts(meta_plugin_opts, "META")
cfg.CONF.register_opts(proxy_plugin_opts, "PROXY")

View File

@ -0,0 +1,40 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
#
# Copyright 2012, Nachi Ueno, NTT MCL, 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.
from sqlalchemy.orm import exc
import quantum.db.api as db
from quantum.plugins.metaplugin import meta_models_v2
def get_flavor_by_network(net_id):
session = db.get_session()
try:
binding = (session.query(meta_models_v2.Flavor).
filter_by(network_id=net_id).
one())
except exc.NoResultFound:
return None
return binding.flavor
def add_flavor_binding(flavor, net_id):
session = db.get_session()
binding = meta_models_v2.Flavor(flavor=flavor, network_id=net_id)
session.add(binding)
session.flush()
return binding

View File

@ -0,0 +1,32 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
#
# Copyright 2012, Nachi Ueno, NTT MCL, 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.
import sqlalchemy as sa
from sqlalchemy import Column, String
from quantum.db import models_v2
class Flavor(models_v2.model_base.BASEV2):
"""Represents a binding of network_id to flavor."""
flavor = Column(String(255))
network_id = sa.Column(sa.String(36), sa.ForeignKey('networks.id',
ondelete="CASCADE"),
primary_key=True)
def __repr__(self):
return "<Flavor(%s,%s)>" % (self.flavor, self.network_id)

View File

@ -0,0 +1,216 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
#
# Copyright 2012, Nachi Ueno, NTT MCL, 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.
import logging
from quantum.common import exceptions as exc
from quantum.api.v2 import attributes
from quantum.common.utils import find_config_file
from quantum.db import api as db
from quantum.db import db_base_plugin_v2
from quantum.db import models_v2
from quantum.openstack.common import cfg
from quantum.openstack.common import importutils
from quantum.plugins.metaplugin.common import config
from quantum.plugins.metaplugin import meta_db_v2
from quantum.plugins.metaplugin.meta_models_v2 import Flavor
from quantum import policy
LOG = logging.getLogger("metaplugin")
class MetaPluginV2(db_base_plugin_v2.QuantumDbPluginV2):
def __init__(self, configfile=None):
LOG.debug("Start initializing metaplugin")
options = {"sql_connection": cfg.CONF.DATABASE.sql_connection}
options.update({'base': models_v2.model_base.BASEV2})
sql_max_retries = cfg.CONF.DATABASE.sql_max_retries
options.update({"sql_max_retries": sql_max_retries})
reconnect_interval = cfg.CONF.DATABASE.reconnect_interval
options.update({"reconnect_interval": reconnect_interval})
self.supported_extension_aliases = \
cfg.CONF.META.supported_extension_aliases.split(',')
self.supported_extension_aliases.append('flavor')
# Ignore config option overapping
def _is_opt_registered(opts, opt):
if opt.dest in opts:
return True
else:
return False
cfg._is_opt_registered = _is_opt_registered
# Keep existing tables if multiple plugin use same table name.
db.model_base.QuantumBase.__table_args__ = {'keep_existing': True}
self.plugins = {}
plugin_list = [plugin_set.split(':')
for plugin_set
in cfg.CONF.META.plugin_list.split(',')]
for flavor, plugin_provider in plugin_list:
self.plugins[flavor] = self._load_plugin(plugin_provider)
self.extension_map = {}
if not cfg.CONF.META.extension_map == '':
extension_list = [method_set.split(':')
for method_set
in cfg.CONF.META.extension_map.split(',')]
for method_name, flavor in extension_list:
self.extension_map[method_name] = flavor
self.default_flavor = cfg.CONF.META.default_flavor
if not self.default_flavor in self.plugins:
raise exc.Invalid('default_flavor %s is not plugin list' %
self.default_flavor)
def _load_plugin(self, plugin_provider):
LOG.debug("Plugin location:%s", plugin_provider)
# If the plugin can't be found let them know gracefully
try:
LOG.info("Loading Plugin: %s" % plugin_provider)
plugin_klass = importutils.import_class(plugin_provider)
except exc.ClassNotFound:
LOG.exception("Error loading plugin")
raise Exception("Plugin not found. You can install a "
"plugin with: pip install <plugin-name>\n"
"Example: pip install quantum-sample-plugin")
return plugin_klass()
def _get_plugin(self, flavor):
if not flavor in self.plugins:
raise Exception("Plugin for flavor %s not found." % flavor)
return self.plugins[flavor]
def __getattr__(self, key):
# At first, try to pickup extension command from extension_map
if key in self.extension_map:
flavor = self.extension_map[key]
plugin = self._get_plugin(flavor)
if plugin and hasattr(plugin, key):
return getattr(plugin, key)
# Second, try to match extension method in order of pluign list
for flavor, plugin in self.plugins.items():
if hasattr(plugin, key):
return getattr(plugin, key)
# if no plugin support the method, then raise
raise AttributeError
def _extend_network_dict(self, context, network):
network['flavor:id'] = self._get_flavor_by_network_id(network['id'])
def create_network(self, context, network):
n = network['network']
flavor = n.get('flavor:id')
if not str(flavor) in self.plugins:
flavor = self.default_flavor
plugin = self._get_plugin(flavor)
net = plugin.create_network(context, network)
LOG.debug("Created network: %s with flavor %s " % (net['id'], flavor))
try:
meta_db_v2.add_flavor_binding(flavor, str(net['id']))
except Exception as e:
LOG.error('failed to add flavor bindings')
plugin.delete_network(context, net['id'])
raise Exception('Failed to create network')
LOG.debug("Created network: %s" % net['id'])
self._extend_network_dict(context, net)
return net
def delete_network(self, context, id):
flavor = meta_db_v2.get_flavor_by_network(id)
plugin = self._get_plugin(flavor)
return plugin.delete_network(context, id)
def get_network(self, context, id, fields=None, verbose=None):
flavor = meta_db_v2.get_flavor_by_network(id)
plugin = self._get_plugin(flavor)
net = plugin.get_network(context, id, fields, verbose)
if not fields or 'flavor:id' in fields:
self._extend_network_dict(context, net)
return net
def get_networks_with_flavor(self, context, filters=None,
fields=None, verbose=None):
collection = self._model_query(context, models_v2.Network)
collection = collection.join(Flavor,
models_v2.Network.id == Flavor.network_id)
if filters:
for key, value in filters.iteritems():
if key == 'flavor:id':
column = Flavor.flavor
else:
column = getattr(models_v2.Network, key, None)
if column:
collection = collection.filter(column.in_(value))
return [self._make_network_dict(c, fields) for c in collection.all()]
def get_networks(self, context, filters=None, fields=None, verbose=None):
nets = self.get_networks_with_flavor(context, filters,
None, verbose)
return [self.get_network(context, net['id'],
fields, verbose)
for net in nets]
def _get_flavor_by_network_id(self, network_id):
return meta_db_v2.get_flavor_by_network(network_id)
def _get_plugin_by_network_id(self, network_id):
flavor = self._get_flavor_by_network_id(network_id)
return self._get_plugin(flavor)
def create_port(self, context, port):
p = port['port']
if not 'network_id' in p:
raise exc.NotFound
plugin = self._get_plugin_by_network_id(p['network_id'])
return plugin.create_port(context, port)
def update_port(self, context, id, port):
port_in_db = self.get_port(context, id)
plugin = self._get_plugin_by_network_id(port_in_db['network_id'])
return plugin.update_port(context, id, port)
def delete_port(self, context, id):
port_in_db = self.get_port(context, id)
plugin = self._get_plugin_by_network_id(port_in_db['network_id'])
return plugin.delete_port(context, id)
def create_subnet(self, context, subnet):
s = subnet['subnet']
if not 'network_id' in s:
raise exc.NotFound
plugin = self._get_plugin_by_network_id(s['network_id'])
return plugin.create_subnet(context, subnet)
def update_subnet(self, context, id, subnet):
s = self.get_subnet(context, id)
plugin = self._get_plugin_by_network_id(s['network_id'])
return plugin.update_subnet(context, id, subnet)
def delete_subnet(self, context, id):
s = self.get_subnet(context, id)
plugin = self._get_plugin_by_network_id(s['network_id'])
return plugin.delete_subnet(context, id)

View File

@ -0,0 +1,132 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
#
# Copyright 2012, Nachi Ueno, NTT MCL, 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.
from quantum.db import api as db
from quantum.db import db_base_plugin_v2
from quantum.db import models_v2
from quantum.openstack.common import cfg
from quantumclient.common import exceptions
from quantumclient.v2_0 import client
class ProxyPluginV2(db_base_plugin_v2.QuantumDbPluginV2):
def __init__(self, configfile=None):
options = {"sql_connection": cfg.CONF.DATABASE.sql_connection}
options.update({'base': models_v2.model_base.BASEV2})
sql_max_retries = cfg.CONF.DATABASE.sql_max_retries
options.update({"sql_max_retries": sql_max_retries})
reconnect_interval = cfg.CONF.DATABASE.reconnect_interval
options.update({"reconnect_interval": reconnect_interval})
db.configure_db(options)
self.quantum = client.Client(
username=cfg.CONF.PROXY.admin_user,
password=cfg.CONF.PROXY.admin_password,
tenant_name=cfg.CONF.PROXY.admin_tenant_name,
auth_url=cfg.CONF.PROXY.auth_url,
auth_strategy=cfg.CONF.PROXY.auth_strategy,
auth_region=cfg.CONF.PROXY.auth_region
)
def _get_client(self):
return self.quantum
def create_subnet(self, context, subnet):
subnet_remote = self._get_client().create_subnet(subnet)
subnet['subnet']['id'] = subnet_remote['id']
tenant_id = self._get_tenant_id_for_create(context, subnet['subnet'])
subnet['subnet']['tenant_id'] = tenant_id
try:
subnet_in_db = super(ProxyPluginV2, self).create_subnet(
context, subnet)
except:
self._get_client().delete_subnet(subnet_remote['id'])
return subnet_in_db
def update_subnet(self, context, id, subnet):
subnet_in_db = super(ProxyPluginV2, self).update_subnet(
context, id, subnet)
try:
self._get_client().update_subnet(id, subnet)
except Exception as e:
LOG.error("update subnet failed: %e" % e)
return subnet_in_db
def delete_subnet(self, context, id):
try:
self._get_client().delete_subnet(id)
except exceptions.NotFound:
LOG.warn("subnet in remote have already deleted")
pass
return super(ProxyPluginV2, self).delete_subnet(context, id)
def create_network(self, context, network):
network_remote = self._get_client().create_network(network)
network['network']['id'] = network_remote['id']
tenant_id = self._get_tenant_id_for_create(context, network['network'])
network['network']['tenant_id'] = tenant_id
try:
network_in_db = super(ProxyPluginV2, self).create_network(
context, network)
except:
self._get_client().delete_network(network_remote['id'])
return network_in_db
def update_network(self, context, id, network):
network_in_db = super(ProxyPluginV2, self).update_network(
context, id, network)
try:
self._get_client().update_network(id, network)
except Exception as e:
LOG.error("update network failed: %e" % e)
return network_in_db
def delete_network(self, context, id):
try:
self._get_client().delete_network(id)
except exceptions.NetworkNotFound:
LOG.warn("network in remote have already deleted")
pass
return super(ProxyPluginV2, self).delete_network(context, id)
def create_port(self, context, port):
port_remote = self._get_client().create_port(port)
port['port']['id'] = port_remote['id']
tenant_id = self._get_tenant_id_for_create(context, port['port'])
port['port']['tenant_id'] = tenant_id
try:
port_in_db = super(ProxyPluginV2, self).create_port(
context, port)
except:
self._get_client().delete_port(port_remote['id'])
return port_in_db
def update_port(self, context, id, port):
port_in_db = super(ProxyPluginV2, self).update_port(
context, id, port)
try:
self._get_client().update_port(id, port)
except Exception as e:
LOG.error("update port failed: %e" % e)
return port_in_db
def delete_port(self, context, id):
try:
self._get_client().delete_port(id)
except exceptions.portNotFound:
LOG.warn("port in remote have already deleted")
pass
return super(ProxyPluginV2, self).delete_port(context, id)

View File

@ -0,0 +1,57 @@
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2010 OpenStack, LLC
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Unittest runner for quantum Meta plugin
This file should be run from the top dir in the quantum directory
To run all tests::
PLUGIN_DIR=quantum/plugins/metaplugin ./run_tests.sh
"""
import os
import sys
from nose import config
from nose import core
sys.path.append(os.getcwd())
sys.path.append(os.path.dirname(__file__))
from quantum.common.test_lib import run_tests, test_config
if __name__ == '__main__':
exit_status = False
# if a single test case was specified,
# we should only invoked the tests once
invoke_once = len(sys.argv) > 1
test_config['plugin_name'] = "meta_quantum_plugin.MetaPluginV2"
cwd = os.getcwd()
working_dir = os.path.abspath("quantum/plugins/metaplugin")
c = config.Config(stream=sys.stdout,
env=os.environ,
verbosity=3,
workingDir=working_dir)
exit_status = exit_status or run_tests(c)
sys.exit(exit_status)

View File

@ -0,0 +1,16 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
#
# Copyright 2012, Nachi Ueno, NTT MCL, 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.

View File

@ -0,0 +1,16 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
#
# Copyright 2012, Nachi Ueno, NTT MCL, 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.

View File

@ -0,0 +1,44 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
#
# Copyright 2012, Nachi Ueno, NTT MCL, 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.
import mox
import stubout
import unittest
import quantum.db.api as db
from quantum.db import models_v2
from quantum.plugins.metaplugin.tests.unit import utils
class BaseMetaTest(unittest.TestCase):
"""base test class for MetaPlugin unit tests"""
def setUp(self):
config = utils.get_config()
options = {"sql_connection": config.get("DATABASE", "sql_connection")}
options.update({'base': models_v2.model_base.BASEV2})
db.configure_db(options)
self.config = config
self.mox = mox.Mox()
self.stubs = stubout.StubOutForTesting()
def tearDown(self):
self.mox.UnsetStubs()
self.stubs.UnsetAll()
self.stubs.SmartUnsetAll()
self.mox.VerifyAll()
db.clear_db()

View File

@ -0,0 +1,57 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
#
# Copyright 2012, Nachi Ueno, NTT MCL, Inc.
#
# 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.
from quantum.common import exceptions as q_exc
from quantum.common.utils import find_config_file
from quantum.db import api as db
from quantum.db import db_base_plugin_v2
from quantum.db import models_v2
class Fake1(db_base_plugin_v2.QuantumDbPluginV2):
def fake_func(self):
return 'fake1'
def create_network(self, context, network):
net = super(Fake1, self).create_network(context, network)
return net
def delete_network(self, context, id):
return super(Fake1, self).delete_network(context, id)
def create_port(self, context, port):
port['port']['device_id'] = self.fake_func()
port = super(Fake1, self).create_port(context, port)
return port
def create_subnet(self, context, subnet):
subnet = super(Fake1, self).create_subnet(context, subnet)
return subnet
def update_port(self, context, id, port):
port = super(Fake1, self).update_port(context, id, port)
return port
def delete_port(self, context, id):
return super(Fake1, self).delete_port(context, id)
class Fake2(Fake1):
def fake_func(self):
return 'fake2'
def fake_func2(self):
return 'fake2'

View File

@ -0,0 +1,268 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
#
# Copyright 2012, Nachi Ueno, NTT MCL, 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.
import os
import mox
import mock
import uuid
from quantum.common import config
from quantum.common.exceptions import NotImplementedError
from quantum.db import api as db
from quantum.openstack.common import cfg
from quantum.plugins.metaplugin.meta_quantum_plugin import MetaPluginV2
from quantum.plugins.metaplugin.proxy_quantum_plugin import ProxyPluginV2
from quantum.plugins.metaplugin.tests.unit.basetest import BaseMetaTest
from quantum.plugins.metaplugin.tests.unit import fake_plugin
from quantum import context
CONF_FILE = ""
ROOTDIR = os.path.dirname(os.path.dirname(__file__))
ETCDIR = os.path.join(ROOTDIR, 'etc')
META_PATH = "quantum.plugins.metaplugin"
FAKE_PATH = "%s.tests.unit" % META_PATH
PROXY_PATH = "%s.proxy_quantum_plugin.ProxyPluginV2" % META_PATH
PLUGIN_LIST = \
'fake1:%s.fake_plugin.Fake1,fake2:%s.fake_plugin.Fake2,proxy:%s' % \
(FAKE_PATH, FAKE_PATH, PROXY_PATH)
def etcdir(*p):
return os.path.join(ETCDIR, *p)
class PluginBaseTest(BaseMetaTest):
"""Class conisting of MetaQuantumPluginV2 unit tests"""
def setUp(self):
super(PluginBaseTest, self).setUp()
db._ENGINE = None
db._MAKER = None
self.fake_tenant_id = str(uuid.uuid4())
self.context = context.get_admin_context()
args = ['--config-file', etcdir('quantum.conf.test')]
#config.parse(args=args)
# Update the plugin
cfg.CONF.set_override('auth_url', 'http://localhost:35357/v2.0',
'PROXY')
cfg.CONF.set_override('auth_region', 'RegionOne', 'PROXY')
cfg.CONF.set_override('admin_user', 'quantum', 'PROXY')
cfg.CONF.set_override('admin_password', 'password', 'PROXY')
cfg.CONF.set_override('admin_tenant_name', 'service', 'PROXY')
cfg.CONF.set_override('plugin_list', PLUGIN_LIST, 'META')
cfg.CONF.set_override('default_flavor', 'fake2', 'META')
cfg.CONF.set_override('base_mac', "12:34:56:78:90:ab")
self.client_cls_p = mock.patch('quantumclient.v2_0.client.Client')
client_cls = self.client_cls_p.start()
self.client_inst = mock.Mock()
client_cls.return_value = self.client_inst
self.client_inst.create_network.return_value = \
{'id': 'fake_id'}
self.client_inst.create_port.return_value = \
{'id': 'fake_id'}
self.client_inst.create_subnet.return_value = \
{'id': 'fake_id'}
self.client_inst.update_network.return_value = \
{'id': 'fake_id'}
self.client_inst.update_port.return_value = \
{'id': 'fake_id'}
self.client_inst.update_subnet.return_value = \
{'id': 'fake_id'}
self.client_inst.delete_network.return_value = True
self.client_inst.delete_port.return_value = True
self.client_inst.delete_subnet.return_value = True
self.plugin = MetaPluginV2(configfile=None)
def _fake_network(self, flavor):
data = {'network': {'name': flavor,
'admin_state_up': True,
'tenant_id': self.fake_tenant_id,
'flavor:id': flavor}}
return data
def _fake_port(self, net_id):
return {'port': {'name': net_id,
'network_id': net_id,
'admin_state_up': True,
'device_id': 'bad_device_id',
'admin_state_up': True,
'fixed_ips': [],
'mac_address':
self.plugin._generate_mac(self.context, net_id),
'tenant_id': self.fake_tenant_id}}
def _fake_subnet(self, net_id):
allocation_pools = [{'start': '10.0.0.2',
'end': '10.0.0.254'}]
return {'subnet': {'name': net_id,
'network_id': net_id,
'gateway_ip': '10.0.0.1',
'cidr': '10.0.0.0/24',
'allocation_pools': allocation_pools,
'enable_dhcp': True,
'ip_version': 4}}
def test_create_delete_network(self):
network1 = self._fake_network('fake1')
ret1 = self.plugin.create_network(self.context, network1)
self.assertEqual('fake1', ret1['flavor:id'])
network2 = self._fake_network('fake2')
ret2 = self.plugin.create_network(self.context, network2)
self.assertEqual('fake2', ret2['flavor:id'])
network3 = self._fake_network('proxy')
ret3 = self.plugin.create_network(self.context, network3)
self.assertEqual('proxy', ret3['flavor:id'])
db_ret1 = self.plugin.get_network(self.context, ret1['id'])
self.assertEqual('fake1', db_ret1['name'])
db_ret2 = self.plugin.get_network(self.context, ret2['id'])
self.assertEqual('fake2', db_ret2['name'])
db_ret3 = self.plugin.get_network(self.context, ret3['id'])
self.assertEqual('proxy', db_ret3['name'])
db_ret4 = self.plugin.get_networks(self.context)
self.assertEqual(3, len(db_ret4))
db_ret5 = self.plugin.get_networks(self.context,
{'flavor:id': ['fake1']})
self.assertEqual(1, len(db_ret5))
self.assertEqual('fake1', db_ret5[0]['name'])
self.plugin.delete_network(self.context, ret1['id'])
self.plugin.delete_network(self.context, ret2['id'])
self.plugin.delete_network(self.context, ret3['id'])
def test_create_delete_port(self):
network1 = self._fake_network('fake1')
network_ret1 = self.plugin.create_network(self.context, network1)
network2 = self._fake_network('fake2')
network_ret2 = self.plugin.create_network(self.context, network2)
network3 = self._fake_network('proxy')
network_ret3 = self.plugin.create_network(self.context, network3)
port1 = self._fake_port(network_ret1['id'])
port2 = self._fake_port(network_ret2['id'])
port3 = self._fake_port(network_ret3['id'])
port1_ret = self.plugin.create_port(self.context, port1)
port2_ret = self.plugin.create_port(self.context, port2)
port3_ret = self.plugin.create_port(self.context, port3)
self.assertEqual('fake1', port1_ret['device_id'])
self.assertEqual('fake2', port2_ret['device_id'])
self.assertEqual('bad_device_id', port3_ret['device_id'])
port_in_db1 = self.plugin.get_port(self.context, port1_ret['id'])
port_in_db2 = self.plugin.get_port(self.context, port2_ret['id'])
port_in_db3 = self.plugin.get_port(self.context, port3_ret['id'])
self.assertEqual('fake1', port_in_db1['device_id'])
self.assertEqual('fake2', port_in_db2['device_id'])
self.assertEqual('bad_device_id', port_in_db3['device_id'])
port1['port']['admin_state_up'] = False
port2['port']['admin_state_up'] = False
port3['port']['admin_state_up'] = False
self.plugin.update_port(self.context, port1_ret['id'], port1)
self.plugin.update_port(self.context, port2_ret['id'], port2)
self.plugin.update_port(self.context, port3_ret['id'], port3)
port_in_db1 = self.plugin.get_port(self.context, port1_ret['id'])
port_in_db2 = self.plugin.get_port(self.context, port2_ret['id'])
port_in_db3 = self.plugin.get_port(self.context, port3_ret['id'])
self.assertEqual(False, port_in_db1['admin_state_up'])
self.assertEqual(False, port_in_db2['admin_state_up'])
self.assertEqual(False, port_in_db3['admin_state_up'])
self.plugin.delete_port(self.context, port1_ret['id'])
self.plugin.delete_port(self.context, port2_ret['id'])
self.plugin.delete_port(self.context, port3_ret['id'])
self.plugin.delete_network(self.context, network_ret1['id'])
self.plugin.delete_network(self.context, network_ret2['id'])
self.plugin.delete_network(self.context, network_ret3['id'])
def test_create_delete_subnet(self):
network1 = self._fake_network('fake1')
network_ret1 = self.plugin.create_network(self.context, network1)
network2 = self._fake_network('fake2')
network_ret2 = self.plugin.create_network(self.context, network2)
network3 = self._fake_network('proxy')
network_ret3 = self.plugin.create_network(self.context, network3)
subnet1 = self._fake_subnet(network_ret1['id'])
subnet2 = self._fake_subnet(network_ret2['id'])
subnet3 = self._fake_subnet(network_ret3['id'])
subnet1_ret = self.plugin.create_subnet(self.context, subnet1)
subnet2_ret = self.plugin.create_subnet(self.context, subnet2)
subnet3_ret = self.plugin.create_subnet(self.context, subnet3)
self.assertEqual(network_ret1['id'], subnet1_ret['network_id'])
self.assertEqual(network_ret2['id'], subnet2_ret['network_id'])
self.assertEqual(network_ret3['id'], subnet3_ret['network_id'])
subnet_in_db1 = self.plugin.get_subnet(self.context, subnet1_ret['id'])
subnet_in_db2 = self.plugin.get_subnet(self.context, subnet2_ret['id'])
subnet_in_db3 = self.plugin.get_subnet(self.context, subnet3_ret['id'])
subnet1['subnet']['ip_version'] = 6
subnet1['subnet']['allocation_pools'].pop()
subnet2['subnet']['ip_version'] = 6
subnet2['subnet']['allocation_pools'].pop()
subnet3['subnet']['ip_version'] = 6
subnet3['subnet']['allocation_pools'].pop()
self.plugin.update_subnet(self.context,
subnet1_ret['id'], subnet1)
self.plugin.update_subnet(self.context,
subnet2_ret['id'], subnet2)
self.plugin.update_subnet(self.context,
subnet3_ret['id'], subnet3)
subnet_in_db1 = self.plugin.get_subnet(self.context, subnet1_ret['id'])
subnet_in_db2 = self.plugin.get_subnet(self.context, subnet2_ret['id'])
subnet_in_db3 = self.plugin.get_subnet(self.context, subnet3_ret['id'])
self.assertEqual(6, subnet_in_db1['ip_version'])
self.assertEqual(6, subnet_in_db2['ip_version'])
self.assertEqual(6, subnet_in_db3['ip_version'])
self.plugin.delete_subnet(self.context, subnet1_ret['id'])
self.plugin.delete_subnet(self.context, subnet2_ret['id'])
self.plugin.delete_subnet(self.context, subnet3_ret['id'])
self.plugin.delete_network(self.context, network_ret1['id'])
self.plugin.delete_network(self.context, network_ret2['id'])
self.plugin.delete_network(self.context, network_ret3['id'])
def test_extension_method(self):
self.assertEqual('fake1', self.plugin.fake_func())
self.assertEqual('fake2', self.plugin.fake_func2())
def test_extension_not_implemented_method(self):
try:
self.plugin.not_implemented()
except AttributeError:
return
except:
self.fail("AttributeError Error is not raised")
self.fail("No Error is not raised")

View File

@ -24,6 +24,7 @@ from quantum.agent.linux import interface
from quantum.agent.linux import ip_lib
from quantum.agent.linux import utils
from quantum.openstack.common import cfg
from quantum.agent.dhcp_agent import DeviceManager
class BaseChild(interface.LinuxInterfaceDriver):
@ -332,3 +333,55 @@ class TestRyuInterfaceDriver(TestBase):
expected.extend([mock.call().device().link.set_up()])
self.ip.assert_has_calls(expected)
class TestMetaInterfaceDriver(TestBase):
def setUp(self):
super(TestMetaInterfaceDriver, self).setUp()
self.conf.register_opts(DeviceManager.OPTS)
self.client_cls_p = mock.patch('quantumclient.v2_0.client.Client')
client_cls = self.client_cls_p.start()
self.client_inst = mock.Mock()
client_cls.return_value = self.client_inst
fake_network = {'network': {'flavor:id': 'fake1'}}
fake_port = {'ports':
[{'mac_address':
'aa:bb:cc:dd:ee:ffa', 'network_id': 'test'}]}
self.client_inst.list_ports.return_value = fake_port
self.client_inst.show_network.return_value = fake_network
self.conf.set_override('auth_url', 'http://localhost:35357/v2.0')
self.conf.set_override('auth_region', 'RegionOne')
self.conf.set_override('admin_user', 'quantum')
self.conf.set_override('admin_password', 'password')
self.conf.set_override('admin_tenant_name', 'service')
self.conf.set_override(
'meta_flavor_driver_mappings',
'fake1:quantum.agent.linux.interface.OVSInterfaceDriver,'
'fake2:quantum.agent.linux.interface.BridgeInterfaceDriver')
def tearDown(self):
self.client_cls_p.stop()
super(TestMetaInterfaceDriver, self).tearDown()
def test_get_driver_by_network_id(self):
meta_interface = interface.MetaInterfaceDriver(self.conf)
driver = meta_interface._get_driver_by_network_id('test')
self.assertTrue(isinstance(
driver,
interface.OVSInterfaceDriver))
def test_get_driver_by_device_name(self):
device_address_p = mock.patch(
'quantum.agent.linux.ip_lib.IpLinkCommand.address')
device_address = device_address_p.start()
device_address.return_value = 'aa:bb:cc:dd:ee:ffa'
meta_interface = interface.MetaInterfaceDriver(self.conf)
driver = meta_interface._get_driver_by_device_name('test')
self.assertTrue(isinstance(
driver,
interface.OVSInterfaceDriver))
device_address_p.stop()

View File

@ -48,6 +48,7 @@ cisco_plugin_config_path = 'etc/quantum/plugins/cisco'
linuxbridge_plugin_config_path = 'etc/quantum/plugins/linuxbridge'
nvp_plugin_config_path = 'etc/quantum/plugins/nicira'
ryu_plugin_config_path = 'etc/quantum/plugins/ryu'
meta_plugin_config_path = 'etc/quantum/plugins/metaplugin'
DataFiles = [
(config_path,
@ -70,6 +71,8 @@ DataFiles = [
(nvp_plugin_config_path,
['etc/quantum/plugins/nicira/nvp.ini']),
(ryu_plugin_config_path, ['etc/quantum/plugins/ryu/ryu.ini']),
(meta_plugin_config_path,
['etc/quantum/plugins/metaplugin/metaplugin.ini'])
]
setuptools.setup(