diff --git a/.pydevproject b/.pydevproject
deleted file mode 100644
index a9cca037b..000000000
--- a/.pydevproject
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-Default
-python 2.7
-
diff --git a/bin/quantum b/bin/quantum
old mode 100644
new mode 100755
index 16ef7346d..0913c31c0
--- a/bin/quantum
+++ b/bin/quantum
@@ -19,11 +19,10 @@
# If ../quantum/__init__.py exists, add ../ to Python search path, so that
# it will override what happens to be installed in /usr/(local/)lib/python...
+import gettext
import optparse
import os
-import re
import sys
-import time
possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]),
@@ -32,14 +31,15 @@ possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]),
if os.path.exists(os.path.join(possible_topdir, 'quantum', '__init__.py')):
sys.path.insert(0, possible_topdir)
+gettext.install('quantum', unicode=1)
-from quantum.common import wsgi
+from quantum import service
from quantum.common import config
def create_options(parser):
- """
+ """
Sets up the CLI and config-file options that may be
- parsed and program commands.
+ parsed and program commands.
:param parser: The option parser
"""
config.add_common_options(parser)
@@ -52,11 +52,10 @@ if __name__ == '__main__':
(options, args) = config.parse_options(oparser)
try:
- conf, app = config.load_paste_app('quantum', options, args)
-
- server = wsgi.Server()
- server.start(app, int(conf['bind_port']), conf['bind_host'])
- server.wait()
+ service = service.serve_wsgi(service.QuantumApiService,
+ options=options,
+ args=args)
+ service.wait()
except RuntimeError, e:
- sys.exit("ERROR: %s" % e)
+ sys.exit("ERROR: %s" % e)
diff --git a/etc/quantum.conf b/etc/quantum.conf
new file mode 100644
index 000000000..ba96a9a27
--- /dev/null
+++ b/etc/quantum.conf
@@ -0,0 +1,25 @@
+[DEFAULT]
+# Show more verbose log output (sets INFO log level output)
+verbose = True
+
+# Show debugging output in logs (sets DEBUG log level output)
+debug = True
+
+# Address to bind the API server
+bind_host = 0.0.0.0
+
+# Port the bind the API server to
+bind_port = 9696
+
+[composite:quantum]
+use = egg:Paste#urlmap
+/: quantumversions
+/v0.1: quantumapi
+
+[app:quantumversions]
+paste.app_factory = quantum.api.versions:Versions.factory
+
+[app:quantumapi]
+paste.app_factory = quantum.api:APIRouterV01.factory
+
+
diff --git a/etc/quantum/api-paste.ini b/etc/quantum/api-paste.ini
new file mode 100644
index 000000000..48f79849c
--- /dev/null
+++ b/etc/quantum/api-paste.ini
@@ -0,0 +1,29 @@
+#############
+# Quantum #
+#############
+
+[composite:quantumapi]
+use = egg:Paste#urlmap
+/: quantumversions
+/v1.0: quantumapi10
+
+[pipeline:quantumapi10]
+pipeline = faultwrap auth ratelimit quantumapiapp10
+
+[filter:faultwrap]
+paste.filter_factory = quantum.api:FaultWrapper.factory
+
+[filter:auth]
+paste.filter_factory = quantum.api.auth:AuthMiddleware.factory
+
+[filter:ratelimit]
+paste.filter_factory = quantum.api.limits:RateLimitingMiddleware.factory
+
+[app:quantumapiapp10]
+paste.app_factory = nova.api.quantum:APIRouterV10.factory
+
+[pipeline:quantumversions]
+pipeline = faultwrap quantumversionapp
+
+[app:quantumversionapp]
+paste.app_factory = quantum.api.versions:Versions.factory
diff --git a/quantum/api/__init__.py b/quantum/api/__init__.py
new file mode 100644
index 000000000..25b66f557
--- /dev/null
+++ b/quantum/api/__init__.py
@@ -0,0 +1,77 @@
+# 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.
+# @author: Salvatore Orlando, Citrix Systems
+
+"""
+Quantum API controllers.
+"""
+
+import logging
+import routes
+import webob.dec
+import webob.exc
+
+from quantum.api import faults
+from quantum.api import networks
+from quantum.api import ports
+from quantum.common import flags
+from quantum.common import wsgi
+
+
+LOG = logging.getLogger('quantum.api')
+FLAGS = flags.FLAGS
+
+
+class APIRouterV01(wsgi.Router):
+ """
+ Routes requests on the Quantum API to the appropriate controller
+ """
+
+ def __init__(self, ext_mgr=None):
+ mapper = routes.Mapper()
+ self._setup_routes(mapper)
+ super(APIRouterV01, self).__init__(mapper)
+
+ def _setup_routes(self, mapper):
+
+ uri_prefix = '/tenants/{tenant_id}/'
+ mapper.resource('network', 'networks',
+ controller=networks.Controller(),
+ path_prefix=uri_prefix)
+ mapper.resource('port', 'ports',
+ controller=ports.Controller(),
+ parent_resource=dict(member_name='network',
+ collection_name=\
+ uri_prefix + 'networks'))
+
+ mapper.connect("get_resource",
+ uri_prefix + 'networks/{network_id}/' \
+ 'ports/{id}/attachment{.format}',
+ controller=ports.Controller(),
+ action="get_resource",
+ conditions=dict(method=['GET']))
+ mapper.connect("attach_resource",
+ uri_prefix + 'networks/{network_id}/' \
+ 'ports/{id}/attachment{.format}',
+ controller=ports.Controller(),
+ action="attach_resource",
+ conditions=dict(method=['PUT']))
+ mapper.connect("detach_resource",
+ uri_prefix + 'networks/{network_id}/' \
+ 'ports/{id}/attachment{.format}',
+ controller=ports.Controller(),
+ action="detach_resource",
+ conditions=dict(method=['DELETE']))
diff --git a/quantum/api/api_common.py b/quantum/api/api_common.py
new file mode 100644
index 000000000..df8608df3
--- /dev/null
+++ b/quantum/api/api_common.py
@@ -0,0 +1,70 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2011 Citrix System.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import logging
+
+from webob import exc
+
+from quantum import manager
+from quantum.common import wsgi
+
+XML_NS_V01 = 'http://netstack.org/quantum/api/v0.1'
+XML_NS_V10 = 'http://netstack.org/quantum/api/v1.0'
+LOG = logging.getLogger('quantum.api.api_common')
+
+
+class QuantumController(wsgi.Controller):
+ """ Base controller class for Quantum API """
+
+ def __init__(self, plugin_conf_file=None):
+ self._setup_network_manager()
+ super(QuantumController, self).__init__()
+
+ def _parse_request_params(self, req, params):
+ results = {}
+ for param in params:
+ param_name = param['param-name']
+ param_value = None
+ # 1- parse request body
+ if req.body:
+ des_body = self._deserialize(req.body,
+ req.best_match_content_type())
+ data = des_body and des_body.get(self._resource_name, None)
+ param_value = data and data.get(param_name, None)
+ if not param_value:
+ # 2- parse request headers
+ # prepend param name with a 'x-' prefix
+ param_value = req.headers.get("x-" + param_name, None)
+ # 3- parse request query parameters
+ if not param_value:
+ try:
+ param_value = req.str_GET[param_name]
+ except KeyError:
+ #param not found
+ pass
+ if not param_value and param['required']:
+ msg = ("Failed to parse request. " +
+ "Parameter: %(param_name)s " +
+ "not specified" % locals())
+ for line in msg.split('\n'):
+ LOG.error(line)
+ raise exc.HTTPBadRequest(msg)
+ results[param_name] = param_value or param.get('default-value')
+ return results
+
+ def _setup_network_manager(self):
+ self.network_manager = manager.QuantumManager().get_manager()
diff --git a/quantum/api/faults.py b/quantum/api/faults.py
new file mode 100644
index 000000000..a10364df1
--- /dev/null
+++ b/quantum/api/faults.py
@@ -0,0 +1,148 @@
+# 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 webob.dec
+import webob.exc
+
+from quantum.api import api_common as common
+from quantum.common import wsgi
+
+
+class Fault(webob.exc.HTTPException):
+ """Error codes for API faults"""
+
+ _fault_names = {
+ 400: "malformedRequest",
+ 401: "unauthorized",
+ 420: "networkNotFound",
+ 421: "networkInUse",
+ 430: "portNotFound",
+ 431: "requestedStateInvalid",
+ 432: "portInUse",
+ 440: "alreadyAttached",
+ 470: "serviceUnavailable",
+ 471: "pluginFault"
+ }
+
+ def __init__(self, exception):
+ """Create a Fault for the given webob.exc.exception."""
+ self.wrapped_exc = exception
+
+ @webob.dec.wsgify(RequestClass=wsgi.Request)
+ def __call__(self, req):
+ """Generate a WSGI response based on the exception passed to ctor."""
+ # Replace the body with fault details.
+ code = self.wrapped_exc.status_int
+ fault_name = self._fault_names.get(code, "quantumServiceFault")
+ fault_data = {
+ fault_name: {
+ 'code': code,
+ 'message': self.wrapped_exc.explanation,
+ 'detail': self.wrapped_exc.detail}}
+ # 'code' is an attribute on the fault tag itself
+ metadata = {'application/xml': {'attributes': {fault_name: 'code'}}}
+ default_xmlns = common.XML_NS_V10
+ serializer = wsgi.Serializer(metadata, default_xmlns)
+ content_type = req.best_match_content_type()
+ self.wrapped_exc.body = serializer.serialize(fault_data, content_type)
+ self.wrapped_exc.content_type = content_type
+ return self.wrapped_exc
+
+
+class NetworkNotFound(webob.exc.HTTPClientError):
+ """
+ subclass of :class:`~HTTPClientError`
+
+ This indicates that the server did not find the network specified
+ in the HTTP request
+
+ code: 420, title: Network not Found
+ """
+ code = 420
+ title = 'Network not Found'
+ explanation = ('Unable to find a network with the specified identifier.')
+
+
+class NetworkInUse(webob.exc.HTTPClientError):
+ """
+ subclass of :class:`~HTTPClientError`
+
+ This indicates that the server could not delete the network as there is
+ at least an attachment plugged into its ports
+
+ code: 421, title: Network In Use
+ """
+ code = 421
+ title = 'Network in Use'
+ explanation = ('Unable to remove the network: attachments still plugged.')
+
+
+class PortNotFound(webob.exc.HTTPClientError):
+ """
+ subclass of :class:`~HTTPClientError`
+
+ This indicates that the server did not find the port specified
+ in the HTTP request for a given network
+
+ code: 430, title: Port not Found
+ """
+ code = 430
+ title = 'Port not Found'
+ explanation = ('Unable to find a port with the specified identifier.')
+
+
+class RequestedStateInvalid(webob.exc.HTTPClientError):
+ """
+ subclass of :class:`~HTTPClientError`
+
+ This indicates that the server could not update the port state to
+ to the request value
+
+ code: 431, title: Requested State Invalid
+ """
+ code = 431
+ title = 'Requested State Invalid'
+ explanation = ('Unable to update port state with specified value.')
+
+
+class PortInUse(webob.exc.HTTPClientError):
+ """
+ subclass of :class:`~HTTPClientError`
+
+ This indicates that the server could not remove o port or attach
+ a resource to it because there is an attachment plugged into the port
+
+ code: 432, title: PortInUse
+ """
+ code = 432
+ title = 'Port in Use'
+ explanation = ('A resource is currently attached to the logical port')
+
+
+class AlreadyAttached(webob.exc.HTTPClientError):
+ """
+ subclass of :class:`~HTTPClientError`
+
+ This indicates that the server refused an attempt to re-attach a resource
+ already attached to the network
+
+ code: 440, title: AlreadyAttached
+ """
+ code = 440
+ title = 'Already Attached'
+ explanation = ('The resource is already attached to another port')
diff --git a/quantum/api/networks.py b/quantum/api/networks.py
new file mode 100644
index 000000000..98dd5e305
--- /dev/null
+++ b/quantum/api/networks.py
@@ -0,0 +1,111 @@
+# 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 logging
+
+from webob import exc
+
+from quantum.api import api_common as common
+from quantum.api import faults
+from quantum.api.views import networks as networks_view
+from quantum.common import exceptions as exception
+
+LOG = logging.getLogger('quantum.api.networks')
+
+
+class Controller(common.QuantumController):
+ """ Network API controller for Quantum API """
+
+ _network_ops_param_list = [{
+ 'param-name': 'network-name',
+ 'required': True}, ]
+
+ _serialization_metadata = {
+ "application/xml": {
+ "attributes": {
+ "network": ["id", "name"],
+ },
+ },
+ }
+
+ def __init__(self, plugin_conf_file=None):
+ self._resource_name = 'network'
+ super(Controller, self).__init__()
+
+ def index(self, req, tenant_id):
+ """ Returns a list of network ids """
+ #TODO: this should be for a given tenant!!!
+ return self._items(req, tenant_id, is_detail=False)
+
+ def _items(self, req, tenant_id, is_detail):
+ """ Returns a list of networks. """
+ networks = self.network_manager.get_all_networks(tenant_id)
+ builder = networks_view.get_view_builder(req)
+ result = [builder.build(network, is_detail)['network']
+ for network in networks]
+ return dict(networks=result)
+
+ def show(self, req, tenant_id, id):
+ """ Returns network details for the given network id """
+ try:
+ network = self.network_manager.get_network_details(
+ tenant_id, id)
+ builder = networks_view.get_view_builder(req)
+ #build response with details
+ result = builder.build(network, True)
+ return dict(networks=result)
+ except exception.NetworkNotFound as e:
+ return faults.Fault(faults.NetworkNotFound(e))
+
+ def create(self, req, tenant_id):
+ """ Creates a new network for a given tenant """
+ #look for network name in request
+ try:
+ req_params = \
+ self._parse_request_params(req, self._network_ops_param_list)
+ except exc.HTTPError as e:
+ return faults.Fault(e)
+ network = self.network_manager.\
+ create_network(tenant_id,req_params['network-name'])
+ builder = networks_view.get_view_builder(req)
+ result = builder.build(network)
+ return dict(networks=result)
+
+ def update(self, req, tenant_id, id):
+ """ Updates the name for the network with the given id """
+ try:
+ req_params = \
+ self._parse_request_params(req, self._network_ops_param_list)
+ except exc.HTTPError as e:
+ return faults.Fault(e)
+ try:
+ network = self.network_manager.rename_network(tenant_id,
+ id, req_params['network-name'])
+
+ builder = networks_view.get_view_builder(req)
+ result = builder.build(network, True)
+ return dict(networks=result)
+ except exception.NetworkNotFound as e:
+ return faults.Fault(faults.NetworkNotFound(e))
+
+ def delete(self, req, tenant_id, id):
+ """ Destroys the network with the given id """
+ try:
+ self.network_manager.delete_network(tenant_id, id)
+ return exc.HTTPAccepted()
+ except exception.NetworkNotFound as e:
+ return faults.Fault(faults.NetworkNotFound(e))
+ except exception.NetworkInUse as e:
+ return faults.Fault(faults.NetworkInUse(e))
diff --git a/quantum/api/ports.py b/quantum/api/ports.py
new file mode 100644
index 000000000..2b93cdec7
--- /dev/null
+++ b/quantum/api/ports.py
@@ -0,0 +1,183 @@
+# 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 logging
+
+from webob import exc
+
+from quantum.api import api_common as common
+from quantum.api import faults
+from quantum.api.views import ports as ports_view
+from quantum.common import exceptions as exception
+
+LOG = logging.getLogger('quantum.api.ports')
+
+class Controller(common.QuantumController):
+ """ Port API controller for Quantum API """
+
+ _port_ops_param_list = [{
+ 'param-name': 'port-state',
+ 'default-value': 'DOWN',
+ 'required': False},]
+
+
+ _attachment_ops_param_list = [{
+ 'param-name': 'attachment-id',
+ 'required': True},]
+
+
+ _serialization_metadata = {
+ "application/xml": {
+ "attributes": {
+ "port": ["id","state"],
+ },
+ },
+ }
+
+ def __init__(self, plugin_conf_file=None):
+ self._resource_name = 'port'
+ super(Controller, self).__init__()
+
+ def index(self, req, tenant_id, network_id):
+ """ Returns a list of port ids for a given network """
+ return self._items(req, tenant_id, network_id, is_detail=False)
+
+ def _items(self, req, tenant_id, network_id, is_detail):
+ """ Returns a list of networks. """
+ try :
+ ports = self.network_manager.get_all_ports(tenant_id, network_id)
+ builder = ports_view.get_view_builder(req)
+ result = [builder.build(port, is_detail)['port']
+ for port in ports]
+ return dict(ports=result)
+ except exception.NetworkNotFound as e:
+ return faults.Fault(faults.NetworkNotFound(e))
+
+ def show(self, req, tenant_id, network_id, id):
+ """ Returns port details for given port and network """
+ try:
+ port = self.network_manager.get_port_details(
+ tenant_id, network_id, id)
+ builder = ports_view.get_view_builder(req)
+ #build response with details
+ result = builder.build(port, True)
+ return dict(ports=result)
+ except exception.NetworkNotFound as e:
+ return faults.Fault(faults.NetworkNotFound(e))
+ except exception.PortNotFound as e:
+ return faults.Fault(faults.PortNotFound(e))
+
+ def create(self, req, tenant_id, network_id):
+ """ Creates a new port for a given network """
+ #look for port state in request
+ try:
+ req_params = \
+ self._parse_request_params(req, self._port_ops_param_list)
+ except exc.HTTPError as e:
+ return faults.Fault(e)
+ try:
+ port = self.network_manager.create_port(tenant_id,
+ network_id,
+ req_params['port-state'])
+ builder = ports_view.get_view_builder(req)
+ result = builder.build(port)
+ return dict(ports=result)
+ except exception.NetworkNotFound as e:
+ return faults.Fault(faults.NetworkNotFound(e))
+ except exception.StateInvalid as e:
+ return faults.Fault(faults.RequestedStateInvalid(e))
+
+ def update(self, req, tenant_id, network_id, id):
+ """ Updates the state of a port for a given network """
+ #look for port state in request
+ try:
+ req_params = \
+ self._parse_request_params(req, self._port_ops_param_list)
+ except exc.HTTPError as e:
+ return faults.Fault(e)
+ try:
+ port = self.network_manager.update_port(tenant_id,network_id, id,
+ req_params['port-state'])
+ builder = ports_view.get_view_builder(req)
+ result = builder.build(port, True)
+ return dict(ports=result)
+ except exception.NetworkNotFound as e:
+ return faults.Fault(faults.NetworkNotFound(e))
+ except exception.PortNotFound as e:
+ return faults.Fault(faults.PortNotFound(e))
+ except exception.StateInvalid as e:
+ return faults.Fault(faults.RequestedStateInvalid(e))
+
+
+ def delete(self, req, tenant_id, network_id, id):
+ """ Destroys the port with the given id """
+ #look for port state in request
+ try:
+ self.network_manager.delete_port(tenant_id, network_id, id)
+ return exc.HTTPAccepted()
+ #TODO(salvatore-orlando): Handle portInUse error
+ except exception.NetworkNotFound as e:
+ return faults.Fault(faults.NetworkNotFound(e))
+ except exception.PortNotFound as e:
+ return faults.Fault(faults.PortNotFound(e))
+ except exception.PortInUse as e:
+ return faults.Fault(faults.PortInUse(e))
+
+
+ def get_resource(self,req,tenant_id, network_id, id):
+ try:
+ result = self.network_manager.get_interface_details(
+ tenant_id, network_id, id)
+ return dict(attachment=result)
+ except exception.NetworkNotFound as e:
+ return faults.Fault(faults.NetworkNotFound(e))
+ except exception.PortNotFound as e:
+ return faults.Fault(faults.PortNotFound(e))
+
+ #TODO - Complete implementation of these APIs
+ def attach_resource(self,req,tenant_id, network_id, id):
+ content_type = req.best_match_content_type()
+ print "Content type:%s" %content_type
+ try:
+ req_params = \
+ self._parse_request_params(req,
+ self._attachment_ops_param_list)
+ except exc.HTTPError as e:
+ return faults.Fault(e)
+ try:
+ self.network_manager.plug_interface(tenant_id,
+ network_id,id,
+ req_params['attachment-id'])
+ return exc.HTTPAccepted()
+ except exception.NetworkNotFound as e:
+ return faults.Fault(faults.NetworkNotFound(e))
+ except exception.PortNotFound as e:
+ return faults.Fault(faults.PortNotFound(e))
+ except exception.PortInUse as e:
+ return faults.Fault(faults.PortInUse(e))
+ except exception.AlreadyAttached as e:
+ return faults.Fault(faults.AlreadyAttached(e))
+
+
+ #TODO - Complete implementation of these APIs
+ def detach_resource(self,req,tenant_id, network_id, id):
+ try:
+ self.network_manager.unplug_interface(tenant_id,
+ network_id,id)
+ return exc.HTTPAccepted()
+ except exception.NetworkNotFound as e:
+ return faults.Fault(faults.NetworkNotFound(e))
+ except exception.PortNotFound as e:
+ return faults.Fault(faults.PortNotFound(e))
diff --git a/quantum/api/versions.py b/quantum/api/versions.py
new file mode 100644
index 000000000..18635040a
--- /dev/null
+++ b/quantum/api/versions.py
@@ -0,0 +1,63 @@
+# 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 logging
+import webob.dec
+
+from quantum.common import wsgi
+from quantum.api.views import versions as versions_view
+
+LOG = logging.getLogger('quantum.api.versions')
+
+
+class Versions(wsgi.Application):
+
+ @webob.dec.wsgify(RequestClass=wsgi.Request)
+ def __call__(self, req):
+ """Respond to a request for all Quantum API versions."""
+ version_objs = [
+ {
+ "id": "v0.1",
+ "status": "CURRENT",
+ },
+ {
+ "id": "v1.0",
+ "status": "FUTURE",
+ },
+ ]
+
+ builder = versions_view.get_view_builder(req)
+ versions = [builder.build(version) for version in version_objs]
+ response = dict(versions=versions)
+ metadata = {
+ "application/xml": {
+ "attributes": {
+ "version": ["status", "id"],
+ "link": ["rel", "href"],
+ }
+ }
+ }
+
+ content_type = req.best_match_content_type()
+ body = wsgi.Serializer(metadata=metadata). \
+ serialize(response, content_type)
+
+ response = webob.Response()
+ response.content_type = content_type
+ response.body = body
+
+ return response
diff --git a/quantum/api/views/__init__.py b/quantum/api/views/__init__.py
new file mode 100644
index 000000000..ea9103400
--- /dev/null
+++ b/quantum/api/views/__init__.py
@@ -0,0 +1,16 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+# Copyright 2011 Citrix Systems, 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.
\ No newline at end of file
diff --git a/quantum/api/views/networks.py b/quantum/api/views/networks.py
new file mode 100644
index 000000000..2a64d87f0
--- /dev/null
+++ b/quantum/api/views/networks.py
@@ -0,0 +1,50 @@
+# 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 os
+
+
+def get_view_builder(req):
+ base_url = req.application_url
+ return ViewBuilder(base_url)
+
+
+class ViewBuilder(object):
+
+ def __init__(self, base_url):
+ """
+ :param base_url: url of the root wsgi application
+ """
+ self.base_url = base_url
+
+ def build(self, network_data, is_detail=False):
+ """Generic method used to generate a network entity."""
+ print "NETWORK-DATA:%s" %network_data
+ if is_detail:
+ network = self._build_detail(network_data)
+ else:
+ network = self._build_simple(network_data)
+ return network
+
+ def _build_simple(self, network_data):
+ """Return a simple model of a server."""
+ return dict(network=dict(id=network_data['net-id']))
+
+ def _build_detail(self, network_data):
+ """Return a simple model of a server."""
+ return dict(network=dict(id=network_data['net-id'],
+ name=network_data['net-name']))
diff --git a/quantum/api/views/ports.py b/quantum/api/views/ports.py
new file mode 100644
index 000000000..2d93a35f6
--- /dev/null
+++ b/quantum/api/views/ports.py
@@ -0,0 +1,48 @@
+# 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.
+
+
+def get_view_builder(req):
+ base_url = req.application_url
+ return ViewBuilder(base_url)
+
+
+class ViewBuilder(object):
+
+ def __init__(self, base_url):
+ """
+ :param base_url: url of the root wsgi application
+ """
+ self.base_url = base_url
+
+ def build(self, port_data, is_detail=False):
+ """Generic method used to generate a port entity."""
+ print "PORT-DATA:%s" %port_data
+ if is_detail:
+ port = self._build_detail(port_data)
+ else:
+ port = self._build_simple(port_data)
+ return port
+
+ def _build_simple(self, port_data):
+ """Return a simple model of a server."""
+ return dict(port=dict(id=port_data['port-id']))
+
+ def _build_detail(self, port_data):
+ """Return a simple model of a server."""
+ return dict(port=dict(id=port_data['port-id'],
+ state=port_data['port-state']))
diff --git a/quantum/api/views/versions.py b/quantum/api/views/versions.py
new file mode 100644
index 000000000..d0145c94a
--- /dev/null
+++ b/quantum/api/views/versions.py
@@ -0,0 +1,59 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2010-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.
+
+import os
+
+
+def get_view_builder(req):
+ base_url = req.application_url
+ return ViewBuilder(base_url)
+
+
+class ViewBuilder(object):
+
+ def __init__(self, base_url):
+ """
+ :param base_url: url of the root wsgi application
+ """
+ self.base_url = base_url
+
+ def build(self, version_data):
+ """Generic method used to generate a version entity."""
+ version = {
+ "id": version_data["id"],
+ "status": version_data["status"],
+ "links": self._build_links(version_data),
+ }
+
+ return version
+
+ def _build_links(self, version_data):
+ """Generate a container of links that refer to the provided version."""
+ href = self.generate_href(version_data["id"])
+
+ links = [
+ {
+ "rel": "self",
+ "href": href,
+ },
+ ]
+
+ return links
+
+ def generate_href(self, version_number):
+ """Create an url that refers to a specific version_number."""
+ return os.path.join(self.base_url, version_number)
diff --git a/quantum/cli.py b/quantum/cli.py
index 0614f95f2..78f1a6b48 100644
--- a/quantum/cli.py
+++ b/quantum/cli.py
@@ -20,37 +20,37 @@ import sys
from manager import QuantumManager
-def usage():
- print "\nUsage:"
- print "list_nets "
+def usage():
+ print "\nUsage:"
+ print "list_nets "
print "create_net "
- print "delete_net "
+ print "delete_net "
print "detail_net "
print "rename_net "
print "list_ports "
print "create_port "
print "delete_port "
- print "detail_port "
+ print "detail_port "
print "plug_iface "
print "unplug_iface "
print "detail_iface "
print "list_iface \n"
if len(sys.argv) < 2 or len(sys.argv) > 6:
- usage()
- exit(1)
-
+ usage()
+ exit(1)
+
quantum = QuantumManager()
manager = quantum.get_manager()
-if sys.argv[1] == "list_nets" and len(sys.argv) == 3:
+if sys.argv[1] == "list_nets" and len(sys.argv) == 3:
network_on_tenant = manager.get_all_networks(sys.argv[2])
print "Virtual Networks on Tenant:%s\n" % sys.argv[2]
for k, v in network_on_tenant.iteritems():
print"\tNetwork ID:%s \n\tNetwork Name:%s \n" % (k, v)
elif sys.argv[1] == "create_net" and len(sys.argv) == 4:
new_net_id = manager.create_network(sys.argv[2], sys.argv[3])
- print "Created a new Virtual Network with ID:%s\n" % new_net_id
+ 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]
@@ -58,7 +58,7 @@ elif sys.argv[1] == "detail_net" and len(sys.argv) == 4:
vif_list = manager.get_network_details(sys.argv[2], sys.argv[3])
print "Remote Interfaces on Virtual Network:%s\n" % sys.argv[3]
for iface in vif_list:
- print "\tRemote interface :%s" % iface
+ print "\tRemote interface :%s" % iface
elif sys.argv[1] == "rename_net" and len(sys.argv) == 5:
manager.rename_network(sys.argv[2], sys.argv[3], sys.argv[4])
print "Renamed Virtual Network with ID:%s" % sys.argv[3]
@@ -66,33 +66,45 @@ elif sys.argv[1] == "list_ports" and len(sys.argv) == 4:
ports = manager.get_all_ports(sys.argv[2], sys.argv[3])
print " Virtual Ports on Virtual Network:%s\n" % sys.argv[3]
for port in ports:
- print "\tVirtual Port:%s" % port
+ print "\tVirtual Port:%s" % port
elif sys.argv[1] == "create_port" and len(sys.argv) == 4:
new_port = manager.create_port(sys.argv[2], sys.argv[3])
- print "Created Virtual Port:%s on Virtual Network:%s" % (new_port, sys.argv[3])
+ print "Created Virtual Port:%s " \
+ "on Virtual Network:%s" % (new_port, sys.argv[3])
elif sys.argv[1] == "delete_port" and len(sys.argv) == 5:
manager.delete_port(sys.argv[2], sys.argv[3], sys.argv[4])
- print "Deleted Virtual Port:%s on Virtual Network:%s" % (sys.argv[3], sys.argv[4])
+ print "Deleted Virtual Port:%s " \
+ "on Virtual Network:%s" % (sys.argv[3], sys.argv[4])
elif sys.argv[1] == "detail_port" and len(sys.argv) == 5:
- port_detail = manager.get_port_details(sys.argv[2], sys.argv[3], sys.argv[4])
- print "Virtual Port:%s on Virtual Network:%s contains remote interface:%s" % (sys.argv[3], sys.argv[4], port_detail)
+ port_detail = manager.get_port_details(sys.argv[2],
+ sys.argv[3], sys.argv[4])
+ print "Virtual Port:%s on Virtual Network:%s " \
+ "contains remote interface:%s" % (sys.argv[3],
+ sys.argv[4],
+ port_detail)
elif sys.argv[1] == "plug_iface" and len(sys.argv) == 6:
manager.plug_interface(sys.argv[2], sys.argv[3], sys.argv[4], sys.argv[5])
- print "Plugged remote interface:%s into Virtual Network:%s" % (sys.argv[5], sys.argv[3])
+ print "Plugged remote interface:%s " \
+ "into Virtual Network:%s" % (sys.argv[5], sys.argv[3])
elif sys.argv[1] == "unplug_iface" and len(sys.argv) == 5:
manager.unplug_interface(sys.argv[2], sys.argv[3], sys.argv[4])
- print "UnPlugged remote interface from Virtual Port:%s Virtual Network:%s" % (sys.argv[4], sys.argv[3])
+ print "UnPlugged remote interface " \
+ "from Virtual Port:%s Virtual Network:%s" % (sys.argv[4],
+ sys.argv[3])
elif sys.argv[1] == "detail_iface" and len(sys.argv) == 5:
- remote_iface = manager.get_interface_details(sys.argv[2], sys.argv[3], sys.argv[4])
- print "Remote interface on Virtual Port:%s Virtual Network:%s is %s" % (sys.argv[4], sys.argv[3], remote_iface)
+ remote_iface = manager.get_interface_details(sys.argv[2],
+ sys.argv[3], sys.argv[4])
+ print "Remote interface on Virtual Port:%s " \
+ "Virtual Network:%s is %s" % (sys.argv[4],
+ sys.argv[3], remote_iface)
elif sys.argv[1] == "list_iface" and len(sys.argv) == 4:
iface_list = manager.get_all_attached_interfaces(sys.argv[2], sys.argv[3])
print "Remote Interfaces on Virtual Network:%s\n" % sys.argv[3]
for iface in iface_list:
- print "\tRemote interface :%s" % iface
+ print "\tRemote interface :%s" % iface
elif sys.argv[1] == "all" and len(sys.argv) == 2:
print "Not Implemented"
-else:
- print "invalid arguments: %s" % str(sys.argv)
+else:
+ print "invalid arguments: %s" % str(sys.argv)
usage()
diff --git a/quantum/common/config.py b/quantum/common/config.py
index dbbcd260f..cda765078 100644
--- a/quantum/common/config.py
+++ b/quantum/common/config.py
@@ -31,11 +31,15 @@ import sys
from paste import deploy
-import quantum.common.exception as exception
+from quantum.common import flags
+from quantum.common import exceptions as exception
DEFAULT_LOG_FORMAT = "%(asctime)s %(levelname)8s [%(name)s] %(message)s"
DEFAULT_LOG_DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
+FLAGS = flags.FLAGS
+LOG = logging.getLogger('quantum.common.wsgi')
+
def parse_options(parser, cli_args=None):
"""
@@ -186,8 +190,8 @@ def find_config_file(options, args):
* .
* ~.quantum/
* ~
- * /etc/quantum
- * /etc
+ * $FLAGS.state_path/etc/quantum
+ * $FLAGS.state_path/etc
:retval Full path to config file, or None if no config file found
"""
@@ -204,9 +208,10 @@ def find_config_file(options, args):
config_file_dirs = [fix_path(os.getcwd()),
fix_path(os.path.join('~', '.quantum')),
fix_path('~'),
+ os.path.join(FLAGS.state_path, 'etc'),
+ os.path.join(FLAGS.state_path, 'etc','quantum'),
'/etc/quantum/',
'/etc']
-
for cfg_dir in config_file_dirs:
cfg_file = os.path.join(cfg_dir, 'quantum.conf')
if os.path.exists(cfg_file):
@@ -239,10 +244,12 @@ def load_paste_config(app_name, options, args):
problem loading the configuration file.
"""
conf_file = find_config_file(options, args)
+ print "Conf_file:%s" %conf_file
if not conf_file:
raise RuntimeError("Unable to locate any configuration file. "
"Cannot load application %s" % app_name)
try:
+ print "App_name:%s" %app_name
conf = deploy.appconfig("config:%s" % conf_file, name=app_name)
return conf_file, conf
except Exception, e:
@@ -250,7 +257,7 @@ def load_paste_config(app_name, options, args):
% (conf_file, e))
-def load_paste_app(app_name, options, args):
+def load_paste_app(conf_file, app_name):
"""
Builds and returns a WSGI app from a paste config file.
@@ -271,40 +278,16 @@ def load_paste_app(app_name, options, args):
:raises RuntimeError when config file cannot be located or application
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:
- # Setup logging early, supplying both the CLI options and the
- # configuration mapping from the config file
- setup_logging(options, conf)
-
- # We only update the conf dict for the verbose and debug
- # flags. Everything else must be set up in the conf file...
- debug = options.get('debug') or \
- get_option(conf, 'debug', type='bool', default=False)
- verbose = options.get('verbose') or \
- get_option(conf, 'verbose', type='bool', default=False)
- conf['debug'] = debug
- conf['verbose'] = verbose
-
- # Log the options used when starting if we're in debug mode...
- if debug:
- logger = logging.getLogger(app_name)
- logger.debug("*" * 80)
- logger.debug("Configuration options gathered from config file:")
- logger.debug(conf_file)
- logger.debug("================================================")
- items = dict([(k, v) for k, v in conf.items()
- if k not in ('__file__', 'here')])
- for key, value in sorted(items.items()):
- logger.debug("%(key)-30s %(value)s" % locals())
- logger.debug("*" * 80)
+ conf_file = os.path.abspath(conf_file)
app = deploy.loadapp("config:%s" % conf_file, name=app_name)
except (LookupError, ImportError), e:
raise RuntimeError("Unable to load %(app_name)s from "
"configuration file %(conf_file)s."
"\nGot: %(e)r" % locals())
- return conf, app
+ return app
def get_option(options, option, **kwargs):
diff --git a/quantum/common/exceptions.py b/quantum/common/exceptions.py
index c434e736e..7b9784b92 100644
--- a/quantum/common/exceptions.py
+++ b/quantum/common/exceptions.py
@@ -21,10 +21,30 @@ Quantum-type exceptions. SHOULD include dedicated exception logging.
"""
import logging
-import sys
-import traceback
+class QuantumException(Exception):
+ """Base Quantum Exception
+
+ Taken from nova.exception.NovaException
+ To correctly use this class, inherit from it and define
+ a 'message' property. That message will get printf'd
+ with the keyword arguments provided to the constructor.
+
+ """
+ message = _("An unknown exception occurred.")
+
+ def __init__(self, **kwargs):
+ try:
+ self._error_string = self.message % kwargs
+
+ except Exception:
+ # at least get the core message out if something happened
+ self._error_string = self.message
+
+ def __str__(self):
+ return self._error_string
+
class ProcessExecutionError(IOError):
def __init__(self, stdout=None, stderr=None, exit_code=None, cmd=None,
description=None):
@@ -49,10 +69,42 @@ class ApiError(Error):
super(ApiError, self).__init__('%s: %s' % (code, message))
-class NotFound(Error):
+class NotFound(QuantumException):
pass
+class ClassNotFound(NotFound):
+ message = _("Class %(class_name)s could not be found")
+
+
+class NetworkNotFound(NotFound):
+ message = _("Network %(net_id)s could not be found")
+
+
+class PortNotFound(NotFound):
+ message = _("Port %(port_id)s could not be found " \
+ "on network %(net_id)s")
+
+
+class StateInvalid(QuantumException):
+ message = _("Unsupported port state: %(port_state)s")
+
+
+class NetworkInUse(QuantumException):
+ message = _("Unable to complete operation on network %(net_id)s. " \
+ "There is one or more attachments plugged into its ports.")
+
+
+class PortInUse(QuantumException):
+ message = _("Unable to complete operation on port %(port_id)s " \
+ "for network %(net_id)s. The attachment '%(att_id)s" \
+ "is plugged into the logical port.")
+
+class AlreadyAttached(QuantumException):
+ message = _("Unable to plug the attachment %(att_id)s into port " \
+ "%(port_id)s for network %(net_id)s. The attachment is " \
+ "already plugged into port %(att_port_id)s")
+
class Duplicate(Error):
pass
@@ -69,6 +121,10 @@ class Invalid(Error):
pass
+class InvalidContentType(Invalid):
+ message = _("Invalid content type %(content_type)s.")
+
+
class BadInputError(Exception):
"""Error resulting from a client sending bad input to a server"""
pass
diff --git a/quantum/common/flags.py b/quantum/common/flags.py
new file mode 100644
index 000000000..947999d0f
--- /dev/null
+++ b/quantum/common/flags.py
@@ -0,0 +1,252 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2011 Citrix Systems, 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.
+
+"""Command-line flag library.
+
+Wraps gflags.
+Global flags should be defined here, the rest are defined where they're used.
+
+"""
+
+import getopt
+import os
+import string
+import sys
+
+import gflags
+
+
+class FlagValues(gflags.FlagValues):
+ """Extension of gflags.FlagValues that allows undefined and runtime flags.
+
+ Unknown flags will be ignored when parsing the command line, but the
+ command line will be kept so that it can be replayed if new flags are
+ defined after the initial parsing.
+
+ """
+
+ def __init__(self, extra_context=None):
+ gflags.FlagValues.__init__(self)
+ self.__dict__['__dirty'] = []
+ self.__dict__['__was_already_parsed'] = False
+ self.__dict__['__stored_argv'] = []
+ self.__dict__['__extra_context'] = extra_context
+
+ def __call__(self, argv):
+ # We're doing some hacky stuff here so that we don't have to copy
+ # out all the code of the original verbatim and then tweak a few lines.
+ # We're hijacking the output of getopt so we can still return the
+ # leftover args at the end
+ sneaky_unparsed_args = {"value": None}
+ original_argv = list(argv)
+
+ if self.IsGnuGetOpt():
+ orig_getopt = getattr(getopt, 'gnu_getopt')
+ orig_name = 'gnu_getopt'
+ else:
+ orig_getopt = getattr(getopt, 'getopt')
+ orig_name = 'getopt'
+
+ def _sneaky(*args, **kw):
+ optlist, unparsed_args = orig_getopt(*args, **kw)
+ sneaky_unparsed_args['value'] = unparsed_args
+ return optlist, unparsed_args
+
+ try:
+ setattr(getopt, orig_name, _sneaky)
+ args = gflags.FlagValues.__call__(self, argv)
+ except gflags.UnrecognizedFlagError:
+ # Undefined args were found, for now we don't care so just
+ # act like everything went well
+ # (these three lines are copied pretty much verbatim from the end
+ # of the __call__ function we are wrapping)
+ unparsed_args = sneaky_unparsed_args['value']
+ if unparsed_args:
+ if self.IsGnuGetOpt():
+ args = argv[:1] + unparsed_args
+ else:
+ args = argv[:1] + original_argv[-len(unparsed_args):]
+ else:
+ args = argv[:1]
+ finally:
+ setattr(getopt, orig_name, orig_getopt)
+
+ # Store the arguments for later, we'll need them for new flags
+ # added at runtime
+ self.__dict__['__stored_argv'] = original_argv
+ self.__dict__['__was_already_parsed'] = True
+ self.ClearDirty()
+ return args
+
+ def Reset(self):
+ gflags.FlagValues.Reset(self)
+ self.__dict__['__dirty'] = []
+ self.__dict__['__was_already_parsed'] = False
+ self.__dict__['__stored_argv'] = []
+
+ def SetDirty(self, name):
+ """Mark a flag as dirty so that accessing it will case a reparse."""
+ self.__dict__['__dirty'].append(name)
+
+ def IsDirty(self, name):
+ return name in self.__dict__['__dirty']
+
+ def ClearDirty(self):
+ self.__dict__['__is_dirty'] = []
+
+ def WasAlreadyParsed(self):
+ return self.__dict__['__was_already_parsed']
+
+ def ParseNewFlags(self):
+ if '__stored_argv' not in self.__dict__:
+ return
+ new_flags = FlagValues(self)
+ for k in self.__dict__['__dirty']:
+ new_flags[k] = gflags.FlagValues.__getitem__(self, k)
+
+ new_flags(self.__dict__['__stored_argv'])
+ for k in self.__dict__['__dirty']:
+ setattr(self, k, getattr(new_flags, k))
+ self.ClearDirty()
+
+ def __setitem__(self, name, flag):
+ gflags.FlagValues.__setitem__(self, name, flag)
+ if self.WasAlreadyParsed():
+ self.SetDirty(name)
+
+ def __getitem__(self, name):
+ if self.IsDirty(name):
+ self.ParseNewFlags()
+ return gflags.FlagValues.__getitem__(self, name)
+
+ def __getattr__(self, name):
+ if self.IsDirty(name):
+ self.ParseNewFlags()
+ val = gflags.FlagValues.__getattr__(self, name)
+ if type(val) is str:
+ tmpl = string.Template(val)
+ context = [self, self.__dict__['__extra_context']]
+ return tmpl.substitute(StrWrapper(context))
+ return val
+
+
+class StrWrapper(object):
+ """Wrapper around FlagValues objects.
+
+ Wraps FlagValues objects for string.Template so that we're
+ sure to return strings.
+
+ """
+ def __init__(self, context_objs):
+ self.context_objs = context_objs
+
+ def __getitem__(self, name):
+ for context in self.context_objs:
+ val = getattr(context, name, False)
+ if val:
+ return str(val)
+ raise KeyError(name)
+
+
+# Copied from gflags with small mods to get the naming correct.
+# Originally gflags checks for the first module that is not gflags that is
+# in the call chain, we want to check for the first module that is not gflags
+# and not this module.
+def _GetCallingModule():
+ """Returns the name of the module that's calling into this module.
+
+ We generally use this function to get the name of the module calling a
+ DEFINE_foo... function.
+
+ """
+ # Walk down the stack to find the first globals dict that's not ours.
+ for depth in range(1, sys.getrecursionlimit()):
+ if not sys._getframe(depth).f_globals is globals():
+ module_name = __GetModuleName(sys._getframe(depth).f_globals)
+ if module_name == 'gflags':
+ continue
+ if module_name is not None:
+ return module_name
+ raise AssertionError("No module was found")
+
+
+# Copied from gflags because it is a private function
+def __GetModuleName(globals_dict):
+ """Given a globals dict, returns the name of the module that defines it.
+
+ Args:
+ globals_dict: A dictionary that should correspond to an environment
+ providing the values of the globals.
+
+ Returns:
+ A string (the name of the module) or None (if the module could not
+ be identified.
+
+ """
+ for name, module in sys.modules.iteritems():
+ if getattr(module, '__dict__', None) is globals_dict:
+ if name == '__main__':
+ return sys.argv[0]
+ return name
+ return None
+
+
+def _wrapper(func):
+ def _wrapped(*args, **kw):
+ kw.setdefault('flag_values', FLAGS)
+ func(*args, **kw)
+ _wrapped.func_name = func.func_name
+ return _wrapped
+
+
+FLAGS = FlagValues()
+gflags.FLAGS = FLAGS
+gflags._GetCallingModule = _GetCallingModule
+
+
+DEFINE = _wrapper(gflags.DEFINE)
+DEFINE_string = _wrapper(gflags.DEFINE_string)
+DEFINE_integer = _wrapper(gflags.DEFINE_integer)
+DEFINE_bool = _wrapper(gflags.DEFINE_bool)
+DEFINE_boolean = _wrapper(gflags.DEFINE_boolean)
+DEFINE_float = _wrapper(gflags.DEFINE_float)
+DEFINE_enum = _wrapper(gflags.DEFINE_enum)
+DEFINE_list = _wrapper(gflags.DEFINE_list)
+DEFINE_spaceseplist = _wrapper(gflags.DEFINE_spaceseplist)
+DEFINE_multistring = _wrapper(gflags.DEFINE_multistring)
+DEFINE_multi_int = _wrapper(gflags.DEFINE_multi_int)
+DEFINE_flag = _wrapper(gflags.DEFINE_flag)
+HelpFlag = gflags.HelpFlag
+HelpshortFlag = gflags.HelpshortFlag
+HelpXMLFlag = gflags.HelpXMLFlag
+
+
+def DECLARE(name, module_string, flag_values=FLAGS):
+ if module_string not in sys.modules:
+ __import__(module_string, globals(), locals())
+ if name not in flag_values:
+ raise gflags.UnrecognizedFlag(
+ "%s not defined by %s" % (name, module_string))
+
+
+# __GLOBAL FLAGS ONLY__
+# Define any app-specific flags in their own files, docs at:
+# http://code.google.com/p/python-gflags/source/browse/trunk/gflags.py#a9
+
+DEFINE_string('state_path', os.path.join(os.path.dirname(__file__), '../../'),
+ "Top-level directory for maintaining quantum's state")
+
diff --git a/quantum/common/utils.py b/quantum/common/utils.py
index 435ec7b87..7ab4615b2 100644
--- a/quantum/common/utils.py
+++ b/quantum/common/utils.py
@@ -29,12 +29,13 @@ import socket
import sys
import ConfigParser
-from common import exceptions
+import exceptions as exception
+import flags
from exceptions import ProcessExecutionError
TIME_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
-
+FLAGS = flags.FLAGS
def int_from_bool_as_string(subject):
"""
@@ -71,9 +72,11 @@ def import_class(import_str):
"""Returns a class from a string including module and class"""
mod_str, _sep, class_str = import_str.rpartition('.')
try:
+ #mod_str = os.path.join(FLAGS.state_path, mod_str)
__import__(mod_str)
return getattr(sys.modules[mod_str], class_str)
- except (ImportError, ValueError, AttributeError):
+ except (ImportError, ValueError, AttributeError) as e:
+ print e
raise exception.NotFound('Class %s cannot be found' % class_str)
diff --git a/quantum/common/wsgi.py b/quantum/common/wsgi.py
index 6c7caa1dc..4caeab9ab 100644
--- a/quantum/common/wsgi.py
+++ b/quantum/common/wsgi.py
@@ -1,3 +1,4 @@
+
# vim: tabstop=4 shiftwidth=4 softtabstop=4
#
# Copyright 2011, Nicira Networks, Inc.
@@ -24,6 +25,8 @@ import logging
import sys
import datetime
+from xml.dom import minidom
+
import eventlet
import eventlet.wsgi
eventlet.patcher.monkey_patch(all=False, socket=True)
@@ -32,6 +35,10 @@ import routes.middleware
import webob.dec
import webob.exc
+from quantum import utils
+from quantum.common import exceptions as exception
+
+LOG = logging.getLogger('quantum.common.wsgi')
class WritableLogger(object):
"""A thin wrapper that responds to `write` and logs."""
@@ -110,6 +117,104 @@ class Middleware(object):
return self.process_response(response)
+class Request(webob.Request):
+
+ def best_match_content_type(self):
+ """Determine the most acceptable content-type.
+
+ Based on the query extension then the Accept header.
+
+ """
+ parts = self.path.rsplit('.', 1)
+ LOG.debug("Request parts:%s",parts)
+ if len(parts) > 1:
+ format = parts[1]
+ if format in ['json', 'xml']:
+ return 'application/{0}'.format(parts[1])
+
+ ctypes = ['application/json', 'application/xml']
+ bm = self.accept.best_match(ctypes)
+ LOG.debug("BM:%s",bm)
+ return bm or 'application/json'
+
+ def get_content_type(self):
+ allowed_types = ("application/xml", "application/json")
+ if not "Content-Type" in self.headers:
+ msg = _("Missing Content-Type")
+ LOG.debug(msg)
+ raise webob.exc.HTTPBadRequest(msg)
+ type = self.content_type
+ if type in allowed_types:
+ return type
+ LOG.debug(_("Wrong Content-Type: %s") % type)
+ raise webob.exc.HTTPBadRequest("Invalid content type")
+
+
+class Application(object):
+ """Base WSGI application wrapper. Subclasses need to implement __call__."""
+
+ @classmethod
+ def factory(cls, global_config, **local_config):
+ """Used for paste app factories in paste.deploy config files.
+
+ Any local configuration (that is, values under the [app:APPNAME]
+ section of the paste config) will be passed into the `__init__` method
+ as kwargs.
+
+ A hypothetical configuration would look like:
+
+ [app:wadl]
+ latest_version = 1.3
+ paste.app_factory = nova.api.fancy_api:Wadl.factory
+
+ which would result in a call to the `Wadl` class as
+
+ import quantum.api.fancy_api
+ fancy_api.Wadl(latest_version='1.3')
+
+ You could of course re-implement the `factory` method in subclasses,
+ but using the kwarg passing it shouldn't be necessary.
+
+ """
+ return cls(**local_config)
+
+ def __call__(self, environ, start_response):
+ r"""Subclasses will probably want to implement __call__ like this:
+
+ @webob.dec.wsgify(RequestClass=Request)
+ def __call__(self, req):
+ # Any of the following objects work as responses:
+
+ # Option 1: simple string
+ res = 'message\n'
+
+ # Option 2: a nicely formatted HTTP exception page
+ res = exc.HTTPForbidden(detail='Nice try')
+
+ # Option 3: a webob Response object (in case you need to play with
+ # headers, or you want to be treated like an iterable, or or or)
+ res = Response();
+ res.app_iter = open('somefile')
+
+ # Option 4: any wsgi app to be run next
+ res = self.application
+
+ # Option 5: you can get a Response object for a wsgi app, too, to
+ # play with headers etc
+ res = req.get_response(self.application)
+
+ # You can then just return your response...
+ return res
+ # ... or set req.response and return None.
+ req.response = res
+
+ See the end of http://pythonpaste.org/webob/modules/dec.html
+ for more info.
+
+ """
+ raise NotImplementedError(_('You must implement __call__'))
+
+
class Debug(Middleware):
"""
Helper class that can be inserted into any WSGI application chain
@@ -152,6 +257,13 @@ class Router(object):
WSGI middleware that maps incoming requests to WSGI apps.
"""
+ @classmethod
+ def factory(cls, global_config, **local_config):
+ """
+ Returns an instance of the WSGI Router class
+ """
+ return cls()
+
def __init__(self, mapper):
"""
Create a router for the given routes.Mapper.
@@ -169,7 +281,7 @@ class Router(object):
mapper.connect(None, "/svrlist", controller=sc, action="list")
# Actions are all implicitly defined
- mapper.resource("server", "servers", controller=sc)
+ mapper.resource("network", "networks", controller=nc)
# Pointing to an arbitrary WSGI app. You can specify the
# {path_info:.*} parameter so the target app can be handed just that
@@ -186,6 +298,7 @@ class Router(object):
Route the incoming request to a controller based on self.map.
If no match, return a 404.
"""
+ LOG.debug("HERE - wsgi.Router.__call__")
return self._router
@staticmethod
@@ -204,92 +317,197 @@ class Router(object):
class Controller(object):
- """
+ """WSGI app that dispatched to methods.
+
WSGI app that reads routing information supplied by RoutesMiddleware
and calls the requested action method upon itself. All action methods
must, in addition to their normal parameters, accept a 'req' argument
- which is the incoming webob.Request. They raise a webob.exc exception,
+ which is the incoming wsgi.Request. They raise a webob.exc exception,
or return a dict which will be serialized by requested content type.
+
"""
- @webob.dec.wsgify
+ @webob.dec.wsgify(RequestClass=Request)
def __call__(self, req):
"""
Call the method specified in req.environ by RoutesMiddleware.
"""
+ LOG.debug("HERE - wsgi.Controller.__call__")
arg_dict = req.environ['wsgiorg.routing_args'][1]
action = arg_dict['action']
method = getattr(self, action)
+ LOG.debug("ARG_DICT:%s",arg_dict)
+ LOG.debug("Action:%s",action)
+ LOG.debug("Method:%s",method)
+ LOG.debug("%s %s" % (req.method, req.url))
del arg_dict['controller']
del arg_dict['action']
- arg_dict['request'] = req
+ if 'format' in arg_dict:
+ del arg_dict['format']
+ arg_dict['req'] = req
result = method(**arg_dict)
+
if type(result) is dict:
- return self._serialize(result, req)
+ content_type = req.best_match_content_type()
+ LOG.debug("Content type:%s",content_type)
+ LOG.debug("Result:%s",result)
+ default_xmlns = self.get_default_xmlns(req)
+ body = self._serialize(result, content_type, default_xmlns)
+
+ response = webob.Response()
+ response.headers['Content-Type'] = content_type
+ response.body = body
+ msg_dict = dict(url=req.url, status=response.status_int)
+ msg = _("%(url)s returned with HTTP %(status)d") % msg_dict
+ LOG.debug(msg)
+ return response
else:
return result
- def _serialize(self, data, request):
- """
- Serialize the given dict to the response type requested in request.
+ def _serialize(self, data, content_type, default_xmlns):
+ """Serialize the given dict to the provided content_type.
+
Uses self._serialization_metadata if it exists, which is a dict mapping
MIME types to information needed to serialize to that type.
+
"""
- _metadata = getattr(type(self), "_serialization_metadata", {})
- serializer = Serializer(request.environ, _metadata)
- return serializer.to_content_type(data)
+ _metadata = getattr(type(self), '_serialization_metadata', {})
+
+ serializer = Serializer(_metadata, default_xmlns)
+ try:
+ return serializer.serialize(data, content_type)
+ except exception.InvalidContentType:
+ raise webob.exc.HTTPNotAcceptable()
+
+ def _deserialize(self, data, content_type):
+ """Deserialize the request body to the specefied content type.
+
+ Uses self._serialization_metadata if it exists, which is a dict mapping
+ MIME types to information needed to serialize to that type.
+
+ """
+ _metadata = getattr(type(self), '_serialization_metadata', {})
+ serializer = Serializer(_metadata)
+ return serializer.deserialize(data, content_type)
+
+ def get_default_xmlns(self, req):
+ """Provide the XML namespace to use if none is otherwise specified."""
+ return None
class Serializer(object):
- """
- Serializes a dictionary to a Content Type specified by a WSGI environment.
- """
+ """Serializes and deserializes dictionaries to certain MIME types."""
+
+ def __init__(self, metadata=None, default_xmlns=None):
+ """Create a serializer based on the given WSGI environment.
- def __init__(self, environ, metadata=None):
- """
- Create a serializer based on the given WSGI environment.
'metadata' is an optional dict mapping MIME types to information
needed to serialize a dictionary to that type.
- """
- self.environ = environ
- self.metadata = metadata or {}
- self._methods = {
- 'application/json': self._to_json,
- 'application/xml': self._to_xml}
- def to_content_type(self, data):
"""
- Serialize a dictionary into a string. The format of the string
- will be decided based on the Content Type requested in self.environ:
- by Accept: header, or by URL suffix.
+ self.metadata = metadata or {}
+ self.default_xmlns = default_xmlns
+
+ def _get_serialize_handler(self, content_type):
+ handlers = {
+ 'application/json': self._to_json,
+ 'application/xml': self._to_xml,
+ }
+
+ try:
+ return handlers[content_type]
+ except Exception:
+ raise exception.InvalidContentType(content_type=content_type)
+
+ def serialize(self, data, content_type):
+ """Serialize a dictionary into the specified content type."""
+ return self._get_serialize_handler(content_type)(data)
+
+ def deserialize(self, datastring, content_type):
+ """Deserialize a string to a dictionary.
+
+ The string must be in the format of a supported MIME type.
+
"""
- # FIXME(sirp): for now, supporting json only
- #mimetype = 'application/xml'
- mimetype = 'application/json'
- # TODO(gundlach): determine mimetype from request
- return self._methods.get(mimetype, repr)(data)
+ return self.get_deserialize_handler(content_type)(datastring)
+
+ def get_deserialize_handler(self, content_type):
+ handlers = {
+ 'application/json': self._from_json,
+ 'application/xml': self._from_xml,
+ }
+
+ try:
+ return handlers[content_type]
+ except Exception:
+ raise exception.InvalidContentType(content_type=content_type)
+
+ def _from_json(self, datastring):
+ return utils.loads(datastring)
+
+ def _from_xml(self, datastring):
+ xmldata = self.metadata.get('application/xml', {})
+ plurals = set(xmldata.get('plurals', {}))
+ node = minidom.parseString(datastring).childNodes[0]
+ return {node.nodeName: self._from_xml_node(node, plurals)}
+
+ def _from_xml_node(self, node, listnames):
+ """Convert a minidom node to a simple Python type.
+
+ listnames is a collection of names of XML nodes whose subnodes should
+ be considered list items.
+
+ """
+ if len(node.childNodes) == 1 and node.childNodes[0].nodeType == 3:
+ return node.childNodes[0].nodeValue
+ elif node.nodeName in listnames:
+ return [self._from_xml_node(n, listnames) for n in node.childNodes]
+ else:
+ result = dict()
+ for attr in node.attributes.keys():
+ result[attr] = node.attributes[attr].nodeValue
+ for child in node.childNodes:
+ if child.nodeType != node.TEXT_NODE:
+ result[child.nodeName] = self._from_xml_node(child,
+ listnames)
+ return result
def _to_json(self, data):
- def sanitizer(obj):
- if isinstance(obj, datetime.datetime):
- return obj.isoformat()
- return obj
-
- return json.dumps(data, default=sanitizer)
+ return utils.dumps(data)
def _to_xml(self, data):
metadata = self.metadata.get('application/xml', {})
# We expect data to contain a single key which is the XML root.
root_key = data.keys()[0]
- from xml.dom import minidom
doc = minidom.Document()
node = self._to_xml_node(doc, metadata, root_key, data[root_key])
+
+ xmlns = node.getAttribute('xmlns')
+ if not xmlns and self.default_xmlns:
+ node.setAttribute('xmlns', self.default_xmlns)
+
return node.toprettyxml(indent=' ')
def _to_xml_node(self, doc, metadata, nodename, data):
"""Recursive method to convert data members to XML nodes."""
result = doc.createElement(nodename)
+
+ # Set the xml namespace if one is specified
+ # TODO(justinsb): We could also use prefixes on the keys
+ xmlns = metadata.get('xmlns', None)
+ if xmlns:
+ result.setAttribute('xmlns', xmlns)
+ LOG.debug("DATA:%s",data)
if type(data) is list:
+ LOG.debug("TYPE IS LIST")
+ collections = metadata.get('list_collections', {})
+ if nodename in collections:
+ metadata = collections[nodename]
+ for item in data:
+ node = doc.createElement(metadata['item_name'])
+ node.setAttribute(metadata['item_key'], str(item))
+ result.appendChild(node)
+ return result
singular = metadata.get('plurals', {}).get(nodename, None)
if singular is None:
if nodename.endswith('s'):
@@ -300,6 +518,17 @@ class Serializer(object):
node = self._to_xml_node(doc, metadata, singular, item)
result.appendChild(node)
elif type(data) is dict:
+ LOG.debug("TYPE IS DICT")
+ collections = metadata.get('dict_collections', {})
+ if nodename in collections:
+ metadata = collections[nodename]
+ for k, v in data.items():
+ node = doc.createElement(metadata['item_name'])
+ node.setAttribute(metadata['item_key'], str(k))
+ text = doc.createTextNode(str(v))
+ node.appendChild(text)
+ result.appendChild(node)
+ return result
attrs = metadata.get('attributes', {}).get(nodename, {})
for k, v in data.items():
if k in attrs:
@@ -307,7 +536,10 @@ class Serializer(object):
else:
node = self._to_xml_node(doc, metadata, k, v)
result.appendChild(node)
- else: # atom
+ else:
+ # Type is atom
+ LOG.debug("TYPE IS ATOM:%s",data)
node = doc.createTextNode(str(data))
result.appendChild(node)
return result
+
diff --git a/quantum/manager.py b/quantum/manager.py
index a36e9b28f..79a5139d8 100644
--- a/quantum/manager.py
+++ b/quantum/manager.py
@@ -23,26 +23,33 @@ plugin that concretely implement quantum_plugin_base class
The caller should make sure that QuantumManager is a singleton.
"""
+import gettext
+gettext.install('quantum', unicode=1)
from common import utils
from quantum_plugin_base import QuantumPluginBase
CONFIG_FILE = "plugins.ini"
+
class QuantumManager(object):
-
- def __init__(self,config=CONFIG_FILE):
+
+ def __init__(self,config=CONFIG_FILE):
self.configuration_file = CONFIG_FILE
plugin_location = utils.getPluginFromConfig(CONFIG_FILE)
+ print "PLUGIN LOCATION:%s" % plugin_location
plugin_klass = utils.import_class(plugin_location)
if not issubclass(plugin_klass, QuantumPluginBase):
- raise Exception("Configured Quantum plug-in didn't pass compatibility test")
+ raise Exception("Configured Quantum plug-in " \
+ "didn't pass compatibility test")
else:
- print("Successfully imported Quantum plug-in. All compatibility tests passed\n")
+ print("Successfully imported Quantum plug-in." \
+ "All compatibility tests passed\n")
self.plugin = plugin_klass()
-
- def get_manager(self):
- return self.plugin
+
+ def get_manager(self):
+ return self.plugin
+
# TODO(somik): rmove the main class
# Added for temporary testing purposes
@@ -55,4 +62,3 @@ def main():
# Standard boilerplate to call the main() function.
if __name__ == '__main__':
main()
-
diff --git a/quantum/plugins.ini b/quantum/plugins.ini
index 61c7694cd..307d2b48d 100644
--- a/quantum/plugins.ini
+++ b/quantum/plugins.ini
@@ -1,3 +1,3 @@
[PLUGIN]
# Quantum plugin provider module
-provider = plugins.SamplePlugin.DummyDataPlugin
+provider = quantum.plugins.SamplePlugin.FakePlugin
diff --git a/quantum/plugins/SamplePlugin.py b/quantum/plugins/SamplePlugin.py
index 5088b71fa..41dd3271d 100644
--- a/quantum/plugins/SamplePlugin.py
+++ b/quantum/plugins/SamplePlugin.py
@@ -15,6 +15,8 @@
# under the License.
# @author: Somik Behera, Nicira Networks, Inc.
+from quantum.common import exceptions as exc
+
class QuantumEchoPlugin(object):
"""
@@ -89,6 +91,12 @@ class QuantumEchoPlugin(object):
is deleted.
"""
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):
@@ -113,22 +121,7 @@ class QuantumEchoPlugin(object):
specified Virtual Network.
"""
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")
-
-
- 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")
+
class DummyDataPlugin(object):
@@ -202,6 +195,12 @@ class DummyDataPlugin(object):
print("create_port() called\n")
#return the port id
return 201
+
+ 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 delete_port(self, tenant_id, net_id, port_id):
@@ -240,23 +239,226 @@ class DummyDataPlugin(object):
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"
+class FakePlugin(object):
+ """
+ FakePlugin is a demo plugin that provides
+ in-memory data structures to aid in quantum
+ client/cli/api development
+ """
+
+ #static data for networks and ports
+ _port_dict_1 = {
+ 1 : {'port-id': 1,
+ 'port-state': 'DOWN',
+ 'attachment': None},
+ 2 : {'port-id': 2,
+ 'port-state':'UP',
+ 'attachment': None}
+ }
+ _port_dict_2 = {
+ 1 : {'port-id': 1,
+ 'port-state': 'UP',
+ 'attachment': 'SomeFormOfVIFID'},
+ 2 : {'port-id': 2,
+ 'port-state':'DOWN',
+ 'attachment': None}
+ }
+ _networks={'001':
+ {
+ 'net-id':'001',
+ 'net-name':'pippotest',
+ 'net-ports': _port_dict_1
+ },
+ '002':
+ {
+ 'net-id':'002',
+ 'net-name':'cicciotest',
+ 'net-ports': _port_dict_2
+ }}
- def get_all_attached_interfaces(self, tenant_id, net_id):
+ def __init__(self):
+ FakePlugin._net_counter=len(FakePlugin._networks)
+
+ def _get_network(self, tenant_id, network_id):
+ network = FakePlugin._networks.get(network_id)
+ if not network:
+ raise exc.NetworkNotFound(net_id=network_id)
+ return network
+
+
+ def _get_port(self, tenant_id, network_id, port_id):
+ net = self._get_network(tenant_id, network_id)
+ port = net['net-ports'].get(int(port_id))
+ if not port:
+ raise exc.PortNotFound(net_id=network_id, port_id=port_id)
+ return port
+
+ def _validate_port_state(self, port_state):
+ if port_state.upper() not in ('UP','DOWN'):
+ raise exc.StateInvalid(port_state=port_state)
+ return True
+
+ def _validate_attachment(self, tenant_id, network_id, port_id,
+ remote_interface_id):
+ network = self._get_network(tenant_id, network_id)
+ for port in network['net-ports'].values():
+ if port['attachment'] == remote_interface_id:
+ raise exc.AlreadyAttached(net_id = network_id,
+ port_id = port_id,
+ att_id = port['attachment'],
+ att_port_id = port['port-id'])
+
+ def get_all_networks(self, tenant_id):
"""
- Retrieves all remote interfaces that are attached to
- a particular Virtual Network.
+ Returns a dictionary containing all
+ for
+ the specified tenant.
"""
- 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
+ print("get_all_networks() called\n")
+ return FakePlugin._networks.values()
+
+ def get_network_details(self, tenant_id, net_id):
+ """
+ retrieved a list of all the remote vifs that
+ are attached to the network
+ """
+ print("get_network_details() called\n")
+ return self._get_network(tenant_id, net_id)
+
+ def create_network(self, tenant_id, net_name):
+ """
+ Creates a new Virtual Network, and assigns it
+ a symbolic name.
+ """
+ print("create_network() called\n")
+ FakePlugin._net_counter += 1
+ new_net_id=("0" * (3 - len(str(FakePlugin._net_counter)))) + \
+ str(FakePlugin._net_counter)
+ print new_net_id
+ new_net_dict={'net-id':new_net_id,
+ 'net-name':net_name,
+ 'net-ports': {}}
+ FakePlugin._networks[new_net_id]=new_net_dict
+ # return network_id of the created network
+ return new_net_dict
+
+ def delete_network(self, tenant_id, net_id):
+ """
+ Deletes the network with the specified network identifier
+ belonging to the specified tenant.
+ """
+ print("delete_network() called\n")
+ net = FakePlugin._networks.get(net_id)
+ # Verify that no attachments are plugged into the network
+ if net:
+ if net['net-ports']:
+ for port in net['net-ports'].values():
+ if port['attachment']:
+ raise exc.NetworkInUse(net_id=net_id)
+ FakePlugin._networks.pop(net_id)
+ return net
+ # Network not found
+ raise exc.NetworkNotFound(net_id=net_id)
+
+ def rename_network(self, tenant_id, net_id, new_name):
+ """
+ Updates the symbolic name belonging to a particular
+ Virtual Network.
+ """
+ print("rename_network() called\n")
+ net = self._get_network(tenant_id, net_id)
+ net['net-name']=new_name
+ return net
+
+ def get_all_ports(self, tenant_id, net_id):
+ """
+ Retrieves all port identifiers belonging to the
+ specified Virtual Network.
+ """
+ print("get_all_ports() called\n")
+ network = self._get_network(tenant_id, net_id)
+ ports_on_net = network['net-ports'].values()
+ return ports_on_net
+
+ def get_port_details(self, tenant_id, net_id, port_id):
+ """
+ This method allows the user to retrieve a remote interface
+ that is attached to this particular port.
+ """
+ print("get_port_details() called\n")
+ return self._get_port(tenant_id, net_id, port_id)
+
+ def create_port(self, tenant_id, net_id, port_state=None):
+ """
+ Creates a port on the specified Virtual Network.
+ """
+ print("create_port() called\n")
+ net = self._get_network(tenant_id, net_id)
+ # check port state
+ # TODO(salvatore-orlando): Validate port state in API?
+ self._validate_port_state(port_state)
+ ports = net['net-ports']
+ new_port_id = max(ports.keys())+1
+ new_port_dict = {'port-id':new_port_id,
+ 'port-state': port_state,
+ 'attachment': None}
+ ports[new_port_id] = new_port_dict
+ return new_port_dict
+
+ def update_port(self, tenant_id, net_id, port_id, port_state):
+ """
+ Updates the state of a port on the specified Virtual Network.
+ """
+ print("create_port() called\n")
+ port = self._get_port(tenant_id, net_id, port_id)
+ self._validate_port_state(port_state)
+ port['port-state'] = port_state
+ return port
+
+ def delete_port(self, tenant_id, net_id, port_id):
+ """
+ Deletes a port on a specified Virtual Network,
+ if the port contains a remote interface attachment,
+ the remote interface is first un-plugged and then the port
+ is deleted.
+ """
+ print("delete_port() called\n")
+ net = self._get_network(tenant_id, net_id)
+ port = self._get_port(tenant_id, net_id, port_id)
+ if port['attachment']:
+ raise exc.PortInUse(net_id=net_id,port_id=port_id,
+ att_id=port['attachment'])
+ try:
+ net['net-ports'].pop(int(port_id))
+ except KeyError:
+ raise exc.PortNotFound(net_id=net_id, port_id=port_id)
+
+
+ def plug_interface(self, tenant_id, net_id, port_id, remote_interface_id):
+ """
+ Attaches a remote interface to the specified port on the
+ specified Virtual Network.
+ """
+ print("plug_interface() called\n")
+ # Validate attachment
+ self._validate_attachment(tenant_id, net_id, port_id,
+ remote_interface_id)
+ port = self._get_port(tenant_id, net_id, port_id)
+ if port['attachment']:
+ raise exc.PortInUse(net_id=net_id,port_id=port_id,
+ att_id=port['attachment'])
+ port['attachment'] = remote_interface_id
+
+ def unplug_interface(self, tenant_id, net_id, port_id):
+ """
+ Detaches a remote interface from the specified port on the
+ specified Virtual Network.
+ """
+ print("unplug_interface() called\n")
+ port = self._get_port(tenant_id, net_id, port_id)
+ # TODO(salvatore-orlando):
+ # Should unplug on port without attachment raise an Error?
+ port['attachment'] = None
+
\ No newline at end of file
diff --git a/quantum/quantum_plugin_base.py b/quantum/quantum_plugin_base.py
index b84940c70..0bc156d49 100644
--- a/quantum/quantum_plugin_base.py
+++ b/quantum/quantum_plugin_base.py
@@ -79,12 +79,20 @@ class QuantumPluginBase(object):
pass
@abstractmethod
- def create_port(self, tenant_id, net_id):
+ def create_port(self, tenant_id, net_id, port_state=None):
"""
Creates a port on the specified Virtual Network.
"""
pass
+ @abstractmethod
+ def update_port(self, tenant_id, net_id, port_id, port_state):
+ """
+ Updates the state of a specific port on the
+ specified Virtual Network
+ """
+ pass
+
@abstractmethod
def delete_port(self, tenant_id, net_id, port_id):
"""
@@ -119,28 +127,13 @@ class QuantumPluginBase(object):
"""
pass
- @abstractmethod
- def get_interface_details(self, tenant_id, net_id, port_id):
- """
- 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
-
@classmethod
def __subclasshook__(cls, klass):
"""
The __subclasshook__ method is a class method
that will be called everytime a class is tested
- using issubclass(klass, Plugin).
+ using issubclass(klass, Plugin).
In that case, it will check that every method
marked with the abstractmethod decorator is
provided by the plugin class.
@@ -152,5 +145,3 @@ class QuantumPluginBase(object):
return NotImplemented
return True
return NotImplemented
-
-
diff --git a/quantum/service.py b/quantum/service.py
index 50a8effa3..193725ef3 100644
--- a/quantum/service.py
+++ b/quantum/service.py
@@ -15,30 +15,99 @@
# License for the specific language governing permissions and limitations
# under the License.
-import json
-import routes
-from common import wsgi
-from webob import Response
+import logging
+from quantum.common import config
+from quantum.common import wsgi
+from quantum.common import exceptions as exception
-class NetworkController(wsgi.Controller):
-
- def version(self, request):
- return "Quantum version 0.1"
+LOG = logging.getLogger('quantum.service')
-class API(wsgi.Router):
- def __init__(self, options):
- self.options = options
- mapper = routes.Mapper()
- network_controller = NetworkController()
- mapper.resource("net_controller", "/network",
- controller=network_controller)
- mapper.connect("/", controller=network_controller, action="version")
- super(API, self).__init__(mapper)
+class WsgiService(object):
+ """Base class for WSGI based services.
+
+ For each api you define, you must also define these flags:
+ :_listen: The address on which to listen
+ :_listen_port: The port on which to listen
+
+ """
+
+ def __init__(self, app_name, conf_file, conf):
+ self.app_name = app_name
+ self.conf_file = conf_file
+ self.conf = conf
+ self.wsgi_app = None
+
+ def start(self):
+ self.wsgi_app = _run_wsgi(self.app_name, self.conf, self.conf_file)
+
+ def wait(self):
+ self.wsgi_app.wait()
-def app_factory(global_conf, **local_conf):
- conf = global_conf.copy()
- conf.update(local_conf)
- return API(conf)
+class QuantumApiService(WsgiService):
+ """Class for quantum-api service."""
+
+ @classmethod
+ def create(cls, conf=None, options=None, args=None):
+ app_name = "quantum"
+ if not conf:
+ conf_file, conf = config.load_paste_config(
+ app_name, options, args)
+ if not conf:
+ message = (_('No paste configuration found for: %s'),
+ app_name)
+ raise exception.Error(message)
+
+ # Setup logging early, supplying both the CLI options and the
+ # configuration mapping from the config file
+ # We only update the conf dict for the verbose and debug
+ # flags. Everything else must be set up in the conf file...
+ # Log the options used when starting if we're in debug mode...
+
+ config.setup_logging(options, conf)
+ debug = options.get('debug') or \
+ config.get_option(conf, 'debug',
+ type='bool', default=False)
+ verbose = options.get('verbose') or \
+ config.get_option(conf, 'verbose',
+ type='bool', default=False)
+ conf['debug'] = debug
+ conf['verbose'] = verbose
+ LOG.debug("*" * 80)
+ LOG.debug("Configuration options gathered from config file:")
+ LOG.debug(conf_file)
+ LOG.debug("================================================")
+ items = dict([(k, v) for k, v in conf.items()
+ if k not in ('__file__', 'here')])
+ for key, value in sorted(items.items()):
+ LOG.debug("%(key)-30s %(value)s" % locals())
+ LOG.debug("*" * 80)
+ service = cls(app_name, conf_file, conf)
+ return service
+
+
+def serve_wsgi(cls, conf=None, options = None, args=None):
+ try:
+ service = cls.create(conf, options, args)
+ except Exception:
+ logging.exception('in WsgiService.create()')
+ raise
+
+ service.start()
+
+ return service
+
+
+def _run_wsgi(app_name, paste_conf, paste_config_file):
+ LOG.info(_('Using paste.deploy config at: %s'), paste_config_file)
+ app = config.load_paste_app(paste_config_file, app_name)
+ if not app:
+ LOG.error(_('No known API applications configured in %s.'),
+ paste_config_file)
+ return
+ server = wsgi.Server()
+ server.start(app,
+ int(paste_conf['bind_port']), paste_conf['bind_host'])
+ return server
diff --git a/quantum/utils.py b/quantum/utils.py
index 284b18443..508debb35 100644
--- a/quantum/utils.py
+++ b/quantum/utils.py
@@ -35,6 +35,8 @@ import sys
import time
import types
+from common import exceptions as exception
+
def import_class(import_str):
"""Returns a class from a string including module and class."""
@@ -55,3 +57,36 @@ def import_object(import_str):
except ImportError:
cls = import_class(import_str)
return cls()
+
+
+def to_primitive(value):
+ if type(value) is type([]) or type(value) is type((None,)):
+ o = []
+ for v in value:
+ o.append(to_primitive(v))
+ return o
+ elif type(value) is type({}):
+ o = {}
+ for k, v in value.iteritems():
+ o[k] = to_primitive(v)
+ return o
+ elif isinstance(value, datetime.datetime):
+ return str(value)
+ elif hasattr(value, 'iteritems'):
+ return to_primitive(dict(value.iteritems()))
+ elif hasattr(value, '__iter__'):
+ return to_primitive(list(value))
+ else:
+ return value
+
+
+def dumps(value):
+ try:
+ return json.dumps(value)
+ except TypeError:
+ pass
+ return json.dumps(to_primitive(value))
+
+
+def loads(s):
+ return json.loads(s)
diff --git a/smoketests/__init__.py b/smoketests/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/smoketests/miniclient.py b/smoketests/miniclient.py
new file mode 100644
index 000000000..fb1ebc8fe
--- /dev/null
+++ b/smoketests/miniclient.py
@@ -0,0 +1,98 @@
+# 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 httplib
+import socket
+import urllib
+
+class MiniClient(object):
+
+ """A base client class - derived from Glance.BaseClient"""
+
+ action_prefix = '/v0.1/tenants/{tenant_id}'
+
+ def __init__(self, host, port, use_ssl):
+ """
+ Creates a new client to some service.
+
+ :param host: The host where service resides
+ :param port: The port where service resides
+ :param use_ssl: Should we use HTTPS?
+ """
+ self.host = host
+ self.port = port
+ self.use_ssl = use_ssl
+ self.connection = None
+
+ def get_connection_type(self):
+ """
+ Returns the proper connection type
+ """
+ if self.use_ssl:
+ return httplib.HTTPSConnection
+ else:
+ return httplib.HTTPConnection
+
+ def do_request(self, tenant, method, action, body=None,
+ headers=None, params=None):
+ """
+ Connects to the server and issues a request.
+ Returns the result data, or raises an appropriate exception if
+ HTTP status code is not 2xx
+
+ :param method: HTTP method ("GET", "POST", "PUT", etc...)
+ :param body: string of data to send, or None (default)
+ :param headers: mapping of key/value pairs to add as headers
+ :param params: dictionary of key/value pairs to add to append
+ to action
+
+ """
+ action = MiniClient.action_prefix + action
+ action = action.replace('{tenant_id}',tenant)
+ if type(params) is dict:
+ action += '?' + urllib.urlencode(params)
+
+ 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):
+ """
+ Returns the integer status code from the response, which
+ can be either a Webob.Response (used in testing) or httplib.Response
+ """
+ if hasattr(response, 'status_int'):
+ return response.status_int
+ else:
+ return response.status
\ No newline at end of file
diff --git a/smoketests/tests.py b/smoketests/tests.py
new file mode 100644
index 000000000..f9cd77418
--- /dev/null
+++ b/smoketests/tests.py
@@ -0,0 +1,133 @@
+# 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)
diff --git a/test_scripts/__init__.py b/test_scripts/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/test_scripts/miniclient.py b/test_scripts/miniclient.py
new file mode 100644
index 000000000..fb1ebc8fe
--- /dev/null
+++ b/test_scripts/miniclient.py
@@ -0,0 +1,98 @@
+# 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 httplib
+import socket
+import urllib
+
+class MiniClient(object):
+
+ """A base client class - derived from Glance.BaseClient"""
+
+ action_prefix = '/v0.1/tenants/{tenant_id}'
+
+ def __init__(self, host, port, use_ssl):
+ """
+ Creates a new client to some service.
+
+ :param host: The host where service resides
+ :param port: The port where service resides
+ :param use_ssl: Should we use HTTPS?
+ """
+ self.host = host
+ self.port = port
+ self.use_ssl = use_ssl
+ self.connection = None
+
+ def get_connection_type(self):
+ """
+ Returns the proper connection type
+ """
+ if self.use_ssl:
+ return httplib.HTTPSConnection
+ else:
+ return httplib.HTTPConnection
+
+ def do_request(self, tenant, method, action, body=None,
+ headers=None, params=None):
+ """
+ Connects to the server and issues a request.
+ Returns the result data, or raises an appropriate exception if
+ HTTP status code is not 2xx
+
+ :param method: HTTP method ("GET", "POST", "PUT", etc...)
+ :param body: string of data to send, or None (default)
+ :param headers: mapping of key/value pairs to add as headers
+ :param params: dictionary of key/value pairs to add to append
+ to action
+
+ """
+ action = MiniClient.action_prefix + action
+ action = action.replace('{tenant_id}',tenant)
+ if type(params) is dict:
+ action += '?' + urllib.urlencode(params)
+
+ 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):
+ """
+ Returns the integer status code from the response, which
+ can be either a Webob.Response (used in testing) or httplib.Response
+ """
+ if hasattr(response, 'status_int'):
+ return response.status_int
+ else:
+ return response.status
\ No newline at end of file
diff --git a/test_scripts/tests.py b/test_scripts/tests.py
new file mode 100644
index 000000000..589d9da22
--- /dev/null
+++ b/test_scripts/tests.py
@@ -0,0 +1,150 @@
+# 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()
\ No newline at end of file