Merged Brad's ovsplugin code
This commit is contained in:
commit
ef3285903c
@ -56,7 +56,6 @@ class APIRouterV01(wsgi.Router):
|
||||
parent_resource=dict(member_name='network',
|
||||
collection_name=\
|
||||
uri_prefix + 'networks'))
|
||||
|
||||
mapper.connect("get_resource",
|
||||
uri_prefix + 'networks/{network_id}/' \
|
||||
'ports/{id}/attachment{.format}',
|
||||
|
@ -29,15 +29,13 @@ class Controller(common.QuantumController):
|
||||
|
||||
_port_ops_param_list = [{
|
||||
'param-name': 'port-state',
|
||||
'default-value': 'DOWN',
|
||||
'default-value': 'DOWN',
|
||||
'required': False},]
|
||||
|
||||
|
||||
_attachment_ops_param_list = [{
|
||||
'param-name': 'attachment-id',
|
||||
'required': True},]
|
||||
|
||||
|
||||
_serialization_metadata = {
|
||||
"application/xml": {
|
||||
"attributes": {
|
||||
@ -49,7 +47,7 @@ class Controller(common.QuantumController):
|
||||
def __init__(self, plugin_conf_file=None):
|
||||
self._resource_name = 'port'
|
||||
super(Controller, self).__init__()
|
||||
|
||||
|
||||
def index(self, req, tenant_id, network_id):
|
||||
""" Returns a list of port ids for a given network """
|
||||
return self._items(req, tenant_id, network_id, is_detail=False)
|
||||
@ -64,7 +62,7 @@ class Controller(common.QuantumController):
|
||||
return dict(ports=result)
|
||||
except exception.NetworkNotFound as e:
|
||||
return faults.Fault(faults.NetworkNotFound(e))
|
||||
|
||||
|
||||
def show(self, req, tenant_id, network_id, id):
|
||||
""" Returns port details for given port and network """
|
||||
try:
|
||||
@ -77,7 +75,7 @@ class Controller(common.QuantumController):
|
||||
except exception.NetworkNotFound as e:
|
||||
return faults.Fault(faults.NetworkNotFound(e))
|
||||
except exception.PortNotFound as e:
|
||||
return faults.Fault(faults.PortNotFound(e))
|
||||
return faults.Fault(faults.PortNotFound(e))
|
||||
|
||||
def create(self, req, tenant_id, network_id):
|
||||
""" Creates a new port for a given network """
|
||||
@ -87,17 +85,17 @@ class Controller(common.QuantumController):
|
||||
self._parse_request_params(req, self._port_ops_param_list)
|
||||
except exc.HTTPError as e:
|
||||
return faults.Fault(e)
|
||||
try:
|
||||
try:
|
||||
port = self.network_manager.create_port(tenant_id,
|
||||
network_id,
|
||||
network_id,
|
||||
req_params['port-state'])
|
||||
builder = ports_view.get_view_builder(req)
|
||||
result = builder.build(port)
|
||||
return dict(ports=result)
|
||||
except exception.NetworkNotFound as e:
|
||||
return faults.Fault(faults.NetworkNotFound(e))
|
||||
except exception.StateInvalid as e:
|
||||
return faults.Fault(faults.RequestedStateInvalid(e))
|
||||
except exception.StateInvalid as e:
|
||||
return faults.Fault(faults.RequestedStateInvalid(e))
|
||||
|
||||
def update(self, req, tenant_id, network_id, id):
|
||||
""" Updates the state of a port for a given network """
|
||||
@ -106,8 +104,8 @@ class Controller(common.QuantumController):
|
||||
req_params = \
|
||||
self._parse_request_params(req, self._port_ops_param_list)
|
||||
except exc.HTTPError as e:
|
||||
return faults.Fault(e)
|
||||
try:
|
||||
return faults.Fault(e)
|
||||
try:
|
||||
port = self.network_manager.update_port(tenant_id,network_id, id,
|
||||
req_params['port-state'])
|
||||
builder = ports_view.get_view_builder(req)
|
||||
@ -117,10 +115,9 @@ class Controller(common.QuantumController):
|
||||
return faults.Fault(faults.NetworkNotFound(e))
|
||||
except exception.PortNotFound as e:
|
||||
return faults.Fault(faults.PortNotFound(e))
|
||||
except exception.StateInvalid as e:
|
||||
except exception.StateInvalid as e:
|
||||
return faults.Fault(faults.RequestedStateInvalid(e))
|
||||
|
||||
|
||||
def delete(self, req, tenant_id, network_id, id):
|
||||
""" Destroys the port with the given id """
|
||||
#look for port state in request
|
||||
@ -131,11 +128,10 @@ class Controller(common.QuantumController):
|
||||
except exception.NetworkNotFound as e:
|
||||
return faults.Fault(faults.NetworkNotFound(e))
|
||||
except exception.PortNotFound as e:
|
||||
return faults.Fault(faults.PortNotFound(e))
|
||||
return faults.Fault(faults.PortNotFound(e))
|
||||
except exception.PortInUse as e:
|
||||
return faults.Fault(faults.PortInUse(e))
|
||||
|
||||
|
||||
def get_resource(self,req,tenant_id, network_id, id):
|
||||
try:
|
||||
result = self.network_manager.get_interface_details(
|
||||
@ -144,9 +140,9 @@ class Controller(common.QuantumController):
|
||||
except exception.NetworkNotFound as e:
|
||||
return faults.Fault(faults.NetworkNotFound(e))
|
||||
except exception.PortNotFound as e:
|
||||
return faults.Fault(faults.PortNotFound(e))
|
||||
return faults.Fault(faults.PortNotFound(e))
|
||||
|
||||
#TODO - Complete implementation of these APIs
|
||||
#TODO - Complete implementation of these APIs
|
||||
def attach_resource(self,req,tenant_id, network_id, id):
|
||||
content_type = req.best_match_content_type()
|
||||
print "Content type:%s" %content_type
|
||||
@ -164,14 +160,13 @@ class Controller(common.QuantumController):
|
||||
except exception.NetworkNotFound as e:
|
||||
return faults.Fault(faults.NetworkNotFound(e))
|
||||
except exception.PortNotFound as e:
|
||||
return faults.Fault(faults.PortNotFound(e))
|
||||
return faults.Fault(faults.PortNotFound(e))
|
||||
except exception.PortInUse as e:
|
||||
return faults.Fault(faults.PortInUse(e))
|
||||
except exception.AlreadyAttached as e:
|
||||
return faults.Fault(faults.AlreadyAttached(e))
|
||||
|
||||
|
||||
#TODO - Complete implementation of these APIs
|
||||
#TODO - Complete implementation of these APIs
|
||||
def detach_resource(self,req,tenant_id, network_id, id):
|
||||
try:
|
||||
self.network_manager.unplug_interface(tenant_id,
|
||||
@ -180,4 +175,4 @@ class Controller(common.QuantumController):
|
||||
except exception.NetworkNotFound as e:
|
||||
return faults.Fault(faults.NetworkNotFound(e))
|
||||
except exception.PortNotFound as e:
|
||||
return faults.Fault(faults.PortNotFound(e))
|
||||
return faults.Fault(faults.PortNotFound(e))
|
||||
|
@ -37,12 +37,13 @@ class ViewBuilder(object):
|
||||
else:
|
||||
port = self._build_simple(port_data)
|
||||
return port
|
||||
|
||||
|
||||
def _build_simple(self, port_data):
|
||||
"""Return a simple model of a server."""
|
||||
return dict(port=dict(id=port_data['port-id']))
|
||||
|
||||
|
||||
def _build_detail(self, port_data):
|
||||
"""Return a simple model of a server."""
|
||||
return dict(port=dict(id=port_data['port-id'],
|
||||
state=port_data['port-state']))
|
||||
attachment=port_data['attachment'],
|
||||
state=port_data['port-state']))
|
||||
|
126
quantum/cli.py
126
quantum/cli.py
@ -1,6 +1,7 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2011, Nicira Networks, Inc.
|
||||
# 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
|
||||
@ -15,95 +16,46 @@
|
||||
# under the License.
|
||||
# @author: Somik Behera, Nicira Networks, Inc.
|
||||
|
||||
import sys
|
||||
|
||||
from manager import QuantumManager
|
||||
"""
|
||||
Quantum's Manager class is responsible for parsing a config file and instantiating the correct
|
||||
plugin that concretely implement quantum_plugin_base class
|
||||
|
||||
The caller should make sure that QuantumManager is a singleton.
|
||||
"""
|
||||
import gettext
|
||||
gettext.install('quantum', unicode=1)
|
||||
|
||||
def usage():
|
||||
print "\nUsage:"
|
||||
print "list_nets <tenant-id>"
|
||||
print "create_net <tenant-id> <net-name>"
|
||||
print "delete_net <tenant-id> <net-id>"
|
||||
print "detail_net <tenant-id> <net-id>"
|
||||
print "rename_net <tenant-id> <net-id> <new name>"
|
||||
print "list_ports <tenant-id> <net-id>"
|
||||
print "create_port <tenant-id> <net-id>"
|
||||
print "delete_port <tenant-id> <net-id> <port-id>"
|
||||
print "detail_port <tenant-id> <net-id> <port-id>"
|
||||
print "plug_iface <tenant-id> <net-id> <port-id> <iface-id>"
|
||||
print "unplug_iface <tenant-id> <net-id> <port-id>"
|
||||
print "detail_iface <tenant-id> <net-id> <port-id>"
|
||||
print "list_iface <tenant-id> <net-id>\n"
|
||||
import os
|
||||
|
||||
if len(sys.argv) < 2 or len(sys.argv) > 6:
|
||||
usage()
|
||||
exit(1)
|
||||
from common import utils
|
||||
from quantum_plugin_base import QuantumPluginBase
|
||||
|
||||
quantum = QuantumManager()
|
||||
manager = quantum.get_manager()
|
||||
CONFIG_FILE = "plugins.ini"
|
||||
|
||||
def find_config(basepath):
|
||||
for root, dirs, files in os.walk(basepath):
|
||||
if CONFIG_FILE in files:
|
||||
return os.path.join(root, CONFIG_FILE)
|
||||
return None
|
||||
|
||||
class QuantumManager(object):
|
||||
def __init__(self, config=None):
|
||||
if config == None:
|
||||
self.configuration_file = find_config(os.path.abspath(os.path.dirname(__file__)))
|
||||
else:
|
||||
self.configuration_file = config
|
||||
plugin_location = utils.getPluginFromConfig(self.configuration_file)
|
||||
print "PLUGIN LOCATION:%s" % plugin_location
|
||||
plugin_klass = utils.import_class(plugin_location)
|
||||
if not issubclass(plugin_klass, QuantumPluginBase):
|
||||
raise Exception("Configured Quantum plug-in " \
|
||||
"didn't pass compatibility test")
|
||||
else:
|
||||
print("Successfully imported Quantum plug-in." \
|
||||
"All compatibility tests passed\n")
|
||||
self.plugin = plugin_klass()
|
||||
|
||||
def get_manager(self):
|
||||
return self.plugin
|
||||
|
||||
if sys.argv[1] == "list_nets" and len(sys.argv) == 3:
|
||||
network_on_tenant = manager.get_all_networks(sys.argv[2])
|
||||
print "Virtual Networks on Tenant:%s\n" % sys.argv[2]
|
||||
for k, v in network_on_tenant.iteritems():
|
||||
print"\tNetwork ID:%s \n\tNetwork Name:%s \n" % (k, v)
|
||||
elif sys.argv[1] == "create_net" and len(sys.argv) == 4:
|
||||
new_net_id = manager.create_network(sys.argv[2], sys.argv[3])
|
||||
print "Created a new Virtual Network with ID:%s\n" % new_net_id
|
||||
elif sys.argv[1] == "delete_net" and len(sys.argv) == 4:
|
||||
manager.delete_network(sys.argv[2], sys.argv[3])
|
||||
print "Deleted Virtual Network with ID:%s" % sys.argv[3]
|
||||
elif sys.argv[1] == "detail_net" and len(sys.argv) == 4:
|
||||
vif_list = manager.get_network_details(sys.argv[2], sys.argv[3])
|
||||
print "Remote Interfaces on Virtual Network:%s\n" % sys.argv[3]
|
||||
for iface in vif_list:
|
||||
print "\tRemote interface :%s" % iface
|
||||
elif sys.argv[1] == "rename_net" and len(sys.argv) == 5:
|
||||
manager.rename_network(sys.argv[2], sys.argv[3], sys.argv[4])
|
||||
print "Renamed Virtual Network with ID:%s" % sys.argv[3]
|
||||
elif sys.argv[1] == "list_ports" and len(sys.argv) == 4:
|
||||
ports = manager.get_all_ports(sys.argv[2], sys.argv[3])
|
||||
print " Virtual Ports on Virtual Network:%s\n" % sys.argv[3]
|
||||
for port in ports:
|
||||
print "\tVirtual Port:%s" % port
|
||||
elif sys.argv[1] == "create_port" and len(sys.argv) == 4:
|
||||
new_port = manager.create_port(sys.argv[2], sys.argv[3])
|
||||
print "Created Virtual Port:%s " \
|
||||
"on Virtual Network:%s" % (new_port, sys.argv[3])
|
||||
elif sys.argv[1] == "delete_port" and len(sys.argv) == 5:
|
||||
manager.delete_port(sys.argv[2], sys.argv[3], sys.argv[4])
|
||||
print "Deleted Virtual Port:%s " \
|
||||
"on Virtual Network:%s" % (sys.argv[3], sys.argv[4])
|
||||
elif sys.argv[1] == "detail_port" and len(sys.argv) == 5:
|
||||
port_detail = manager.get_port_details(sys.argv[2],
|
||||
sys.argv[3], sys.argv[4])
|
||||
print "Virtual Port:%s on Virtual Network:%s " \
|
||||
"contains remote interface:%s" % (sys.argv[3],
|
||||
sys.argv[4],
|
||||
port_detail)
|
||||
elif sys.argv[1] == "plug_iface" and len(sys.argv) == 6:
|
||||
manager.plug_interface(sys.argv[2], sys.argv[3], sys.argv[4], sys.argv[5])
|
||||
print "Plugged remote interface:%s " \
|
||||
"into Virtual Network:%s" % (sys.argv[5], sys.argv[3])
|
||||
elif sys.argv[1] == "unplug_iface" and len(sys.argv) == 5:
|
||||
manager.unplug_interface(sys.argv[2], sys.argv[3], sys.argv[4])
|
||||
print "UnPlugged remote interface " \
|
||||
"from Virtual Port:%s Virtual Network:%s" % (sys.argv[4],
|
||||
sys.argv[3])
|
||||
elif sys.argv[1] == "detail_iface" and len(sys.argv) == 5:
|
||||
remote_iface = manager.get_interface_details(sys.argv[2],
|
||||
sys.argv[3], sys.argv[4])
|
||||
print "Remote interface on Virtual Port:%s " \
|
||||
"Virtual Network:%s is %s" % (sys.argv[4],
|
||||
sys.argv[3], remote_iface)
|
||||
elif sys.argv[1] == "list_iface" and len(sys.argv) == 4:
|
||||
iface_list = manager.get_all_attached_interfaces(sys.argv[2], sys.argv[3])
|
||||
print "Remote Interfaces on Virtual Network:%s\n" % sys.argv[3]
|
||||
for iface in iface_list:
|
||||
print "\tRemote interface :%s" % iface
|
||||
elif sys.argv[1] == "all" and len(sys.argv) == 2:
|
||||
print "Not Implemented"
|
||||
else:
|
||||
print "invalid arguments: %s" % str(sys.argv)
|
||||
usage()
|
||||
|
17
quantum/db/__init__.py
Normal file
17
quantum/db/__init__.py
Normal file
@ -0,0 +1,17 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
# Copyright 2011 Nicira Networks, Inc.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
# @author: Somik Behera, Nicira Networks, Inc.
|
||||
# @author: Brad Hall, Nicira Networks, Inc.
|
173
quantum/db/api.py
Normal file
173
quantum/db/api.py
Normal file
@ -0,0 +1,173 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
# Copyright 2011 Nicira Networks, Inc.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
# @author: Somik Behera, Nicira Networks, Inc.
|
||||
# @author: Brad Hall, Nicira Networks, Inc.
|
||||
# @author: Dan Wendlandt, Nicira Networks, Inc.
|
||||
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker, exc
|
||||
import models
|
||||
|
||||
_ENGINE = None
|
||||
_MAKER = None
|
||||
BASE = models.BASE
|
||||
|
||||
def configure_db(options):
|
||||
"""
|
||||
Establish the database, create an engine if needed, and
|
||||
register the models.
|
||||
|
||||
:param options: Mapping of configuration options
|
||||
"""
|
||||
global _ENGINE
|
||||
if not _ENGINE:
|
||||
_ENGINE = create_engine(options['sql_connection'],
|
||||
echo=False,
|
||||
echo_pool=True,
|
||||
pool_recycle=3600)
|
||||
register_models()
|
||||
|
||||
def get_session(autocommit=True, expire_on_commit=False):
|
||||
"""Helper method to grab session"""
|
||||
global _MAKER, _ENGINE
|
||||
if not _MAKER:
|
||||
assert _ENGINE
|
||||
_MAKER = sessionmaker(bind=_ENGINE,
|
||||
autocommit=autocommit,
|
||||
expire_on_commit=expire_on_commit)
|
||||
return _MAKER()
|
||||
|
||||
def register_models():
|
||||
"""Register Models and create properties"""
|
||||
global _ENGINE
|
||||
assert _ENGINE
|
||||
BASE.metadata.create_all(_ENGINE)
|
||||
|
||||
def unregister_models():
|
||||
"""Unregister Models, useful clearing out data before testing"""
|
||||
global _ENGINE
|
||||
assert _ENGINE
|
||||
BASE.metadata.drop_all(_ENGINE)
|
||||
|
||||
def network_create(tenant_id, name):
|
||||
session = get_session()
|
||||
net = None
|
||||
try:
|
||||
net = session.query(models.Network).\
|
||||
filter_by(name=name).\
|
||||
one()
|
||||
raise Exception("Network with name \"%s\" already exists" % name)
|
||||
except exc.NoResultFound:
|
||||
with session.begin():
|
||||
net = models.Network(tenant_id, name)
|
||||
session.add(net)
|
||||
session.flush()
|
||||
return net
|
||||
|
||||
def network_list(tenant_id):
|
||||
session = get_session()
|
||||
return session.query(models.Network).\
|
||||
filter_by(tenant_id=tenant_id).\
|
||||
all()
|
||||
|
||||
def network_get(net_id):
|
||||
session = get_session()
|
||||
try:
|
||||
return session.query(models.Network).\
|
||||
filter_by(uuid=net_id).\
|
||||
one()
|
||||
except exc.NoResultFound:
|
||||
raise Exception("No net found with id = %s" % net_id)
|
||||
|
||||
def network_rename(net_id, tenant_id, new_name):
|
||||
session = get_session()
|
||||
try:
|
||||
res = session.query(models.Network).\
|
||||
filter_by(name=new_name).\
|
||||
one()
|
||||
except exc.NoResultFound:
|
||||
net = network_get(net_id)
|
||||
net.name = new_name
|
||||
session.merge(net)
|
||||
session.flush()
|
||||
return net
|
||||
raise Exception("A network with name \"%s\" already exists" % new_name)
|
||||
|
||||
def network_destroy(net_id):
|
||||
session = get_session()
|
||||
try:
|
||||
net = session.query(models.Network).\
|
||||
filter_by(uuid=net_id).\
|
||||
one()
|
||||
session.delete(net)
|
||||
session.flush()
|
||||
return net
|
||||
except exc.NoResultFound:
|
||||
raise Exception("No network found with id = %s" % net_id)
|
||||
|
||||
def port_create(net_id):
|
||||
session = get_session()
|
||||
with session.begin():
|
||||
port = models.Port(net_id)
|
||||
session.add(port)
|
||||
session.flush()
|
||||
return port
|
||||
|
||||
def port_list(net_id):
|
||||
session = get_session()
|
||||
return session.query(models.Port).\
|
||||
filter_by(network_id=net_id).\
|
||||
all()
|
||||
|
||||
def port_get(port_id):
|
||||
session = get_session()
|
||||
try:
|
||||
return session.query(models.Port).\
|
||||
filter_by(uuid=port_id).\
|
||||
one()
|
||||
except exc.NoResultFound:
|
||||
raise Exception("No port found with id = %s " % port_id)
|
||||
|
||||
def port_set_attachment(port_id, new_interface_id):
|
||||
session = get_session()
|
||||
ports = None
|
||||
try:
|
||||
ports = session.query(models.Port).\
|
||||
filter_by(interface_id=new_interface_id).\
|
||||
all()
|
||||
except exc.NoResultFound:
|
||||
pass
|
||||
if len(ports) == 0:
|
||||
port = port_get(port_id)
|
||||
port.interface_id = new_interface_id
|
||||
session.merge(port)
|
||||
session.flush()
|
||||
return port
|
||||
else:
|
||||
raise Exception("Port with attachment \"%s\" already exists" % (new_interface_id))
|
||||
|
||||
def port_destroy(port_id):
|
||||
session = get_session()
|
||||
try:
|
||||
port = session.query(models.Port).\
|
||||
filter_by(uuid=port_id).\
|
||||
one()
|
||||
session.delete(port)
|
||||
session.flush()
|
||||
return port
|
||||
except exc.NoResultFound:
|
||||
raise Exception("No port found with id = %s " % port_id)
|
||||
|
59
quantum/db/models.py
Normal file
59
quantum/db/models.py
Normal file
@ -0,0 +1,59 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
# Copyright 2011 Nicira Networks, Inc.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
# @author: Somik Behera, Nicira Networks, Inc.
|
||||
# @author: Brad Hall, Nicira Networks, Inc.
|
||||
# @author: Dan Wendlandt, Nicira Networks, Inc.
|
||||
|
||||
import uuid
|
||||
|
||||
from sqlalchemy import Column, Integer, String, ForeignKey
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import relation
|
||||
|
||||
BASE = declarative_base()
|
||||
|
||||
class Port(BASE):
|
||||
"""Represents a port on a quantum network"""
|
||||
__tablename__ = 'ports'
|
||||
|
||||
uuid = Column(String(255), primary_key=True)
|
||||
network_id = Column(String(255), ForeignKey("networks.uuid"), nullable=False)
|
||||
interface_id = Column(String(255))
|
||||
|
||||
def __init__(self, network_id):
|
||||
self.uuid = uuid.uuid4()
|
||||
self.network_id = network_id
|
||||
|
||||
def __repr__(self):
|
||||
return "<Port(%s,%s,%s)>" % (self.uuid, self.network_id, self.interface_id)
|
||||
|
||||
class Network(BASE):
|
||||
"""Represents a quantum network"""
|
||||
__tablename__ = 'networks'
|
||||
|
||||
uuid = Column(String(255), primary_key=True)
|
||||
tenant_id = Column(String(255), nullable=False)
|
||||
name = Column(String(255))
|
||||
ports = relation(Port, order_by=Port.uuid, backref="network")
|
||||
|
||||
def __init__(self, tenant_id, name):
|
||||
self.uuid = uuid.uuid4()
|
||||
self.tenant_id = tenant_id
|
||||
self.name = name
|
||||
|
||||
def __repr__(self):
|
||||
return "<Network(%s,%s,%s)>" % \
|
||||
(self.uuid,self.name,self.tenant_id)
|
@ -27,17 +27,33 @@ The caller should make sure that QuantumManager is a singleton.
|
||||
import gettext
|
||||
gettext.install('quantum', unicode=1)
|
||||
|
||||
import os
|
||||
|
||||
from common import utils
|
||||
from quantum_plugin_base import QuantumPluginBase
|
||||
|
||||
CONFIG_FILE = "plugins.ini"
|
||||
|
||||
def find_config(basepath):
|
||||
for root, dirs, files in os.walk(basepath):
|
||||
if CONFIG_FILE in files:
|
||||
return os.path.join(root, CONFIG_FILE)
|
||||
return None
|
||||
|
||||
class QuantumManager(object):
|
||||
<<<<<<< TREE
|
||||
|
||||
def __init__(self, config=CONFIG_FILE):
|
||||
self.configuration_file = CONFIG_FILE
|
||||
plugin_location = utils.getPluginFromConfig(CONFIG_FILE)
|
||||
=======
|
||||
def __init__(self, config=None):
|
||||
if config == None:
|
||||
self.configuration_file = find_config(os.path.abspath(os.path.dirname(__file__)))
|
||||
else:
|
||||
self.configuration_file = config
|
||||
plugin_location = utils.getPluginFromConfig(self.configuration_file)
|
||||
>>>>>>> MERGE-SOURCE
|
||||
print "PLUGIN LOCATION:%s" % plugin_location
|
||||
plugin_klass = utils.import_class(plugin_location)
|
||||
if not issubclass(plugin_klass, QuantumPluginBase):
|
||||
@ -51,15 +67,3 @@ class QuantumManager(object):
|
||||
def get_manager(self):
|
||||
return self.plugin
|
||||
|
||||
|
||||
# TODO(somik): rmove the main class
|
||||
# Added for temporary testing purposes
|
||||
def main():
|
||||
manager = QuantumManager()
|
||||
myManager = manager.get_manager()
|
||||
myManager.get_all_networks("tesst")
|
||||
#print("is a plugin")
|
||||
|
||||
# Standard boilerplate to call the main() function.
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
30
quantum/plugins/openvswitch/Makefile
Normal file
30
quantum/plugins/openvswitch/Makefile
Normal file
@ -0,0 +1,30 @@
|
||||
QUANTUM_PATH=../../../
|
||||
|
||||
# TODO(bgh): DIST_DIR and target for plugin
|
||||
|
||||
AGENT_DIST_DIR=ovs_quantum_agent
|
||||
AGENT_DIST_TARBALL=ovs_quantum_agent.tgz
|
||||
|
||||
agent-dist: distclean
|
||||
mkdir $(AGENT_DIST_DIR)
|
||||
cp agent/*.py $(AGENT_DIST_DIR)
|
||||
cp agent/*.sh $(AGENT_DIST_DIR)
|
||||
cp README $(AGENT_DIST_DIR)
|
||||
cp ovs_quantum_plugin.ini $(AGENT_DIST_DIR)
|
||||
tar -zcvf $(AGENT_DIST_TARBALL) $(AGENT_DIST_DIR)/
|
||||
@echo "Agent tarball created: $(AGENT_DIST_TARBALL)"
|
||||
@echo "See README for installation details"
|
||||
|
||||
all:
|
||||
|
||||
clean:
|
||||
$(find . -name *.pyc | xargs rm)
|
||||
|
||||
distclean:
|
||||
-rm -rf $(AGENT_DIST_DIR)
|
||||
-rm -f $(AGENT_DIST_TARBALL)
|
||||
|
||||
check:
|
||||
PYTHONPATH=$(QUANTUM_PATH):. python ovs_quantum_plugin.py
|
||||
|
||||
PHONY: agent-dist check clean distclean
|
106
quantum/plugins/openvswitch/README
Normal file
106
quantum/plugins/openvswitch/README
Normal file
@ -0,0 +1,106 @@
|
||||
# -- Background
|
||||
|
||||
The quantum openvswitch plugin is a simple plugin that allows you to manage
|
||||
connectivity between VMs on hypervisors running openvswitch.
|
||||
|
||||
The quantum openvswitch plugin consists of two components:
|
||||
|
||||
1) The plugin itself: The plugin uses a database backend (mysql for now) to
|
||||
store configuration and mappings that are used by the agent.
|
||||
|
||||
2) An agent which runs on the hypervisor (dom0) and communicates with
|
||||
openvswitch. The agent gathers the configuration and mappings from the
|
||||
mysql database running on the quantum host.
|
||||
|
||||
The sections below describe how to configure and run the quantum service with
|
||||
the openvswitch plugin.
|
||||
|
||||
# -- Nova configuration
|
||||
|
||||
- Make sure to set up nova using flat networking. Also, make sure that the
|
||||
integration bridge (see below under agent configuration) matches the
|
||||
flat_network_bridge specified in your nova flag file. Here are the relevant
|
||||
entries from my nova flag file.
|
||||
--network_manager=nova.network.manager.FlatManager
|
||||
--flat_network_bridge=xapi1
|
||||
|
||||
# -- Quantum configuration
|
||||
|
||||
Make the openvswitch plugin the current quantum plugin
|
||||
|
||||
- edit ../../plugins.ini and change the provider line to be:
|
||||
provider = quantum.plugins.openvswitch.ovs_quantum_plugin.OVSQuantumPlugin
|
||||
|
||||
# -- Database config. The OVS quantum service requires access to a mysql
|
||||
# database in order to store configuration and mappings that will be used by
|
||||
# the agent. Here is how to set up the database on the host that you will be
|
||||
# running the quantum service on.
|
||||
|
||||
MySQL should be installed on the host, and all plugins and clients must be
|
||||
configured with access to the database.
|
||||
|
||||
To prep mysql, run:
|
||||
|
||||
$ mysql -u root -p -e "create database ovs_quantum"
|
||||
|
||||
Make sure any xenserver running the ovs quantum agent will be able to communicate with the host running the quantum service:
|
||||
|
||||
//log in to mysql service
|
||||
$ mysql -u root -p
|
||||
//grant access to user-remote host combination
|
||||
mysql> GRANT USAGE ON *.* to root@'yourremotehost' IDENTIFIED BY 'newpassword';
|
||||
//force update of authorization changes
|
||||
mysql> FLUSH PRIVILEGES;
|
||||
|
||||
# -- Plugin configuration.
|
||||
|
||||
- Edit the configuration file (ovs_quantum_plugin.ini). Make sure it matches
|
||||
your mysql configuration. This file must be updated with the addresses and
|
||||
credentials to access the database. This file will be included in the agent
|
||||
distribution tarball (see below) and the agent will use the credentials here
|
||||
to access the database.
|
||||
|
||||
# -- Agent configuration
|
||||
|
||||
- Create the agent distribution tarball
|
||||
|
||||
$ make agent-dist
|
||||
- Copy the resulting tarball to your xenserver(s) (copy to dom0, not the nova
|
||||
compute node)
|
||||
- Unpack the tarball and run install.sh. This will install all of the
|
||||
necessary pieces into /etc/xapi.d/plugins. It will also spit out the name
|
||||
of the integration bridge that you'll need for your nova configuration.
|
||||
- Run the agent [on your hypervisor (dom0)]:
|
||||
$ /etc/xapi.d/plugins/ovs_quantum_agent.py /etc/xapi.d/plugins/ovs_quantum_plugin.ini
|
||||
|
||||
# -- Getting quantum up and running
|
||||
|
||||
- Start quantum [on the quantum service host]:
|
||||
~/src/quantum-framework$ PYTHONPATH=.:$PYTHONPATH python bin/quantum etc/quantum.conf
|
||||
- Run ovs_quantum_plugin.py via the quantum plugin framework cli [on the
|
||||
quantum service host]
|
||||
~/src/quantum-framework$ PYTHONPATH=.:$PYTHONPATH python quantum/cli.py
|
||||
|
||||
This will show help all of the available commands.
|
||||
|
||||
An example session looks like this:
|
||||
|
||||
$ export TENANT=t1
|
||||
$ PYTHONPATH=. python quantum/cli.py -v create_net $TENANT network1
|
||||
Created a new Virtual Network with ID:e754e7c0-a8eb-40e5-861a-b182d30c3441
|
||||
$ export NETWORK=e754e7c0-a8eb-40e5-861a-b182d30c3441
|
||||
$ PYTHONPATH=. python quantum/cli.py -v create_port $TENANT $NETWORK
|
||||
Created Virtual Port:5a1e121b-ccc8-471d-9445-24f15f9f854c on Virtual Network:e754e7c0-a8eb-40e5-861a-b182d30c3441
|
||||
$ export PORT=5a1e121b-ccc8-471d-9445-24f15f9f854c
|
||||
$ PYTHONPATH=. python quantum/cli.py -v plug_iface $TENANT $NETWORK $PORT ubuntu1-eth1
|
||||
Plugged interface "ubuntu1-eth1" to port:5a1e121b-ccc8-471d-9445-24f15f9f854c on network:e754e7c0-a8eb-40e5-861a-b182d30c3441
|
||||
$ PYTHONPATH=. python quantum/cli.py -v plug_iface $TENANT $NETWORK $PORT ubuntu2-eth1
|
||||
Plugged interface "ubuntu2-eth1" to port:5a1e121b-ccc8-471d-9445-24f15f9f854c on network:e754e7c0-a8eb-40e5-861a-b182d30c3441
|
||||
|
||||
Now you should have connectivity between ubuntu1-eth1 and ubuntu2-eth1..
|
||||
|
||||
# -- Other items
|
||||
|
||||
- To get a listing of the vif names that the ovs quantum service will expect
|
||||
them in, issue the following command on the hypervisor (dom0):
|
||||
$ for vif in `xe vif-list params=uuid --minimal | sed s/,/" "/g`; do echo $(xe vif-list params=vm-name-label uuid=${vif} --minimal)-eth$(xe vif-list params=device uuid=${vif} --minimal); done
|
0
quantum/plugins/openvswitch/__init__.py
Normal file
0
quantum/plugins/openvswitch/__init__.py
Normal file
38
quantum/plugins/openvswitch/agent/install.sh
Executable file
38
quantum/plugins/openvswitch/agent/install.sh
Executable file
@ -0,0 +1,38 @@
|
||||
#!/bin/bash
|
||||
|
||||
CONF_FILE=/etc/xapi.d/plugins/ovs_quantum_plugin.ini
|
||||
|
||||
if [ ! -d /etc/xapi.d/plugins ]; then
|
||||
echo "Am I on a xenserver? I can't find the plugins directory!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Make sure we have mysql-python
|
||||
rpm -qa | grep MYyQL-python >/dev/null 2>&1
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "MySQL-python not found"
|
||||
echo "Please enable the centos repositories and install mysql-python:"
|
||||
echo "yum --enablerepo=base -y install MySQL-python"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cp ovs_quantum_agent.py /etc/xapi.d/plugins
|
||||
cp ovs_quantum_plugin.ini /etc/xapi.d/plugins
|
||||
cp set_external_ids.sh /etc/xapi.d/plugins
|
||||
|
||||
xe network-list name-label="integration-bridge" | grep xapi >/dev/null 2>&1
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "No integration bridge found. Creating."
|
||||
xe network-create name-label="integration-bridge"
|
||||
fi
|
||||
|
||||
BR=$(xe network-list name-label="integration-bridge" | grep "bridge.*:" | awk '{print $4}')
|
||||
CONF_BR=$(grep integration-bridge ${CONF_FILE} | cut -d= -f2)
|
||||
if [ "X$BR" != "X$CONF_BR" ]; then
|
||||
echo "Integration bridge doesn't match configuration file; fixing."
|
||||
sed -i -e "s/^integration-bridge =.*$/integration-bridge = ${BR}/g" $CONF_FILE
|
||||
fi
|
||||
|
||||
echo "Using integration bridge: $BR (make sure this is set in the nova configuration)"
|
||||
|
||||
echo "Make sure to edit: $CONF_FILE"
|
290
quantum/plugins/openvswitch/agent/ovs_quantum_agent.py
Executable file
290
quantum/plugins/openvswitch/agent/ovs_quantum_agent.py
Executable file
@ -0,0 +1,290 @@
|
||||
#!/usr/bin/env python
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
# Copyright 2011 Nicira Networks, Inc.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
# @author: Somik Behera, Nicira Networks, Inc.
|
||||
# @author: Brad Hall, Nicira Networks, Inc.
|
||||
# @author: Dan Wendlandt, Nicira Networks, Inc.
|
||||
|
||||
import ConfigParser
|
||||
import logging as LOG
|
||||
import MySQLdb
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
from optparse import OptionParser
|
||||
from subprocess import *
|
||||
|
||||
# A class to represent a VIF (i.e., a port that has 'iface-id' and 'vif-mac'
|
||||
# attributes set).
|
||||
class VifPort:
|
||||
def __init__(self, port_name, ofport, vif_id, vif_mac, switch):
|
||||
self.port_name = port_name
|
||||
self.ofport = ofport
|
||||
self.vif_id = vif_id
|
||||
self.vif_mac = vif_mac
|
||||
self.switch = switch
|
||||
def __str__(self):
|
||||
return "iface-id=" + self.vif_id + ", vif_mac=" + \
|
||||
self.vif_mac + ", port_name=" + self.port_name + \
|
||||
", ofport=" + self.ofport + ", bridge name = " + self.switch.br_name
|
||||
|
||||
class OVSBridge:
|
||||
def __init__(self, br_name):
|
||||
self.br_name = br_name
|
||||
|
||||
def run_cmd(self, args):
|
||||
# LOG.debug("## running command: " + " ".join(args))
|
||||
return Popen(args, stdout=PIPE).communicate()[0]
|
||||
|
||||
def run_vsctl(self, args):
|
||||
full_args = ["ovs-vsctl" ] + args
|
||||
return self.run_cmd(full_args)
|
||||
|
||||
def reset_bridge(self):
|
||||
self.run_vsctl([ "--" , "--if-exists", "del-br", self.br_name])
|
||||
self.run_vsctl(["add-br", self.br_name])
|
||||
|
||||
def delete_port(self, port_name):
|
||||
self.run_vsctl([ "--" , "--if-exists", "del-port", self.br_name,
|
||||
port_name])
|
||||
|
||||
def set_db_attribute(self, table_name, record, column, value):
|
||||
args = [ "set", table_name, record, "%s=%s" % (column,value) ]
|
||||
self.run_vsctl(args)
|
||||
|
||||
def clear_db_attribute(self, table_name,record, column):
|
||||
args = [ "clear", table_name, record, column ]
|
||||
self.run_vsctl(args)
|
||||
|
||||
def run_ofctl(self, cmd, args):
|
||||
full_args = ["ovs-ofctl", cmd, self.br_name ] + args
|
||||
return self.run_cmd(full_args)
|
||||
|
||||
def remove_all_flows(self):
|
||||
self.run_ofctl("del-flows", [])
|
||||
|
||||
def get_port_ofport(self, port_name):
|
||||
return self.db_get_val("Interface", port_name, "ofport")
|
||||
|
||||
def add_flow(self,**dict):
|
||||
if "actions" not in dict:
|
||||
raise Exception("must specify one or more actions")
|
||||
if "priority" not in dict:
|
||||
dict["priority"] = "0"
|
||||
|
||||
flow_str = "priority=%s" % dict["priority"]
|
||||
if "match" in dict:
|
||||
flow_str += "," + dict["match"]
|
||||
flow_str += ",actions=%s" % (dict["actions"])
|
||||
self.run_ofctl("add-flow", [ flow_str ] )
|
||||
|
||||
def delete_flows(self,**dict):
|
||||
all_args = []
|
||||
if "priority" in dict:
|
||||
all_args.append("priority=%s" % dict["priority"])
|
||||
if "match" in dict:
|
||||
all_args.append(dict["match"])
|
||||
if "actions" in dict:
|
||||
all_args.append("actions=%s" % (dict["actions"]))
|
||||
flow_str = ",".join(all_args)
|
||||
self.run_ofctl("del-flows", [ flow_str ] )
|
||||
|
||||
def db_get_map(self, table, record, column):
|
||||
str = self.run_vsctl([ "get" , table, record, column ]).rstrip("\n\r")
|
||||
return self.db_str_to_map(str)
|
||||
|
||||
def db_get_val(self, table, record, column):
|
||||
return self.run_vsctl([ "get" , table, record, column ]).rstrip("\n\r")
|
||||
|
||||
def db_str_to_map(self, full_str):
|
||||
list = full_str.strip("{}").split(", ")
|
||||
ret = {}
|
||||
for e in list:
|
||||
if e.find("=") == -1:
|
||||
continue
|
||||
arr = e.split("=")
|
||||
ret[arr[0]] = arr[1].strip("\"")
|
||||
return ret
|
||||
|
||||
def get_port_name_list(self):
|
||||
res = self.run_vsctl([ "list-ports", self.br_name])
|
||||
return res.split("\n")[0:-1]
|
||||
|
||||
def get_port_stats(self, port_name):
|
||||
return self.db_get_map("Interface", port_name, "statistics")
|
||||
|
||||
# returns a VIF object for each VIF port
|
||||
def get_vif_ports(self):
|
||||
edge_ports = []
|
||||
port_names = self.get_port_name_list()
|
||||
for name in port_names:
|
||||
external_ids = self.db_get_map("Interface",name,"external_ids")
|
||||
if "iface-id" in external_ids and "attached-mac" in external_ids:
|
||||
ofport = self.db_get_val("Interface",name,"ofport")
|
||||
p = VifPort(name, ofport, external_ids["iface-id"],
|
||||
external_ids["attached-mac"], self)
|
||||
edge_ports.append(p)
|
||||
else:
|
||||
# iface-id might not be set. See if we can figure it out and
|
||||
# set it here.
|
||||
external_ids = self.db_get_map("Interface",name,"external_ids")
|
||||
if "attached-mac" not in external_ids:
|
||||
continue
|
||||
vif_uuid = external_ids.get("xs-vif-uuid", "")
|
||||
if len(vif_uuid) == 0:
|
||||
continue
|
||||
LOG.debug("iface-id not set, got vif-uuid: %s" % vif_uuid)
|
||||
res = os.popen("xe vif-param-get param-name=other-config uuid=%s | grep nicira-iface-id | awk '{print $2}'" % vif_uuid).readline()
|
||||
res = res.strip()
|
||||
if len(res) == 0:
|
||||
continue
|
||||
external_ids["iface-id"] = res
|
||||
LOG.info("Setting interface \"%s\" iface-id to \"%s\"" % (name, res))
|
||||
self.set_db_attribute("Interface", name,
|
||||
"external-ids:iface-id", res)
|
||||
ofport = self.db_get_val("Interface",name,"ofport")
|
||||
p = VifPort(name, ofport, external_ids["iface-id"],
|
||||
external_ids["attached-mac"], self)
|
||||
edge_ports.append(p)
|
||||
return edge_ports
|
||||
|
||||
class OVSNaaSPlugin:
|
||||
def __init__(self, integ_br):
|
||||
self.setup_integration_br(integ_br)
|
||||
|
||||
def port_bound(self, port, vlan_id):
|
||||
self.int_br.set_db_attribute("Port", port.port_name,"tag",
|
||||
str(vlan_id))
|
||||
|
||||
def port_unbound(self, port, still_exists):
|
||||
if still_exists:
|
||||
self.int_br.clear_db_attribute("Port", port.port_name,"tag")
|
||||
|
||||
def setup_integration_br(self, integ_br):
|
||||
self.int_br = OVSBridge(integ_br)
|
||||
self.int_br.remove_all_flows()
|
||||
# drop all traffic on the 'dead vlan'
|
||||
self.int_br.add_flow(priority=2, match="dl_vlan=4095", actions="drop")
|
||||
# switch all other traffic using L2 learning
|
||||
self.int_br.add_flow(priority=1, actions="normal")
|
||||
# FIXME send broadcast everywhere, regardless of tenant
|
||||
#int_br.add_flow(priority=3, match="dl_dst=ff:ff:ff:ff:ff:ff", actions="normal")
|
||||
|
||||
def daemon_loop(self, conn):
|
||||
self.local_vlan_map = {}
|
||||
old_local_bindings = {}
|
||||
old_vif_ports = {}
|
||||
|
||||
while True:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT * FROM network_bindings")
|
||||
rows = cursor.fetchall()
|
||||
cursor.close()
|
||||
all_bindings = {}
|
||||
for r in rows:
|
||||
all_bindings[r[2]] = r[1]
|
||||
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT * FROM vlan_bindings")
|
||||
rows = cursor.fetchall()
|
||||
cursor.close()
|
||||
vlan_bindings = {}
|
||||
for r in rows:
|
||||
vlan_bindings[r[1]] = r[0]
|
||||
|
||||
new_vif_ports = {}
|
||||
new_local_bindings = {}
|
||||
vif_ports = self.int_br.get_vif_ports()
|
||||
for p in vif_ports:
|
||||
new_vif_ports[p.vif_id] = p
|
||||
if p.vif_id in all_bindings:
|
||||
new_local_bindings[p.vif_id] = all_bindings[p.vif_id]
|
||||
else:
|
||||
# no binding, put him on the 'dead vlan'
|
||||
self.int_br.set_db_attribute("Port", p.port_name, "tag",
|
||||
"4095")
|
||||
old_b = old_local_bindings.get(p.vif_id,None)
|
||||
new_b = new_local_bindings.get(p.vif_id,None)
|
||||
if old_b != new_b:
|
||||
if old_b is not None:
|
||||
LOG.info("Removing binding to net-id = %s for %s"
|
||||
% (old_b, str(p)))
|
||||
self.port_unbound(p, True)
|
||||
if new_b is not None:
|
||||
LOG.info("Adding binding to net-id = %s for %s" \
|
||||
% (new_b, str(p)))
|
||||
# If we don't have a binding we have to stick it on
|
||||
# the dead vlan
|
||||
vlan_id = vlan_bindings.get(all_bindings[p.vif_id],
|
||||
"4095")
|
||||
self.port_bound(p, vlan_id)
|
||||
for vif_id in old_vif_ports.keys():
|
||||
if vif_id not in new_vif_ports:
|
||||
LOG.info("Port Disappeared: %s" % vif_id)
|
||||
if vif_id in old_local_bindings:
|
||||
old_b = old_local_bindings[vif_id]
|
||||
self.port_unbound(old_vif_ports[vif_id], False)
|
||||
|
||||
old_vif_ports = new_vif_ports
|
||||
old_local_bindings = new_local_bindings
|
||||
self.int_br.run_cmd(["bash",
|
||||
"/etc/xapi.d/plugins/set_external_ids.sh"])
|
||||
time.sleep(2)
|
||||
|
||||
if __name__ == "__main__":
|
||||
usagestr = "%prog [OPTIONS] <config file>"
|
||||
parser = OptionParser(usage=usagestr)
|
||||
parser.add_option("-v", "--verbose", dest="verbose",
|
||||
action="store_true", default=False, help="turn on verbose logging")
|
||||
|
||||
options, args = parser.parse_args()
|
||||
|
||||
if options.verbose:
|
||||
LOG.basicConfig(level=LOG.DEBUG)
|
||||
else:
|
||||
LOG.basicConfig(level=LOG.WARN)
|
||||
|
||||
if len(args) != 1:
|
||||
parser.print_help()
|
||||
sys.exit(1)
|
||||
|
||||
config_file = args[0]
|
||||
config = ConfigParser.ConfigParser()
|
||||
try:
|
||||
config.read(config_file)
|
||||
except Exception, e:
|
||||
LOG.error("Unable to parse config file \"%s\": %s" % (config_file,
|
||||
str(e)))
|
||||
|
||||
integ_br = config.get("OVS", "integration-bridge")
|
||||
|
||||
db_name = config.get("DATABASE", "name")
|
||||
db_user = config.get("DATABASE", "user")
|
||||
db_pass = config.get("DATABASE", "pass")
|
||||
db_host = config.get("DATABASE", "host")
|
||||
conn = None
|
||||
try:
|
||||
LOG.info("Connecting to database \"%s\" on %s" % (db_name, db_host))
|
||||
conn = MySQLdb.connect(host=db_host, user=db_user,
|
||||
passwd=db_pass, db=db_name)
|
||||
plugin = OVSNaaSPlugin(integ_br)
|
||||
plugin.daemon_loop(conn)
|
||||
finally:
|
||||
if conn:
|
||||
conn.close()
|
||||
|
||||
sys.exit(0)
|
15
quantum/plugins/openvswitch/agent/set_external_ids.sh
Executable file
15
quantum/plugins/openvswitch/agent/set_external_ids.sh
Executable file
@ -0,0 +1,15 @@
|
||||
#!/bin/sh
|
||||
VIFLIST=`xe vif-list params=uuid --minimal | sed s/,/" "/g`
|
||||
for VIF_UUID in $VIFLIST; do
|
||||
DEVICE_NUM=`xe vif-list params=device uuid=$VIF_UUID --minimal`
|
||||
VM_NAME=`xe vif-list params=vm-name-label uuid=$VIF_UUID --minimal`
|
||||
NAME="$VM_NAME-eth$DEVICE_NUM"
|
||||
echo "Vif: $VIF_UUID is '$NAME'"
|
||||
xe vif-param-set uuid=$VIF_UUID other-config:nicira-iface-id="$NAME"
|
||||
done
|
||||
|
||||
ps auxw | grep -v grep | grep ovs-xapi-sync > /dev/null 2>&1
|
||||
if [ $? -eq 0 ]; then
|
||||
killall -HUP ovs-xapi-sync
|
||||
fi
|
||||
|
72
quantum/plugins/openvswitch/ovs_db.py
Normal file
72
quantum/plugins/openvswitch/ovs_db.py
Normal file
@ -0,0 +1,72 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
# Copyright 2011 Nicira Networks, Inc.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
# @author: Somik Behera, Nicira Networks, Inc.
|
||||
# @author: Brad Hall, Nicira Networks, Inc.
|
||||
# @author: Dan Wendlandt, Nicira Networks, Inc.
|
||||
|
||||
|
||||
from sqlalchemy.orm import exc
|
||||
|
||||
import quantum.db.api as db
|
||||
import quantum.db.models as models
|
||||
import ovs_models
|
||||
|
||||
def get_vlans():
|
||||
session = db.get_session()
|
||||
try:
|
||||
bindings = session.query(ovs_models.VlanBinding).\
|
||||
all()
|
||||
except exc.NoResultFound:
|
||||
return []
|
||||
res = []
|
||||
for x in bindings:
|
||||
res.append((x.vlan_id, x.network_id))
|
||||
return res
|
||||
|
||||
def add_vlan_binding(vlanid, netid):
|
||||
session = db.get_session()
|
||||
binding = ovs_models.VlanBinding(vlanid, netid)
|
||||
session.add(binding)
|
||||
session.flush()
|
||||
return binding.vlan_id
|
||||
|
||||
def remove_vlan_binding(netid):
|
||||
session = db.get_session()
|
||||
try:
|
||||
binding = session.query(ovs_models.VlanBinding).\
|
||||
filter_by(network_id=netid).\
|
||||
one()
|
||||
session.delete(binding)
|
||||
except exc.NoResultFound:
|
||||
pass
|
||||
session.flush()
|
||||
|
||||
def update_network_binding(netid, ifaceid):
|
||||
session = db.get_session()
|
||||
# Add to or delete from the bindings table
|
||||
if ifaceid == None:
|
||||
try:
|
||||
binding = session.query(ovs_models.NetworkBinding).\
|
||||
filter_by(network_id=netid).\
|
||||
one()
|
||||
session.delete(binding)
|
||||
except exc.NoResultFound:
|
||||
raise Exception("No binding found with network_id = %s" % netid)
|
||||
else:
|
||||
binding = ovs_models.NetworkBinding(netid, ifaceid)
|
||||
session.add(binding)
|
||||
|
||||
session.flush()
|
58
quantum/plugins/openvswitch/ovs_models.py
Normal file
58
quantum/plugins/openvswitch/ovs_models.py
Normal file
@ -0,0 +1,58 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
# Copyright 2011 Nicira Networks, Inc.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
# @author: Somik Behera, Nicira Networks, Inc.
|
||||
# @author: Brad Hall, Nicira Networks, Inc.
|
||||
# @author: Dan Wendlandt, Nicira Networks, Inc.
|
||||
|
||||
|
||||
import uuid
|
||||
|
||||
from sqlalchemy import Column, Integer, String, ForeignKey
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import relation
|
||||
|
||||
from quantum.db.models import BASE
|
||||
|
||||
class NetworkBinding(BASE):
|
||||
"""Represents a binding of network_id, vif_id"""
|
||||
__tablename__ = 'network_bindings'
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
network_id = Column(String(255))
|
||||
vif_id = Column(String(255))
|
||||
|
||||
def __init__(self, network_id, vif_id):
|
||||
self.network_id = network_id
|
||||
self.vif_id = vif_id
|
||||
|
||||
def __repr__(self):
|
||||
return "<NetworkBinding(%s,%s)>" % \
|
||||
(self.network_id, self.vif_id)
|
||||
|
||||
class VlanBinding(BASE):
|
||||
"""Represents a binding of network_id, vlan_id"""
|
||||
__tablename__ = 'vlan_bindings'
|
||||
|
||||
vlan_id = Column(Integer, primary_key=True)
|
||||
network_id = Column(String(255))
|
||||
|
||||
def __init__(self, vlan_id, network_id):
|
||||
self.network_id = network_id
|
||||
self.vlan_id = vlan_id
|
||||
|
||||
def __repr__(self):
|
||||
return "<VlanBinding(%s,%s)>" % \
|
||||
(self.vlan_id, self.network_id)
|
9
quantum/plugins/openvswitch/ovs_quantum_plugin.ini
Normal file
9
quantum/plugins/openvswitch/ovs_quantum_plugin.ini
Normal file
@ -0,0 +1,9 @@
|
||||
[DATABASE]
|
||||
name = ovs_naas
|
||||
user = root
|
||||
pass = foobar
|
||||
host = 127.0.0.1
|
||||
port = 3306
|
||||
|
||||
[OVS]
|
||||
integration-bridge = xapi1
|
334
quantum/plugins/openvswitch/ovs_quantum_plugin.py
Normal file
334
quantum/plugins/openvswitch/ovs_quantum_plugin.py
Normal file
@ -0,0 +1,334 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
# Copyright 2011 Nicira Networks, Inc.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
# @author: Somik Behera, Nicira Networks, Inc.
|
||||
# @author: Brad Hall, Nicira Networks, Inc.
|
||||
# @author: Dan Wendlandt, Nicira Networks, Inc.
|
||||
|
||||
import ConfigParser
|
||||
import logging as LOG
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
from quantum.quantum_plugin_base import QuantumPluginBase
|
||||
from optparse import OptionParser
|
||||
|
||||
import quantum.db.api as db
|
||||
import ovs_db
|
||||
|
||||
CONF_FILE="ovs_quantum_plugin.ini"
|
||||
|
||||
LOG.basicConfig(level=LOG.WARN)
|
||||
LOG.getLogger("ovs_quantum_plugin")
|
||||
|
||||
def find_config(basepath):
|
||||
for root, dirs, files in os.walk(basepath):
|
||||
if CONF_FILE in files:
|
||||
return os.path.join(root, CONF_FILE)
|
||||
return None
|
||||
|
||||
class VlanMap(object):
|
||||
vlans = {}
|
||||
def __init__(self):
|
||||
for x in xrange(2, 4094):
|
||||
self.vlans[x] = None
|
||||
def set(self, vlan_id, network_id):
|
||||
self.vlans[vlan_id] = network_id
|
||||
def acquire(self, network_id):
|
||||
for x in xrange(2, 4094):
|
||||
if self.vlans[x] == None:
|
||||
self.vlans[x] = network_id
|
||||
# LOG.debug("VlanMap::acquire %s -> %s" % (x, network_id))
|
||||
return x
|
||||
raise Exception("No free vlans..")
|
||||
def get(self, vlan_id):
|
||||
return self.vlans[vlan_id]
|
||||
def release(self, network_id):
|
||||
for x in self.vlans.keys():
|
||||
if self.vlans[x] == network_id:
|
||||
self.vlans[x] = None
|
||||
# LOG.debug("VlanMap::release %s" % (x))
|
||||
return
|
||||
LOG.error("No vlan found with network \"%s\"" % network_id)
|
||||
|
||||
class OVSQuantumPlugin(QuantumPluginBase):
|
||||
def __init__(self, configfile=None):
|
||||
config = ConfigParser.ConfigParser()
|
||||
if configfile == None:
|
||||
if os.path.exists(CONF_FILE):
|
||||
configfile = CONF_FILE
|
||||
else:
|
||||
configfile = find_config(os.path.abspath(os.path.dirname(__file__)))
|
||||
if configfile == None:
|
||||
raise Exception("Configuration file \"%s\" doesn't exist" %
|
||||
(configfile))
|
||||
LOG.debug("Using configuration file: %s" % configfile)
|
||||
config.read(configfile)
|
||||
LOG.debug("Config: %s" % config)
|
||||
|
||||
DB_NAME = config.get("DATABASE", "name")
|
||||
DB_USER = config.get("DATABASE", "user")
|
||||
DB_PASS = config.get("DATABASE", "pass")
|
||||
DB_HOST = config.get("DATABASE", "host")
|
||||
options = {"sql_connection": "mysql://%s:%s@%s/%s" % (DB_USER,
|
||||
DB_PASS, DB_HOST, DB_NAME)}
|
||||
db.configure_db(options)
|
||||
|
||||
self.vmap = VlanMap()
|
||||
# Populate the map with anything that is already present in the
|
||||
# database
|
||||
vlans = ovs_db.get_vlans()
|
||||
for x in vlans:
|
||||
vlan_id, network_id = x
|
||||
# LOG.debug("Adding already populated vlan %s -> %s" % (vlan_id, network_id))
|
||||
self.vmap.set(vlan_id, network_id)
|
||||
|
||||
def get_all_networks(self, tenant_id):
|
||||
nets = []
|
||||
for x in db.network_list(tenant_id):
|
||||
LOG.debug("Adding network: %s" % x.uuid)
|
||||
d = {}
|
||||
d["net-id"] = str(x.uuid)
|
||||
d["net-name"] = x.name
|
||||
nets.append(d)
|
||||
return nets
|
||||
|
||||
def create_network(self, tenant_id, net_name):
|
||||
d = {}
|
||||
try:
|
||||
res = db.network_create(tenant_id, net_name)
|
||||
LOG.debug("Created newtork: %s" % res)
|
||||
except Exception, e:
|
||||
LOG.error("Error: %s" % str(e))
|
||||
return d
|
||||
d["net-id"] = str(res.uuid)
|
||||
d["net-name"] = res.name
|
||||
vlan_id = self.vmap.acquire(str(res.uuid))
|
||||
ovs_db.add_vlan_binding(vlan_id, str(res.uuid))
|
||||
return d
|
||||
|
||||
def delete_network(self, tenant_id, net_id):
|
||||
net = db.network_destroy(net_id)
|
||||
d = {}
|
||||
d["net-id"] = str(net.uuid)
|
||||
ovs_db.remove_vlan_binding(net_id)
|
||||
self.vmap.release(net_id)
|
||||
return d
|
||||
|
||||
def get_network_details(self, tenant_id, net_id):
|
||||
ports = db.port_list(net_id)
|
||||
ifaces = []
|
||||
for p in ports:
|
||||
ifaces.append(p.interface_id)
|
||||
return ifaces
|
||||
|
||||
def rename_network(self, tenant_id, net_id, new_name):
|
||||
try:
|
||||
net = db.network_rename(net_id, tenant_id, new_name)
|
||||
except Exception, e:
|
||||
raise Exception("Failed to rename network: %s" % str(e))
|
||||
d = {}
|
||||
d["net-id"] = str(net.uuid)
|
||||
d["net-name"] = net.name
|
||||
return d
|
||||
|
||||
def get_all_ports(self, tenant_id, net_id):
|
||||
ids = []
|
||||
ports = db.port_list(net_id)
|
||||
for x in ports:
|
||||
LOG.debug("Appending port: %s" % x.uuid)
|
||||
d = {}
|
||||
d["port-id"] = str(x.uuid)
|
||||
ids.append(d)
|
||||
return ids
|
||||
|
||||
def create_port(self, tenant_id, net_id, port_state=None):
|
||||
LOG.debug("Creating port with network_id: %s" % net_id)
|
||||
port = db.port_create(net_id)
|
||||
d = {}
|
||||
d["port-id"] = str(port.uuid)
|
||||
LOG.debug("-> %s" % (port.uuid))
|
||||
return d
|
||||
|
||||
def delete_port(self, tenant_id, net_id, port_id):
|
||||
try:
|
||||
port = db.port_destroy(port_id)
|
||||
except Exception, e:
|
||||
raise Exception("Failed to delete port: %s" % str(e))
|
||||
d = {}
|
||||
d["port-id"] = str(port.uuid)
|
||||
return d
|
||||
|
||||
def update_port(self, tenant_id, net_id, port_id, port_state):
|
||||
"""
|
||||
Updates the state of a port on the specified Virtual Network.
|
||||
"""
|
||||
LOG.debug("update_port() called\n")
|
||||
port = db.port_get(port_id)
|
||||
port['port-state'] = port_state
|
||||
return port
|
||||
|
||||
def get_port_details(self, tenant_id, net_id, port_id):
|
||||
port = db.port_get(port_id)
|
||||
rv = {"port-id": port.uuid, "attachment": port.interface_id,
|
||||
"net-id": port.network_id, "port-state": "UP"}
|
||||
return rv
|
||||
|
||||
def plug_interface(self, tenant_id, net_id, port_id, remote_iface_id):
|
||||
db.port_set_attachment(port_id, remote_iface_id)
|
||||
ovs_db.update_network_binding(net_id, remote_iface_id)
|
||||
|
||||
def unplug_interface(self, tenant_id, net_id, port_id):
|
||||
db.port_set_attachment(port_id, "")
|
||||
ovs_db.update_network_binding(net_id, None)
|
||||
|
||||
def get_interface_details(self, tenant_id, net_id, port_id):
|
||||
res = db.port_get(port_id)
|
||||
return res.interface_id
|
||||
|
||||
class VlanMapTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.vmap = VlanMap()
|
||||
def tearDown(self):
|
||||
pass
|
||||
def testAddVlan(self):
|
||||
vlan_id = self.vmap.acquire("foobar")
|
||||
self.assertTrue(vlan_id == 2)
|
||||
def testReleaseVlan(self):
|
||||
vlan_id = self.vmap.acquire("foobar")
|
||||
self.vmap.release("foobar")
|
||||
self.assertTrue(self.vmap.get(vlan_id) == None)
|
||||
|
||||
# TODO(bgh): Make the tests use a sqlite database instead of mysql
|
||||
class OVSPluginTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.quantum = OVSQuantumPlugin()
|
||||
self.tenant_id = "testtenant"
|
||||
|
||||
def testCreateNetwork(self):
|
||||
net1 = self.quantum.create_network(self.tenant_id, "plugin_test1")
|
||||
self.assertTrue(net1["net-name"] == "plugin_test1")
|
||||
|
||||
def testGetNetworks(self):
|
||||
net1 = self.quantum.create_network(self.tenant_id, "plugin_test1")
|
||||
net2 = self.quantum.create_network(self.tenant_id, "plugin_test2")
|
||||
nets = self.quantum.get_all_networks(self.tenant_id)
|
||||
count = 0
|
||||
for x in nets:
|
||||
if "plugin_test" in x["net-name"]:
|
||||
count += 1
|
||||
self.assertTrue(count == 2)
|
||||
|
||||
def testDeleteNetwork(self):
|
||||
net = self.quantum.create_network(self.tenant_id, "plugin_test1")
|
||||
self.quantum.delete_network(self.tenant_id, net["net-id"])
|
||||
nets = self.quantum.get_all_networks(self.tenant_id)
|
||||
count = 0
|
||||
for x in nets:
|
||||
if "plugin_test" in x["net-name"]:
|
||||
count += 1
|
||||
self.assertTrue(count == 0)
|
||||
|
||||
def testRenameNetwork(self):
|
||||
net = self.quantum.create_network(self.tenant_id, "plugin_test1")
|
||||
net = self.quantum.rename_network(self.tenant_id, net["net-id"],
|
||||
"plugin_test_renamed")
|
||||
self.assertTrue(net["net-name"] == "plugin_test_renamed")
|
||||
|
||||
def testCreatePort(self):
|
||||
net1 = self.quantum.create_network(self.tenant_id, "plugin_test1")
|
||||
port = self.quantum.create_port(self.tenant_id, net1["net-id"])
|
||||
ports = self.quantum.get_all_ports(self.tenant_id, net1["net-id"])
|
||||
count = 0
|
||||
for p in ports:
|
||||
count += 1
|
||||
self.assertTrue(count == 1)
|
||||
|
||||
def testDeletePort(self):
|
||||
net1 = self.quantum.create_network(self.tenant_id, "plugin_test1")
|
||||
port = self.quantum.create_port(self.tenant_id, net1["net-id"])
|
||||
ports = self.quantum.get_all_ports(self.tenant_id, net1["net-id"])
|
||||
count = 0
|
||||
for p in ports:
|
||||
count += 1
|
||||
self.assertTrue(count == 1)
|
||||
for p in ports:
|
||||
self.quantum.delete_port(self.tenant_id, id, p["port-id"])
|
||||
ports = self.quantum.get_all_ports(self.tenant_id, net1["net-id"])
|
||||
count = 0
|
||||
for p in ports:
|
||||
count += 1
|
||||
self.assertTrue(count == 0)
|
||||
|
||||
def testGetPorts(self):
|
||||
pass
|
||||
|
||||
def testPlugInterface(self):
|
||||
net1 = self.quantum.create_network(self.tenant_id, "plugin_test1")
|
||||
port = self.quantum.create_port(self.tenant_id, net1["net-id"])
|
||||
self.quantum.plug_interface(self.tenant_id, net1["net-id"],
|
||||
port["port-id"], "vif1.1")
|
||||
port = self.quantum.get_port_details(self.tenant_id, net1["net-id"],
|
||||
port["port-id"])
|
||||
self.assertTrue(port["attachment"] == "vif1.1")
|
||||
|
||||
def testUnPlugInterface(self):
|
||||
net1 = self.quantum.create_network(self.tenant_id, "plugin_test1")
|
||||
port = self.quantum.create_port(self.tenant_id, net1["net-id"])
|
||||
self.quantum.plug_interface(self.tenant_id, net1["net-id"],
|
||||
port["port-id"], "vif1.1")
|
||||
port = self.quantum.get_port_details(self.tenant_id, net1["net-id"],
|
||||
port["port-id"])
|
||||
self.assertTrue(port["attachment"] == "vif1.1")
|
||||
self.quantum.unplug_interface(self.tenant_id, net1["net-id"],
|
||||
port["port-id"])
|
||||
port = self.quantum.get_port_details(self.tenant_id, net1["net-id"],
|
||||
port["port-id"])
|
||||
self.assertTrue(port["attachment"] == "")
|
||||
|
||||
def tearDown(self):
|
||||
networks = self.quantum.get_all_networks(self.tenant_id)
|
||||
# Clean up any test networks lying around
|
||||
for net in networks:
|
||||
id = net["net-id"]
|
||||
name = net["net-name"]
|
||||
if "plugin_test" in name:
|
||||
# Clean up any test ports lying around
|
||||
ports = self.quantum.get_all_ports(self.tenant_id, id)
|
||||
for p in ports:
|
||||
self.quantum.delete_port(self.tenant_id, id, p["port-id"])
|
||||
self.quantum.delete_network(self.tenant_id, id)
|
||||
|
||||
if __name__ == "__main__":
|
||||
usagestr = "Usage: %prog [OPTIONS] <command> [args]"
|
||||
parser = OptionParser(usage=usagestr)
|
||||
parser.add_option("-v", "--verbose", dest="verbose",
|
||||
action="store_true", default=False, help="turn on verbose logging")
|
||||
|
||||
options, args = parser.parse_args()
|
||||
|
||||
if options.verbose:
|
||||
LOG.basicConfig(level=LOG.DEBUG)
|
||||
else:
|
||||
LOG.basicConfig(level=LOG.WARN)
|
||||
|
||||
# Make sqlalchemy quieter
|
||||
LOG.getLogger('sqlalchemy.engine').setLevel(LOG.WARN)
|
||||
# Run the tests
|
||||
suite = unittest.TestLoader().loadTestsFromTestCase(OVSPluginTest)
|
||||
unittest.TextTestRunner(verbosity=2).run(suite)
|
||||
suite = unittest.TestLoader().loadTestsFromTestCase(VlanMapTest)
|
||||
unittest.TextTestRunner(verbosity=2).run(suite)
|
Loading…
x
Reference in New Issue
Block a user