Add API v2 support

* Implements BP v2-api-melange-integration
* Adds v2 Plugin specification
* Refactors SQLAlchemy usage for multiple BASE's

Change-Id: I45f008f181c18269afdfe4a9b589a7c5ae56d225
This commit is contained in:
Jason Kölker 2012-05-09 12:04:11 -05:00
parent a164532cab
commit 73f41d370e
22 changed files with 2210 additions and 87 deletions

View File

@ -23,6 +23,7 @@ use = egg:Paste#urlmap
/: quantumversions
/v1.0: quantumapi_v1_0
/v1.1: quantumapi_v1_1
/v2.0: quantumapi_v2_0
[pipeline:quantumapi_v1_0]
# By default, authentication is disabled.
@ -38,6 +39,13 @@ pipeline = extensions quantumapiapp_v1_0
pipeline = extensions quantumapiapp_v1_1
# pipeline = authtoken keystonecontext extensions quantumapiapp_v1_1
[pipeline:quantumapi_v2_0]
# By default, authentication is disabled.
# To enable Keystone integration comment out the
# following line and uncomment the next one
pipeline = extensions quantumapiapp_v2_0
# pipeline = authtoken keystonecontext extensions quantumapiapp_v2_0
[filter:keystonecontext]
paste.filter_factory = quantum.auth:QuantumKeystoneContext.factory
@ -61,3 +69,6 @@ paste.app_factory = quantum.api:APIRouterV10.factory
[app:quantumapiapp_v1_1]
paste.app_factory = quantum.api:APIRouterV11.factory
[app:quantumapiapp_v2_0]
paste.app_factory = quantum.api.v2.router:APIRouter.factory

View File

@ -0,0 +1,14 @@
# Copyright (c) 2012 OpenStack, LLC.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.

208
quantum/api/v2/base.py Normal file
View File

@ -0,0 +1,208 @@
# Copyright (c) 2012 OpenStack, LLC.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
import webob.exc
from quantum.common import exceptions
from quantum.api.v2 import resource as wsgi_resource
from quantum.common import utils
from quantum.api.v2 import views
LOG = logging.getLogger(__name__)
XML_NS_V20 = 'http://openstack.org/quantum/api/v2.0'
FAULT_MAP = {exceptions.NotFound: webob.exc.HTTPNotFound,
exceptions.InUse: webob.exc.HTTPConflict,
exceptions.StateInvalid: webob.exc.HTTPBadRequest}
def fields(request):
"""
Extracts the list of fields to return
"""
return [v for v in request.GET.getall('fields') if v]
def filters(request):
"""
Extracts the filters from the request string
Returns a dict of lists for the filters:
check=a&check=b&name=Bob&verbose=True&verbose=other
becomes
{'check': [u'a', u'b'], 'name': [u'Bob']}
"""
res = {}
for key in set(request.GET):
if key in ('verbose', 'fields'):
continue
values = [v for v in request.GET.getall(key) if v]
if values:
res[key] = values
return res
def verbose(request):
"""
Determines the verbose fields for a request
Returns a list of items that are requested to be verbose:
check=a&check=b&name=Bob&verbose=True&verbose=other
returns
[True]
and
check=a&check=b&name=Bob&verbose=other
returns
['other']
"""
verbose = [utils.boolize(v) for v in request.GET.getall('verbose') if v]
# NOTE(jkoelker) verbose=<bool> trumps all other verbose settings
if True in verbose:
return True
elif False in verbose:
return False
return verbose
class Controller(object):
def __init__(self, plugin, collection, resource, params):
self._plugin = plugin
self._collection = collection
self._resource = resource
self._params = params
self._view = getattr(views, self._resource)
def _items(self, request):
"""Retrieves and formats a list of elements of the requested entity"""
kwargs = {'filters': filters(request),
'verbose': verbose(request),
'fields': fields(request)}
obj_getter = getattr(self._plugin, "get_%s" % self._collection)
obj_list = obj_getter(request.context, **kwargs)
return {self._collection: [self._view(obj) for obj in obj_list]}
def _item(self, request, id):
"""Retrieves and formats a single element of the requested entity"""
kwargs = {'verbose': verbose(request),
'fields': fields(request)}
obj_getter = getattr(self._plugin,
"get_%s" % self._resource)
obj = obj_getter(request.context, id, **kwargs)
return {self._resource: self._view(obj)}
def index(self, request):
"""Returns a list of the requested entity"""
return self._items(request)
def show(self, request, id):
"""Returns detailed information about the requested entity"""
return self._item(request, id)
def create(self, request, body=None):
"""Creates a new instance of the requested entity"""
body = self._prepare_request_body(body, allow_bulk=True)
obj_creator = getattr(self._plugin,
"create_%s" % self._resource)
kwargs = {self._resource: body}
obj = obj_creator(request.context, **kwargs)
return {self._resource: self._view(obj)}
def delete(self, request, id):
"""Deletes the specified entity"""
obj_deleter = getattr(self._plugin,
"delete_%s" % self._resource)
obj_deleter(request.context, id)
def update(self, request, id, body=None):
"""Updates the specified entity's attributes"""
obj_updater = getattr(self._plugin,
"update_%s" % self._resource)
kwargs = {self._resource: body}
obj = obj_updater(request.context, id, **kwargs)
return {self._resource: self._view(obj)}
def _prepare_request_body(self, body, allow_bulk=False):
""" verifies required parameters are in request body.
Parameters with default values are considered to be
optional.
body argument must be the deserialized body
"""
if not body:
raise webob.exc.HTTPBadRequest(_("Resource body required"))
body = body or {self._resource: {}}
if self._collection in body and allow_bulk:
bulk_body = [self._prepare_request_body({self._resource: b})
if self._resource not in b
else self._prepare_request_body(b)
for b in body[self._collection]]
if not bulk_body:
raise webob.exc.HTTPBadRequest(_("Resources required"))
return {self._collection: bulk_body}
elif self._collection in body and not allow_bulk:
raise webob.exc.HTTPBadRequest("Bulk operation not supported")
res_dict = body.get(self._resource)
if res_dict is None:
msg = _("Unable to find '%s' in request body") % self._resource
raise webob.exc.HTTPBadRequest(msg)
for param in self._params:
param_value = res_dict.get(param['attr'], param.get('default'))
if param_value is None:
msg = _("Failed to parse request. Parameter %s not "
"specified") % param
raise webob.exc.HTTPUnprocessableEntity(msg)
res_dict[param['attr']] = param_value
return body
def create_resource(collection, resource, plugin, conf, params):
controller = Controller(plugin, collection, resource, params)
# NOTE(jkoelker) To anyone wishing to add "proper" xml support
# this is where you do it
serializers = {
# 'application/xml': wsgi.XMLDictSerializer(metadata, XML_NS_V20),
}
deserializers = {
# 'application/xml': wsgi.XMLDeserializer(metadata),
}
return wsgi_resource.Resource(controller, FAULT_MAP, deserializers,
serializers)

126
quantum/api/v2/resource.py Normal file
View File

@ -0,0 +1,126 @@
# Copyright 2012 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.
"""
Utility methods for working with WSGI servers redux
"""
import logging
import webob
import webob.exc
import webob.dec
from quantum import context
from quantum.common import exceptions
from quantum.openstack.common import jsonutils as json
from quantum import wsgi
LOG = logging.getLogger(__name__)
class Request(webob.Request):
"""Add some Openstack API-specific logic to the base webob.Request."""
def best_match_content_type(self):
supported = ('application/json', )
return self.accept.best_match(supported,
default_match='application/json')
@property
def context(self):
#Eventually the Auth[NZ] code will supply this. (mdragon)
#when that happens this if block should raise instead.
if 'quantum.context' not in self.environ:
self.environ['quantum.context'] = context.get_admin_context()
return self.environ['quantum.context']
def Resource(controller, faults=None, deserializers=None, serializers=None):
"""Represents an API entity resource and the associated serialization and
deserialization logic
"""
default_deserializers = {'application/xml': wsgi.XMLDeserializer(),
'application/json': lambda x: json.loads(x)}
default_serializers = {'application/xml': wsgi.XMLDictSerializer(),
'application/json': lambda x: json.dumps(x)}
format_types = {'xml': 'application/xml',
'json': 'application/json'}
action_status = dict(create=201, delete=204)
default_deserializers.update(deserializers or {})
default_serializers.update(serializers or {})
deserializers = default_deserializers
serializers = default_serializers
faults = faults or {}
@webob.dec.wsgify(RequestClass=Request)
def resource(request):
route_args = request.environ.get('wsgiorg.routing_args')
if route_args:
args = route_args[1].copy()
else:
args = {}
# NOTE(jkoelker) by now the controller is already found, remove
# it from the args if it is in the matchdict
args.pop('controller', None)
fmt = args.pop('format', None)
action = args.pop('action', None)
content_type = format_types.get(fmt,
request.best_match_content_type())
deserializer = deserializers.get(content_type)
serializer = serializers.get(content_type)
try:
if request.body:
args['body'] = deserializer(request.body)
method = getattr(controller, action)
result = method(request=request, **args)
except exceptions.QuantumException as e:
LOG.exception('%s failed' % action)
body = serializer({'QuantumError': str(e)})
kwargs = {'body': body, 'content_type': content_type}
for fault in faults:
if isinstance(e, fault):
raise faults[fault](**kwargs)
raise webob.exc.HTTPInternalServerError(**kwargs)
except webob.exc.HTTPException as e:
LOG.exception('%s failed' % action)
e.body = serializer({'QuantumError': str(e)})
e.content_type = content_type
raise
except Exception as e:
# NOTE(jkoelker) Everyting else is 500
LOG.exception('%s failed' % action)
body = serializer({'QuantumError': str(e)})
kwargs = {'body': body, 'content_type': content_type}
raise webob.exc.HTTPInternalServerError(**kwargs)
status = action_status.get(action, 200)
body = serializer(result)
# NOTE(jkoelker) Comply with RFC2616 section 9.7
if status == 204:
content_type = ''
body = None
return webob.Response(request=request, status=status,
content_type=content_type,
body=body)
return resource

119
quantum/api/v2/router.py Normal file
View File

@ -0,0 +1,119 @@
# Copyright (c) 2012 OpenStack, LLC.
#
# Licensed under the Apache License, Version 2.0 (the 'License');
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an 'AS IS' BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
import urlparse
import routes as routes_mapper
import webob
import webob.dec
import webob.exc
from quantum import manager
from quantum import wsgi
from quantum.api.v2 import base
LOG = logging.getLogger(__name__)
HEX_ELEM = '[0-9A-Fa-f]'
UUID_PATTERN = '-'.join([HEX_ELEM + '{8}', HEX_ELEM + '{4}',
HEX_ELEM + '{4}', HEX_ELEM + '{4}',
HEX_ELEM + '{12}'])
COLLECTION_ACTIONS = ['index', 'create']
MEMBER_ACTIONS = ['show', 'update', 'delete']
REQUIREMENTS = {'id': UUID_PATTERN, 'format': 'xml|json'}
RESOURCE_PARAM_MAP = {
'networks': [
{'attr': 'name'},
],
'ports': [
{'attr': 'state', 'default': 'DOWN'},
],
'subnets': [
{'attr': 'prefix'},
{'attr': 'network_id'},
]
}
class Index(wsgi.Application):
def __init__(self, resources):
self.resources = resources
@webob.dec.wsgify(RequestClass=wsgi.Request)
def __call__(self, req):
metadata = {'application/xml': {
'attributes': {
'resource': ['name', 'collection'],
'link': ['href', 'rel'],
}
}
}
layout = []
for name, collection in self.resources.iteritems():
href = urlparse.urljoin(req.path_url, collection)
resource = {'name': name,
'collection': collection,
'links': [{'rel': 'self',
'href': href}]}
layout.append(resource)
response = dict(resources=layout)
content_type = req.best_match_content_type()
body = wsgi.Serializer(metadata=metadata).serialize(response,
content_type)
return webob.Response(body=body, content_type=content_type)
class APIRouter(wsgi.Router):
@classmethod
def factory(cls, global_config, **local_config):
return cls(global_config, **local_config)
def __init__(self, conf, **local_config):
mapper = routes_mapper.Mapper()
plugin_provider = manager.get_plugin_provider(conf)
plugin = manager.get_plugin(plugin_provider)
# NOTE(jkoelker) Merge local_conf into conf after the plugin
# is discovered
conf.update(local_config)
col_kwargs = dict(collection_actions=COLLECTION_ACTIONS,
member_actions=MEMBER_ACTIONS)
resources = {'network': 'networks',
'subnet': 'subnets',
'port': 'ports'}
def _map_resource(collection, resource, params):
controller = base.create_resource(collection, resource,
plugin, conf,
params)
mapper_kwargs = dict(controller=controller,
requirements=REQUIREMENTS,
**col_kwargs)
return mapper.collection(collection, resource,
**mapper_kwargs)
mapper.connect('index', '/', controller=Index(resources))
for resource in resources:
_map_resource(resources[resource], resource,
RESOURCE_PARAM_MAP.get(resources[resource],
dict()))
super(APIRouter, self).__init__(mapper)

40
quantum/api/v2/views.py Normal file
View File

@ -0,0 +1,40 @@
# Copyright (c) 2012 OpenStack, LLC.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
def resource(data, keys):
"""Formats the specified entity"""
return dict(item for item in data.iteritems() if item[0] in keys)
def port(port_data):
"""Represents a view for a port object"""
keys = ('id', 'network_id', 'mac_address', 'fixed_ips',
'device_id', 'admin_state_up', 'tenant_id', 'op_status')
return resource(port_data, keys)
def network(network_data):
"""Represents a view for a network object"""
keys = ('id', 'name', 'subnets', 'admin_state_up', 'op_status',
'tenant_id', 'mac_ranges')
return resource(network_data, keys)
def subnet(subnet_data):
"""Represents a view for a subnet object"""
keys = ('id', 'network_id', 'tenant_id', 'gateway_ip', 'ip_version',
'prefix')
return resource(subnet_data, keys)

View File

@ -37,12 +37,16 @@ class Versions(object):
version_objs = [
{
"id": "v1.0",
"status": "CURRENT",
"status": "DEPRECATED",
},
{
"id": "v1.1",
"status": "CURRENT",
},
{
"id": "v2.0",
"status": "PROPOSED",
},
]
if req.path != '/':

View File

@ -47,6 +47,14 @@ class NotFound(QuantumException):
pass
class NotAuthorized(QuantumException):
message = _("Not authorized.")
class AdminRequired(NotAuthorized):
message = _("User does not have admin privileges: %(reason)s")
class ClassNotFound(NotFound):
message = _("Class %(class_name)s could not be found")
@ -55,6 +63,10 @@ class NetworkNotFound(NotFound):
message = _("Network %(net_id)s could not be found")
class SubnetNotFound(NotFound):
message = _("Subnet %(subnet_id)s could not be found")
class PortNotFound(NotFound):
message = _("Port %(port_id)s could not be found "
"on network %(net_id)s")
@ -64,12 +76,16 @@ class StateInvalid(QuantumException):
message = _("Unsupported port state: %(port_state)s")
class NetworkInUse(QuantumException):
class InUse(QuantumException):
message = _("The resource is inuse")
class NetworkInUse(InUse):
message = _("Unable to complete operation on network %(net_id)s. "
"There is one or more attachments plugged into its ports.")
class PortInUse(QuantumException):
class PortInUse(InUse):
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.")
@ -112,3 +128,8 @@ class InvalidContentType(Invalid):
class NotImplementedError(Error):
pass
class FixedIPNotAvailable(QuantumException):
message = _("Fixed IP (%(ip)s) unavailable for network "
"%(network_uuid)s")

View File

@ -62,14 +62,29 @@ def bool_from_string(subject):
Useful for JSON-decoded stuff and config file parsing
"""
if type(subject) == type(bool):
if isinstance(subject, bool):
return subject
if hasattr(subject, 'startswith'): # str or unicode...
elif isinstance(subject, basestring):
if subject.strip().lower() in ('true', 'on', '1'):
return True
return False
def boolize(subject):
"""
Quak like a boolean
"""
if isinstance(subject, bool):
return subject
elif isinstance(subject, basestring):
sub = subject.strip().lower()
if sub == 'true':
return True
elif sub == 'false':
return False
return subject
def execute(cmd, process_input=None, addl_env=None, check_exit_code=True):
logging.debug("Running cmd: %s", cmd)
env = os.environ.copy()

View File

@ -27,7 +27,7 @@ from sqlalchemy.orm import sessionmaker, exc
from quantum.api.api_common import OperationalStatus
from quantum.common import exceptions as q_exc
from quantum.db import models
from quantum.db import model_base, models
LOG = logging.getLogger(__name__)
@ -35,7 +35,7 @@ LOG = logging.getLogger(__name__)
_ENGINE = None
_MAKER = None
BASE = models.BASE
BASE = model_base.BASE
class MySQLPingListener(object):
@ -79,15 +79,16 @@ def configure_db(options):
engine_args['listeners'] = [MySQLPingListener()]
_ENGINE = create_engine(options['sql_connection'], **engine_args)
if not register_models():
base = options.get('base', BASE)
if not register_models(base):
if 'reconnect_interval' in options:
retry_registration(options['reconnect_interval'])
retry_registration(options['reconnect_interval'], base)
def clear_db():
def clear_db(base=BASE):
global _ENGINE
assert _ENGINE
for table in reversed(BASE.metadata.sorted_tables):
for table in reversed(base.metadata.sorted_tables):
_ENGINE.execute(table.delete())
@ -102,32 +103,32 @@ def get_session(autocommit=True, expire_on_commit=False):
return _MAKER()
def retry_registration(reconnect_interval):
def retry_registration(reconnect_interval, base=BASE):
while True:
LOG.info("Unable to connect to database. Retrying in %s seconds" %
reconnect_interval)
time.sleep(reconnect_interval)
if register_models():
if register_models(base):
break
def register_models():
def register_models(base=BASE):
"""Register Models and create properties"""
global _ENGINE
assert _ENGINE
try:
BASE.metadata.create_all(_ENGINE)
base.metadata.create_all(_ENGINE)
except sql.exc.OperationalError as e:
LOG.info("Database registration exception: %s" % e)
return False
return True
def unregister_models():
def unregister_models(base=BASE):
"""Unregister Models, useful clearing out data before testing"""
global _ENGINE
assert _ENGINE
BASE.metadata.drop_all(_ENGINE)
base.metadata.drop_all(_ENGINE)
def network_create(tenant_id, name, op_status=OperationalStatus.UNKNOWN):
@ -158,7 +159,7 @@ def network_get(net_id):
return (session.query(models.Network).
filter_by(uuid=net_id).
one())
except exc.NoResultFound, e:
except exc.NoResultFound:
raise q_exc.NetworkNotFound(net_id=net_id)
@ -199,7 +200,7 @@ def validate_network_ownership(tenant_id, net_id):
filter_by(uuid=net_id).
filter_by(tenant_id=tenant_id).
one())
except exc.NoResultFound, e:
except exc.NoResultFound:
raise q_exc.NetworkNotFound(net_id=net_id)

View File

@ -0,0 +1,295 @@
# Copyright (c) 2012 OpenStack, LLC.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
from sqlalchemy import orm
from sqlalchemy.orm import exc
from quantum import quantum_plugin_base_v2
from quantum.common import exceptions as q_exc
from quantum.db import api as db
from quantum.db import models_v2
LOG = logging.getLogger(__name__)
class QuantumDbPluginV2(quantum_plugin_base_v2.QuantumPluginBaseV2):
""" A class that implements the v2 Quantum plugin interface
using SQLAlchemy models. Whenever a non-read call happens
the plugin will call an event handler class method (e.g.,
network_created()). The result is that this class can be
sub-classed by other classes that add custom behaviors on
certain events.
"""
def __init__(self):
# NOTE(jkoelker) This is an incomlete implementation. Subclasses
# must override __init__ and setup the database
# and not call into this class's __init__.
# This connection is setup as memory for the tests.
sql_connection = 'sqlite:///:memory:'
db.configure_db({'sql_connection': sql_connection,
'base': models_v2.model_base.BASEV2})
def _get_tenant_id_for_create(self, context, resource):
if context.is_admin and 'tenant_id' in resource:
tenant_id = resource['tenant_id']
elif ('tenant_id' in resource and
resource['tenant_id'] != context.tenant_id):
reason = _('Cannot create resource for another tenant')
raise q_exc.AdminRequired(reason=reason)
else:
tenant_id = context.tenant_id
return tenant_id
def _model_query(self, context, model):
query = context.session.query(model)
# NOTE(jkoelker) non-admin queries are scoped to their tenant_id
if not context.is_admin and hasattr(model.tenant_id):
query = query.filter(tenant_id=context.tenant_id)
return query
def _get_by_id(self, context, model, id, joins=(), verbose=None):
query = self._model_query(context, model)
if verbose:
if verbose and isinstance(verbose, list):
options = [orm.joinedload(join) for join in joins
if join in verbose]
else:
options = [orm.joinedload(join) for join in joins]
query = query.options(*options)
return query.filter_by(id=id).one()
def _get_network(self, context, id, verbose=None):
try:
network = self._get_by_id(context, models_v2.Network, id,
joins=('subnets',), verbose=verbose)
except exc.NoResultFound:
raise q_exc.NetworkNotFound(net_id=id)
except exc.MultipleResultsFound:
LOG.error('Multiple networks match for %s' % id)
raise q_exc.NetworkNotFound(net_id=id)
return network
def _get_subnet(self, context, id, verbose=None):
try:
subnet = self._get_by_id(context, models_v2.Subnet, id,
verbose=verbose)
except exc.NoResultFound:
raise q_exc.SubnetNotFound(subnet_id=id)
except exc.MultipleResultsFound:
LOG.error('Multiple subnets match for %s' % id)
raise q_exc.SubnetNotFound(subnet_id=id)
return subnet
def _get_port(self, context, id, verbose=None):
try:
port = self._get_by_id(context, models_v2.Port, id,
verbose=verbose)
except exc.NoResultFound:
# NOTE(jkoelker) The PortNotFound exceptions requires net_id
# kwarg in order to set the message correctly
raise q_exc.PortNotFound(port_id=id, net_id=None)
except exc.MultipleResultsFound:
LOG.error('Multiple ports match for %s' % id)
raise q_exc.PortNotFound(port_id=id)
return port
def _fields(self, resource, fields):
if fields:
return dict(((key, item) for key, item in resource.iteritems()
if key in fields))
return resource
def _get_collection(self, context, model, dict_func, filters=None,
fields=None, verbose=None):
collection = self._model_query(context, model)
if filters:
for key, value in filters.iteritems():
column = getattr(model, key, None)
if column:
collection = collection.filter(column.in_(value))
return [dict_func(c, fields) for c in collection.all()]
def _make_network_dict(self, network, fields=None):
res = {'id': network['id'],
'name': network['name'],
'tenant_id': network['tenant_id'],
'admin_state_up': network['admin_state_up'],
'op_status': network['op_status'],
'subnets': [subnet['id']
for subnet in network['subnets']]}
return self._fields(res, fields)
def _make_subnet_dict(self, subnet, fields=None):
res = {'id': subnet['id'],
'network_id': subnet['network_id'],
'tenant_id': subnet['tenant_id'],
'ip_version': subnet['ip_version'],
'prefix': subnet['prefix'],
'gateway_ip': subnet['gateway_ip']}
return self._fields(res, fields)
def _make_port_dict(self, port, fields=None):
res = {"id": port["id"],
"network_id": port["network_id"],
'tenant_id': port['tenant_id'],
"mac_address": port["mac_address"],
"admin_state_up": port["admin_state_up"],
"op_status": port["op_status"],
"fixed_ips": [ip["address"] for ip in port["fixed_ips"]],
"device_id": port["device_id"]}
return self._fields(res, fields)
def create_network(self, context, network):
n = network['network']
# NOTE(jkoelker) Get the tenant_id outside of the session to avoid
# unneeded db action if the operation raises
tenant_id = self._get_tenant_id_for_create(context, n)
with context.session.begin():
network = models_v2.Network(tenant_id=tenant_id,
name=n['name'],
admin_state_up=n['admin_state_up'],
op_status="ACTIVE")
context.session.add(network)
return self._make_network_dict(network)
def update_network(self, context, id, network):
n = network['network']
with context.session.begin():
network = self._get_network(context, id)
network.update(n)
return self._make_network_dict(network)
def delete_network(self, context, id):
with context.session.begin():
network = self._get_network(context, id)
# TODO(anyone) Delegation?
ports_qry = context.session.query(models_v2.Port)
ports_qry.filter_by(network_id=id).delete()
subnets_qry = context.session.query(models_v2.Subnet)
subnets_qry.filter_by(network_id=id).delete()
context.session.delete(network)
def get_network(self, context, id, fields=None, verbose=None):
network = self._get_network(context, id, verbose=verbose)
return self._make_network_dict(network, fields)
def get_networks(self, context, filters=None, fields=None, verbose=None):
return self._get_collection(context, models_v2.Network,
self._make_network_dict,
filters=filters, fields=fields,
verbose=verbose)
def create_subnet(self, context, subnet):
s = subnet['subnet']
# NOTE(jkoelker) Get the tenant_id outside of the session to avoid
# unneeded db action if the operation raises
tenant_id = self._get_tenant_id_for_create(context, s)
with context.session.begin():
subnet = models_v2.Subnet(tenant_id=tenant_id,
network_id=s['network_id'],
ip_version=s['ip_version'],
prefix=s['prefix'],
gateway_ip=s['gateway_ip'])
context.session.add(subnet)
return self._make_subnet_dict(subnet)
def update_subnet(self, context, id, subnet):
s = subnet['subnet']
with context.session.begin():
subnet = self._get_subnet(context, id)
subnet.update(s)
return self._make_subnet_dict(subnet)
def delete_subnet(self, context, id):
with context.session.begin():
subnet = self._get_subnet(context, id)
allocations_qry = context.session.query(models_v2.IPAllocation)
allocations_qry.filter_by(subnet_id=id).delete()
context.session.delete(subnet)
def get_subnet(self, context, id, fields=None, verbose=None):
subnet = self._get_subnet(context, id, verbose=verbose)
return self._make_subnet_dict(subnet, fields)
def get_subnets(self, context, filters=None, fields=None, verbose=None):
return self._get_collection(context, models_v2.Subnet,
self._make_subnet_dict,
filters=filters, fields=fields,
verbose=verbose)
def create_port(self, context, port):
p = port['port']
# NOTE(jkoelker) Get the tenant_id outside of the session to avoid
# unneeded db action if the operation raises
tenant_id = self._get_tenant_id_for_create(context, p)
#FIXME(danwent): allocate MAC
mac_address = p.get('mac_address', 'ca:fe:de:ad:be:ef')
with context.session.begin():
network = self._get_network(context, p["network_id"])
port = models_v2.Port(tenant_id=tenant_id,
network_id=p['network_id'],
mac_address=mac_address,
admin_state_up=p['admin_state_up'],
op_status="ACTIVE",
device_id=p['device_id'])
context.session.add(port)
# TODO(anyone) ip allocation
#for subnet in network["subnets"]:
# pass
return self._make_port_dict(port)
def update_port(self, context, id, port):
p = port['port']
with context.session.begin():
port = self._get_port(context, id)
port.update(p)
return self._make_port_dict(port)
def delete_port(self, context, id):
with context.session.begin():
port = self._get_port(context, id)
allocations_qry = context.session.query(models_v2.IPAllocation)
allocations_qry.filter_by(port_id=id).delete()
context.session.delete(port)
def get_port(self, context, id, fields=None, verbose=None):
port = self._get_port(context, id, verbose=verbose)
return self._make_port_dict(port, fields)
def get_ports(self, context, filters=None, fields=None, verbose=None):
return self._get_collection(context, models_v2.Port,
self._make_port_dict,
filters=filters, fields=fields,
verbose=verbose)

72
quantum/db/model_base.py Normal file
View File

@ -0,0 +1,72 @@
# Copyright (c) 2012 OpenStack, LLC.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import uuid
import sqlalchemy as sa
from sqlalchemy import orm
from sqlalchemy.ext import declarative
def str_uuid():
return str(uuid.uuid4())
class QuantumBase(object):
"""Base class for Quantum Models."""
def __setitem__(self, key, value):
setattr(self, key, value)
def __getitem__(self, key):
return getattr(self, key)
def get(self, key, default=None):
return getattr(self, key, default)
def __iter__(self):
self._i = iter(orm.object_mapper(self).columns)
return self
def next(self):
n = self._i.next().name
return n, getattr(self, n)
def update(self, values):
"""Make the model object behave like a dict"""
for k, v in values.iteritems():
setattr(self, k, v)
def iteritems(self):
"""Make the model object behave like a dict.
Includes attributes from joins."""
local = dict(self)
joined = dict([(k, v) for k, v in self.__dict__.iteritems()
if not k[0] == '_'])
local.update(joined)
return local.iteritems()
class QuantumBaseV2(QuantumBase):
id = sa.Column(sa.String(36), primary_key=True, default=str_uuid)
@declarative.declared_attr
def __tablename__(cls):
# NOTE(jkoelker) use the pluralized name of the class as the table
return cls.__name__.lower() + 's'
BASE = declarative.declarative_base(cls=QuantumBase)
BASEV2 = declarative.declarative_base(cls=QuantumBaseV2)

View File

@ -21,51 +21,16 @@
import uuid
from sqlalchemy import Column, String, ForeignKey
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relation, object_mapper
from sqlalchemy.orm import relation
from quantum.api import api_common as common
from quantum.db import model_base
BASE = declarative_base()
BASE = model_base.BASE
class QuantumBase(object):
"""Base class for Quantum Models."""
def __setitem__(self, key, value):
setattr(self, key, value)
def __getitem__(self, key):
return getattr(self, key)
def get(self, key, default=None):
return getattr(self, key, default)
def __iter__(self):
self._i = iter(object_mapper(self).columns)
return self
def next(self):
n = self._i.next().name
return n, getattr(self, n)
def update(self, values):
"""Make the model object behave like a dict"""
for k, v in values.iteritems():
setattr(self, k, v)
def iteritems(self):
"""Make the model object behave like a dict.
Includes attributes from joins."""
local = dict(self)
joined = dict([(k, v) for k, v in self.__dict__.iteritems()
if not k[0] == '_'])
local.update(joined)
return local.iteritems()
class Port(BASE, QuantumBase):
class Port(model_base.BASE):
"""Represents a port on a quantum network"""
__tablename__ = 'ports'
@ -90,7 +55,7 @@ class Port(BASE, QuantumBase):
self.interface_id)
class Network(BASE, QuantumBase):
class Network(model_base.BASE):
"""Represents a quantum network"""
__tablename__ = 'networks'

72
quantum/db/models_v2.py Normal file
View File

@ -0,0 +1,72 @@
# Copyright (c) 2012 OpenStack, LLC.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import sqlalchemy as sa
from sqlalchemy import orm
from quantum.db import model_base
class HasTenant(object):
"""Tenant mixin, add to subclasses that have a tenant."""
# NOTE(jkoelker) tenant_id is just a free form string ;(
tenant_id = sa.Column(sa.String(255))
class IPAllocation(model_base.BASEV2):
"""Internal representation of a IP address allocation in a Quantum
subnet
"""
port_id = sa.Column(sa.String(36), sa.ForeignKey('ports.id'))
address = sa.Column(sa.String(16), nullable=False, primary_key=True)
subnet_id = sa.Column(sa.String(36), sa.ForeignKey('subnets.id'),
primary_key=True)
allocated = sa.Column(sa.Boolean(), nullable=False)
class Port(model_base.BASEV2, HasTenant):
"""Represents a port on a quantum v2 network"""
network_id = sa.Column(sa.String(36), sa.ForeignKey("networks.id"),
nullable=False)
fixed_ips = orm.relationship(IPAllocation, backref='ports')
mac_address = sa.Column(sa.String(32), nullable=False)
admin_state_up = sa.Column(sa.Boolean(), nullable=False)
op_status = sa.Column(sa.String(16), nullable=False)
device_id = sa.Column(sa.String(255), nullable=False)
class Subnet(model_base.BASEV2, HasTenant):
"""Represents a quantum subnet"""
network_id = sa.Column(sa.String(36), sa.ForeignKey('networks.id'))
allocations = orm.relationship(IPAllocation,
backref=orm.backref('subnet',
uselist=False))
ip_version = sa.Column(sa.Integer, nullable=False)
prefix = sa.Column(sa.String(255), nullable=False)
gateway_ip = sa.Column(sa.String(255))
#TODO(danwent):
# - dns_namservers
# - excluded_ranges
# - additional_routes
class Network(model_base.BASEV2, HasTenant):
"""Represents a v2 quantum network"""
name = sa.Column(sa.String(255))
ports = orm.relationship(Port, backref='networks')
subnets = orm.relationship(Subnet, backref='networks')
op_status = sa.Column(sa.String(16))
admin_state_up = sa.Column(sa.Boolean)

View File

@ -30,7 +30,6 @@ from quantum.common import utils
from quantum.common.config import find_config_file
from quantum.common.exceptions import ClassNotFound
from quantum.openstack.common import importutils
from quantum.quantum_plugin_base import QuantumPluginBase
LOG = logging.getLogger(__name__)
@ -46,6 +45,29 @@ def find_config(basepath):
return None
def get_plugin(plugin_provider):
# If the plugin can't be found let them know gracefully
try:
LOG.info("Loading Plugin: %s" % plugin_provider)
plugin_klass = importutils.import_class(plugin_provider)
except ClassNotFound:
LOG.exception("Error loading plugin")
raise Exception("Plugin not found. You can install a "
"plugin with: pip install <plugin-name>\n"
"Example: pip install quantum-sample-plugin")
return plugin_klass()
def get_plugin_provider(options, config_file=None):
if config_file:
config_file = [config_file]
if not 'plugin_provider' in options:
cf = find_config_file(options, config_file, CONFIG_FILE)
options['plugin_provider'] = utils.get_plugin_from_config(cf)
return options['plugin_provider']
class QuantumManager(object):
_instance = None
@ -55,31 +77,13 @@ class QuantumManager(object):
if not options:
options = {}
if config_file:
config_file = [config_file]
self.configuration_file = find_config_file(options, config_file,
CONFIG_FILE)
if not 'plugin_provider' in options:
options['plugin_provider'] = utils.get_plugin_from_config(
self.configuration_file)
LOG.debug("Plugin location:%s", options['plugin_provider'])
# If the plugin can't be found let them know gracefully
try:
plugin_klass = importutils.import_class(options['plugin_provider'])
except ClassNotFound:
raise Exception("Plugin not found. You can install a "
"plugin with: pip install <plugin-name>\n"
"Example: pip install quantum-sample-plugin")
if not issubclass(plugin_klass, QuantumPluginBase):
raise Exception("Configured Quantum plug-in "
"didn't pass compatibility test")
else:
LOG.debug("Successfully imported Quantum plug-in."
"All compatibility tests passed")
self.plugin = plugin_klass()
# NOTE(jkoelker) Testing for the subclass with the __subclasshook__
# breaks tach monitoring. It has been removed
# intentianally to allow v2 plugins to be monitored
# for performance metrics.
plugin_provider = get_plugin_provider(options, config_file)
LOG.debug("Plugin location:%s", plugin_provider)
self.plugin = get_plugin(plugin_provider)
@classmethod
def get_plugin(cls, options=None, config_file=None):

View File

@ -0,0 +1,121 @@
# Copyright (c) 2012 OpenStack, LLC.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
import uuid
from quantum import quantum_plugin_base_v2
LOG = logging.getLogger(__name__)
class QuantumEchoPlugin(quantum_plugin_base_v2.QuantumPluginBaseV2):
"""
QuantumEchoPlugin is a demo plugin that doesn't
do anything but demonstrate the concept of a
concrete Quantum Plugin. Any call to this plugin
will result in just a log statement with the name
method that was called and its arguments.
"""
def _log(self, name, context, **kwargs):
kwarg_msg = ' '.join([('%s: |%s|' % (str(key), kwargs[key]))
for key in kwargs])
# TODO(anyone) Add a nice __repr__ and __str__ to context
#LOG.debug('%s context: %s %s' % (name, context, kwarg_msg))
LOG.debug('%s %s' % (name, kwarg_msg))
def create_subnet(self, context, subnet):
self._log("create_subnet", context, subnet=subnet)
res = {"id": str(uuid.uuid4())}
res.update(subnet)
return res
def update_subnet(self, context, id, subnet):
self._log("update_subnet", context, id=id, subnet=subnet)
res = {"id": id}
res.update(subnet)
return res
def get_subnet(self, context, id, show=None, verbose=None):
self._log("get_subnet", context, id=id, show=show,
verbose=verbose)
return {"id": id}
def delete_subnet(self, context, id):
self._log("delete_subnet", context, id=id)
def get_subnets(self, context, filters=None, show=None, verbose=None):
self._log("get_subnets", context, filters=filters, show=show,
verbose=verbose)
return []
def create_network(self, context, network):
self._log("create_network", context, network=network)
res = {"id": str(uuid.uuid4())}
res.update(network)
return res
def update_network(self, context, id, network):
self._log("update_network", context, id=id, network=network)
res = {"id": id}
res.update(network)
return res
def get_network(self, context, id, show=None, verbose=None):
self._log("get_network", context, id=id, show=show,
verbose=verbose)
return {"id": id}
def delete_network(self, context, id):
self._log("delete_network", context, id=id)
def get_networks(self, context, filters=None, show=None, verbose=None):
self._log("get_networks", context, filters=filters, show=show,
verbose=verbose)
return []
def create_port(self, context, port):
self._log("create_port", context, port=port)
res = {"id": str(uuid.uuid4())}
res.update(port)
return res
def update_port(self, context, id, port):
self._log("update_port", context, id=id, port=port)
res = {"id": id}
res.update(port)
return res
def get_port(self, context, id, show=None, verbose=None):
self._log("get_port", context, id=id, show=show,
verbose=verbose)
return {"id": id}
def delete_port(self, context, id):
self._log("delete_port", context, id=id)
def get_ports(self, context, filters=None, show=None, verbose=None):
self._log("get_ports", context, filters=filters, show=show,
verbose=verbose)
return []
supported_extension_aliases = ["FOXNSOX"]
def method_to_support_foxnsox_extension(self, context):
self._log("method_to_support_foxnsox_extension", context)

View File

@ -0,0 +1,195 @@
# Copyright 2011 Nicira Networks, Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
# @author: Dan Wendlandt, Nicira, Inc.
"""
v2 Quantum Plug-in API specification.
QuantumPluginBase provides the definition of minimum set of
methods that needs to be implemented by a v2 Quantum Plug-in.
"""
from abc import ABCMeta, abstractmethod
class QuantumPluginBaseV2(object):
__metaclass__ = ABCMeta
@abstractmethod
def create_subnet(self, context, subnet):
"""
Create a subnet, which represents a range of IP addresses
that can be allocated to devices
: param subnet_data: data describing the prefix
{
"network_id": UUID of the network to which this subnet
is bound.
"ip_version": integer indicating IP protocol version.
example: 4
"prefix": string indicating IP prefix indicating addresses
that can be allocated for devices on this subnet.
example: "10.0.0.0/24"
"gateway_ip": string indicating the default gateway
for devices on this subnet. example: "10.0.0.1"
"dns_nameservers": list of strings stricting indication the
DNS name servers for devices on this
subnet. example: [ "8.8.8.8", "8.8.4.4" ]
"excluded_ranges" : list of dicts indicating pairs of IPs that
should not be allocated from the prefix.
example: [ { "start" : "10.0.0.2",
"end" : "10.0.0.5" } ]
"additional_routes": list of dicts indicating routes beyond
the default gateway and local prefix route
that should be injected into the device.
example: [{"destination": "192.168.0.0/16",
"nexthop": "10.0.0.5" } ]
}
"""
pass
@abstractmethod
def update_subnet(self, context, id, subnet):
pass
@abstractmethod
def get_subnet(self, context, id, fields=None, verbose=None):
pass
@abstractmethod
def delete_subnet(self, context, id):
pass
@abstractmethod
def get_subnets(self, context, filters=None, fields=None, verbose=None):
pass
@abstractmethod
def create_network(self, context, network):
"""
Creates a new Virtual Network, assigns a name and associates
:param net_data:
{
'name': a human-readable name associated
with network referenced by net-id
example: "net-1"
'admin-state-up': indicates whether this network should
be operational.
'subnets': list of subnet uuids associated with this
network.
}
:raises:
"""
pass
@abstractmethod
def update_network(self, context, id, network):
pass
@abstractmethod
def delete_network(self, context, id):
pass
@abstractmethod
def get_network(self, context, id, fields=None, verbose=None):
pass
@abstractmethod
def get_networks(self, context, filters=None, fields=None, verbose=None):
pass
@abstractmethod
def create_port(self, context, port):
"""
Creates a port on the specified Virtual Network. Optionally
specify customization of port IP-related attributes, otherwise
the port gets the default values of these attributes associated with
the subnet.
:param port_data:
{"network_id" : UUID of network that this port is attached to.
"admin-state-up" : boolean indicating whether this port should be
operational.
"mac_address" : (optional) mac address used on this port. If no
value is specified, the plugin will generate a
MAC address based on internal configuration.
"fixed_ips" : (optional) list of dictionaries describing the
fixed IPs to be allocated for use by the device on
this port. If not specified, the plugin will
attempt to find a v4 and v6 subnet associated
with the network and allocate an IP for that
subnet.
Note: "address" is optional, in which case an
address from the specified subnet is
selected.
example: [{"subnet": "<uuid>",
"address": "10.0.0.9"}]
"routes" : (optional) list of routes to be injected into this
device. If not specified, the port will get a
route for its local subnet, a route for the default
gateway, and each of the routes in the
'additional_routes' field of the subnet.
example: [ { "destination" : "192.168.0.0/16",
"nexthop" : "10.0.0.5" } ]
}
:raises: exception.NetworkNotFound
:raises: exception.RequestedFixedIPNotAvailable
:raises: exception.FixedIPNotAvailable
:raises: exception.RouteInvalid
"""
pass
@abstractmethod
def update_port(self, context, id, port):
"""
Updates the attributes of a specific port on the
specified Virtual Network.
:returns: a mapping sequence with the following signature:
{'port-id': uuid representing the
updated port on specified quantum network
'port-state': update port state( UP or DOWN)
}
:raises: exception.StateInvalid
:raises: exception.PortNotFound
"""
pass
@abstractmethod
def delete_port(self, context, 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.
:returns: a mapping sequence with the following signature:
{'port-id': uuid representing the deleted port
on specified quantum network
}
:raises: exception.PortInUse
:raises: exception.PortNotFound
:raises: exception.NetworkNotFound
"""
pass
@abstractmethod
def get_port(self, context, id, fields=None, verbose=None):
pass
@abstractmethod
def get_ports(self, context, filters=None, fields=None, verbose=None):
pass

View File

@ -0,0 +1,486 @@
# Copyright 2012 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 spec
import logging
import unittest
import uuid
import mock
import webtest
from webob import exc
from quantum.common import exceptions as q_exc
from quantum.api.v2 import resource as wsgi_resource
from quantum.api.v2 import router
from quantum.api.v2 import views
LOG = logging.getLogger(__name__)
def _get_path(resource, id=None, fmt=None):
path = '/%s' % resource
if id is not None:
path = path + '/%s' % id
if fmt is not None:
path = path + '.%s' % fmt
return path
class V2WsgiResourceTestCase(unittest.TestCase):
def test_unmapped_quantum_error(self):
controller = mock.MagicMock()
controller.test.side_effect = q_exc.QuantumException()
resource = webtest.TestApp(wsgi_resource.Resource(controller))
environ = {'wsgiorg.routing_args': (None, {'action': 'test'})}
res = resource.get('', extra_environ=environ, expect_errors=True)
self.assertEqual(res.status_int, exc.HTTPInternalServerError.code)
def test_mapped_quantum_error(self):
controller = mock.MagicMock()
controller.test.side_effect = q_exc.QuantumException()
faults = {q_exc.QuantumException: exc.HTTPGatewayTimeout}
resource = webtest.TestApp(wsgi_resource.Resource(controller,
faults=faults))
environ = {'wsgiorg.routing_args': (None, {'action': 'test'})}
res = resource.get('', extra_environ=environ, expect_errors=True)
self.assertEqual(res.status_int, exc.HTTPGatewayTimeout.code)
def test_http_error(self):
controller = mock.MagicMock()
controller.test.side_effect = exc.HTTPGatewayTimeout()
resource = webtest.TestApp(wsgi_resource.Resource(controller))
environ = {'wsgiorg.routing_args': (None, {'action': 'test'})}
res = resource.get('', extra_environ=environ, expect_errors=True)
self.assertEqual(res.status_int, exc.HTTPGatewayTimeout.code)
def test_unhandled_error(self):
controller = mock.MagicMock()
controller.test.side_effect = Exception()
resource = webtest.TestApp(wsgi_resource.Resource(controller))
environ = {'wsgiorg.routing_args': (None, {'action': 'test'})}
res = resource.get('', extra_environ=environ, expect_errors=True)
self.assertEqual(res.status_int, exc.HTTPInternalServerError.code)
class ResourceIndexTestCase(unittest.TestCase):
def test_index_json(self):
index = webtest.TestApp(router.Index({'foo': 'bar'}))
res = index.get('')
self.assertTrue('resources' in res.json)
self.assertTrue(len(res.json['resources']) == 1)
resource = res.json['resources'][0]
self.assertTrue('collection' in resource)
self.assertTrue(resource['collection'] == 'bar')
self.assertTrue('name' in resource)
self.assertTrue(resource['name'] == 'foo')
self.assertTrue('links' in resource)
self.assertTrue(len(resource['links']) == 1)
link = resource['links'][0]
self.assertTrue('href' in link)
self.assertTrue(link['href'] == 'http://localhost/bar')
self.assertTrue('rel' in link)
self.assertTrue(link['rel'] == 'self')
class APIv2TestCase(unittest.TestCase):
# NOTE(jkoelker) This potentially leaks the mock object if the setUp
# raises without being caught. Using unittest2
# or dropping 2.6 support so we can use addCleanup
# will get around this.
def setUp(self):
plugin = 'quantum.quantum_plugin_base_v2.QuantumPluginBaseV2'
self._plugin_patcher = mock.patch(plugin, autospec=True)
self.plugin = self._plugin_patcher.start()
api = router.APIRouter({'plugin_provider': plugin})
self.api = webtest.TestApp(api)
def tearDown(self):
self._plugin_patcher.stop()
self.api = None
self.plugin = None
def test_verbose_attr(self):
instance = self.plugin.return_value
instance.get_networks.return_value = []
self.api.get(_get_path('networks'), {'verbose': 'foo'})
instance.get_networks.assert_called_once_with(mock.ANY,
filters=mock.ANY,
fields=mock.ANY,
verbose=['foo'])
def test_multiple_verbose_attr(self):
instance = self.plugin.return_value
instance.get_networks.return_value = []
self.api.get(_get_path('networks'), {'verbose': ['foo', 'bar']})
instance.get_networks.assert_called_once_with(mock.ANY,
filters=mock.ANY,
fields=mock.ANY,
verbose=['foo',
'bar'])
def test_verbose_false_str(self):
instance = self.plugin.return_value
instance.get_networks.return_value = []
self.api.get(_get_path('networks'), {'verbose': 'false'})
instance.get_networks.assert_called_once_with(mock.ANY,
filters=mock.ANY,
fields=mock.ANY,
verbose=False)
def test_verbose_true_str(self):
instance = self.plugin.return_value
instance.get_networks.return_value = []
self.api.get(_get_path('networks'), {'verbose': 'true'})
instance.get_networks.assert_called_once_with(mock.ANY,
filters=mock.ANY,
fields=mock.ANY,
verbose=True)
def test_verbose_true_trump_attr(self):
instance = self.plugin.return_value
instance.get_networks.return_value = []
self.api.get(_get_path('networks'), {'verbose': ['true', 'foo']})
instance.get_networks.assert_called_once_with(mock.ANY,
filters=mock.ANY,
fields=mock.ANY,
verbose=True)
def test_verbose_false_trump_attr(self):
instance = self.plugin.return_value
instance.get_networks.return_value = []
self.api.get(_get_path('networks'), {'verbose': ['false', 'foo']})
instance.get_networks.assert_called_once_with(mock.ANY,
filters=mock.ANY,
fields=mock.ANY,
verbose=False)
def test_verbose_true_trump_false(self):
instance = self.plugin.return_value
instance.get_networks.return_value = []
self.api.get(_get_path('networks'), {'verbose': ['true', 'false']})
instance.get_networks.assert_called_once_with(mock.ANY,
filters=mock.ANY,
fields=mock.ANY,
verbose=True)
def test_fields(self):
instance = self.plugin.return_value
instance.get_networks.return_value = []
self.api.get(_get_path('networks'), {'fields': 'foo'})
instance.get_networks.assert_called_once_with(mock.ANY,
filters=mock.ANY,
fields=['foo'],
verbose=mock.ANY)
def test_fields_multiple(self):
instance = self.plugin.return_value
instance.get_networks.return_value = []
self.api.get(_get_path('networks'), {'fields': ['foo', 'bar']})
instance.get_networks.assert_called_once_with(mock.ANY,
filters=mock.ANY,
fields=['foo', 'bar'],
verbose=mock.ANY)
def test_fields_multiple_with_empty(self):
instance = self.plugin.return_value
instance.get_networks.return_value = []
self.api.get(_get_path('networks'), {'fields': ['foo', '']})
instance.get_networks.assert_called_once_with(mock.ANY,
filters=mock.ANY,
fields=['foo'],
verbose=mock.ANY)
def test_fields_empty(self):
instance = self.plugin.return_value
instance.get_networks.return_value = []
self.api.get(_get_path('networks'), {'fields': ''})
instance.get_networks.assert_called_once_with(mock.ANY,
filters=mock.ANY,
fields=[],
verbose=mock.ANY)
def test_fields_multiple_empty(self):
instance = self.plugin.return_value
instance.get_networks.return_value = []
self.api.get(_get_path('networks'), {'fields': ['', '']})
instance.get_networks.assert_called_once_with(mock.ANY,
filters=mock.ANY,
fields=[],
verbose=mock.ANY)
def test_filters(self):
instance = self.plugin.return_value
instance.get_networks.return_value = []
self.api.get(_get_path('networks'), {'foo': 'bar'})
filters = {'foo': ['bar']}
instance.get_networks.assert_called_once_with(mock.ANY,
filters=filters,
fields=mock.ANY,
verbose=mock.ANY)
def test_filters_empty(self):
instance = self.plugin.return_value
instance.get_networks.return_value = []
self.api.get(_get_path('networks'), {'foo': ''})
filters = {}
instance.get_networks.assert_called_once_with(mock.ANY,
filters=filters,
fields=mock.ANY,
verbose=mock.ANY)
def test_filters_multiple_empty(self):
instance = self.plugin.return_value
instance.get_networks.return_value = []
self.api.get(_get_path('networks'), {'foo': ['', '']})
filters = {}
instance.get_networks.assert_called_once_with(mock.ANY,
filters=filters,
fields=mock.ANY,
verbose=mock.ANY)
def test_filters_multiple_with_empty(self):
instance = self.plugin.return_value
instance.get_networks.return_value = []
self.api.get(_get_path('networks'), {'foo': ['bar', '']})
filters = {'foo': ['bar']}
instance.get_networks.assert_called_once_with(mock.ANY,
filters=filters,
fields=mock.ANY,
verbose=mock.ANY)
def test_filters_multiple_values(self):
instance = self.plugin.return_value
instance.get_networks.return_value = []
self.api.get(_get_path('networks'), {'foo': ['bar', 'bar2']})
filters = {'foo': ['bar', 'bar2']}
instance.get_networks.assert_called_once_with(mock.ANY,
filters=filters,
fields=mock.ANY,
verbose=mock.ANY)
def test_filters_multiple(self):
instance = self.plugin.return_value
instance.get_networks.return_value = []
self.api.get(_get_path('networks'), {'foo': 'bar',
'foo2': 'bar2'})
filters = {'foo': ['bar'], 'foo2': ['bar2']}
instance.get_networks.assert_called_once_with(mock.ANY,
filters=filters,
fields=mock.ANY,
verbose=mock.ANY)
def test_filters_with_fields(self):
instance = self.plugin.return_value
instance.get_networks.return_value = []
self.api.get(_get_path('networks'), {'foo': 'bar', 'fields': 'foo'})
filters = {'foo': ['bar']}
instance.get_networks.assert_called_once_with(mock.ANY,
filters=filters,
fields=['foo'],
verbose=mock.ANY)
def test_filters_with_verbose(self):
instance = self.plugin.return_value
instance.get_networks.return_value = []
self.api.get(_get_path('networks'), {'foo': 'bar',
'verbose': 'true'})
filters = {'foo': ['bar']}
instance.get_networks.assert_called_once_with(mock.ANY,
filters=filters,
fields=mock.ANY,
verbose=True)
def test_filters_with_fields_and_verbose(self):
instance = self.plugin.return_value
instance.get_networks.return_value = []
self.api.get(_get_path('networks'), {'foo': 'bar',
'fields': 'foo',
'verbose': 'true'})
filters = {'foo': ['bar']}
instance.get_networks.assert_called_once_with(mock.ANY,
filters=filters,
fields=['foo'],
verbose=True)
class JSONV2TestCase(APIv2TestCase):
def test_list(self):
return_value = [{'network': {'name': 'net1',
'admin_state_up': True,
'subnets': []}}]
instance = self.plugin.return_value
instance.get_networks.return_value = return_value
res = self.api.get(_get_path('networks'))
self.assertTrue('networks' in res.json)
def test_create(self):
data = {'network': {'name': 'net1', 'admin_state_up': True}}
return_value = {'subnets': []}
return_value.update(data['network'].copy())
instance = self.plugin.return_value
instance.create_network.return_value = return_value
res = self.api.post_json(_get_path('networks'), data)
self.assertEqual(res.status_int, exc.HTTPCreated.code)
def test_create_no_body(self):
data = {'whoa': None}
res = self.api.post_json(_get_path('networks'), data,
expect_errors=True)
self.assertEqual(res.status_int, exc.HTTPBadRequest.code)
def test_create_no_resource(self):
res = self.api.post_json(_get_path('networks'), dict(),
expect_errors=True)
self.assertEqual(res.status_int, exc.HTTPBadRequest.code)
def test_create_missing_attr(self):
data = {'network': {'what': 'who'}}
res = self.api.post_json(_get_path('networks'), data,
expect_errors=True)
self.assertEqual(res.status_int, 422)
def test_create_bulk(self):
data = {'networks': [{'name': 'net1', 'admin_state_up': True},
{'name': 'net2', 'admin_state_up': True}]}
def side_effect(context, network):
nets = network.copy()
for net in nets['networks']:
net.update({'subnets': []})
return nets
instance = self.plugin.return_value
instance.create_network.side_effect = side_effect
res = self.api.post_json(_get_path('networks'), data)
self.assertEqual(res.status_int, exc.HTTPCreated.code)
def test_create_bulk_no_networks(self):
data = {'networks': []}
res = self.api.post_json(_get_path('networks'), data,
expect_errors=True)
self.assertEqual(res.status_int, exc.HTTPBadRequest.code)
def test_create_bulk_missing_attr(self):
data = {'networks': [{'what': 'who'}]}
res = self.api.post_json(_get_path('networks'), data,
expect_errors=True)
self.assertEqual(res.status_int, 422)
def test_create_bulk_partial_body(self):
data = {'networks': [{'name': 'net1', 'admin_state_up': True},
{}]}
res = self.api.post_json(_get_path('networks'), data,
expect_errors=True)
self.assertEqual(res.status_int, 422)
def test_fields(self):
return_value = {'name': 'net1', 'admin_state_up': True,
'subnets': []}
instance = self.plugin.return_value
instance.get_network.return_value = return_value
self.api.get(_get_path('networks', id=str(uuid.uuid4())))
def test_delete(self):
instance = self.plugin.return_value
instance.delete_network.return_value = None
res = self.api.delete(_get_path('networks', id=str(uuid.uuid4())))
self.assertEqual(res.status_int, exc.HTTPNoContent.code)
def test_update(self):
data = {'network': {'name': 'net1', 'admin_state_up': True}}
return_value = {'subnets': []}
return_value.update(data['network'].copy())
instance = self.plugin.return_value
instance.update_network.return_value = return_value
self.api.put_json(_get_path('networks',
id=str(uuid.uuid4())), data)
class V2Views(unittest.TestCase):
def _view(self, keys, func):
data = dict((key, 'value') for key in keys)
data['fake'] = 'value'
res = func(data)
self.assertTrue('fake' not in res)
for key in keys:
self.assertTrue(key in res)
def test_resource(self):
res = views.resource({'one': 1, 'two': 2}, ['one'])
self.assertTrue('one' in res)
self.assertTrue('two' not in res)
def test_network(self):
keys = ('id', 'name', 'subnets', 'admin_state_up', 'op_status',
'tenant_id', 'mac_ranges')
self._view(keys, views.network)
def test_port(self):
keys = ('id', 'network_id', 'mac_address', 'fixed_ips',
'device_id', 'admin_state_up', 'tenant_id', 'op_status')
self._view(keys, views.port)
def test_subnet(self):
keys = ('id', 'network_id', 'tenant_id', 'gateway_ip',
'ip_version', 'prefix')
self._view(keys, views.subnet)

View File

@ -0,0 +1,317 @@
# Copyright (c) 2012 OpenStack, LLC.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
import unittest
import contextlib
from quantum.api.v2.router import APIRouter
from quantum.db import api as db
from quantum.tests.unit.testlib_api import create_request
from quantum.wsgi import Serializer, JSONDeserializer
LOG = logging.getLogger(__name__)
class QuantumDbPluginV2TestCase(unittest.TestCase):
def setUp(self):
super(QuantumDbPluginV2TestCase, self).setUp()
# NOTE(jkoelker) for a 'pluggable' framework, Quantum sure
# doesn't like when the plugin changes ;)
db._ENGINE = None
db._MAKER = None
self._tenant_id = 'test-tenant'
json_deserializer = JSONDeserializer()
self._deserializers = {
'application/json': json_deserializer,
}
plugin = 'quantum.db.db_base_plugin_v2.QuantumDbPluginV2'
self.api = APIRouter({'plugin_provider': plugin})
def tearDown(self):
super(QuantumDbPluginV2TestCase, self).tearDown()
# NOTE(jkoelker) for a 'pluggable' framework, Quantum sure
# doesn't like when the plugin changes ;)
db._ENGINE = None
db._MAKER = None
def _req(self, method, resource, data=None, fmt='json', id=None):
if id:
path = '/%(resource)s/%(id)s.%(fmt)s' % locals()
else:
path = '/%(resource)s.%(fmt)s' % locals()
content_type = 'application/%s' % fmt
body = None
if data:
body = Serializer().serialize(data, content_type)
return create_request(path, body, content_type, method)
def new_create_request(self, resource, data, fmt='json'):
return self._req('POST', resource, data, fmt)
def new_list_request(self, resource, fmt='json'):
return self._req('GET', resource, None, fmt)
def new_show_request(self, resource, id, fmt='json'):
return self._req('GET', resource, None, fmt, id=id)
def new_delete_request(self, resource, id, fmt='json'):
return self._req('DELETE', resource, None, fmt, id=id)
def new_update_request(self, resource, data, id, fmt='json'):
return self._req('PUT', resource, data, fmt, id=id)
def deserialize(self, content_type, response):
ctype = 'application/%s' % content_type
data = self._deserializers[ctype].\
deserialize(response.body)['body']
return data
def _create_network(self, fmt, name, admin_status_up):
data = {'network': {'name': name,
'admin_state_up': admin_status_up}}
network_req = self.new_create_request('networks', data, fmt)
return network_req.get_response(self.api)
def _create_subnet(self, fmt, net_id, gateway_ip, prefix):
data = {'subnet': {'network_id': net_id,
'allocations': [],
'prefix': prefix,
'ip_version': 4,
'gateway_ip': gateway_ip}}
subnet_req = self.new_create_request('subnets', data, fmt)
return subnet_req.get_response(self.api)
def _make_subnet(self, fmt, network, gateway, prefix):
res = self._create_subnet(fmt, network['network']['id'],
gateway, prefix)
return self.deserialize(fmt, res)
def _delete(self, collection, id):
req = self.new_delete_request(collection, id)
req.get_response(self.api)
@contextlib.contextmanager
def network(self, name='net1', admin_status_up=True, fmt='json'):
res = self._create_network(fmt, name, admin_status_up)
network = self.deserialize(fmt, res)
yield network
self._delete('networks', network['network']['id'])
@contextlib.contextmanager
def subnet(self, network=None, gateway='10.0.0.1',
prefix='10.0.0.0/24', fmt='json'):
# TODO(anyone) DRY this
if not network:
with self.network() as network:
subnet = self._make_subnet(fmt, network, gateway, prefix)
yield subnet
self._delete('subnets', subnet['subnet']['id'])
else:
subnet = self._make_subnet(fmt, network, gateway, prefix)
yield subnet
self._delete('subnets', subnet['subnet']['id'])
class TestV2HTTPResponse(QuantumDbPluginV2TestCase):
def test_create_returns_201(self):
res = self._create_network('json', 'net2', True)
self.assertEquals(res.status_int, 201)
def test_list_returns_200(self):
req = self.new_list_request('networks')
res = req.get_response(self.api)
self.assertEquals(res.status_int, 200)
def test_show_returns_200(self):
with self.network() as net:
req = self.new_show_request('networks', net['network']['id'])
res = req.get_response(self.api)
self.assertEquals(res.status_int, 200)
def test_delete_returns_204(self):
res = self._create_network('json', 'net1', True)
net = self.deserialize('json', res)
req = self.new_delete_request('networks', net['network']['id'])
res = req.get_response(self.api)
self.assertEquals(res.status_int, 204)
def test_update_returns_200(self):
with self.network() as net:
req = self.new_update_request('networks',
{'network': {'name': 'steve'}},
net['network']['id'])
res = req.get_response(self.api)
self.assertEquals(res.status_int, 200)
def test_bad_route_404(self):
req = self.new_list_request('doohickeys')
res = req.get_response(self.api)
self.assertEquals(res.status_int, 404)
#class TestPortsV2(APIv2TestCase):
# def setUp(self):
# super(TestPortsV2, self).setUp()
# res = self._create_network('json', 'net1', True)
# data = self._deserializers['application/json'].\
# deserialize(res.body)['body']
# self.net_id = data['network']['id']
#
# def _create_port(self, fmt, net_id, admin_state_up, device_id,
# custom_req_body=None,
# expected_res_status=None):
# content_type = 'application/' + fmt
# data = {'port': {'network_id': net_id,
# 'admin_state_up': admin_state_up,
# 'device_id': device_id}}
# port_req = self.new_create_request('ports', data, fmt)
# port_res = port_req.get_response(self.api)
# return json.loads(port_res.body)
#
# def test_create_port_json(self):
# port = self._create_port('json', self.net_id, True, 'dev_id_1')
# self.assertEqual(port['id'], 'dev_id_1')
# self.assertEqual(port['admin_state_up'], 'DOWN')
# self.assertEqual(port['device_id'], 'dev_id_1')
# self.assertTrue('mac_address' in port)
# self.assertTrue('op_status' in port)
#
# def test_list_ports(self):
# port1 = self._create_port('json', self.net_id, True, 'dev_id_1')
# port2 = self._create_port('json', self.net_id, True, 'dev_id_2')
#
# res = self.new_list_request('ports', 'json')
# port_list = json.loads(res.body)['body']
# self.assertTrue(port1 in port_list['ports'])
# self.assertTrue(port2 in port_list['ports'])
#
# def test_show_port(self):
# port = self._create_port('json', self.net_id, True, 'dev_id_1')
# res = self.new_show_request('port', 'json', port['id'])
# port = json.loads(res.body)['body']
# self.assertEquals(port['port']['name'], 'dev_id_1')
#
# def test_delete_port(self):
# port = self._create_port('json', self.net_id, True, 'dev_id_1')
# self.new_delete_request('port', 'json', port['id'])
#
# port = self.new_show_request('port', 'json', port['id'])
#
# self.assertEquals(res.status_int, 404)
#
# def test_update_port(self):
# port = self._create_port('json', self.net_id, True, 'dev_id_1')
# port_body = {'port': {'device_id': 'Bob'}}
# res = self.new_update_request('port', port_body, port['id'])
# port = json.loads(res.body)['body']
# self.assertEquals(port['device_id'], 'Bob')
#
# def test_delete_non_existent_port_404(self):
# res = self.new_delete_request('port', 'json', 1)
# self.assertEquals(res.status_int, 404)
#
# def test_show_non_existent_port_404(self):
# res = self.new_show_request('port', 'json', 1)
# self.assertEquals(res.status_int, 404)
#
# def test_update_non_existent_port_404(self):
# res = self.new_update_request('port', 'json', 1)
# self.assertEquals(res.status_int, 404)
class TestNetworksV2(QuantumDbPluginV2TestCase):
# NOTE(cerberus): successful network update and delete are
# effectively tested above
def test_create_network(self):
name = 'net1'
keys = [('subnets', []), ('name', name), ('admin_state_up', True),
('op_status', 'ACTIVE')]
with self.network(name=name) as net:
for k, v in keys:
self.assertEquals(net['network'][k], v)
def test_list_networks(self):
with self.network(name='net1') as net1:
with self.network(name='net2') as net2:
req = self.new_list_request('networks')
res = self.deserialize('json', req.get_response(self.api))
self.assertEquals(res['networks'][0]['name'],
net1['network']['name'])
self.assertEquals(res['networks'][1]['name'],
net2['network']['name'])
def test_show_network(self):
with self.network(name='net1') as net:
req = self.new_show_request('networks', net['network']['id'])
res = self.deserialize('json', req.get_response(self.api))
self.assertEquals(res['network']['name'],
net['network']['name'])
class TestSubnetsV2(QuantumDbPluginV2TestCase):
def test_create_subnet(self):
gateway = '10.0.0.1'
prefix = '10.0.0.0/24'
keys = [('ip_version', 4), ('gateway_ip', gateway),
('prefix', prefix)]
with self.subnet(gateway=gateway, prefix=prefix) as subnet:
for k, v in keys:
self.assertEquals(subnet['subnet'][k], v)
def test_update_subnet(self):
with self.subnet() as subnet:
data = {'subnet': {'network_id': 'blarg',
'prefix': '192.168.0.0/24'}}
req = self.new_update_request('subnets', data,
subnet['subnet']['id'])
res = self.deserialize('json', req.get_response(self.api))
self.assertEqual(res['subnet']['prefix'],
data['subnet']['prefix'])
def test_show_subnet(self):
with self.network() as network:
with self.subnet(network=network) as subnet:
req = self.new_show_request('subnets',
subnet['subnet']['id'])
res = self.deserialize('json', req.get_response(self.api))
self.assertEquals(res['subnet']['id'],
subnet['subnet']['id'])
self.assertEquals(res['subnet']['network_id'],
network['network']['id'])
def test_list_subnets(self):
# NOTE(jkoelker) This would be a good place to use contextlib.nested
# or just drop 2.6 support ;)
with self.network() as network:
with self.subnet(network=network, gateway='10.0.0.1',
prefix='10.0.1.0/24') as subnet:
with self.subnet(network=network, gateway='10.0.1.1',
prefix='10.0.1.0/24') as subnet2:
req = self.new_list_request('subnets')
res = self.deserialize('json',
req.get_response(self.api))
res1 = res['subnets'][0]
res2 = res['subnets'][1]
self.assertEquals(res1['prefix'],
subnet['subnet']['prefix'])
self.assertEquals(res2['prefix'],
subnet2['subnet']['prefix'])

View File

@ -89,6 +89,33 @@ class Middleware(object):
behavior.
"""
@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 [filter:APPNAME]
section of the paste config) will be passed into the `__init__` method
as kwargs.
A hypothetical configuration would look like:
[filter:analytics]
redis_host = 127.0.0.1
paste.filter_factory = nova.api.analytics:Analytics.factory
which would result in a call to the `Analytics` class as
import nova.api.analytics
analytics.Analytics(app_from_paste, redis_host='127.0.0.1')
You could of course re-implement the `factory` method in subclasses,
but using the kwarg passing it shouldn't be necessary.
"""
def _factory(app):
return cls(app, **local_config)
return _factory
def __init__(self, application):
self.application = application
@ -204,6 +231,12 @@ class XMLDictSerializer(DictSerializer):
return self.to_xml_string(node)
def __call__(self, data):
# Provides a migration path to a cleaner WSGI layer, this
# "default" stuff and extreme extensibility isn't being used
# like originally intended
return self.default(data)
def to_xml_string(self, node, has_atom=False):
self._add_xmlns(node, has_atom)
return node.toxml('UTF-8')
@ -427,6 +460,10 @@ class XMLDeserializer(TextDeserializer):
def default(self, datastring):
return {'body': self._from_xml(datastring)}
def __call__(self, datastring):
# Adding a migration path to allow us to remove unncessary classes
return self.default(datastring)
class RequestHeadersDeserializer(ActionDispatcher):
"""Default request headers deserializer"""

View File

@ -4,5 +4,5 @@ Routes>=1.12.3
eventlet>=0.9.12
lxml
python-gflags==1.3
sqlalchemy
sqlalchemy>0.6.4
webob==1.2.0

View File

@ -1,6 +1,6 @@
distribute>=0.6.24
coverage
mock>=0.7.1
mock>=0.8
mox==0.5.3
nose
nosexcover