/: 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.
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
[app:quantumapiapp_v1_1]
paste.app_factory = quantum.api:APIRouterV11.factory
+
+[app:quantumapiapp_v2_0]
+paste.app_factory = quantum.api.v2.router:APIRouter.factory
--- /dev/null
+# 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.
--- /dev/null
+# 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)
--- /dev/null
+# 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
--- /dev/null
+# 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)
--- /dev/null
+# 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)
version_objs = [
{
"id": "v1.0",
- "status": "CURRENT",
+ "status": "DEPRECATED",
},
{
"id": "v1.1",
"status": "CURRENT",
},
+ {
+ "id": "v2.0",
+ "status": "PROPOSED",
+ },
]
if req.path != '/':
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")
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")
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.")
class NotImplementedError(Error):
pass
+
+
+class FixedIPNotAvailable(QuantumException):
+ message = _("Fixed IP (%(ip)s) unavailable for network "
+ "%(network_uuid)s")
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()
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__)
_ENGINE = None
_MAKER = None
-BASE = models.BASE
+BASE = model_base.BASE
class MySQLPingListener(object):
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())
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):
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)
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)
--- /dev/null
+# 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)
--- /dev/null
+# 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)
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'
self.interface_id)
-class Network(BASE, QuantumBase):
+class Network(model_base.BASE):
"""Represents a quantum network"""
__tablename__ = 'networks'
--- /dev/null
+# 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)
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__)
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
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):
--- /dev/null
+# 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)
--- /dev/null
+# 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
--- /dev/null
+# 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)
--- /dev/null
+# 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'])
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
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')
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"""
eventlet>=0.9.12
lxml
python-gflags==1.3
-sqlalchemy
+sqlalchemy>0.6.4
webob==1.2.0
distribute>=0.6.24
coverage
-mock>=0.7.1
+mock>=0.8
mox==0.5.3
nose
nosexcover