Merge trunk
Resolving conflicts
This commit is contained in:
commit
a94c424f89
108
README
Normal file
108
README
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
# -- Welcome!
|
||||||
|
|
||||||
|
You have come across a cloud computing network fabric controller. It has
|
||||||
|
identified itself as "Quantum." It aims to tame your (cloud) networking!
|
||||||
|
|
||||||
|
# -- Basics:
|
||||||
|
|
||||||
|
1) Quantum REST API: Quantum supports a REST-ful programmatic interface to
|
||||||
|
manage your cloud networking fabric.
|
||||||
|
|
||||||
|
2) Quantum Plugins: Quantum sports a plug-able architecture that allows
|
||||||
|
Quantum's REST API to be backed by various entities that can create a
|
||||||
|
cloud-class virtual networking fabric. The advantages of this plug-able
|
||||||
|
architecture is two-folds:
|
||||||
|
|
||||||
|
a) Allows for ANY open-source project or commercial vendor to write a
|
||||||
|
Quantum plug-in.
|
||||||
|
|
||||||
|
b) Allows Quantum users to not be tied down to a single Quantum
|
||||||
|
implementation and enables them to switch out a plug-in by simple editing a
|
||||||
|
config file - plugins.ini
|
||||||
|
|
||||||
|
# -- Dependencies
|
||||||
|
|
||||||
|
The following python packages are required to run quantum. These can be
|
||||||
|
installed using pip:
|
||||||
|
|
||||||
|
eventlet>=0.9.12
|
||||||
|
nose
|
||||||
|
Paste
|
||||||
|
PasteDeploy
|
||||||
|
pep8==0.5.0
|
||||||
|
python-gflags
|
||||||
|
routes
|
||||||
|
simplejson
|
||||||
|
webob
|
||||||
|
webtest
|
||||||
|
|
||||||
|
1) Install easy_install (there is probably a distribution specific package for
|
||||||
|
this)
|
||||||
|
|
||||||
|
2) Install pip:
|
||||||
|
$ easy_install pip==dev
|
||||||
|
3) Install packages with pip:
|
||||||
|
$ pip install <package name>
|
||||||
|
|
||||||
|
# -- Configuring Quantum plug-in
|
||||||
|
|
||||||
|
1) Identify your desired plug-in. Choose a plugin from one of he options in
|
||||||
|
the quantum/plugins directory.
|
||||||
|
|
||||||
|
2) Update plug-in configuration by editing the quantum/plugins.ini file and
|
||||||
|
modify "provider" property to point to the location of the Quantum plug-in.
|
||||||
|
It should specify the class path to the plugin and the class name (i.e. for
|
||||||
|
a plugin class MyPlugin in quantum/plugins/myplugin/myplugin.py the
|
||||||
|
provider would be: quantum.plugins.myplugin.myplugin.MyPlugin)
|
||||||
|
|
||||||
|
3) Read the plugin specific README, this is usually found in the same
|
||||||
|
directory as your Quantum plug-in, and follow configuration instructions.
|
||||||
|
|
||||||
|
# -- Launching the Quantum Service
|
||||||
|
|
||||||
|
1) Start quantum using the following command [on the quantum service host]:
|
||||||
|
~/src/quantum$ PYTHONPATH=.:$PYTHONPATH python bin/quantum etc/quantum.conf
|
||||||
|
|
||||||
|
# -- Making requests against the Quantum Service
|
||||||
|
|
||||||
|
Please refer to sample Web Service client code in:
|
||||||
|
|
||||||
|
../quantum/test_scripts/miniclient.py
|
||||||
|
|
||||||
|
# -- CLI tools to program the Quantum-managed Cloud networking fabric
|
||||||
|
|
||||||
|
Quantum comes with a programmatic CLI that is driven by the Quantum Web
|
||||||
|
Service. You can use the CLI by issuing the following command:
|
||||||
|
|
||||||
|
~/src/quantum$ 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
|
||||||
|
|
||||||
|
# -- Writing your own Quantum plug-in
|
||||||
|
|
||||||
|
If you wish the write your own Quantum plugin, please refer to some concrete as
|
||||||
|
well as sample plugins available in:
|
||||||
|
|
||||||
|
../quantum/quantum/plugins/.. directory.
|
||||||
|
|
||||||
|
There are a few requirements to writing your own plugin:
|
||||||
|
|
||||||
|
1) Your plugin should implement all methods defined in the
|
||||||
|
quantum/quantum_plugin_base.QuantumPluginBase class
|
||||||
|
|
||||||
|
2) Copy your Quantum plug-in over to the quantum/quantum/plugins/.. directory
|
||||||
|
|
||||||
|
3) The next step is to edit the plugins.ini file in the same directory
|
||||||
|
as QuantumPluginBase class and specify the location of your custom plugin
|
||||||
|
as the "provider"
|
||||||
|
|
||||||
|
4) Launch the Quantum Service, and your plug-in is configured and ready to
|
||||||
|
manage a Cloud Networking Fabric.
|
||||||
|
|
||||||
|
|
@ -36,6 +36,7 @@ gettext.install('quantum', unicode=1)
|
|||||||
from quantum import service
|
from quantum import service
|
||||||
from quantum.common import config
|
from quantum.common import config
|
||||||
|
|
||||||
|
|
||||||
def create_options(parser):
|
def create_options(parser):
|
||||||
"""
|
"""
|
||||||
Sets up the CLI and config-file options that may be
|
Sets up the CLI and config-file options that may be
|
||||||
@ -58,4 +59,3 @@ if __name__ == '__main__':
|
|||||||
service.wait()
|
service.wait()
|
||||||
except RuntimeError, e:
|
except RuntimeError, e:
|
||||||
sys.exit("ERROR: %s" % e)
|
sys.exit("ERROR: %s" % e)
|
||||||
|
|
||||||
|
@ -20,6 +20,7 @@ Quantum API controllers.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import pprint
|
||||||
import routes
|
import routes
|
||||||
import webob.dec
|
import webob.dec
|
||||||
import webob.exc
|
import webob.exc
|
||||||
|
@ -36,8 +36,7 @@ class Fault(webob.exc.HTTPException):
|
|||||||
432: "portInUse",
|
432: "portInUse",
|
||||||
440: "alreadyAttached",
|
440: "alreadyAttached",
|
||||||
470: "serviceUnavailable",
|
470: "serviceUnavailable",
|
||||||
471: "pluginFault"
|
471: "pluginFault"}
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self, exception):
|
def __init__(self, exception):
|
||||||
"""Create a Fault for the given webob.exc.exception."""
|
"""Create a Fault for the given webob.exc.exception."""
|
||||||
|
@ -45,10 +45,10 @@ class Controller(common.QuantumController):
|
|||||||
self._resource_name = 'network'
|
self._resource_name = 'network'
|
||||||
super(Controller, self).__init__()
|
super(Controller, self).__init__()
|
||||||
|
|
||||||
def index(self, req, tenant_id):
|
def index(self, request, tenant_id):
|
||||||
""" Returns a list of network ids """
|
""" Returns a list of network ids """
|
||||||
#TODO: this should be for a given tenant!!!
|
#TODO: this should be for a given tenant!!!
|
||||||
return self._items(req, tenant_id, net_detail=False)
|
return self._items(request, tenant_id, net_detail=False)
|
||||||
|
|
||||||
def _item(self, req, tenant_id, network_id,
|
def _item(self, req, tenant_id, network_id,
|
||||||
net_details, port_details):
|
net_details, port_details):
|
||||||
@ -66,60 +66,63 @@ class Controller(common.QuantumController):
|
|||||||
for network in networks]
|
for network in networks]
|
||||||
return dict(networks=result)
|
return dict(networks=result)
|
||||||
|
|
||||||
def show(self, req, tenant_id, id):
|
def show(self, request, tenant_id, id):
|
||||||
""" Returns network details for the given network id """
|
""" Returns network details for the given network id """
|
||||||
try:
|
try:
|
||||||
return self._item(req, tenant_id, id,
|
return self._item(request, tenant_id, id,
|
||||||
net_details=True, port_details=False)
|
net_details=True, port_details=False)
|
||||||
except exception.NetworkNotFound as e:
|
except exception.NetworkNotFound as e:
|
||||||
return faults.Fault(faults.NetworkNotFound(e))
|
return faults.Fault(faults.NetworkNotFound(e))
|
||||||
|
|
||||||
def detail(self, req, **kwargs):
|
def detail(self, request, **kwargs):
|
||||||
tenant_id = kwargs.get('tenant_id')
|
tenant_id = kwargs.get('tenant_id')
|
||||||
network_id = kwargs.get('id')
|
network_id = kwargs.get('id')
|
||||||
try:
|
try:
|
||||||
if network_id:
|
if network_id:
|
||||||
return self._item(req, tenant_id, network_id,
|
return self._item(request, tenant_id, network_id,
|
||||||
net_details=True, port_details=True)
|
net_details=True, port_details=True)
|
||||||
else:
|
else:
|
||||||
#do like show but with detaik
|
#do like show but with detaik
|
||||||
return self._items(req, tenant_id,
|
return self._items(request, tenant_id,
|
||||||
net_details=True, port_details=False)
|
net_details=True, port_details=False)
|
||||||
except exception.NetworkNotFound as e:
|
except exception.NetworkNotFound as e:
|
||||||
return faults.Fault(faults.NetworkNotFound(e))
|
return faults.Fault(faults.NetworkNotFound(e))
|
||||||
|
|
||||||
def create(self, req, tenant_id):
|
def create(self, request, tenant_id):
|
||||||
""" Creates a new network for a given tenant """
|
""" Creates a new network for a given tenant """
|
||||||
#look for network name in request
|
#look for network name in request
|
||||||
try:
|
try:
|
||||||
req_params = \
|
request_params = \
|
||||||
self._parse_request_params(req, self._network_ops_param_list)
|
self._parse_request_params(request,
|
||||||
|
self._network_ops_param_list)
|
||||||
except exc.HTTPError as e:
|
except exc.HTTPError as e:
|
||||||
return faults.Fault(e)
|
return faults.Fault(e)
|
||||||
network = self.network_manager.\
|
network = self.network_manager.\
|
||||||
create_network(tenant_id, req_params['network-name'])
|
create_network(tenant_id,
|
||||||
builder = networks_view.get_view_builder(req)
|
request_params['network-name'])
|
||||||
|
builder = networks_view.get_view_builder(request)
|
||||||
result = builder.build(network)
|
result = builder.build(network)
|
||||||
return dict(networks=result)
|
return dict(networks=result)
|
||||||
|
|
||||||
def update(self, req, tenant_id, id):
|
def update(self, request, tenant_id, id):
|
||||||
""" Updates the name for the network with the given id """
|
""" Updates the name for the network with the given id """
|
||||||
try:
|
try:
|
||||||
req_params = \
|
request_params = \
|
||||||
self._parse_request_params(req, self._network_ops_param_list)
|
self._parse_request_params(request,
|
||||||
|
self._network_ops_param_list)
|
||||||
except exc.HTTPError as e:
|
except exc.HTTPError as e:
|
||||||
return faults.Fault(e)
|
return faults.Fault(e)
|
||||||
try:
|
try:
|
||||||
network = self.network_manager.rename_network(tenant_id,
|
network = self.network_manager.rename_network(tenant_id,
|
||||||
id, req_params['network-name'])
|
id, request_params['network-name'])
|
||||||
|
|
||||||
builder = networks_view.get_view_builder(req)
|
builder = networks_view.get_view_builder(request)
|
||||||
result = builder.build(network, True)
|
result = builder.build(network, True)
|
||||||
return dict(networks=result)
|
return dict(networks=result)
|
||||||
except exception.NetworkNotFound as e:
|
except exception.NetworkNotFound as e:
|
||||||
return faults.Fault(faults.NetworkNotFound(e))
|
return faults.Fault(faults.NetworkNotFound(e))
|
||||||
|
|
||||||
def delete(self, req, tenant_id, id):
|
def delete(self, request, tenant_id, id):
|
||||||
""" Destroys the network with the given id """
|
""" Destroys the network with the given id """
|
||||||
try:
|
try:
|
||||||
self.network_manager.delete_network(tenant_id, id)
|
self.network_manager.delete_network(tenant_id, id)
|
||||||
|
@ -31,47 +31,43 @@ class Controller(common.QuantumController):
|
|||||||
_port_ops_param_list = [{
|
_port_ops_param_list = [{
|
||||||
'param-name': 'port-state',
|
'param-name': 'port-state',
|
||||||
'default-value': 'DOWN',
|
'default-value': 'DOWN',
|
||||||
'required': False},
|
'required': False}, ]
|
||||||
]
|
|
||||||
|
|
||||||
_attachment_ops_param_list = [{
|
_attachment_ops_param_list = [{
|
||||||
'param-name': 'attachment-id',
|
'param-name': 'attachment-id',
|
||||||
'required': True},
|
'required': True}, ]
|
||||||
]
|
|
||||||
|
|
||||||
_serialization_metadata = {
|
_serialization_metadata = {
|
||||||
"application/xml": {
|
"application/xml": {
|
||||||
"attributes": {
|
"attributes": {
|
||||||
"port": ["id", "state"],
|
"port": ["id", "state"], }, }, }
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self, plugin_conf_file=None):
|
def __init__(self, plugin_conf_file=None):
|
||||||
self._resource_name = 'port'
|
self._resource_name = 'port'
|
||||||
super(Controller, self).__init__()
|
super(Controller, self).__init__()
|
||||||
|
|
||||||
def index(self, req, tenant_id, network_id):
|
def index(self, request, tenant_id, network_id):
|
||||||
""" Returns a list of port ids for a given network """
|
""" Returns a list of port ids for a given network """
|
||||||
return self._items(req, tenant_id, network_id, is_detail=False)
|
return self._items(request, tenant_id, network_id, is_detail=False)
|
||||||
|
|
||||||
def _items(self, req, tenant_id, network_id, is_detail):
|
def _items(self, request, tenant_id, network_id, is_detail):
|
||||||
""" Returns a list of networks. """
|
""" Returns a list of networks. """
|
||||||
try:
|
try:
|
||||||
ports = self.network_manager.get_all_ports(tenant_id, network_id)
|
ports = self.network_manager.get_all_ports(tenant_id, network_id)
|
||||||
builder = ports_view.get_view_builder(req)
|
builder = ports_view.get_view_builder(request)
|
||||||
result = [builder.build(port, is_detail)['port']
|
result = [builder.build(port, is_detail)['port']
|
||||||
for port in ports]
|
for port in ports]
|
||||||
return dict(ports=result)
|
return dict(ports=result)
|
||||||
except exception.NetworkNotFound as e:
|
except exception.NetworkNotFound as e:
|
||||||
return faults.Fault(faults.NetworkNotFound(e))
|
return faults.Fault(faults.NetworkNotFound(e))
|
||||||
|
|
||||||
def show(self, req, tenant_id, network_id, id):
|
def show(self, request, tenant_id, network_id, id):
|
||||||
""" Returns port details for given port and network """
|
""" Returns port details for given port and network """
|
||||||
try:
|
try:
|
||||||
port = self.network_manager.get_port_details(
|
port = self.network_manager.get_port_details(
|
||||||
tenant_id, network_id, id)
|
tenant_id, network_id, id)
|
||||||
builder = ports_view.get_view_builder(req)
|
builder = ports_view.get_view_builder(request)
|
||||||
#build response with details
|
#build response with details
|
||||||
result = builder.build(port, True)
|
result = builder.build(port, True)
|
||||||
return dict(ports=result)
|
return dict(ports=result)
|
||||||
@ -80,19 +76,19 @@ class Controller(common.QuantumController):
|
|||||||
except exception.PortNotFound as 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):
|
def create(self, request, tenant_id, network_id):
|
||||||
""" Creates a new port for a given network """
|
""" Creates a new port for a given network """
|
||||||
#look for port state in request
|
#look for port state in request
|
||||||
try:
|
try:
|
||||||
req_params = \
|
request_params = \
|
||||||
self._parse_request_params(req, self._port_ops_param_list)
|
self._parse_request_params(request, self._port_ops_param_list)
|
||||||
except exc.HTTPError as e:
|
except exc.HTTPError as e:
|
||||||
return faults.Fault(e)
|
return faults.Fault(e)
|
||||||
try:
|
try:
|
||||||
port = self.network_manager.create_port(tenant_id,
|
port = self.network_manager.create_port(tenant_id,
|
||||||
network_id,
|
network_id,
|
||||||
req_params['port-state'])
|
request_params['port-state'])
|
||||||
builder = ports_view.get_view_builder(req)
|
builder = ports_view.get_view_builder(request)
|
||||||
result = builder.build(port)
|
result = builder.build(port)
|
||||||
return dict(ports=result)
|
return dict(ports=result)
|
||||||
except exception.NetworkNotFound as e:
|
except exception.NetworkNotFound as e:
|
||||||
@ -100,18 +96,19 @@ class Controller(common.QuantumController):
|
|||||||
except exception.StateInvalid as e:
|
except exception.StateInvalid as e:
|
||||||
return faults.Fault(faults.RequestedStateInvalid(e))
|
return faults.Fault(faults.RequestedStateInvalid(e))
|
||||||
|
|
||||||
def update(self, req, tenant_id, network_id, id):
|
def update(self, request, tenant_id, network_id, id):
|
||||||
""" Updates the state of a port for a given network """
|
""" Updates the state of a port for a given network """
|
||||||
#look for port state in request
|
#look for port state in request
|
||||||
try:
|
try:
|
||||||
req_params = \
|
request_params = \
|
||||||
self._parse_request_params(req, self._port_ops_param_list)
|
self._parse_request_params(request, self._port_ops_param_list)
|
||||||
except exc.HTTPError as e:
|
except exc.HTTPError as e:
|
||||||
return faults.Fault(e)
|
return faults.Fault(e)
|
||||||
try:
|
try:
|
||||||
port = self.network_manager.update_port(tenant_id, network_id, id,
|
port = self.network_manager.\
|
||||||
req_params['port-state'])
|
update_port(tenant_id, network_id, id,
|
||||||
builder = ports_view.get_view_builder(req)
|
request_params['port-state'])
|
||||||
|
builder = ports_view.get_view_builder(request)
|
||||||
result = builder.build(port, True)
|
result = builder.build(port, True)
|
||||||
return dict(ports=result)
|
return dict(ports=result)
|
||||||
except exception.NetworkNotFound as e:
|
except exception.NetworkNotFound as e:
|
||||||
@ -121,7 +118,7 @@ class Controller(common.QuantumController):
|
|||||||
except exception.StateInvalid as e:
|
except exception.StateInvalid as e:
|
||||||
return faults.Fault(faults.RequestedStateInvalid(e))
|
return faults.Fault(faults.RequestedStateInvalid(e))
|
||||||
|
|
||||||
def delete(self, req, tenant_id, network_id, id):
|
def delete(self, request, tenant_id, network_id, id):
|
||||||
""" Destroys the port with the given id """
|
""" Destroys the port with the given id """
|
||||||
#look for port state in request
|
#look for port state in request
|
||||||
try:
|
try:
|
||||||
@ -135,7 +132,7 @@ class Controller(common.QuantumController):
|
|||||||
except exception.PortInUse as e:
|
except exception.PortInUse as e:
|
||||||
return faults.Fault(faults.PortInUse(e))
|
return faults.Fault(faults.PortInUse(e))
|
||||||
|
|
||||||
def get_resource(self, req, tenant_id, network_id, id):
|
def get_resource(self, request, tenant_id, network_id, id):
|
||||||
try:
|
try:
|
||||||
result = self.network_manager.get_interface_details(
|
result = self.network_manager.get_interface_details(
|
||||||
tenant_id, network_id, id)
|
tenant_id, network_id, id)
|
||||||
@ -145,17 +142,20 @@ class Controller(common.QuantumController):
|
|||||||
except exception.PortNotFound as e:
|
except exception.PortNotFound as e:
|
||||||
return faults.Fault(faults.PortNotFound(e))
|
return faults.Fault(faults.PortNotFound(e))
|
||||||
|
|
||||||
def attach_resource(self, req, tenant_id, network_id, id):
|
|
||||||
|
def attach_resource(self, request, tenant_id, network_id, id):
|
||||||
|
content_type = request.best_match_content_type()
|
||||||
|
print "Content type:%s" % content_type
|
||||||
try:
|
try:
|
||||||
req_params = \
|
request_params = \
|
||||||
self._parse_request_params(req,
|
self._parse_request_params(request,
|
||||||
self._attachment_ops_param_list)
|
self._attachment_ops_param_list)
|
||||||
except exc.HTTPError as e:
|
except exc.HTTPError as e:
|
||||||
return faults.Fault(e)
|
return faults.Fault(e)
|
||||||
try:
|
try:
|
||||||
self.network_manager.plug_interface(tenant_id,
|
self.network_manager.plug_interface(tenant_id,
|
||||||
network_id, id,
|
network_id, id,
|
||||||
req_params['attachment-id'])
|
request_params['attachment-id'])
|
||||||
return exc.HTTPAccepted()
|
return exc.HTTPAccepted()
|
||||||
except exception.NetworkNotFound as e:
|
except exception.NetworkNotFound as e:
|
||||||
return faults.Fault(faults.NetworkNotFound(e))
|
return faults.Fault(faults.NetworkNotFound(e))
|
||||||
@ -166,7 +166,7 @@ class Controller(common.QuantumController):
|
|||||||
except exception.AlreadyAttached as e:
|
except exception.AlreadyAttached as e:
|
||||||
return faults.Fault(faults.AlreadyAttached(e))
|
return faults.Fault(faults.AlreadyAttached(e))
|
||||||
|
|
||||||
def detach_resource(self, req, tenant_id, network_id, id):
|
def detach_resource(self, request, tenant_id, network_id, id):
|
||||||
try:
|
try:
|
||||||
self.network_manager.unplug_interface(tenant_id,
|
self.network_manager.unplug_interface(tenant_id,
|
||||||
network_id, id)
|
network_id, id)
|
||||||
|
503
quantum/cli.py
503
quantum/cli.py
@ -1,6 +1,7 @@
|
|||||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
# Copyright 2011, Nicira Networks, Inc.
|
# Copyright 2011 Nicira Networks, Inc.
|
||||||
|
# Copyright 2011 Citrix Systems
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
# 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
|
# not use this file except in compliance with the License. You may obtain
|
||||||
@ -14,96 +15,436 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
# @author: Somik Behera, Nicira Networks, Inc.
|
# @author: Somik Behera, Nicira Networks, Inc.
|
||||||
|
# @author: Brad Hall, Nicira Networks, Inc.
|
||||||
|
|
||||||
|
import httplib
|
||||||
|
import logging as LOG
|
||||||
|
import json
|
||||||
|
import socket
|
||||||
import sys
|
import sys
|
||||||
|
import urllib
|
||||||
|
|
||||||
from manager import QuantumManager
|
from manager import QuantumManager
|
||||||
|
from optparse import OptionParser
|
||||||
|
from quantum.common.wsgi import Serializer
|
||||||
|
|
||||||
|
FORMAT = "json"
|
||||||
|
CONTENT_TYPE = "application/" + FORMAT
|
||||||
|
|
||||||
|
|
||||||
def usage():
|
### --- Miniclient (taking from the test directory)
|
||||||
print "\nUsage:"
|
### TODO(bgh): move this to a library within quantum
|
||||||
print "list_nets <tenant-id>"
|
class MiniClient(object):
|
||||||
print "create_net <tenant-id> <net-name>"
|
"""A base client class - derived from Glance.BaseClient"""
|
||||||
print "delete_net <tenant-id> <net-id>"
|
action_prefix = '/v0.1/tenants/{tenant_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"
|
|
||||||
|
|
||||||
if len(sys.argv) < 2 or len(sys.argv) > 6:
|
def __init__(self, host, port, use_ssl):
|
||||||
usage()
|
self.host = host
|
||||||
exit(1)
|
self.port = port
|
||||||
|
self.use_ssl = use_ssl
|
||||||
|
self.connection = None
|
||||||
|
|
||||||
quantum = QuantumManager()
|
def get_connection_type(self):
|
||||||
manager = quantum.get_manager()
|
if self.use_ssl:
|
||||||
|
return httplib.HTTPSConnection
|
||||||
|
else:
|
||||||
|
return httplib.HTTPConnection
|
||||||
|
|
||||||
if sys.argv[1] == "list_nets" and len(sys.argv) == 3:
|
def do_request(self, tenant, method, action, body=None,
|
||||||
network_on_tenant = manager.get_all_networks(sys.argv[2])
|
headers=None, params=None):
|
||||||
print "Virtual Networks on Tenant:%s\n" % sys.argv[2]
|
action = MiniClient.action_prefix + action
|
||||||
for k, v in network_on_tenant.iteritems():
|
action = action.replace('{tenant_id}', tenant)
|
||||||
print"\tNetwork ID:%s \n\tNetwork Name:%s \n" % (k, v)
|
if type(params) is dict:
|
||||||
elif sys.argv[1] == "create_net" and len(sys.argv) == 4:
|
action += '?' + urllib.urlencode(params)
|
||||||
new_net_id = manager.create_network(sys.argv[2], sys.argv[3])
|
try:
|
||||||
|
connection_type = self.get_connection_type()
|
||||||
|
headers = headers or {}
|
||||||
|
# Open connection and send request
|
||||||
|
c = connection_type(self.host, self.port)
|
||||||
|
c.request(method, action, body, headers)
|
||||||
|
res = c.getresponse()
|
||||||
|
status_code = self.get_status_code(res)
|
||||||
|
if status_code in (httplib.OK, httplib.CREATED,
|
||||||
|
httplib.ACCEPTED, httplib.NO_CONTENT):
|
||||||
|
return res
|
||||||
|
else:
|
||||||
|
raise Exception("Server returned error: %s" % res.read())
|
||||||
|
except (socket.error, IOError), e:
|
||||||
|
raise Exception("Unable to connect to server. Got error: %s" % e)
|
||||||
|
|
||||||
|
def get_status_code(self, response):
|
||||||
|
if hasattr(response, 'status_int'):
|
||||||
|
return response.status_int
|
||||||
|
else:
|
||||||
|
return response.status
|
||||||
|
### -- end of miniclient
|
||||||
|
|
||||||
|
### -- Core CLI functions
|
||||||
|
|
||||||
|
|
||||||
|
def list_nets(manager, *args):
|
||||||
|
tenant_id = args[0]
|
||||||
|
networks = manager.get_all_networks(tenant_id)
|
||||||
|
print "Virtual Networks on Tenant:%s\n" % tenant_id
|
||||||
|
for net in networks:
|
||||||
|
id = net["net-id"]
|
||||||
|
name = net["net-name"]
|
||||||
|
print "\tNetwork ID:%s \n\tNetwork Name:%s \n" % (id, name)
|
||||||
|
|
||||||
|
|
||||||
|
def api_list_nets(client, *args):
|
||||||
|
tenant_id = args[0]
|
||||||
|
res = client.do_request(tenant_id, 'GET', "/networks." + FORMAT)
|
||||||
|
resdict = json.loads(res.read())
|
||||||
|
LOG.debug(resdict)
|
||||||
|
print "Virtual Networks on Tenant:%s\n" % tenant_id
|
||||||
|
for n in resdict["networks"]:
|
||||||
|
net_id = n["id"]
|
||||||
|
print "\tNetwork ID:%s\n" % (net_id)
|
||||||
|
# TODO(bgh): we should make this call pass back the name too
|
||||||
|
# name = n["net-name"]
|
||||||
|
# LOG.info("\tNetwork ID:%s \n\tNetwork Name:%s \n" % (id, name))
|
||||||
|
|
||||||
|
|
||||||
|
def create_net(manager, *args):
|
||||||
|
tid, name = args
|
||||||
|
new_net_id = manager.create_network(tid, name)
|
||||||
print "Created a new Virtual Network with ID:%s\n" % new_net_id
|
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]
|
def api_create_net(client, *args):
|
||||||
elif sys.argv[1] == "detail_net" and len(sys.argv) == 4:
|
tid, name = args
|
||||||
vif_list = manager.get_network_details(sys.argv[2], sys.argv[3])
|
data = {'network': {'network-name': '%s' % name}}
|
||||||
print "Remote Interfaces on Virtual Network:%s\n" % sys.argv[3]
|
body = Serializer().serialize(data, CONTENT_TYPE)
|
||||||
for iface in vif_list:
|
res = client.do_request(tid, 'POST', "/networks." + FORMAT, body=body)
|
||||||
print "\tRemote interface :%s" % iface
|
rd = json.loads(res.read())
|
||||||
elif sys.argv[1] == "rename_net" and len(sys.argv) == 5:
|
LOG.debug(rd)
|
||||||
manager.rename_network(sys.argv[2], sys.argv[3], sys.argv[4])
|
nid = None
|
||||||
print "Renamed Virtual Network with ID:%s" % sys.argv[3]
|
try:
|
||||||
elif sys.argv[1] == "list_ports" and len(sys.argv) == 4:
|
nid = rd["networks"]["network"]["id"]
|
||||||
ports = manager.get_all_ports(sys.argv[2], sys.argv[3])
|
except Exception, e:
|
||||||
print " Virtual Ports on Virtual Network:%s\n" % sys.argv[3]
|
print "Failed to create network"
|
||||||
for port in ports:
|
# TODO(bgh): grab error details from ws request result
|
||||||
print "\tVirtual Port:%s" % port
|
return
|
||||||
elif sys.argv[1] == "create_port" and len(sys.argv) == 4:
|
print "Created a new Virtual Network with ID:%s\n" % nid
|
||||||
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])
|
def delete_net(manager, *args):
|
||||||
elif sys.argv[1] == "delete_port" and len(sys.argv) == 5:
|
tid, nid = args
|
||||||
manager.delete_port(sys.argv[2], sys.argv[3], sys.argv[4])
|
manager.delete_network(tid, nid)
|
||||||
print "Deleted Virtual Port:%s " \
|
print "Deleted Virtual Network with ID:%s" % nid
|
||||||
"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],
|
def api_delete_net(client, *args):
|
||||||
sys.argv[3], sys.argv[4])
|
tid, nid = args
|
||||||
print "Virtual Port:%s on Virtual Network:%s " \
|
res = client.do_request(tid, 'DELETE', "/networks/" + nid + "." + FORMAT)
|
||||||
"contains remote interface:%s" % (sys.argv[3],
|
status = res.status
|
||||||
sys.argv[4],
|
if status != 202:
|
||||||
port_detail)
|
print "Failed to delete network"
|
||||||
elif sys.argv[1] == "plug_iface" and len(sys.argv) == 6:
|
output = res.read()
|
||||||
manager.plug_interface(sys.argv[2], sys.argv[3], sys.argv[4], sys.argv[5])
|
print output
|
||||||
print "Plugged remote interface:%s " \
|
else:
|
||||||
"into Virtual Network:%s" % (sys.argv[5], sys.argv[3])
|
print "Deleted Virtual Network with ID:%s" % nid
|
||||||
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 " \
|
def detail_net(manager, *args):
|
||||||
"from Virtual Port:%s Virtual Network:%s" % (sys.argv[4],
|
tid, nid = args
|
||||||
sys.argv[3])
|
iface_list = manager.get_network_details(tid, nid)
|
||||||
elif sys.argv[1] == "detail_iface" and len(sys.argv) == 5:
|
print "Remote Interfaces on Virtual Network:%s\n" % nid
|
||||||
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:
|
for iface in iface_list:
|
||||||
print "\tRemote interface:%s" % iface
|
print "\tRemote interface:%s" % iface
|
||||||
elif sys.argv[1] == "all" and len(sys.argv) == 2:
|
|
||||||
print "Not Implemented"
|
|
||||||
|
def api_detail_net(client, *args):
|
||||||
|
tid, nid = args
|
||||||
|
res = client.do_request(tid, 'GET',
|
||||||
|
"/networks/%s/ports.%s" % (nid, FORMAT))
|
||||||
|
output = res.read()
|
||||||
|
if res.status != 200:
|
||||||
|
LOG.error("Failed to list ports: %s" % output)
|
||||||
|
return
|
||||||
|
rd = json.loads(output)
|
||||||
|
LOG.debug(rd)
|
||||||
|
print "Remote Interfaces on Virtual Network:%s\n" % nid
|
||||||
|
for port in rd["ports"]:
|
||||||
|
pid = port["id"]
|
||||||
|
res = client.do_request(tid, 'GET',
|
||||||
|
"/networks/%s/ports/%s/attachment.%s" % (nid, pid, FORMAT))
|
||||||
|
output = res.read()
|
||||||
|
rd = json.loads(output)
|
||||||
|
LOG.debug(rd)
|
||||||
|
remote_iface = rd["attachment"]
|
||||||
|
print "\tRemote interface:%s" % remote_iface
|
||||||
|
|
||||||
|
|
||||||
|
def rename_net(manager, *args):
|
||||||
|
tid, nid, name = args
|
||||||
|
manager.rename_network(tid, nid, name)
|
||||||
|
print "Renamed Virtual Network with ID:%s" % nid
|
||||||
|
|
||||||
|
|
||||||
|
def api_rename_net(client, *args):
|
||||||
|
tid, nid, name = args
|
||||||
|
data = {'network': {'network-name': '%s' % name}}
|
||||||
|
body = Serializer().serialize(data, CONTENT_TYPE)
|
||||||
|
res = client.do_request(tid, 'PUT', "/networks/%s.%s" % (nid, FORMAT),
|
||||||
|
body=body)
|
||||||
|
resdict = json.loads(res.read())
|
||||||
|
LOG.debug(resdict)
|
||||||
|
print "Renamed Virtual Network with ID:%s" % nid
|
||||||
|
|
||||||
|
|
||||||
|
def list_ports(manager, *args):
|
||||||
|
tid, nid = args
|
||||||
|
ports = manager.get_all_ports(tid, nid)
|
||||||
|
print "Ports on Virtual Network:%s\n" % nid
|
||||||
|
for port in ports:
|
||||||
|
print "\tVirtual Port:%s" % port["port-id"]
|
||||||
|
|
||||||
|
|
||||||
|
def api_list_ports(client, *args):
|
||||||
|
tid, nid = args
|
||||||
|
res = client.do_request(tid, 'GET',
|
||||||
|
"/networks/%s/ports.%s" % (nid, FORMAT))
|
||||||
|
output = res.read()
|
||||||
|
if res.status != 200:
|
||||||
|
LOG.error("Failed to list ports: %s" % output)
|
||||||
|
return
|
||||||
|
rd = json.loads(output)
|
||||||
|
LOG.debug(rd)
|
||||||
|
print "Ports on Virtual Network:%s\n" % nid
|
||||||
|
for port in rd["ports"]:
|
||||||
|
print "\tVirtual Port:%s" % port["id"]
|
||||||
|
|
||||||
|
|
||||||
|
def create_port(manager, *args):
|
||||||
|
tid, nid = args
|
||||||
|
new_port = manager.create_port(tid, nid)
|
||||||
|
print "Created Virtual Port:%s " \
|
||||||
|
"on Virtual Network:%s" % (new_port, nid)
|
||||||
|
|
||||||
|
|
||||||
|
def api_create_port(client, *args):
|
||||||
|
tid, nid = args
|
||||||
|
res = client.do_request(tid, 'POST',
|
||||||
|
"/networks/%s/ports.%s" % (nid, FORMAT))
|
||||||
|
output = res.read()
|
||||||
|
if res.status != 200:
|
||||||
|
LOG.error("Failed to create port: %s" % output)
|
||||||
|
return
|
||||||
|
rd = json.loads(output)
|
||||||
|
new_port = rd["ports"]["port"]["id"]
|
||||||
|
print "Created Virtual Port:%s " \
|
||||||
|
"on Virtual Network:%s" % (new_port, nid)
|
||||||
|
|
||||||
|
|
||||||
|
def delete_port(manager, *args):
|
||||||
|
tid, nid, pid = args
|
||||||
|
LOG.info("Deleted Virtual Port:%s " \
|
||||||
|
"on Virtual Network:%s" % (pid, nid))
|
||||||
|
|
||||||
|
|
||||||
|
def api_delete_port(client, *args):
|
||||||
|
tid, nid, pid = args
|
||||||
|
res = client.do_request(tid, 'DELETE',
|
||||||
|
"/networks/%s/ports/%s.%s" % (nid, pid, FORMAT))
|
||||||
|
output = res.read()
|
||||||
|
if res.status != 202:
|
||||||
|
LOG.error("Failed to delete port: %s" % output)
|
||||||
|
return
|
||||||
|
LOG.info("Deleted Virtual Port:%s " \
|
||||||
|
"on Virtual Network:%s" % (pid, nid))
|
||||||
|
|
||||||
|
|
||||||
|
def detail_port(manager, *args):
|
||||||
|
tid, nid, pid = args
|
||||||
|
port_detail = manager.get_port_details(tid, nid, pid)
|
||||||
|
print "Virtual Port:%s on Virtual Network:%s " \
|
||||||
|
"contains remote interface:%s" % (pid, nid, port_detail)
|
||||||
|
|
||||||
|
|
||||||
|
def api_detail_port(client, *args):
|
||||||
|
tid, nid, pid = args
|
||||||
|
res = client.do_request(tid, 'GET',
|
||||||
|
"/networks/%s/ports/%s.%s" % (nid, pid, FORMAT))
|
||||||
|
output = res.read()
|
||||||
|
if res.status != 200:
|
||||||
|
LOG.error("Failed to get port details: %s" % output)
|
||||||
|
return
|
||||||
|
rd = json.loads(output)
|
||||||
|
port = rd["ports"]["port"]
|
||||||
|
id = port["id"]
|
||||||
|
attachment = port["attachment"]
|
||||||
|
LOG.debug(port)
|
||||||
|
print "Virtual Port:%s on Virtual Network:%s " \
|
||||||
|
"contains remote interface:%s" % (pid, nid, attachment)
|
||||||
|
|
||||||
|
|
||||||
|
def plug_iface(manager, *args):
|
||||||
|
tid, nid, pid, vid = args
|
||||||
|
manager.plug_interface(tid, nid, pid, vid)
|
||||||
|
print "Plugged remote interface:%s " \
|
||||||
|
"into Virtual Network:%s" % (vid, nid)
|
||||||
|
|
||||||
|
|
||||||
|
def api_plug_iface(client, *args):
|
||||||
|
tid, nid, pid, vid = args
|
||||||
|
data = {'port': {'attachment-id': '%s' % vid}}
|
||||||
|
body = Serializer().serialize(data, CONTENT_TYPE)
|
||||||
|
res = client.do_request(tid, 'PUT',
|
||||||
|
"/networks/%s/ports/%s/attachment.%s" % (nid, pid, FORMAT), body=body)
|
||||||
|
output = res.read()
|
||||||
|
LOG.debug(output)
|
||||||
|
if res.status != 202:
|
||||||
|
LOG.error("Failed to plug iface \"%s\" to port \"%s\": %s" % (vid,
|
||||||
|
pid, output))
|
||||||
|
return
|
||||||
|
print "Plugged interface \"%s\" to port:%s on network:%s" % (vid, pid, nid)
|
||||||
|
|
||||||
|
|
||||||
|
def unplug_iface(manager, *args):
|
||||||
|
tid, nid, pid = args
|
||||||
|
manager.unplug_interface(tid, nid, pid)
|
||||||
|
print "UnPlugged remote interface " \
|
||||||
|
"from Virtual Port:%s Virtual Network:%s" % (pid, nid)
|
||||||
|
|
||||||
|
|
||||||
|
def api_unplug_iface(client, *args):
|
||||||
|
tid, nid, pid = args
|
||||||
|
data = {'port': {'attachment-id': ''}}
|
||||||
|
body = Serializer().serialize(data, CONTENT_TYPE)
|
||||||
|
res = client.do_request(tid, 'DELETE',
|
||||||
|
"/networks/%s/ports/%s/attachment.%s" % (nid, pid, FORMAT), body=body)
|
||||||
|
output = res.read()
|
||||||
|
LOG.debug(output)
|
||||||
|
if res.status != 202:
|
||||||
|
LOG.error("Failed to unplug iface from port \"%s\": %s" % (vid,
|
||||||
|
pid, output))
|
||||||
|
return
|
||||||
|
print "Unplugged interface from port:%s on network:%s" % (pid, nid)
|
||||||
|
|
||||||
|
|
||||||
|
commands = {
|
||||||
|
"list_nets": {
|
||||||
|
"func": list_nets,
|
||||||
|
"api_func": api_list_nets,
|
||||||
|
"args": ["tenant-id"]},
|
||||||
|
"create_net": {
|
||||||
|
"func": create_net,
|
||||||
|
"api_func": api_create_net,
|
||||||
|
"args": ["tenant-id", "net-name"]},
|
||||||
|
"delete_net": {
|
||||||
|
"func": delete_net,
|
||||||
|
"api_func": api_delete_net,
|
||||||
|
"args": ["tenant-id", "net-id"]},
|
||||||
|
"detail_net": {
|
||||||
|
"func": detail_net,
|
||||||
|
"api_func": api_detail_net,
|
||||||
|
"args": ["tenant-id", "net-id"]},
|
||||||
|
"rename_net": {
|
||||||
|
"func": rename_net,
|
||||||
|
"api_func": api_rename_net,
|
||||||
|
"args": ["tenant-id", "net-id", "new-name"]},
|
||||||
|
"list_ports": {
|
||||||
|
"func": list_ports,
|
||||||
|
"api_func": api_list_ports,
|
||||||
|
"args": ["tenant-id", "net-id"]},
|
||||||
|
"create_port": {
|
||||||
|
"func": create_port,
|
||||||
|
"api_func": api_create_port,
|
||||||
|
"args": ["tenant-id", "net-id"]},
|
||||||
|
"delete_port": {
|
||||||
|
"func": delete_port,
|
||||||
|
"api_func": api_delete_port,
|
||||||
|
"args": ["tenant-id", "net-id", "port-id"]},
|
||||||
|
"detail_port": {
|
||||||
|
"func": detail_port,
|
||||||
|
"api_func": api_detail_port,
|
||||||
|
"args": ["tenant-id", "net-id", "port-id"]},
|
||||||
|
"plug_iface": {
|
||||||
|
"func": plug_iface,
|
||||||
|
"api_func": api_plug_iface,
|
||||||
|
"args": ["tenant-id", "net-id", "port-id", "iface-id"]},
|
||||||
|
"unplug_iface": {
|
||||||
|
"func": unplug_iface,
|
||||||
|
"api_func": api_unplug_iface,
|
||||||
|
"args": ["tenant-id", "net-id", "port-id"]}, }
|
||||||
|
|
||||||
|
|
||||||
|
def help():
|
||||||
|
print "\nCommands:"
|
||||||
|
for k in commands.keys():
|
||||||
|
print " %s %s" % (k,
|
||||||
|
" ".join(["<%s>" % y for y in commands[k]["args"]]))
|
||||||
|
|
||||||
|
|
||||||
|
def build_args(cmd, cmdargs, arglist):
|
||||||
|
args = []
|
||||||
|
orig_arglist = arglist[:]
|
||||||
|
try:
|
||||||
|
for x in cmdargs:
|
||||||
|
args.append(arglist[0])
|
||||||
|
del arglist[0]
|
||||||
|
except Exception, e:
|
||||||
|
LOG.error("Not enough arguments for \"%s\" (expected: %d, got: %d)" % (
|
||||||
|
cmd, len(cmdargs), len(orig_arglist)))
|
||||||
|
print "Usage:\n %s %s" % (cmd,
|
||||||
|
" ".join(["<%s>" % y for y in commands[cmd]["args"]]))
|
||||||
|
return None
|
||||||
|
if len(arglist) > 0:
|
||||||
|
LOG.error("Too many arguments for \"%s\" (expected: %d, got: %d)" % (
|
||||||
|
cmd, len(cmdargs), len(orig_arglist)))
|
||||||
|
print "Usage:\n %s %s" % (cmd,
|
||||||
|
" ".join(["<%s>" % y for y in commands[cmd]["args"]]))
|
||||||
|
return None
|
||||||
|
return args
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
usagestr = "Usage: %prog [OPTIONS] <command> [args]"
|
||||||
|
parser = OptionParser(usage=usagestr)
|
||||||
|
parser.add_option("-l", "--load-plugin", dest="load_plugin",
|
||||||
|
action="store_true", default=False,
|
||||||
|
help="Load plugin directly instead of using WS API")
|
||||||
|
parser.add_option("-H", "--host", dest="host",
|
||||||
|
type="string", default="127.0.0.1", help="ip address of api host")
|
||||||
|
parser.add_option("-p", "--port", dest="port",
|
||||||
|
type="int", default=9696, help="api poort")
|
||||||
|
parser.add_option("-s", "--ssl", dest="ssl",
|
||||||
|
action="store_true", default=False, help="use ssl")
|
||||||
|
parser.add_option("-v", "--verbose", dest="verbose",
|
||||||
|
action="store_true", default=False, help="turn on verbose logging")
|
||||||
|
|
||||||
|
options, args = parser.parse_args()
|
||||||
|
|
||||||
|
if options.verbose:
|
||||||
|
LOG.basicConfig(level=LOG.DEBUG)
|
||||||
else:
|
else:
|
||||||
print "invalid arguments: %s" % str(sys.argv)
|
LOG.basicConfig(level=LOG.WARN)
|
||||||
usage()
|
|
||||||
|
if len(args) < 1:
|
||||||
|
parser.print_help()
|
||||||
|
help()
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
cmd = args[0]
|
||||||
|
if cmd not in commands.keys():
|
||||||
|
LOG.error("Unknown command: %s" % cmd)
|
||||||
|
help()
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
args = build_args(cmd, commands[cmd]["args"], args[1:])
|
||||||
|
if not args:
|
||||||
|
sys.exit(1)
|
||||||
|
LOG.debug("Executing command \"%s\" with args: %s" % (cmd, args))
|
||||||
|
if not options.load_plugin:
|
||||||
|
client = MiniClient(options.host, options.port, options.ssl)
|
||||||
|
if "api_func" not in commands[cmd]:
|
||||||
|
LOG.error("API version of \"%s\" is not yet implemented" % cmd)
|
||||||
|
sys.exit(1)
|
||||||
|
commands[cmd]["api_func"](client, *args)
|
||||||
|
else:
|
||||||
|
quantum = QuantumManager()
|
||||||
|
manager = quantum.get_manager()
|
||||||
|
commands[cmd]["func"](manager, *args)
|
||||||
|
sys.exit(0)
|
||||||
|
@ -255,7 +255,7 @@ def load_paste_config(app_name, options, args):
|
|||||||
% (conf_file, e))
|
% (conf_file, e))
|
||||||
|
|
||||||
|
|
||||||
def load_paste_app(conf_file, app_name):
|
def load_paste_app(app_name, options, args):
|
||||||
"""
|
"""
|
||||||
Builds and returns a WSGI app from a paste config file.
|
Builds and returns a WSGI app from a paste config file.
|
||||||
|
|
||||||
@ -276,16 +276,15 @@ def load_paste_app(conf_file, app_name):
|
|||||||
:raises RuntimeError when config file cannot be located or application
|
:raises RuntimeError when config file cannot be located or application
|
||||||
cannot be loaded from config file
|
cannot be loaded from config file
|
||||||
"""
|
"""
|
||||||
#conf_file, conf = load_paste_config(app_name, options, args)
|
conf_file, conf = load_paste_config(app_name, options, args)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
conf_file = os.path.abspath(conf_file)
|
|
||||||
app = deploy.loadapp("config:%s" % conf_file, name=app_name)
|
app = deploy.loadapp("config:%s" % conf_file, name=app_name)
|
||||||
except (LookupError, ImportError), e:
|
except (LookupError, ImportError), e:
|
||||||
raise RuntimeError("Unable to load %(app_name)s from "
|
raise RuntimeError("Unable to load %(app_name)s from "
|
||||||
"configuration file %(conf_file)s."
|
"configuration file %(conf_file)s."
|
||||||
"\nGot: %(e)r" % locals())
|
"\nGot: %(e)r" % locals())
|
||||||
return app
|
return conf, app
|
||||||
|
|
||||||
|
|
||||||
def get_option(options, option, **kwargs):
|
def get_option(options, option, **kwargs):
|
||||||
|
@ -192,7 +192,7 @@ def parse_isotime(timestr):
|
|||||||
|
|
||||||
def getPluginFromConfig(file="config.ini"):
|
def getPluginFromConfig(file="config.ini"):
|
||||||
Config = ConfigParser.ConfigParser()
|
Config = ConfigParser.ConfigParser()
|
||||||
Config.read(os.path.join(FLAGS.state_path, file))
|
Config.read(file)
|
||||||
return Config.get("PLUGIN", "provider")
|
return Config.get("PLUGIN", "provider")
|
||||||
|
|
||||||
|
|
||||||
|
@ -340,7 +340,7 @@ class Controller(object):
|
|||||||
del arg_dict['action']
|
del arg_dict['action']
|
||||||
if 'format' in arg_dict:
|
if 'format' in arg_dict:
|
||||||
del arg_dict['format']
|
del arg_dict['format']
|
||||||
arg_dict['req'] = req
|
arg_dict['request'] = req
|
||||||
result = method(**arg_dict)
|
result = method(**arg_dict)
|
||||||
|
|
||||||
if type(result) is dict:
|
if type(result) is dict:
|
||||||
@ -528,7 +528,7 @@ class Serializer(object):
|
|||||||
node = self._to_xml_node(doc, metadata, k, v)
|
node = self._to_xml_node(doc, metadata, k, v)
|
||||||
result.appendChild(node)
|
result.appendChild(node)
|
||||||
else:
|
else:
|
||||||
# Type is atom
|
# Type is atom.
|
||||||
node = doc.createTextNode(str(data))
|
node = doc.createTextNode(str(data))
|
||||||
result.appendChild(node)
|
result.appendChild(node)
|
||||||
return result
|
return result
|
||||||
|
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.
|
187
quantum/db/api.py
Normal file
187
quantum/db/api.py
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
# 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)
|
63
quantum/db/models.py
Normal file
63
quantum/db/models.py
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
# 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)
|
@ -18,27 +18,39 @@
|
|||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Quantum's Manager class is responsible for parsing a config file
|
Quantum's Manager class is responsible for parsing a config file and
|
||||||
and instantiating the correct plugin that concretely implement
|
instantiating the correct plugin that concretely implement quantum_plugin_base
|
||||||
quantum_plugin_base class
|
class.
|
||||||
|
|
||||||
The caller should make sure that QuantumManager is a singleton.
|
The caller should make sure that QuantumManager is a singleton.
|
||||||
"""
|
"""
|
||||||
import gettext
|
import gettext
|
||||||
|
import os
|
||||||
gettext.install('quantum', unicode=1)
|
gettext.install('quantum', unicode=1)
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
from common import utils
|
from common import utils
|
||||||
from quantum_plugin_base import QuantumPluginBase
|
from quantum_plugin_base import QuantumPluginBase
|
||||||
|
|
||||||
CONFIG_FILE = "quantum/plugins.ini"
|
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):
|
class QuantumManager(object):
|
||||||
|
|
||||||
def __init__(self, config=CONFIG_FILE):
|
def __init__(self, config=None):
|
||||||
self.configuration_file = CONFIG_FILE
|
if config == None:
|
||||||
plugin_location = utils.getPluginFromConfig(CONFIG_FILE)
|
self.configuration_file = find_config(
|
||||||
print "PLUGIN LOCATION:%s" % plugin_location
|
os.path.abspath(os.path.dirname(__file__)))
|
||||||
|
else:
|
||||||
|
self.configuration_file = config
|
||||||
|
plugin_location = utils.getPluginFromConfig(self.configuration_file)
|
||||||
plugin_klass = utils.import_class(plugin_location)
|
plugin_klass = utils.import_class(plugin_location)
|
||||||
if not issubclass(plugin_klass, QuantumPluginBase):
|
if not issubclass(plugin_klass, QuantumPluginBase):
|
||||||
raise Exception("Configured Quantum plug-in " \
|
raise Exception("Configured Quantum plug-in " \
|
||||||
@ -50,16 +62,3 @@ class QuantumManager(object):
|
|||||||
|
|
||||||
def get_manager(self):
|
def get_manager(self):
|
||||||
return self.plugin
|
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()
|
|
||||||
|
@ -86,6 +86,12 @@ class QuantumEchoPlugin(object):
|
|||||||
"""
|
"""
|
||||||
print("delete_port() called\n")
|
print("delete_port() called\n")
|
||||||
|
|
||||||
|
def update_port(self, tenant_id, net_id, port_id, port_state):
|
||||||
|
"""
|
||||||
|
Updates the state of a port on the specified Virtual Network.
|
||||||
|
"""
|
||||||
|
print("update_port() called\n")
|
||||||
|
|
||||||
def get_port_details(self, tenant_id, net_id, port_id):
|
def get_port_details(self, tenant_id, net_id, port_id):
|
||||||
"""
|
"""
|
||||||
This method allows the user to retrieve a remote interface
|
This method allows the user to retrieve a remote interface
|
||||||
@ -161,9 +167,7 @@ class DummyDataPlugin(object):
|
|||||||
retrieved a list of all the remote vifs that
|
retrieved a list of all the remote vifs that
|
||||||
are attached to the network
|
are attached to the network
|
||||||
"""
|
"""
|
||||||
print("get_network_details() called\n")
|
vifs_on_net = ["/tenant1/networks/net_id/portid/vif2.0"]
|
||||||
vifs_on_net = ["/tenant1/networks/net_id/portid/vif2.0",
|
|
||||||
"/tenant1/networks/10/121/vif1.1"]
|
|
||||||
return vifs_on_net
|
return vifs_on_net
|
||||||
|
|
||||||
def rename_network(self, tenant_id, net_id, new_name):
|
def rename_network(self, tenant_id, net_id, new_name):
|
||||||
@ -222,27 +226,6 @@ class DummyDataPlugin(object):
|
|||||||
"""
|
"""
|
||||||
print("unplug_interface() called\n")
|
print("unplug_interface() called\n")
|
||||||
|
|
||||||
def get_interface_details(self, tenant_id, net_id, port_id):
|
|
||||||
"""
|
|
||||||
Retrieves the remote interface that is attached at this
|
|
||||||
particular port.
|
|
||||||
"""
|
|
||||||
print("get_interface_details() called\n")
|
|
||||||
#returns the remote interface UUID
|
|
||||||
return "/tenant1/networks/net_id/portid/vif2.0"
|
|
||||||
|
|
||||||
def get_all_attached_interfaces(self, tenant_id, net_id):
|
|
||||||
"""
|
|
||||||
Retrieves all remote interfaces that are attached to
|
|
||||||
a particular Virtual Network.
|
|
||||||
"""
|
|
||||||
print("get_all_attached_interfaces() called\n")
|
|
||||||
# returns a list of all attached remote interfaces
|
|
||||||
vifs_on_net = ["/tenant1/networks/net_id/portid/vif2.0",
|
|
||||||
"/tenant1/networks/10/121/vif1.1"]
|
|
||||||
return vifs_on_net
|
|
||||||
|
|
||||||
|
|
||||||
class FakePlugin(object):
|
class FakePlugin(object):
|
||||||
"""
|
"""
|
||||||
FakePlugin is a demo plugin that provides
|
FakePlugin is a demo plugin that provides
|
||||||
@ -257,16 +240,14 @@ class FakePlugin(object):
|
|||||||
'attachment': None},
|
'attachment': None},
|
||||||
2: {'port-id': 2,
|
2: {'port-id': 2,
|
||||||
'port-state': 'UP',
|
'port-state': 'UP',
|
||||||
'attachment': None}
|
'attachment': None}}
|
||||||
}
|
|
||||||
_port_dict_2 = {
|
_port_dict_2 = {
|
||||||
1: {'port-id': 1,
|
1: {'port-id': 1,
|
||||||
'port-state': 'UP',
|
'port-state': 'UP',
|
||||||
'attachment': 'SomeFormOfVIFID'},
|
'attachment': 'SomeFormOfVIFID'},
|
||||||
2: {'port-id': 2,
|
2: {'port-id': 2,
|
||||||
'port-state': 'DOWN',
|
'port-state': 'DOWN',
|
||||||
'attachment': None}
|
'attachment': None}}
|
||||||
}
|
|
||||||
_networks = {'001':
|
_networks = {'001':
|
||||||
{
|
{
|
||||||
'net-id': '001',
|
'net-id': '001',
|
||||||
@ -277,8 +258,7 @@ class FakePlugin(object):
|
|||||||
{
|
{
|
||||||
'net-id': '002',
|
'net-id': '002',
|
||||||
'net-name': 'cicciotest',
|
'net-name': 'cicciotest',
|
||||||
'net-ports': _port_dict_2
|
'net-ports': _port_dict_2}}
|
||||||
}}
|
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
FakePlugin._net_counter = len(FakePlugin._networks)
|
FakePlugin._net_counter = len(FakePlugin._networks)
|
||||||
@ -341,6 +321,7 @@ class FakePlugin(object):
|
|||||||
new_net_dict = {'net-id': new_net_id,
|
new_net_dict = {'net-id': new_net_id,
|
||||||
'net-name': net_name,
|
'net-name': net_name,
|
||||||
'net-ports': {}}
|
'net-ports': {}}
|
||||||
|
|
||||||
FakePlugin._networks[new_net_id] = new_net_dict
|
FakePlugin._networks[new_net_id] = new_net_dict
|
||||||
# return network_id of the created network
|
# return network_id of the created network
|
||||||
return new_net_dict
|
return new_net_dict
|
||||||
@ -471,7 +452,6 @@ class FakePlugin(object):
|
|||||||
# Should unplug on port without attachment raise an Error?
|
# Should unplug on port without attachment raise an Error?
|
||||||
port['attachment'] = None
|
port['attachment'] = None
|
||||||
|
|
||||||
# TODO - neeed to update methods from this point onwards
|
|
||||||
def get_all_attached_interfaces(self, tenant_id, net_id):
|
def get_all_attached_interfaces(self, tenant_id, net_id):
|
||||||
"""
|
"""
|
||||||
Retrieves all remote interfaces that are attached to
|
Retrieves all remote interfaces that are attached to
|
||||||
|
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
|
107
quantum/plugins/openvswitch/README
Normal file
107
quantum/plugins/openvswitch/README
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
# -- 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. Note: if you're going to use
|
||||||
|
// a wildcard here it should be a management network with only trusted hosts.
|
||||||
|
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.
|
||||||
|
Make sure to specify this in your nova flagfile as --flat_network_bridge.
|
||||||
|
- 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- $ 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 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 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 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
|
||||||
|
|
||||||
|
(.. repeat for more ports and interface combinations..)
|
||||||
|
|
||||||
|
# -- Other items
|
||||||
|
|
||||||
|
- To get a listing of the vif names in the format 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
|
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 MySQL-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"
|
300
quantum/plugins/openvswitch/agent/ovs_quantum_agent.py
Executable file
300
quantum/plugins/openvswitch/agent/ovs_quantum_agent.py
Executable file
@ -0,0 +1,300 @@
|
|||||||
|
#!/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
|
||||||
|
|
76
quantum/plugins/openvswitch/ovs_db.py
Normal file
76
quantum/plugins/openvswitch/ovs_db.py
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
# 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()
|
59
quantum/plugins/openvswitch/ovs_models.py
Normal file
59
quantum/plugins/openvswitch/ovs_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
|
||||||
|
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
|
353
quantum/plugins/openvswitch/ovs_quantum_plugin.py
Normal file
353
quantum/plugins/openvswitch/ovs_quantum_plugin.py
Normal file
@ -0,0 +1,353 @@
|
|||||||
|
# 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)
|
@ -35,6 +35,20 @@ class QuantumPluginBase(object):
|
|||||||
Returns a dictionary containing all
|
Returns a dictionary containing all
|
||||||
<network_uuid, network_name> for
|
<network_uuid, network_name> for
|
||||||
the specified tenant.
|
the specified tenant.
|
||||||
|
:returns: a list of mapping sequences with the following signature:
|
||||||
|
[ {'net-id': uuid that uniquely identifies
|
||||||
|
the particular quantum network,
|
||||||
|
'net-name': a human-readable name associated
|
||||||
|
with network referenced by net-id
|
||||||
|
},
|
||||||
|
....
|
||||||
|
{'net-id': uuid that uniquely identifies the
|
||||||
|
particular quantum network,
|
||||||
|
'net-name': a human-readable name associated
|
||||||
|
with network referenced by net-id
|
||||||
|
}
|
||||||
|
]
|
||||||
|
:raises: None
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -43,6 +57,14 @@ class QuantumPluginBase(object):
|
|||||||
"""
|
"""
|
||||||
Creates a new Virtual Network, and assigns it
|
Creates a new Virtual Network, and assigns it
|
||||||
a symbolic name.
|
a symbolic name.
|
||||||
|
|
||||||
|
:returns: a sequence of mappings with the following signature:
|
||||||
|
{'net-id': uuid that uniquely identifies the
|
||||||
|
particular quantum network,
|
||||||
|
'net-name': a human-readable name associated
|
||||||
|
with network referenced by net-id
|
||||||
|
}
|
||||||
|
:raises:
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -51,14 +73,31 @@ class QuantumPluginBase(object):
|
|||||||
"""
|
"""
|
||||||
Deletes the network with the specified network identifier
|
Deletes the network with the specified network identifier
|
||||||
belonging to the specified tenant.
|
belonging to the specified tenant.
|
||||||
|
|
||||||
|
:returns: a sequence of mappings with the following signature:
|
||||||
|
{'net-id': uuid that uniquely identifies the
|
||||||
|
particular quantum network
|
||||||
|
}
|
||||||
|
:raises: exception.NetworkInUse
|
||||||
|
:raises: exception.NetworkNotFound
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_network_details(self, tenant_id, net_id):
|
def get_network_details(self, tenant_id, net_id):
|
||||||
"""
|
"""
|
||||||
retrieved a list of all the remote vifs that
|
Retrieves a list of all the remote vifs that
|
||||||
are attached to the network
|
are attached to the network.
|
||||||
|
|
||||||
|
:returns: a sequence of mappings with the following signature:
|
||||||
|
{'net-id': uuid that uniquely identifies the
|
||||||
|
particular quantum network
|
||||||
|
'net-name': a human-readable name associated
|
||||||
|
with network referenced by net-id
|
||||||
|
'net-ifaces': ['vif1_on_network_uuid',
|
||||||
|
'vif2_on_network_uuid',...,'vifn_uuid']
|
||||||
|
}
|
||||||
|
:raises: exception.NetworkNotFound
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -67,6 +106,15 @@ class QuantumPluginBase(object):
|
|||||||
"""
|
"""
|
||||||
Updates the symbolic name belonging to a particular
|
Updates the symbolic name belonging to a particular
|
||||||
Virtual Network.
|
Virtual Network.
|
||||||
|
|
||||||
|
:returns: a sequence of mappings representing the new network
|
||||||
|
attributes, with the following signature:
|
||||||
|
{'net-id': uuid that uniquely identifies the
|
||||||
|
particular quantum network
|
||||||
|
'net-name': the new human-readable name
|
||||||
|
associated with network referenced by net-id
|
||||||
|
}
|
||||||
|
:raises: exception.NetworkNotFound
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -75,6 +123,17 @@ class QuantumPluginBase(object):
|
|||||||
"""
|
"""
|
||||||
Retrieves all port identifiers belonging to the
|
Retrieves all port identifiers belonging to the
|
||||||
specified Virtual Network.
|
specified Virtual Network.
|
||||||
|
|
||||||
|
:returns: a list of mapping sequences with the following signature:
|
||||||
|
[ {'port-id': uuid representing a particular port
|
||||||
|
on the specified quantum network
|
||||||
|
},
|
||||||
|
....
|
||||||
|
{'port-id': uuid representing a particular port
|
||||||
|
on the specified quantum network
|
||||||
|
}
|
||||||
|
]
|
||||||
|
:raises: exception.NetworkNotFound
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -82,6 +141,13 @@ class QuantumPluginBase(object):
|
|||||||
def create_port(self, tenant_id, net_id, port_state=None):
|
def create_port(self, tenant_id, net_id, port_state=None):
|
||||||
"""
|
"""
|
||||||
Creates a port on the specified Virtual Network.
|
Creates a port on the specified Virtual Network.
|
||||||
|
|
||||||
|
:returns: a mapping sequence with the following signature:
|
||||||
|
{'port-id': uuid representing the created port
|
||||||
|
on specified quantum network
|
||||||
|
}
|
||||||
|
:raises: exception.NetworkNotFound
|
||||||
|
:raises: exception.StateInvalid
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -89,7 +155,15 @@ class QuantumPluginBase(object):
|
|||||||
def update_port(self, tenant_id, net_id, port_id, port_state):
|
def update_port(self, tenant_id, net_id, port_id, port_state):
|
||||||
"""
|
"""
|
||||||
Updates the state of a specific port on the
|
Updates the state of a specific port on the
|
||||||
specified Virtual Network
|
specified Virtual Network.
|
||||||
|
|
||||||
|
:returns: a mapping sequence with the following signature:
|
||||||
|
{'port-id': uuid representing the
|
||||||
|
updated port on specified quantum network
|
||||||
|
'port-state': update port state( UP or DOWN)
|
||||||
|
}
|
||||||
|
:raises: exception.StateInvalid
|
||||||
|
:raises: exception.PortNotFound
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -100,6 +174,14 @@ class QuantumPluginBase(object):
|
|||||||
if the port contains a remote interface attachment,
|
if the port contains a remote interface attachment,
|
||||||
the remote interface is first un-plugged and then the port
|
the remote interface is first un-plugged and then the port
|
||||||
is deleted.
|
is deleted.
|
||||||
|
|
||||||
|
:returns: a mapping sequence with the following signature:
|
||||||
|
{'port-id': uuid representing the deleted port
|
||||||
|
on specified quantum network
|
||||||
|
}
|
||||||
|
:raises: exception.PortInUse
|
||||||
|
:raises: exception.PortNotFound
|
||||||
|
:raises: exception.NetworkNotFound
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -108,6 +190,17 @@ class QuantumPluginBase(object):
|
|||||||
"""
|
"""
|
||||||
This method allows the user to retrieve a remote interface
|
This method allows the user to retrieve a remote interface
|
||||||
that is attached to this particular port.
|
that is attached to this particular port.
|
||||||
|
|
||||||
|
:returns: a mapping sequence with the following signature:
|
||||||
|
{'port-id': uuid representing the port on
|
||||||
|
specified quantum network
|
||||||
|
'net-id': uuid representing the particular
|
||||||
|
quantum network
|
||||||
|
'attachment': uuid of the virtual interface
|
||||||
|
bound to the port, None otherwise
|
||||||
|
}
|
||||||
|
:raises: exception.PortNotFound
|
||||||
|
:raises: exception.NetworkNotFound
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -116,6 +209,12 @@ class QuantumPluginBase(object):
|
|||||||
"""
|
"""
|
||||||
Attaches a remote interface to the specified port on the
|
Attaches a remote interface to the specified port on the
|
||||||
specified Virtual Network.
|
specified Virtual Network.
|
||||||
|
|
||||||
|
:returns: None
|
||||||
|
:raises: exception.NetworkNotFound
|
||||||
|
:raises: exception.PortNotFound
|
||||||
|
:raises: exception.AlreadyAttached
|
||||||
|
(? should the network automatically unplug/replug)
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -124,22 +223,10 @@ class QuantumPluginBase(object):
|
|||||||
"""
|
"""
|
||||||
Detaches a remote interface from the specified port on the
|
Detaches a remote interface from the specified port on the
|
||||||
specified Virtual Network.
|
specified Virtual Network.
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
:returns: None
|
||||||
def get_interface_details(self, tenant_id, net_id, port_id):
|
:raises: exception.NetworkNotFound
|
||||||
"""
|
:raises: exception.PortNotFound
|
||||||
Retrieves the remote interface that is attached at this
|
|
||||||
particular port.
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def get_all_attached_interfaces(self, tenant_id, net_id):
|
|
||||||
"""
|
|
||||||
Retrieves all remote interfaces that are attached to
|
|
||||||
a particular Virtual Network.
|
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -102,7 +102,9 @@ def serve_wsgi(cls, conf=None, options=None, args=None):
|
|||||||
|
|
||||||
def _run_wsgi(app_name, paste_conf, paste_config_file):
|
def _run_wsgi(app_name, paste_conf, paste_config_file):
|
||||||
LOG.info(_('Using paste.deploy config at: %s'), paste_config_file)
|
LOG.info(_('Using paste.deploy config at: %s'), paste_config_file)
|
||||||
app = config.load_paste_app(paste_config_file, app_name)
|
conf, app = config.load_paste_app(app_name,
|
||||||
|
{'config_file': paste_config_file},
|
||||||
|
None)
|
||||||
if not app:
|
if not app:
|
||||||
LOG.error(_('No known API applications configured in %s.'),
|
LOG.error(_('No known API applications configured in %s.'),
|
||||||
paste_config_file)
|
paste_config_file)
|
||||||
|
293
run_tests.py
Normal file
293
run_tests.py
Normal file
@ -0,0 +1,293 @@
|
|||||||
|
#!/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.
|
||||||
|
|
||||||
|
# Colorizer Code is borrowed from Twisted:
|
||||||
|
# Copyright (c) 2001-2010 Twisted Matrix Laboratories.
|
||||||
|
#
|
||||||
|
# Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
# a copy of this software and associated documentation files (the
|
||||||
|
# "Software"), to deal in the Software without restriction, including
|
||||||
|
# without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
# permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
# the following conditions:
|
||||||
|
#
|
||||||
|
# The above copyright notice and this permission notice shall be
|
||||||
|
# included in all copies or substantial portions of the Software.
|
||||||
|
#
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||||
|
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
"""Unittest runner for quantum
|
||||||
|
|
||||||
|
To run all test::
|
||||||
|
python run_tests.py
|
||||||
|
|
||||||
|
To run all unit tests::
|
||||||
|
python run_tests.py unit
|
||||||
|
|
||||||
|
To run all functional tests::
|
||||||
|
python run_tests.py functional
|
||||||
|
|
||||||
|
To run a single unit test::
|
||||||
|
python run_tests.py unit.test_stores:TestSwiftBackend.test_get
|
||||||
|
|
||||||
|
To run a single functional test::
|
||||||
|
python run_tests.py functional.test_service:TestController.test_create
|
||||||
|
|
||||||
|
To run a single unit test module::
|
||||||
|
python run_tests.py unit.test_stores
|
||||||
|
|
||||||
|
To run a single functional test module::
|
||||||
|
python run_tests.py functional.test_stores
|
||||||
|
"""
|
||||||
|
|
||||||
|
import gettext
|
||||||
|
import os
|
||||||
|
import unittest
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from nose import config
|
||||||
|
from nose import result
|
||||||
|
from nose import core
|
||||||
|
|
||||||
|
|
||||||
|
class _AnsiColorizer(object):
|
||||||
|
"""
|
||||||
|
A colorizer is an object that loosely wraps around a stream, allowing
|
||||||
|
callers to write text to the stream in a particular color.
|
||||||
|
|
||||||
|
Colorizer classes must implement C{supported()} and C{write(text, color)}.
|
||||||
|
"""
|
||||||
|
_colors = dict(black=30, red=31, green=32, yellow=33,
|
||||||
|
blue=34, magenta=35, cyan=36, white=37)
|
||||||
|
|
||||||
|
def __init__(self, stream):
|
||||||
|
self.stream = stream
|
||||||
|
|
||||||
|
def supported(cls, stream=sys.stdout):
|
||||||
|
"""
|
||||||
|
A class method that returns True if the current platform supports
|
||||||
|
coloring terminal output using this method. Returns False otherwise.
|
||||||
|
"""
|
||||||
|
if not stream.isatty():
|
||||||
|
return False # auto color only on TTYs
|
||||||
|
try:
|
||||||
|
import curses
|
||||||
|
except ImportError:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
return curses.tigetnum("colors") > 2
|
||||||
|
except curses.error:
|
||||||
|
curses.setupterm()
|
||||||
|
return curses.tigetnum("colors") > 2
|
||||||
|
except:
|
||||||
|
raise
|
||||||
|
# guess false in case of error
|
||||||
|
return False
|
||||||
|
supported = classmethod(supported)
|
||||||
|
|
||||||
|
def write(self, text, color):
|
||||||
|
"""
|
||||||
|
Write the given text to the stream in the given color.
|
||||||
|
|
||||||
|
@param text: Text to be written to the stream.
|
||||||
|
|
||||||
|
@param color: A string label for a color. e.g. 'red', 'white'.
|
||||||
|
"""
|
||||||
|
color = self._colors[color]
|
||||||
|
self.stream.write('\x1b[%s;1m%s\x1b[0m' % (color, text))
|
||||||
|
|
||||||
|
|
||||||
|
class _Win32Colorizer(object):
|
||||||
|
"""
|
||||||
|
See _AnsiColorizer docstring.
|
||||||
|
"""
|
||||||
|
def __init__(self, stream):
|
||||||
|
from win32console import GetStdHandle, STD_OUT_HANDLE, \
|
||||||
|
FOREGROUND_RED, FOREGROUND_BLUE, FOREGROUND_GREEN, \
|
||||||
|
FOREGROUND_INTENSITY
|
||||||
|
red, green, blue, bold = (FOREGROUND_RED, FOREGROUND_GREEN,
|
||||||
|
FOREGROUND_BLUE, FOREGROUND_INTENSITY)
|
||||||
|
self.stream = stream
|
||||||
|
self.screenBuffer = GetStdHandle(STD_OUT_HANDLE)
|
||||||
|
self._colors = {
|
||||||
|
'normal': red | green | blue,
|
||||||
|
'red': red | bold,
|
||||||
|
'green': green | bold,
|
||||||
|
'blue': blue | bold,
|
||||||
|
'yellow': red | green | bold,
|
||||||
|
'magenta': red | blue | bold,
|
||||||
|
'cyan': green | blue | bold,
|
||||||
|
'white': red | green | blue | bold}
|
||||||
|
|
||||||
|
def supported(cls, stream=sys.stdout):
|
||||||
|
try:
|
||||||
|
import win32console
|
||||||
|
screenBuffer = win32console.GetStdHandle(
|
||||||
|
win32console.STD_OUT_HANDLE)
|
||||||
|
except ImportError:
|
||||||
|
return False
|
||||||
|
import pywintypes
|
||||||
|
try:
|
||||||
|
screenBuffer.SetConsoleTextAttribute(
|
||||||
|
win32console.FOREGROUND_RED |
|
||||||
|
win32console.FOREGROUND_GREEN |
|
||||||
|
win32console.FOREGROUND_BLUE)
|
||||||
|
except pywintypes.error:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
supported = classmethod(supported)
|
||||||
|
|
||||||
|
def write(self, text, color):
|
||||||
|
color = self._colors[color]
|
||||||
|
self.screenBuffer.SetConsoleTextAttribute(color)
|
||||||
|
self.stream.write(text)
|
||||||
|
self.screenBuffer.SetConsoleTextAttribute(self._colors['normal'])
|
||||||
|
|
||||||
|
|
||||||
|
class _NullColorizer(object):
|
||||||
|
"""
|
||||||
|
See _AnsiColorizer docstring.
|
||||||
|
"""
|
||||||
|
def __init__(self, stream):
|
||||||
|
self.stream = stream
|
||||||
|
|
||||||
|
def supported(cls, stream=sys.stdout):
|
||||||
|
return True
|
||||||
|
supported = classmethod(supported)
|
||||||
|
|
||||||
|
def write(self, text, color):
|
||||||
|
self.stream.write(text)
|
||||||
|
|
||||||
|
|
||||||
|
class QuantumTestResult(result.TextTestResult):
|
||||||
|
def __init__(self, *args, **kw):
|
||||||
|
result.TextTestResult.__init__(self, *args, **kw)
|
||||||
|
self._last_case = None
|
||||||
|
self.colorizer = None
|
||||||
|
# NOTE(vish, tfukushima): reset stdout for the terminal check
|
||||||
|
stdout = sys.__stdout__
|
||||||
|
for colorizer in [_Win32Colorizer, _AnsiColorizer, _NullColorizer]:
|
||||||
|
if colorizer.supported():
|
||||||
|
self.colorizer = colorizer(self.stream)
|
||||||
|
break
|
||||||
|
sys.stdout = stdout
|
||||||
|
|
||||||
|
def getDescription(self, test):
|
||||||
|
return str(test)
|
||||||
|
|
||||||
|
# NOTE(vish, tfukushima): copied from unittest with edit to add color
|
||||||
|
def addSuccess(self, test):
|
||||||
|
unittest.TestResult.addSuccess(self, test)
|
||||||
|
if self.showAll:
|
||||||
|
self.colorizer.write("OK", 'green')
|
||||||
|
self.stream.writeln()
|
||||||
|
elif self.dots:
|
||||||
|
self.stream.write('.')
|
||||||
|
self.stream.flush()
|
||||||
|
|
||||||
|
# NOTE(vish, tfukushima): copied from unittest with edit to add color
|
||||||
|
def addFailure(self, test, err):
|
||||||
|
unittest.TestResult.addFailure(self, test, err)
|
||||||
|
if self.showAll:
|
||||||
|
self.colorizer.write("FAIL", 'red')
|
||||||
|
self.stream.writeln()
|
||||||
|
elif self.dots:
|
||||||
|
self.stream.write('F')
|
||||||
|
self.stream.flush()
|
||||||
|
|
||||||
|
# NOTE(vish, tfukushima): copied from unittest with edit to add color
|
||||||
|
def addError(self, test, err):
|
||||||
|
"""Overrides normal addError to add support for errorClasses.
|
||||||
|
If the exception is a registered class, the error will be added
|
||||||
|
to the list for that class, not errors.
|
||||||
|
"""
|
||||||
|
stream = getattr(self, 'stream', None)
|
||||||
|
ec, ev, tb = err
|
||||||
|
try:
|
||||||
|
exc_info = self._exc_info_to_string(err, test)
|
||||||
|
except TypeError:
|
||||||
|
# This is for compatibility with Python 2.3.
|
||||||
|
exc_info = self._exc_info_to_string(err)
|
||||||
|
for cls, (storage, label, isfail) in self.errorClasses.items():
|
||||||
|
if result.isclass(ec) and issubclass(ec, cls):
|
||||||
|
if isfail:
|
||||||
|
test.passwd = False
|
||||||
|
storage.append((test, exc_info))
|
||||||
|
# Might get patched into a streamless result
|
||||||
|
if stream is not None:
|
||||||
|
if self.showAll:
|
||||||
|
message = [label]
|
||||||
|
detail = result._exception_details(err[1])
|
||||||
|
if detail:
|
||||||
|
message.append(detail)
|
||||||
|
stream.writeln(": ".join(message))
|
||||||
|
elif self.dots:
|
||||||
|
stream.write(label[:1])
|
||||||
|
return
|
||||||
|
self.errors.append((test, exc_info))
|
||||||
|
test.passed = False
|
||||||
|
if stream is not None:
|
||||||
|
if self.showAll:
|
||||||
|
self.colorizer.write("ERROR", 'red')
|
||||||
|
self.stream.writeln()
|
||||||
|
elif self.dots:
|
||||||
|
stream.write('E')
|
||||||
|
|
||||||
|
def startTest(self, test):
|
||||||
|
unittest.TestResult.startTest(self, test)
|
||||||
|
current_case = test.test.__class__.__name__
|
||||||
|
|
||||||
|
if self.showAll:
|
||||||
|
if current_case != self._last_case:
|
||||||
|
self.stream.writeln(current_case)
|
||||||
|
self._last_case = current_case
|
||||||
|
|
||||||
|
self.stream.write(
|
||||||
|
' %s' % str(test.test._testMethodName).ljust(60))
|
||||||
|
self.stream.flush()
|
||||||
|
|
||||||
|
|
||||||
|
class QuantumTestRunner(core.TextTestRunner):
|
||||||
|
def _makeResult(self):
|
||||||
|
return QuantumTestResult(self.stream,
|
||||||
|
self.descriptions,
|
||||||
|
self.verbosity,
|
||||||
|
self.config)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
working_dir = os.path.abspath("tests")
|
||||||
|
c = config.Config(stream=sys.stdout,
|
||||||
|
env=os.environ,
|
||||||
|
verbosity=3,
|
||||||
|
workingDir=working_dir)
|
||||||
|
|
||||||
|
runner = QuantumTestRunner(stream=c.stream,
|
||||||
|
verbosity=c.verbosity,
|
||||||
|
config=c)
|
||||||
|
sys.exit(not core.run(config=c, testRunner=runner))
|
83
run_tests.sh
Executable file
83
run_tests.sh
Executable file
@ -0,0 +1,83 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
function usage {
|
||||||
|
echo "Usage: $0 [OPTION]..."
|
||||||
|
echo "Run Melange's test suite(s)"
|
||||||
|
echo ""
|
||||||
|
echo " -V, --virtual-env Always use virtualenv. Install automatically if not present"
|
||||||
|
echo " -N, --no-virtual-env Don't use virtualenv. Run tests in local environment"
|
||||||
|
echo " -f, --force Force a clean re-build of the virtual environment. Useful when dependencies have been added."
|
||||||
|
echo " -h, --help Print this usage message"
|
||||||
|
echo ""
|
||||||
|
echo "Note: with no options specified, the script will try to run the tests in a virtual environment,"
|
||||||
|
echo " If no virtualenv is found, the script will ask if you would like to create one. If you "
|
||||||
|
echo " prefer to run tests NOT in a virtual environment, simply pass the -N option."
|
||||||
|
exit
|
||||||
|
}
|
||||||
|
|
||||||
|
function process_option {
|
||||||
|
case "$1" in
|
||||||
|
-h|--help) usage;;
|
||||||
|
-V|--virtual-env) let always_venv=1; let never_venv=0;;
|
||||||
|
-N|--no-virtual-env) let always_venv=0; let never_venv=1;;
|
||||||
|
-f|--force) let force=1;;
|
||||||
|
*) noseargs="$noseargs $1"
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
venv=.quantum-venv
|
||||||
|
with_venv=tools/with_venv.sh
|
||||||
|
always_venv=0
|
||||||
|
never_venv=0
|
||||||
|
force=0
|
||||||
|
noseargs=
|
||||||
|
wrapper=""
|
||||||
|
|
||||||
|
for arg in "$@"; do
|
||||||
|
process_option $arg
|
||||||
|
done
|
||||||
|
|
||||||
|
function run_tests {
|
||||||
|
# Just run the test suites in current environment
|
||||||
|
${wrapper} rm -f tests.sqlite
|
||||||
|
${wrapper} $NOSETESTS 2> run_tests.err.log
|
||||||
|
}
|
||||||
|
|
||||||
|
NOSETESTS="python run_tests.py $noseargs"
|
||||||
|
|
||||||
|
if [ $never_venv -eq 0 ]
|
||||||
|
then
|
||||||
|
# Remove the virtual environment if --force used
|
||||||
|
if [ $force -eq 1 ]; then
|
||||||
|
echo "Cleaning virtualenv..."
|
||||||
|
rm -rf ${venv}
|
||||||
|
fi
|
||||||
|
if [ -e ${venv} ]; then
|
||||||
|
wrapper="${with_venv}"
|
||||||
|
else
|
||||||
|
if [ $always_venv -eq 1 ]; then
|
||||||
|
# Automatically install the virtualenv
|
||||||
|
python tools/install_venv.py
|
||||||
|
wrapper="${with_venv}"
|
||||||
|
else
|
||||||
|
echo -e "No virtual environment found...create one? (Y/n) \c"
|
||||||
|
read use_ve
|
||||||
|
if [ "x$use_ve" = "xY" -o "x$use_ve" = "x" -o "x$use_ve" = "xy" ]; then
|
||||||
|
# Install the virtualenv and run the test suite in it
|
||||||
|
python tools/install_venv.py
|
||||||
|
wrapper=${with_venv}
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# FIXME(sirp): bzr version-info is not currently pep-8. This was fixed with
|
||||||
|
# lp701898 [1], however, until that version of bzr becomes standard, I'm just
|
||||||
|
# excluding the vcsversion.py file
|
||||||
|
#
|
||||||
|
# [1] https://bugs.launchpad.net/bzr/+bug/701898
|
||||||
|
#
|
||||||
|
PEP8_EXCLUDE=vcsversion.py
|
||||||
|
PEP8_OPTIONS="--exclude=$PEP8_EXCLUDE --repeat --show-source"
|
||||||
|
PEP8_INCLUDE="bin/* quantum tests tools run_tests.py"
|
||||||
|
run_tests && pep8 $PEP8_OPTIONS $PEP8_INCLUDE || exit 1
|
@ -1,150 +0,0 @@
|
|||||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
|
||||||
|
|
||||||
# Copyright 2011 Citrix Systems
|
|
||||||
# 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 gettext
|
|
||||||
|
|
||||||
gettext.install('quantum', unicode=1)
|
|
||||||
|
|
||||||
from miniclient import MiniClient
|
|
||||||
from quantum.common.wsgi import Serializer
|
|
||||||
|
|
||||||
HOST = '127.0.0.1'
|
|
||||||
PORT = 9696
|
|
||||||
USE_SSL = False
|
|
||||||
TENANT_ID = 'totore'
|
|
||||||
|
|
||||||
test_network_data = \
|
|
||||||
{'network': {'network-name': 'test' }}
|
|
||||||
|
|
||||||
def print_response(res):
|
|
||||||
content = res.read()
|
|
||||||
print "Status: %s" %res.status
|
|
||||||
print "Content: %s" %content
|
|
||||||
return content
|
|
||||||
|
|
||||||
def test_list_networks_and_ports(format = 'xml'):
|
|
||||||
client = MiniClient(HOST, PORT, USE_SSL)
|
|
||||||
print "TEST LIST NETWORKS AND PORTS -- FORMAT:%s" %format
|
|
||||||
print "----------------------------"
|
|
||||||
print "--> Step 1 - List All Networks"
|
|
||||||
res = client.do_request(TENANT_ID,'GET', "/networks." + format)
|
|
||||||
print_response(res)
|
|
||||||
print "--> Step 2 - Details for Network 001"
|
|
||||||
res = client.do_request(TENANT_ID,'GET', "/networks/001." + format)
|
|
||||||
print_response(res)
|
|
||||||
print "--> Step 3 - Ports for Network 001"
|
|
||||||
res = client.do_request(TENANT_ID,'GET', "/networks/001/ports." + format)
|
|
||||||
print_response(res)
|
|
||||||
print "--> Step 4 - Details for Port 1"
|
|
||||||
res = client.do_request(TENANT_ID,'GET', "/networks/001/ports/1." + format)
|
|
||||||
print_response(res)
|
|
||||||
print "COMPLETED"
|
|
||||||
print "----------------------------"
|
|
||||||
|
|
||||||
def test_create_network(format = 'xml'):
|
|
||||||
client = MiniClient(HOST, PORT, USE_SSL)
|
|
||||||
print "TEST CREATE NETWORK -- FORMAT:%s" %format
|
|
||||||
print "----------------------------"
|
|
||||||
print "--> Step 1 - Create Network"
|
|
||||||
content_type = "application/" + format
|
|
||||||
body = Serializer().serialize(test_network_data, content_type)
|
|
||||||
res = client.do_request(TENANT_ID,'POST', "/networks." + format, body=body)
|
|
||||||
print_response(res)
|
|
||||||
print "--> Step 2 - List All Networks"
|
|
||||||
res = client.do_request(TENANT_ID,'GET', "/networks." + format)
|
|
||||||
print_response(res)
|
|
||||||
print "COMPLETED"
|
|
||||||
print "----------------------------"
|
|
||||||
|
|
||||||
def test_rename_network(format = 'xml'):
|
|
||||||
client = MiniClient(HOST, PORT, USE_SSL)
|
|
||||||
content_type = "application/" + format
|
|
||||||
print "TEST RENAME NETWORK -- FORMAT:%s" %format
|
|
||||||
print "----------------------------"
|
|
||||||
print "--> Step 1 - Retrieve network"
|
|
||||||
res = client.do_request(TENANT_ID,'GET', "/networks/001." + format)
|
|
||||||
print_response(res)
|
|
||||||
print "--> Step 2 - Rename network to 'test_renamed'"
|
|
||||||
test_network_data['network']['network-name'] = 'test_renamed'
|
|
||||||
body = Serializer().serialize(test_network_data, content_type)
|
|
||||||
res = client.do_request(TENANT_ID,'PUT', "/networks/001." + format, body=body)
|
|
||||||
print_response(res)
|
|
||||||
print "--> Step 2 - Retrieve network (again)"
|
|
||||||
res = client.do_request(TENANT_ID,'GET', "/networks/001." + format)
|
|
||||||
print_response(res)
|
|
||||||
print "COMPLETED"
|
|
||||||
print "----------------------------"
|
|
||||||
|
|
||||||
def test_delete_network(format = 'xml'):
|
|
||||||
client = MiniClient(HOST, PORT, USE_SSL)
|
|
||||||
content_type = "application/" + format
|
|
||||||
print "TEST DELETE NETWORK -- FORMAT:%s" %format
|
|
||||||
print "----------------------------"
|
|
||||||
print "--> Step 1 - List All Networks"
|
|
||||||
res = client.do_request(TENANT_ID,'GET', "/networks." + format)
|
|
||||||
content = print_response(res)
|
|
||||||
network_data = Serializer().deserialize(content, content_type)
|
|
||||||
print network_data
|
|
||||||
net_id = network_data['networks'][0]['id']
|
|
||||||
print "--> Step 2 - Delete network %s" %net_id
|
|
||||||
res = client.do_request(TENANT_ID,'DELETE',
|
|
||||||
"/networks/" + net_id + "." + format)
|
|
||||||
print_response(res)
|
|
||||||
print "--> Step 3 - List All Networks (Again)"
|
|
||||||
res = client.do_request(TENANT_ID,'GET', "/networks." + format)
|
|
||||||
print_response(res)
|
|
||||||
print "COMPLETED"
|
|
||||||
print "----------------------------"
|
|
||||||
|
|
||||||
|
|
||||||
def test_create_port(format = 'xml'):
|
|
||||||
client = MiniClient(HOST, PORT, USE_SSL)
|
|
||||||
print "TEST CREATE PORT -- FORMAT:%s" %format
|
|
||||||
print "----------------------------"
|
|
||||||
print "--> Step 1 - List Ports for network 001"
|
|
||||||
res = client.do_request(TENANT_ID,'GET', "/networks/001/ports." + format)
|
|
||||||
print_response(res)
|
|
||||||
print "--> Step 2 - Create Port for network 001"
|
|
||||||
res = client.do_request(TENANT_ID,'POST', "/networks/001/ports." + format)
|
|
||||||
print_response(res)
|
|
||||||
print "--> Step 3 - List Ports for network 001 (again)"
|
|
||||||
res = client.do_request(TENANT_ID,'GET', "/networks/001/ports." + format)
|
|
||||||
print_response(res)
|
|
||||||
print "COMPLETED"
|
|
||||||
print "----------------------------"
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
test_list_networks_and_ports('xml')
|
|
||||||
test_list_networks_and_ports('json')
|
|
||||||
test_create_network('xml')
|
|
||||||
test_create_network('json')
|
|
||||||
test_rename_network('xml')
|
|
||||||
test_rename_network('json')
|
|
||||||
# NOTE: XML deserializer does not work properly
|
|
||||||
# disabling XML test - this is NOT a server-side issue
|
|
||||||
#test_delete_network('xml')
|
|
||||||
test_delete_network('json')
|
|
||||||
test_create_port('xml')
|
|
||||||
test_create_port('json')
|
|
||||||
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
# Standard boilerplate to call the main() function.
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
0
tests/functional/__init__.py
Normal file
0
tests/functional/__init__.py
Normal file
@ -19,6 +19,7 @@ import httplib
|
|||||||
import socket
|
import socket
|
||||||
import urllib
|
import urllib
|
||||||
|
|
||||||
|
|
||||||
class MiniClient(object):
|
class MiniClient(object):
|
||||||
|
|
||||||
"""A base client class - derived from Glance.BaseClient"""
|
"""A base client class - derived from Glance.BaseClient"""
|
136
tests/functional/test_service.py
Normal file
136
tests/functional/test_service.py
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright 2011 Citrix Systems
|
||||||
|
# Copyright 2011 Nicira Networks
|
||||||
|
# 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 gettext
|
||||||
|
import simplejson
|
||||||
|
import sys
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
gettext.install('quantum', unicode=1)
|
||||||
|
|
||||||
|
from miniclient import MiniClient
|
||||||
|
from quantum.common.wsgi import Serializer
|
||||||
|
|
||||||
|
HOST = '127.0.0.1'
|
||||||
|
PORT = 9696
|
||||||
|
USE_SSL = False
|
||||||
|
|
||||||
|
TENANT_ID = 'totore'
|
||||||
|
FORMAT = "json"
|
||||||
|
|
||||||
|
test_network1_data = \
|
||||||
|
{'network': {'network-name': 'test1'}}
|
||||||
|
test_network2_data = \
|
||||||
|
{'network': {'network-name': 'test2'}}
|
||||||
|
|
||||||
|
|
||||||
|
def print_response(res):
|
||||||
|
content = res.read()
|
||||||
|
print "Status: %s" % res.status
|
||||||
|
print "Content: %s" % content
|
||||||
|
return content
|
||||||
|
|
||||||
|
|
||||||
|
class QuantumTest(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.client = MiniClient(HOST, PORT, USE_SSL)
|
||||||
|
|
||||||
|
def create_network(self, data):
|
||||||
|
content_type = "application/" + FORMAT
|
||||||
|
body = Serializer().serialize(data, content_type)
|
||||||
|
res = self.client.do_request(TENANT_ID, 'POST', "/networks." + FORMAT,
|
||||||
|
body=body)
|
||||||
|
self.assertEqual(res.status, 200, "bad response: %s" % res.read())
|
||||||
|
|
||||||
|
def test_listNetworks(self):
|
||||||
|
self.create_network(test_network1_data)
|
||||||
|
self.create_network(test_network2_data)
|
||||||
|
res = self.client.do_request(TENANT_ID, 'GET', "/networks." + FORMAT)
|
||||||
|
self.assertEqual(res.status, 200, "bad response: %s" % res.read())
|
||||||
|
|
||||||
|
def test_createNetwork(self):
|
||||||
|
self.create_network(test_network1_data)
|
||||||
|
|
||||||
|
def test_createPort(self):
|
||||||
|
self.create_network(test_network1_data)
|
||||||
|
res = self.client.do_request(TENANT_ID, 'GET', "/networks." + FORMAT)
|
||||||
|
resdict = simplejson.loads(res.read())
|
||||||
|
for n in resdict["networks"]:
|
||||||
|
net_id = n["id"]
|
||||||
|
|
||||||
|
# Step 1 - List Ports for network (should not find any)
|
||||||
|
res = self.client.do_request(TENANT_ID, 'GET',
|
||||||
|
"/networks/%s/ports.%s" % (net_id, FORMAT))
|
||||||
|
self.assertEqual(res.status, 200, "Bad response: %s" % res.read())
|
||||||
|
output = res.read()
|
||||||
|
self.assertTrue(len(output) == 0,
|
||||||
|
"Found unexpected ports: %s" % output)
|
||||||
|
|
||||||
|
# Step 2 - Create Port for network
|
||||||
|
res = self.client.do_request(TENANT_ID, 'POST',
|
||||||
|
"/networks/%s/ports.%s" % (net_id, FORMAT))
|
||||||
|
self.assertEqual(res.status, 200, "Bad response: %s" % output)
|
||||||
|
|
||||||
|
# Step 3 - List Ports for network (again); should find one
|
||||||
|
res = self.client.do_request(TENANT_ID, 'GET',
|
||||||
|
"/networks/%s/ports.%s" % (net_id, FORMAT))
|
||||||
|
output = res.read()
|
||||||
|
self.assertEqual(res.status, 200, "Bad response: %s" % output)
|
||||||
|
resdict = simplejson.loads(output)
|
||||||
|
ids = []
|
||||||
|
for p in resdict["ports"]:
|
||||||
|
ids.append(p["id"])
|
||||||
|
self.assertTrue(len(ids) == 1,
|
||||||
|
"Didn't find expected # of ports (1): %s" % ids)
|
||||||
|
|
||||||
|
def test_renameNetwork(self):
|
||||||
|
self.create_network(test_network1_data)
|
||||||
|
res = self.client.do_request(TENANT_ID, 'GET', "/networks." + FORMAT)
|
||||||
|
resdict = simplejson.loads(res.read())
|
||||||
|
net_id = resdict["networks"][0]["id"]
|
||||||
|
|
||||||
|
data = test_network1_data.copy()
|
||||||
|
data['network']['network-name'] = 'test_renamed'
|
||||||
|
content_type = "application/" + FORMAT
|
||||||
|
body = Serializer().serialize(data, content_type)
|
||||||
|
res = self.client.do_request(TENANT_ID, 'PUT',
|
||||||
|
"/networks/%s.%s" % (net_id, FORMAT), body=body)
|
||||||
|
resdict = simplejson.loads(res.read())
|
||||||
|
self.assertTrue(resdict["networks"]["network"]["id"] == net_id,
|
||||||
|
"Network_rename: renamed network has a different uuid")
|
||||||
|
self.assertTrue(
|
||||||
|
resdict["networks"]["network"]["name"] == "test_renamed",
|
||||||
|
"Network rename didn't take effect")
|
||||||
|
|
||||||
|
def delete_networks(self):
|
||||||
|
# Remove all the networks created on the tenant
|
||||||
|
res = self.client.do_request(TENANT_ID, 'GET', "/networks." + FORMAT)
|
||||||
|
resdict = simplejson.loads(res.read())
|
||||||
|
for n in resdict["networks"]:
|
||||||
|
net_id = n["id"]
|
||||||
|
res = self.client.do_request(TENANT_ID, 'DELETE',
|
||||||
|
"/networks/" + net_id + "." + FORMAT)
|
||||||
|
self.assertEqual(res.status, 202)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
self.delete_networks()
|
||||||
|
|
||||||
|
# Standard boilerplate to call the main() function.
|
||||||
|
if __name__ == '__main__':
|
||||||
|
suite = unittest.TestLoader().loadTestsFromTestCase(QuantumTest)
|
||||||
|
unittest.TextTestRunner(verbosity=2).run(suite)
|
32
tests/unit/__init__.py
Normal file
32
tests/unit/__init__.py
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright 2011 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.
|
||||||
|
|
||||||
|
# See http://code.google.com/p/python-nose/issues/detail?id=373
|
||||||
|
# The code below enables nosetests to work with i18n _() blocks
|
||||||
|
import __builtin__
|
||||||
|
import unittest
|
||||||
|
setattr(__builtin__, '_', lambda x: x)
|
||||||
|
|
||||||
|
|
||||||
|
class BaseTest(unittest.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def setUp():
|
||||||
|
pass
|
137
tools/install_venv.py
Normal file
137
tools/install_venv.py
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright 2010 United States Government as represented by the
|
||||||
|
# Administrator of the National Aeronautics and Space Administration.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Copyright 2010 OpenStack LLC.
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
"""
|
||||||
|
Installation script for Quantum's development virtualenv
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
|
||||||
|
VENV = os.path.join(ROOT, '.quantum-venv')
|
||||||
|
PIP_REQUIRES = os.path.join(ROOT, 'tools', 'pip-requires')
|
||||||
|
|
||||||
|
|
||||||
|
def die(message, *args):
|
||||||
|
print >> sys.stderr, message % args
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def run_command(cmd, redirect_output=True, check_exit_code=True):
|
||||||
|
"""
|
||||||
|
Runs a command in an out-of-process shell, returning the
|
||||||
|
output of that command. Working directory is ROOT.
|
||||||
|
"""
|
||||||
|
if redirect_output:
|
||||||
|
stdout = subprocess.PIPE
|
||||||
|
else:
|
||||||
|
stdout = None
|
||||||
|
|
||||||
|
proc = subprocess.Popen(cmd, cwd=ROOT, stdout=stdout)
|
||||||
|
output = proc.communicate()[0]
|
||||||
|
if check_exit_code and proc.returncode != 0:
|
||||||
|
die('Command "%s" failed.\n%s', ' '.join(cmd), output)
|
||||||
|
return output
|
||||||
|
|
||||||
|
|
||||||
|
HAS_EASY_INSTALL = bool(run_command(['which', 'easy_install'],
|
||||||
|
check_exit_code=False).strip())
|
||||||
|
HAS_VIRTUALENV = bool(run_command(['which', 'virtualenv'],
|
||||||
|
check_exit_code=False).strip())
|
||||||
|
|
||||||
|
|
||||||
|
def check_dependencies():
|
||||||
|
"""Make sure virtualenv is in the path."""
|
||||||
|
|
||||||
|
if not HAS_VIRTUALENV:
|
||||||
|
print 'not found.'
|
||||||
|
# Try installing it via easy_install...
|
||||||
|
if HAS_EASY_INSTALL:
|
||||||
|
print 'Installing virtualenv via easy_install...',
|
||||||
|
if not run_command(['which', 'easy_install']):
|
||||||
|
die('ERROR: virtualenv not found.\n\n'
|
||||||
|
'Quantum requires virtualenv, please install'
|
||||||
|
' it using your favorite package management tool')
|
||||||
|
print 'done.'
|
||||||
|
print 'done.'
|
||||||
|
|
||||||
|
|
||||||
|
def create_virtualenv(venv=VENV):
|
||||||
|
"""Creates the virtual environment and installs PIP only into the
|
||||||
|
virtual environment
|
||||||
|
"""
|
||||||
|
print 'Creating venv...',
|
||||||
|
run_command(['virtualenv', '-q', '--no-site-packages', VENV])
|
||||||
|
print 'done.'
|
||||||
|
print 'Installing pip in virtualenv...',
|
||||||
|
if not run_command(['tools/with_venv.sh', 'easy_install', 'pip']).strip():
|
||||||
|
die("Failed to install pip.")
|
||||||
|
print 'done.'
|
||||||
|
|
||||||
|
|
||||||
|
def install_dependencies(venv=VENV):
|
||||||
|
print 'Installing dependencies with pip (this can take a while)...'
|
||||||
|
|
||||||
|
# Install greenlet by hand - just listing it in the requires file does not
|
||||||
|
# get it in stalled in the right order
|
||||||
|
venv_tool = 'tools/with_venv.sh'
|
||||||
|
run_command([venv_tool, 'pip', 'install', '-E', venv, '-r', PIP_REQUIRES],
|
||||||
|
redirect_output=False)
|
||||||
|
|
||||||
|
# Tell the virtual env how to "import quantum"
|
||||||
|
pthfile = os.path.join(venv, "lib", "python2.6", "site-packages",
|
||||||
|
"quantum.pth")
|
||||||
|
f = open(pthfile, 'w')
|
||||||
|
f.write("%s\n" % ROOT)
|
||||||
|
|
||||||
|
|
||||||
|
def print_help():
|
||||||
|
help = """
|
||||||
|
Quantum development environment setup is complete.
|
||||||
|
|
||||||
|
Quantum development uses virtualenv to track and manage Python dependencies
|
||||||
|
while in development and testing.
|
||||||
|
|
||||||
|
To activate the Quantum virtualenv for the extent of your current shell
|
||||||
|
session you can run:
|
||||||
|
|
||||||
|
$ source .quantum-venv/bin/activate
|
||||||
|
|
||||||
|
Or, if you prefer, you can run commands in the virtualenv on a case by case
|
||||||
|
basis by running:
|
||||||
|
|
||||||
|
$ tools/with_venv.sh <your command>
|
||||||
|
|
||||||
|
Also, make test will automatically use the virtualenv.
|
||||||
|
"""
|
||||||
|
print help
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv):
|
||||||
|
check_dependencies()
|
||||||
|
create_virtualenv()
|
||||||
|
install_dependencies()
|
||||||
|
print_help()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main(sys.argv)
|
10
tools/pip-requires
Normal file
10
tools/pip-requires
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
eventlet>=0.9.12
|
||||||
|
nose
|
||||||
|
Paste
|
||||||
|
PasteDeploy
|
||||||
|
pep8==0.5.0
|
||||||
|
python-gflags
|
||||||
|
routes
|
||||||
|
simplejson
|
||||||
|
webob
|
||||||
|
webtest
|
21
tools/with_venv.sh
Executable file
21
tools/with_venv.sh
Executable file
@ -0,0 +1,21 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright 2011 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.
|
||||||
|
|
||||||
|
TOOLS=`dirname $0`
|
||||||
|
VENV=$TOOLS/../.quantum-venv
|
||||||
|
source $VENV/bin/activate && $@
|
Loading…
Reference in New Issue
Block a user