diff --git a/doc/source/glancemetadefcatalogapi.rst b/doc/source/glancemetadefcatalogapi.rst
new file mode 100644
index 0000000000..e7d56686f7
--- /dev/null
+++ b/doc/source/glancemetadefcatalogapi.rst
@@ -0,0 +1,510 @@
+..
+ Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
+
+
+ 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.
+
+Using Glance's Metadata Definitions Catalog Public APIs
+=======================================================
+
+A common API hosted by the Glance service for vendors, admins, services,
+and users to meaningfully define available key / value pair and tag metadata.
+The intent is to enable better metadata collaboration across artifacts,
+services, and projects for OpenStack users.
+
+This is about the definition of the available metadata that can be used on
+different types of resources (images, artifacts, volumes, flavors,
+aggregates, etc). A definition includes the properties type, its key,
+it's description, and it's constraints. This catalog will not store the
+values for specific instance properties.
+
+Glance Metadata Definitions Catalog implementation started with API version v2.
+
+Authentication
+--------------
+
+Glance depends on Keystone and the OpenStack Identity API to handle
+authentication of clients. You must obtain an authentication token from
+Keystone send it along with all API requests to Glance through the
+``X-Auth-Token`` header. Glance will communicate back to Keystone to verify
+the token validity and obtain your identity credentials.
+
+See :doc:`authentication` for more information on integrating with Keystone.
+
+Using v2.X
+----------
+
+For the purpose of examples, assume there is a Glance API server running
+at the URL ``http://glance.example.com`` on the default port 80.
+
+List Available Namespaces
+*************************
+
+We want to see a list of available namespaces that the authenticated user
+has access to. This includes namespaces owned by the user,
+namespaces shared with the user and public namespaces.
+
+We issue a ``GET`` request to ``http://glance.example.com/v2/metadefs/namespaces``
+to retrieve this list of available images.
+The data is returned as a JSON-encoded mapping in the following format::
+
+ {
+ "namespaces": [
+ {
+ "namespace": "MyNamespace",
+ "display_name": "My User Friendly Namespace",
+ "description": "My description",
+ "property_count": 0,
+ "object_count": 2,
+ "resource_types": [
+ {
+ "name": "OS::Nova::Aggregate"
+ },
+ {
+ "name": "OS::Nova::Flavor",
+ "prefix": "aggregate_instance_extra_specs:"
+ }
+ ],
+ "visibility": "public",
+ "protected": true,
+ "owner": "The Test Owner"
+ }
+ ]
+ }
+
+
+.. note::
+ Listing namespaces will only show the summary of each namespace including
+ counts and resource type associations. Detailed response including all its
+ objects definitions, property definitions etc. will only be available on
+ each individual GET namespace request.
+
+Filtering Namespaces Lists
+**************************
+
+``GET /v2/metadefs/namespaces`` requests take query parameters that serve to
+filter the returned list of namespaces. The following
+list details these query parameters.
+
+* ``resource_types=RESOURCE_TYPES``
+
+ Filters namespaces having a ``resource_types`` within the list of
+ comma separated ``RESOURCE_TYPES``.
+
+GET resource also accepts additional query parameters:
+
+* ``sort_key=KEY``
+
+ Results will be ordered by the specified image attribute ``KEY``. Accepted
+ values include ``namespace``, ``created_at`` (default) and ``updated_at``.
+
+* ``sort_dir=DIR``
+
+ Results will be sorted in the direction ``DIR``. Accepted values are ``asc``
+ for ascending or ``desc`` (default) for descending.
+
+* ``marker=NAMESPACE``
+
+ A namespace identifier marker may be specified. When present only
+ namespaces which occur after the identifier ``NAMESPACE`` will be listed,
+ i.e. the namespaces which have a `sort_key` later than that of the marker
+ ``NAMESPACE`` in the `sort_dir` direction.
+
+* ``limit=LIMIT``
+
+ When present the maximum number of results returned will not exceed ``LIMIT``.
+
+.. note::
+
+ If the specified ``LIMIT`` exceeds the operator defined limit (api_limit_max)
+ then the number of results returned may be less than ``LIMIT``.
+
+* ``visibility=PUBLIC``
+
+ An admin user may use the `visibility` parameter to control which results are
+ returned (PRIVATE or PUBLIC).
+
+
+Retrieve Namespace
+******************
+
+We want to see a more detailed information about a namespace that the
+authenticated user has access to. The detail includes the properties, objects,
+and resource type associations.
+
+We issue a ``GET`` request to ``http://glance.example.com/v2/metadefs/namespaces/{namespace}``
+to retrieve the namespace details.
+The data is returned as a JSON-encoded mapping in the following format::
+
+ {
+ "namespace": "MyNamespace",
+ "display_name": "My User Friendly Namespace",
+ "description": "My description",
+ "property_count": 2,
+ "object_count": 2,
+ "resource_types": [
+ {
+ "name": "OS::Glance::Image",
+ "prefix": "hw_"
+ },
+ {
+ "name": "OS::Cinder::Volume",
+ "prefix": "hw_",
+ "properties_target": "image"
+ },
+ {
+ "name": "OS::Nova::Flavor",
+ "prefix": "filter1:"
+ }
+ ],
+ "properties": {
+ "nsprop1": {
+ "title": "My namespace property1",
+ "description": "More info here",
+ "type": "boolean",
+ "default": true
+ },
+ "nsprop2": {
+ "title": "My namespace property2",
+ "description": "More info here",
+ "type": "string",
+ "default": "value1"
+ }
+ },
+ "objects": [
+ {
+ "name": "object1",
+ "description": "my-description",
+ "properties": {
+ "prop1": {
+ "title": "My object1 property1",
+ "description": "More info here",
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "readonly": false
+ }
+ }
+ },
+ {
+ "name": "object2",
+ "description": "my-description",
+ "properties": {
+ "prop1": {
+ "title": "My object2 property1",
+ "description": "More info here",
+ "type": "integer",
+ "default": 20
+ }
+ }
+ }
+ ],
+ "visibility": "public",
+ "protected": true,
+ "owner": "The Test Owner"
+ }
+
+Retrieve available Resource Types
+*********************************
+
+We want to see the list of all resource types that are available in Glance
+
+We issue a ``GET`` request to ``http://glance.example.com/v2/metadefs/resource_types``
+to retrieve all resource types.
+
+The data is returned as a JSON-encoded mapping in the following format::
+
+ [
+ "OS::Cinder::Volume",
+ "OS::Glance::Image",
+ "OS::Nova::Flavor",
+ "OS::Neutron::Subnet"
+ ]
+
+
+Retrieve Resource Types associated with a Namespace
+***************************************************
+
+We want to see the list of resource types that are associated for a specific
+namespace
+
+We issue a ``GET`` request to ``http://glance.example.com/v2/metadefs/namespaces/{namespace}/resource_types``
+to retrieve resource types.
+
+The data is returned as a JSON-encoded mapping in the following format::
+
+ {
+ "resource_types" : [
+ {
+ "name" : "OS::Glance::Image",
+ "prefix" : "hw_"
+ },
+ {
+ "name" :"OS::Cinder::Volume",
+ "prefix" : "hw_",
+ "properties_target" : "image"
+ },
+ {
+ "name" : "OS::Nova::Flavor",
+ "prefix" : "hw:"
+ }
+ ]
+ }
+
+Add Namespace
+*************
+
+We want to create a new namespace that can contain the properties, objects,
+etc.
+
+We issue a ``POST`` request to add an namespace to Glance::
+
+ POST http://glance.example.com/v2/metadefs/namespaces/
+
+The input data is an JSON-encoded mapping in the following format::
+
+ {
+ "namespace": "MyNamespace",
+ "display_name": "My User Friendly Namespace",
+ "description": "My description",
+ "visibility": "public",
+ "protected": true
+ }
+
+.. note::
+ Optionally properties, objects and resource types could be added in the
+ same input. See GET Namespace output above.
+
+Update Namespace
+****************
+
+We want to update an existing namespace
+
+We issue a ``PUT`` request to update an namespace to Glance::
+
+ PUT http://glance.example.com/v2/metadefs/namespaces/{namespace}
+
+The input data is similar to Add Namespace
+
+
+Delete Namespace
+****************
+
+We want to delete an existing namespace including all its objects,
+properties etc.
+
+We issue a ``DELETE`` request to delete an namespace to Glance::
+
+ DELETE http://glance.example.com/v2/metadefs/namespaces/{namespace}
+
+
+Remove Resource Type associated with a Namespace
+************************************************
+We want to de-associate namespace from a resource type
+
+We issue a ``DELETE`` request to de-associate namespace resource type to
+Glance::
+
+ DELETE http://glance.example.com/v2//metadefs/namespaces/{namespace}/resource_types/{resource_type}
+
+List Objects in Namespace
+*************************
+
+We want to see the list of meta definition objects in a specific namespace
+
+We issue a ``GET`` request to ``http://glance.example.com/v2/metadefs/namespaces/{namespace}/objects``
+to retrieve objects.
+
+The data is returned as a JSON-encoded mapping in the following format::
+
+ {
+ "objects": [
+ {
+ "name": "object1",
+ "description": "object1-description",
+ "properties": {
+ "prop1": {
+ "title": "My Property",
+ "description": "More info here",
+ "type": "boolean",
+ "default": true
+ }
+ }
+ },
+ {
+ "name": "object2",
+ "description": "object2-description",
+ "properties": {
+ "prop1": {
+ "title": "My Property",
+ "description": "More info here",
+ "type": "boolean",
+ "default": true
+ }
+ }
+ }
+ ],
+ "schema": "/schema/metadefs/objects"
+ }
+
+Add object in a specific namespace
+**********************************
+
+We want to create a new object which can group the properties
+
+We issue a ``POST`` request to add object to a namespace in Glance::
+
+ POST http://glance.example.com/v2/metadefs/namespaces/{namespace}/objects
+
+
+The input data is an JSON-encoded mapping in the following format::
+
+ {
+ "name": "StorageQOS",
+ "description": "Our available storage QOS.",
+ "required": [
+ "minIOPS"
+ ],
+ "properties": {
+ "minIOPS": {
+ "type": "integer",
+ "description": "The minimum IOPs required",
+ "default": 100,
+ "minimum": 100,
+ "maximum": 30000369
+ },
+ "burstIOPS": {
+ "type": "integer",
+ "description": "The expected burst IOPs",
+ "default": 1000,
+ "minimum": 100,
+ "maximum": 30000377
+ }
+ }
+ }
+
+Update Object in a specific namespace
+*************************************
+
+We want to update an existing object
+
+We issue a ``PUT`` request to update an object to Glance::
+
+ PUT http://glance.example.com/v2/metadefs/namespaces/{namespace}/objects/{object_name}
+
+The input data is similar to Add Object
+
+
+Delete Object in a specific namespace
+*************************************
+
+We want to delete an existing object.
+
+We issue a ``DELETE`` request to delete object in a namespace to Glance::
+
+ DELETE http://glance.example.com/v2/metadefs/namespaces/{namespace}/objects/{object_name}
+
+
+Add property definition in a specific namespace
+***********************************************
+
+We want to create a new property definition in a namespace
+
+We issue a ``POST`` request to add property definition to a namespace in
+Glance::
+
+ POST http://glance.example.com/v2/metadefs/namespaces/{namespace}/properties
+
+
+The input data is an JSON-encoded mapping in the following format::
+
+ {
+ "name": "hypervisor_type",
+ "title" : "Hypervisor",
+ "type": "array",
+ "description": "The type of hypervisor required",
+ "items": {
+ "type": "string",
+ "enum": [
+ "hyperv",
+ "qemu",
+ "kvm"
+ ]
+ }
+ }
+
+
+Update property definition in a specific namespace
+**************************************************
+
+We want to update an existing object
+
+We issue a ``PUT`` request to update an property definition in a namespace to
+Glance::
+
+ PUT http://glance.example.com/v2/metadefs/namespaces/{namespace}/properties/{property_name}
+
+The input data is similar to Add property definition
+
+
+Delete property definition in a specific namespace
+**************************************************
+
+We want to delete an existing object.
+
+We issue a ``DELETE`` request to delete property definition in a namespace to
+Glance::
+
+ DELETE http://glance.example.com/v2/metadefs/namespaces/{namespace}/properties/{property_name}
+
+
+API Message Localization
+------------------------
+Glance supports HTTP message localization. For example, an HTTP client can
+receive API messages in Chinese even if the locale language of the server is
+English.
+
+How to use it
+*************
+To receive localized API messages, the HTTP client needs to specify the
+**Accept-Language** header to indicate the language to use to translate the
+message. For more info about Accept-Language, please refer http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
+
+A typical curl API request will be like below::
+
+ curl -i -X GET -H 'Accept-Language: zh' -H 'Content-Type: application/json'
+ http://127.0.0.1:9292/v2/images/aaa
+
+Then the response will be like the following::
+
+ HTTP/1.1 404 Not Found
+ Content-Length: 234
+ Content-Type: text/html; charset=UTF-8
+ X-Openstack-Request-Id: req-54d403a0-064e-4544-8faf-4aeef086f45a
+ Date: Sat, 22 Feb 2014 06:26:26 GMT
+
+
+
+ 404 Not Found
+
+
+ 404 Not Found
+ 找不到任何具有标识 aaa 的映像
+
+
+
+.. note::
+ Be sure there is the language package under /usr/share/locale-langpack/ on
+ the target Glance server.
diff --git a/doc/source/index.rst b/doc/source/index.rst
index 842c91e709..ebd2a09bd7 100644
--- a/doc/source/index.rst
+++ b/doc/source/index.rst
@@ -81,3 +81,4 @@ Using Glance
glanceapi
glanceclient
+ glancemetadefcatalogapi
diff --git a/etc/policy.json b/etc/policy.json
index 8b7e6871dd..e72363f6d5 100644
--- a/etc/policy.json
+++ b/etc/policy.json
@@ -28,5 +28,24 @@
"get_task": "",
"get_tasks": "",
"add_task": "",
- "modify_task": ""
+ "modify_task": "",
+
+ "get_metadef_namespace": "",
+ "get_metadef_namespaces":"",
+ "modify_metadef_namespace":"",
+ "add_metadef_namespace":"",
+
+ "get_metadef_object":"",
+ "get_metadef_objects":"",
+ "modify_metadef_object":"",
+ "add_metadef_object":"",
+
+ "list_metadef_resource_types":"",
+ "add_metadef_resource_type_association":"",
+
+ "get_metadef_property":"",
+ "get_metadef_properties":"",
+ "modify_metadef_property":"",
+ "add_metadef_property":""
+
}
diff --git a/glance/api/authorization.py b/glance/api/authorization.py
index 12fc42d868..8e3d79f18d 100644
--- a/glance/api/authorization.py
+++ b/glance/api/authorization.py
@@ -455,3 +455,356 @@ class TaskStubRepoProxy(glance.domain.proxy.TaskStubRepo):
def list(self, *args, **kwargs):
task_stubs = self.task_stub_repo.list(*args, **kwargs)
return [proxy_task_stub(self.context, t) for t in task_stubs]
+
+
+#Metadef Namespace classes
+def is_namespace_mutable(context, namespace):
+ """Return True if the namespace is mutable in this context."""
+ if context.is_admin:
+ return True
+
+ if context.owner is None:
+ return False
+
+ return namespace.owner == context.owner
+
+
+def proxy_namespace(context, namespace):
+ if is_namespace_mutable(context, namespace):
+ return namespace
+ else:
+ return ImmutableMetadefNamespaceProxy(namespace)
+
+
+class ImmutableMetadefNamespaceProxy(object):
+
+ def __init__(self, base):
+ self.base = base
+ self.resource_name = 'namespace'
+
+ namespace_id = _immutable_attr('base', 'namespace_id')
+ namespace = _immutable_attr('base', 'namespace')
+ display_name = _immutable_attr('base', 'display_name')
+ description = _immutable_attr('base', 'description')
+ owner = _immutable_attr('base', 'owner')
+ visibility = _immutable_attr('base', 'visibility')
+ protected = _immutable_attr('base', 'protected')
+ created_at = _immutable_attr('base', 'created_at')
+ updated_at = _immutable_attr('base', 'updated_at')
+
+ def delete(self):
+ message = _("You are not permitted to delete this namespace.")
+ raise exception.Forbidden(message)
+
+ def save(self):
+ message = _("You are not permitted to update this namespace.")
+ raise exception.Forbidden(message)
+
+
+class MetadefNamespaceProxy(glance.domain.proxy.MetadefNamespace):
+
+ def __init__(self, namespace):
+ self.namespace_input = namespace
+ super(MetadefNamespaceProxy, self).__init__(namespace)
+
+
+class MetadefNamespaceFactoryProxy(
+ glance.domain.proxy.MetadefNamespaceFactory):
+
+ def __init__(self, meta_namespace_factory, context):
+ self.meta_namespace_factory = meta_namespace_factory
+ self.context = context
+ super(MetadefNamespaceFactoryProxy, self).__init__(
+ meta_namespace_factory,
+ meta_namespace_proxy_class=MetadefNamespaceProxy)
+
+ def new_namespace(self, **kwargs):
+ owner = kwargs.pop('owner', self.context.owner)
+
+ if not self.context.is_admin:
+ if owner is None or owner != self.context.owner:
+ message = _("You are not permitted to create namespace "
+ "owned by '%s'")
+ raise exception.Forbidden(message % (owner))
+
+ return super(MetadefNamespaceFactoryProxy, self).new_namespace(
+ owner=owner, **kwargs)
+
+
+class MetadefNamespaceRepoProxy(glance.domain.proxy.MetadefNamespaceRepo):
+
+ def __init__(self, namespace_repo, context):
+ self.namespace_repo = namespace_repo
+ self.context = context
+ super(MetadefNamespaceRepoProxy, self).__init__(namespace_repo)
+
+ def get(self, namespace):
+ namespace_obj = self.namespace_repo.get(namespace)
+ return proxy_namespace(self.context, namespace_obj)
+
+ def list(self, *args, **kwargs):
+ namespaces = self.namespace_repo.list(*args, **kwargs)
+ return [proxy_namespace(self.context, namespace) for
+ namespace in namespaces]
+
+
+#Metadef Object classes
+def is_object_mutable(context, object):
+ """Return True if the object is mutable in this context."""
+ if context.is_admin:
+ return True
+
+ if context.owner is None:
+ return False
+
+ return object.namespace.owner == context.owner
+
+
+def proxy_object(context, object):
+ if is_object_mutable(context, object):
+ return object
+ else:
+ return ImmutableMetadefObjectProxy(object)
+
+
+class ImmutableMetadefObjectProxy(object):
+
+ def __init__(self, base):
+ self.base = base
+ self.resource_name = 'object'
+
+ object_id = _immutable_attr('base', 'object_id')
+ name = _immutable_attr('base', 'name')
+ required = _immutable_attr('base', 'required')
+ description = _immutable_attr('base', 'description')
+ properties = _immutable_attr('base', 'properties')
+ created_at = _immutable_attr('base', 'created_at')
+ updated_at = _immutable_attr('base', 'updated_at')
+
+ def delete(self):
+ message = _("You are not permitted to delete this object.")
+ raise exception.Forbidden(message)
+
+ def save(self):
+ message = _("You are not permitted to update this object.")
+ raise exception.Forbidden(message)
+
+
+class MetadefObjectProxy(glance.domain.proxy.MetadefObject):
+
+ def __init__(self, meta_object):
+ self.meta_object = meta_object
+ super(MetadefObjectProxy, self).__init__(meta_object)
+
+
+class MetadefObjectFactoryProxy(glance.domain.proxy.MetadefObjectFactory):
+
+ def __init__(self, meta_object_factory, context):
+ self.meta_object_factory = meta_object_factory
+ self.context = context
+ super(MetadefObjectFactoryProxy, self).__init__(
+ meta_object_factory,
+ meta_object_proxy_class=MetadefObjectProxy)
+
+ def new_object(self, **kwargs):
+ owner = kwargs.pop('owner', self.context.owner)
+
+ if not self.context.is_admin:
+ if owner is None or owner != self.context.owner:
+ message = _("You are not permitted to create object "
+ "owned by '%s'")
+ raise exception.Forbidden(message % (owner))
+
+ return super(MetadefObjectFactoryProxy, self).new_object(**kwargs)
+
+
+class MetadefObjectRepoProxy(glance.domain.proxy.MetadefObjectRepo):
+
+ def __init__(self, object_repo, context):
+ self.object_repo = object_repo
+ self.context = context
+ super(MetadefObjectRepoProxy, self).__init__(object_repo)
+
+ def get(self, namespace, object_name):
+ meta_object = self.object_repo.get(namespace, object_name)
+ return proxy_object(self.context, meta_object)
+
+ def list(self, *args, **kwargs):
+ objects = self.object_repo.list(*args, **kwargs)
+ return [proxy_object(self.context, meta_object) for
+ meta_object in objects]
+
+
+#Metadef ResourceType classes
+def is_meta_resource_type_mutable(context, meta_resource_type):
+ """Return True if the meta_resource_type is mutable in this context."""
+ if context.is_admin:
+ return True
+
+ if context.owner is None:
+ return False
+
+ #(lakshmiS): resource type can exist without an association with
+ # namespace and resource type cannot be created/update/deleted directly(
+ # they have to be associated/de-associated from namespace)
+ if meta_resource_type.namespace:
+ return meta_resource_type.namespace.owner == context.owner
+ else:
+ return False
+
+
+def proxy_meta_resource_type(context, meta_resource_type):
+ if is_meta_resource_type_mutable(context, meta_resource_type):
+ return meta_resource_type
+ else:
+ return ImmutableMetadefResourceTypeProxy(meta_resource_type)
+
+
+class ImmutableMetadefResourceTypeProxy(object):
+
+ def __init__(self, base):
+ self.base = base
+ self.resource_name = 'meta_resource_type'
+
+ namespace = _immutable_attr('base', 'namespace')
+ name = _immutable_attr('base', 'name')
+ prefix = _immutable_attr('base', 'prefix')
+ properties_target = _immutable_attr('base', 'properties_target')
+ created_at = _immutable_attr('base', 'created_at')
+ updated_at = _immutable_attr('base', 'updated_at')
+
+ def delete(self):
+ message = _("You are not permitted to delete this meta_resource_type.")
+ raise exception.Forbidden(message)
+
+
+class MetadefResourceTypeProxy(glance.domain.proxy.MetadefResourceType):
+
+ def __init__(self, meta_resource_type):
+ self.meta_resource_type = meta_resource_type
+ super(MetadefResourceTypeProxy, self).__init__(meta_resource_type)
+
+
+class MetadefResourceTypeFactoryProxy(
+ glance.domain.proxy.MetadefResourceTypeFactory):
+
+ def __init__(self, resource_type_factory, context):
+ self.meta_resource_type_factory = resource_type_factory
+ self.context = context
+ super(MetadefResourceTypeFactoryProxy, self).__init__(
+ resource_type_factory,
+ resource_type_proxy_class=MetadefResourceTypeProxy)
+
+ def new_resource_type(self, **kwargs):
+ owner = kwargs.pop('owner', self.context.owner)
+
+ if not self.context.is_admin:
+ if owner is None or owner != self.context.owner:
+ message = _("You are not permitted to create resource_type "
+ "owned by '%s'")
+ raise exception.Forbidden(message % (owner))
+
+ return super(MetadefResourceTypeFactoryProxy, self).new_resource_type(
+ **kwargs)
+
+
+class MetadefResourceTypeRepoProxy(
+ glance.domain.proxy.MetadefResourceTypeRepo):
+
+ def __init__(self, meta_resource_type_repo, context):
+ self.meta_resource_type_repo = meta_resource_type_repo
+ self.context = context
+ super(MetadefResourceTypeRepoProxy, self).__init__(
+ meta_resource_type_repo)
+
+ def list(self, *args, **kwargs):
+ meta_resource_types = self.meta_resource_type_repo.list(
+ *args, **kwargs)
+ return [proxy_meta_resource_type(self.context, meta_resource_type) for
+ meta_resource_type in meta_resource_types]
+
+
+#Metadef namespace properties classes
+def is_namespace_property_mutable(context, namespace_property):
+ """Return True if the object is mutable in this context."""
+ if context.is_admin:
+ return True
+
+ if context.owner is None:
+ return False
+
+ return namespace_property.namespace.owner == context.owner
+
+
+def proxy_namespace_property(context, namespace_property):
+ if is_namespace_property_mutable(context, namespace_property):
+ return namespace_property
+ else:
+ return ImmutableMetadefPropertyProxy(namespace_property)
+
+
+class ImmutableMetadefPropertyProxy(object):
+
+ def __init__(self, base):
+ self.base = base
+ self.resource_name = 'namespace_property'
+
+ property_id = _immutable_attr('base', 'property_id')
+ name = _immutable_attr('base', 'name')
+ schema = _immutable_attr('base', 'schema')
+
+ def delete(self):
+ message = _("You are not permitted to delete this property.")
+ raise exception.Forbidden(message)
+
+ def save(self):
+ message = _("You are not permitted to update this property.")
+ raise exception.Forbidden(message)
+
+
+class MetadefPropertyProxy(glance.domain.proxy.MetadefProperty):
+
+ def __init__(self, namespace_property):
+ self.meta_object = namespace_property
+ super(MetadefPropertyProxy, self).__init__(namespace_property)
+
+
+class MetadefPropertyFactoryProxy(glance.domain.proxy.MetadefPropertyFactory):
+
+ def __init__(self, namespace_property_factory, context):
+ self.meta_object_factory = namespace_property_factory
+ self.context = context
+ super(MetadefPropertyFactoryProxy, self).__init__(
+ namespace_property_factory,
+ property_proxy_class=MetadefPropertyProxy)
+
+ def new_namespace_property(self, **kwargs):
+ owner = kwargs.pop('owner', self.context.owner)
+
+ if not self.context.is_admin:
+ if owner is None or owner != self.context.owner:
+ message = _("You are not permitted to create property "
+ "owned by '%s'")
+ raise exception.Forbidden(message % (owner))
+
+ return super(MetadefPropertyFactoryProxy, self).\
+ new_namespace_property(**kwargs)
+
+
+class MetadefPropertyRepoProxy(glance.domain.proxy.MetadefPropertyRepo):
+
+ def __init__(self, namespace_property_repo, context):
+ self.namespace_property_repo = namespace_property_repo
+ self.context = context
+ super(MetadefPropertyRepoProxy, self).__init__(namespace_property_repo)
+
+ def get(self, namespace, object_name):
+ namespace_property = self.namespace_property_repo.get(namespace,
+ object_name)
+ return proxy_namespace_property(self.context, namespace_property)
+
+ def list(self, *args, **kwargs):
+ namespace_properties = self.namespace_property_repo.list(
+ *args, **kwargs)
+ return [proxy_namespace_property(self.context, namespace_property) for
+ namespace_property in namespace_properties]
diff --git a/glance/api/policy.py b/glance/api/policy.py
index 332d8d0ef8..a557290d04 100644
--- a/glance/api/policy.py
+++ b/glance/api/policy.py
@@ -446,3 +446,211 @@ class ImageTarget(object):
return getattr(self.image, key)
else:
return self.image.extra_properties[key]
+
+
+#Metadef Namespace classes
+class MetadefNamespaceProxy(glance.domain.proxy.MetadefNamespace):
+
+ def __init__(self, namespace, context, policy):
+ self.namespace_input = namespace
+ self.context = context
+ self.policy = policy
+ super(MetadefNamespaceProxy, self).__init__(namespace)
+
+
+class MetadefNamespaceRepoProxy(glance.domain.proxy.MetadefNamespaceRepo):
+
+ def __init__(self, namespace_repo, context, namespace_policy):
+ self.context = context
+ self.policy = namespace_policy
+ self.namespace_repo = namespace_repo
+ proxy_kwargs = {'context': self.context, 'policy': self.policy}
+ super(MetadefNamespaceRepoProxy,
+ self).__init__(namespace_repo,
+ namespace_proxy_class=MetadefNamespaceProxy,
+ namespace_proxy_kwargs=proxy_kwargs)
+
+ def get(self, namespace):
+ self.policy.enforce(self.context, 'get_metadef_namespace', {})
+ return super(MetadefNamespaceRepoProxy, self).get(namespace)
+
+ def list(self, *args, **kwargs):
+ self.policy.enforce(self.context, 'get_metadef_namespaces', {})
+ return super(MetadefNamespaceRepoProxy, self).list(*args, **kwargs)
+
+ def save(self, namespace):
+ self.policy.enforce(self.context, 'modify_metadef_namespace', {})
+ return super(MetadefNamespaceRepoProxy, self).save(namespace)
+
+ def add(self, namespace):
+ self.policy.enforce(self.context, 'add_metadef_namespace', {})
+ return super(MetadefNamespaceRepoProxy, self).add(namespace)
+
+
+class MetadefNamespaceFactoryProxy(
+ glance.domain.proxy.MetadefNamespaceFactory):
+
+ def __init__(self, meta_namespace_factory, context, policy):
+ self.meta_namespace_factory = meta_namespace_factory
+ self.context = context
+ self.policy = policy
+ proxy_kwargs = {'context': self.context, 'policy': self.policy}
+ super(MetadefNamespaceFactoryProxy, self).__init__(
+ meta_namespace_factory,
+ meta_namespace_proxy_class=MetadefNamespaceProxy,
+ meta_namespace_proxy_kwargs=proxy_kwargs)
+
+
+#Metadef Object classes
+class MetadefObjectProxy(glance.domain.proxy.MetadefObject):
+
+ def __init__(self, meta_object, context, policy):
+ self.meta_object = meta_object
+ self.context = context
+ self.policy = policy
+ super(MetadefObjectProxy, self).__init__(meta_object)
+
+
+class MetadefObjectRepoProxy(glance.domain.proxy.MetadefObjectRepo):
+
+ def __init__(self, object_repo, context, object_policy):
+ self.context = context
+ self.policy = object_policy
+ self.object_repo = object_repo
+ proxy_kwargs = {'context': self.context, 'policy': self.policy}
+ super(MetadefObjectRepoProxy,
+ self).__init__(object_repo,
+ object_proxy_class=MetadefObjectProxy,
+ object_proxy_kwargs=proxy_kwargs)
+
+ def get(self, namespace, object_name):
+ self.policy.enforce(self.context, 'get_metadef_object', {})
+ return super(MetadefObjectRepoProxy, self).get(namespace, object_name)
+
+ def list(self, *args, **kwargs):
+ self.policy.enforce(self.context, 'get_metadef_objects', {})
+ return super(MetadefObjectRepoProxy, self).list(*args, **kwargs)
+
+ def save(self, meta_object):
+ self.policy.enforce(self.context, 'modify_metadef_object', {})
+ return super(MetadefObjectRepoProxy, self).save(meta_object)
+
+ def add(self, meta_object):
+ self.policy.enforce(self.context, 'add_metadef_object', {})
+ return super(MetadefObjectRepoProxy, self).add(meta_object)
+
+
+class MetadefObjectFactoryProxy(glance.domain.proxy.MetadefObjectFactory):
+
+ def __init__(self, meta_object_factory, context, policy):
+ self.meta_object_factory = meta_object_factory
+ self.context = context
+ self.policy = policy
+ proxy_kwargs = {'context': self.context, 'policy': self.policy}
+ super(MetadefObjectFactoryProxy, self).__init__(
+ meta_object_factory,
+ meta_object_proxy_class=MetadefObjectProxy,
+ meta_object_proxy_kwargs=proxy_kwargs)
+
+
+#Metadef ResourceType classes
+class MetadefResourceTypeProxy(glance.domain.proxy.MetadefResourceType):
+
+ def __init__(self, meta_resource_type, context, policy):
+ self.meta_resource_type = meta_resource_type
+ self.context = context
+ self.policy = policy
+ super(MetadefResourceTypeProxy, self).__init__(meta_resource_type)
+
+
+class MetadefResourceTypeRepoProxy(
+ glance.domain.proxy.MetadefResourceTypeRepo):
+
+ def __init__(self, resource_type_repo, context, resource_type_policy):
+ self.context = context
+ self.policy = resource_type_policy
+ self.resource_type_repo = resource_type_repo
+ proxy_kwargs = {'context': self.context, 'policy': self.policy}
+ super(MetadefResourceTypeRepoProxy, self).__init__(
+ resource_type_repo,
+ resource_type_proxy_class=MetadefResourceTypeProxy,
+ resource_type_proxy_kwargs=proxy_kwargs)
+
+ def list(self, *args, **kwargs):
+ self.policy.enforce(self.context, 'list_metadef_resource_types', {})
+ return super(MetadefResourceTypeRepoProxy, self).list(*args, **kwargs)
+
+ def add(self, resource_type):
+ self.policy.enforce(self.context,
+ 'add_metadef_resource_type_association', {})
+ return super(MetadefResourceTypeRepoProxy, self).add(resource_type)
+
+
+class MetadefResourceTypeFactoryProxy(
+ glance.domain.proxy.MetadefResourceTypeFactory):
+
+ def __init__(self, resource_type_factory, context, policy):
+ self.resource_type_factory = resource_type_factory
+ self.context = context
+ self.policy = policy
+ proxy_kwargs = {'context': self.context, 'policy': self.policy}
+ super(MetadefResourceTypeFactoryProxy, self).__init__(
+ resource_type_factory,
+ resource_type_proxy_class=MetadefResourceTypeProxy,
+ resource_type_proxy_kwargs=proxy_kwargs)
+
+
+#Metadef namespace properties classes
+class MetadefPropertyProxy(glance.domain.proxy.MetadefProperty):
+
+ def __init__(self, namespace_property, context, policy):
+ self.namespace_property = namespace_property
+ self.context = context
+ self.policy = policy
+ super(MetadefPropertyProxy, self).__init__(namespace_property)
+
+
+class MetadefPropertyRepoProxy(glance.domain.proxy.MetadefPropertyRepo):
+
+ def __init__(self, property_repo, context, object_policy):
+ self.context = context
+ self.policy = object_policy
+ self.property_repo = property_repo
+ proxy_kwargs = {'context': self.context, 'policy': self.policy}
+ super(MetadefPropertyRepoProxy, self).__init__(
+ property_repo,
+ property_proxy_class=MetadefPropertyProxy,
+ property_proxy_kwargs=proxy_kwargs)
+
+ def get(self, namespace, property_name):
+ self.policy.enforce(self.context, 'get_metadef_property', {})
+ return super(MetadefPropertyRepoProxy, self).get(namespace,
+ property_name)
+
+ def list(self, *args, **kwargs):
+ self.policy.enforce(self.context, 'get_metadef_properties', {})
+ return super(MetadefPropertyRepoProxy, self).list(
+ *args, **kwargs)
+
+ def save(self, namespace_property):
+ self.policy.enforce(self.context, 'modify_metadef_property', {})
+ return super(MetadefPropertyRepoProxy, self).save(
+ namespace_property)
+
+ def add(self, namespace_property):
+ self.policy.enforce(self.context, 'add_metadef_property', {})
+ return super(MetadefPropertyRepoProxy, self).add(
+ namespace_property)
+
+
+class MetadefPropertyFactoryProxy(glance.domain.proxy.MetadefPropertyFactory):
+
+ def __init__(self, namespace_property_factory, context, policy):
+ self.namespace_property_factory = namespace_property_factory
+ self.context = context
+ self.policy = policy
+ proxy_kwargs = {'context': self.context, 'policy': self.policy}
+ super(MetadefPropertyFactoryProxy, self).__init__(
+ namespace_property_factory,
+ property_proxy_class=MetadefPropertyProxy,
+ property_proxy_kwargs=proxy_kwargs)
diff --git a/glance/api/v2/metadef_namespaces.py b/glance/api/v2/metadef_namespaces.py
new file mode 100644
index 0000000000..64ead58e5f
--- /dev/null
+++ b/glance/api/v2/metadef_namespaces.py
@@ -0,0 +1,744 @@
+# Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
+#
+# 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.
+
+from oslo.config import cfg
+import six
+import six.moves.urllib.parse as urlparse
+import webob.exc
+from wsme.rest.json import fromjson
+from wsme.rest.json import tojson
+
+from glance.api import policy
+from glance.api.v2.model.metadef_namespace import Namespace
+from glance.api.v2.model.metadef_namespace import Namespaces
+from glance.api.v2.model.metadef_object import MetadefObject
+from glance.api.v2.model.metadef_property_type import PropertyType
+from glance.api.v2.model.metadef_resource_type import ResourceTypeAssociation
+from glance.common import exception
+from glance.common import utils
+from glance.common import wsgi
+from glance.common import wsme_utils
+import glance.db
+import glance.gateway
+from glance import i18n
+import glance.notifier
+from glance.openstack.common import jsonutils as json
+import glance.openstack.common.log as logging
+import glance.schema
+import glance.store
+
+LOG = logging.getLogger(__name__)
+_LE = i18n._LE
+_LW = i18n._LW
+_LI = i18n._LI
+
+CONF = cfg.CONF
+
+
+class NamespaceController(object):
+ def __init__(self, db_api=None, policy_enforcer=None):
+ self.db_api = db_api or glance.db.get_api()
+ self.policy = policy_enforcer or policy.Enforcer()
+ self.gateway = glance.gateway.Gateway(db_api=self.db_api,
+ policy_enforcer=self.policy)
+ self.ns_schema_link = '/v2/schemas/metadefs/namespace'
+ self.obj_schema_link = '/v2/schemas/metadefs/object'
+
+ def index(self, req, marker=None, limit=None, sort_key='created_at',
+ sort_dir='desc', filters=None):
+ try:
+ ns_repo = self.gateway.get_metadef_namespace_repo(req.context)
+
+ # Get namespace id
+ if marker:
+ namespace_obj = ns_repo.get(marker)
+ marker = namespace_obj.namespace_id
+
+ database_ns_list = ns_repo.list(
+ marker=marker, limit=limit, sort_key=sort_key,
+ sort_dir=sort_dir, filters=filters)
+ for db_namespace in database_ns_list:
+ # Get resource type associations
+ filters = dict()
+ filters['namespace'] = db_namespace.namespace
+ rs_repo = (
+ self.gateway.get_metadef_resource_type_repo(req.context))
+ repo_rs_type_list = rs_repo.list(filters=filters)
+ resource_type_list = [ResourceTypeAssociation.to_wsme_model(
+ resource_type) for resource_type in repo_rs_type_list]
+ if resource_type_list:
+ db_namespace.resource_type_associations = (
+ resource_type_list)
+
+ namespace_list = [Namespace.to_wsme_model(
+ db_namespace,
+ get_namespace_href(db_namespace),
+ self.ns_schema_link) for db_namespace in database_ns_list]
+ namespaces = Namespaces()
+ namespaces.namespaces = namespace_list
+ if len(namespace_list) != 0 and len(namespace_list) == limit:
+ namespaces.next = namespace_list[-1].namespace
+
+ except exception.Forbidden as e:
+ raise webob.exc.HTTPForbidden(explanation=e.msg)
+ except exception.NotFound as e:
+ raise webob.exc.HTTPNotFound(explanation=e.msg)
+ except Exception as e:
+ LOG.error(utils.exception_to_str(e))
+ raise webob.exc.HTTPInternalServerError()
+ return namespaces
+
+ @utils.mutating
+ def create(self, req, namespace):
+ try:
+ namespace_created = False
+ # Create Namespace
+ ns_factory = self.gateway.get_metadef_namespace_factory(
+ req.context)
+ ns_repo = self.gateway.get_metadef_namespace_repo(req.context)
+ new_namespace = ns_factory.new_namespace(**namespace.to_dict())
+ ns_repo.add(new_namespace)
+ namespace_created = True
+
+ # Create Resource Types
+ rs_factory = (
+ self.gateway.get_metadef_resource_type_factory(req.context))
+ rs_repo = self.gateway.get_metadef_resource_type_repo(req.context)
+ if namespace.resource_type_associations:
+ for resource_type in namespace.resource_type_associations:
+ new_resource = rs_factory.new_resource_type(
+ namespace=namespace.namespace,
+ **resource_type.to_dict())
+ rs_repo.add(new_resource)
+
+ # Create Objects
+ object_factory = self.gateway.get_metadef_object_factory(
+ req.context)
+ object_repo = self.gateway.get_metadef_object_repo(req.context)
+
+ if namespace.objects:
+ for metadata_object in namespace.objects:
+ new_meta_object = object_factory.new_object(
+ namespace=namespace.namespace,
+ **metadata_object.to_dict())
+ object_repo.add(new_meta_object)
+
+ # Create Namespace Properties
+ prop_factory = (
+ self.gateway.get_metadef_property_factory(req.context))
+ prop_repo = self.gateway.get_metadef_property_repo(req.context)
+ if namespace.properties:
+ for (name, value) in namespace.properties.items():
+ new_property_type = (
+ prop_factory.new_namespace_property(
+ namespace=namespace.namespace,
+ **self._to_property_dict(name, value)
+ ))
+ prop_repo.add(new_property_type)
+
+ except exception.Forbidden as e:
+ self._cleanup_namespace(ns_repo, namespace, namespace_created)
+ raise webob.exc.HTTPForbidden(explanation=e.msg)
+ except exception.NotFound as e:
+ self._cleanup_namespace(ns_repo, namespace, namespace_created)
+ raise webob.exc.HTTPNotFound(explanation=e.msg)
+ except exception.Duplicate as e:
+ self._cleanup_namespace(ns_repo, namespace, namespace_created)
+ raise webob.exc.HTTPConflict(explanation=e.msg)
+ except Exception as e:
+ LOG.error(utils.exception_to_str(e))
+ raise webob.exc.HTTPInternalServerError()
+
+ # Return the user namespace as we don't expose the id to user
+ new_namespace.properties = namespace.properties
+ new_namespace.objects = namespace.objects
+ new_namespace.resource_type_associations = (
+ namespace.resource_type_associations)
+ return Namespace.to_wsme_model(new_namespace,
+ get_namespace_href(new_namespace),
+ self.ns_schema_link)
+
+ def _to_property_dict(self, name, value):
+ # Convert the model PropertyTypes dict to a JSON string
+ json_data = tojson(PropertyType, value)
+ db_property_type_dict = dict()
+ db_property_type_dict['schema'] = json.dumps(json_data)
+ db_property_type_dict['name'] = name
+ return db_property_type_dict
+
+ def _cleanup_namespace(self, namespace_repo, namespace, namespace_created):
+ if namespace_created:
+ try:
+ namespace_obj = namespace_repo.get(namespace.namespace)
+ namespace_obj.delete()
+ namespace_repo.remove(namespace_obj)
+ msg = ("Cleaned up namespace %(namespace)s "
+ % {'namespace': namespace.namespace})
+ LOG.debug(msg)
+ except exception:
+ msg = (_LE("Failed to delete namespace %(namespace)s ") %
+ {'namespace': namespace.namespace})
+ LOG.error(msg)
+
+ def show(self, req, namespace, filters=None):
+ try:
+ # Get namespace
+ ns_repo = self.gateway.get_metadef_namespace_repo(req.context)
+ namespace_obj = ns_repo.get(namespace)
+ namespace_detail = Namespace.to_wsme_model(
+ namespace_obj,
+ get_namespace_href(namespace_obj),
+ self.ns_schema_link)
+ ns_filters = dict()
+ ns_filters['namespace'] = namespace
+
+ # Get objects
+ object_repo = self.gateway.get_metadef_object_repo(req.context)
+ db_metaobject_list = object_repo.list(filters=ns_filters)
+ object_list = [MetadefObject.to_wsme_model(
+ db_metaobject,
+ get_object_href(namespace, db_metaobject),
+ self.obj_schema_link) for db_metaobject in db_metaobject_list]
+ if object_list:
+ namespace_detail.objects = object_list
+
+ # Get resource type associations
+ rs_repo = self.gateway.get_metadef_resource_type_repo(req.context)
+ db_resource_type_list = rs_repo.list(filters=ns_filters)
+ resource_type_list = [ResourceTypeAssociation.to_wsme_model(
+ resource_type) for resource_type in db_resource_type_list]
+ if resource_type_list:
+ namespace_detail.resource_type_associations = (
+ resource_type_list)
+
+ # Get properties
+ prop_repo = self.gateway.get_metadef_property_repo(req.context)
+ db_properties = prop_repo.list(filters=ns_filters)
+ property_list = Namespace.to_model_properties(db_properties)
+ if property_list:
+ namespace_detail.properties = property_list
+
+ if filters and filters['resource_type']:
+ namespace_detail = self._prefix_property_name(
+ namespace_detail, filters['resource_type'])
+
+ except exception.Forbidden as e:
+ raise webob.exc.HTTPForbidden(explanation=e.msg)
+ except exception.NotFound as e:
+ raise webob.exc.HTTPNotFound(explanation=e.msg)
+ except Exception as e:
+ LOG.error(utils.exception_to_str(e))
+ raise webob.exc.HTTPInternalServerError()
+ return namespace_detail
+
+ def update(self, req, user_ns, namespace):
+ namespace_repo = self.gateway.get_metadef_namespace_repo(req.context)
+ try:
+ ns_obj = namespace_repo.get(namespace)
+ ns_obj.namespace = wsme_utils._get_value(user_ns.namespace)
+ ns_obj.display_name = wsme_utils._get_value(user_ns.display_name)
+ ns_obj.description = wsme_utils._get_value(user_ns.description)
+ # Following optional fields will default to same values as in
+ # create namespace if not specified
+ ns_obj.visibility = (
+ wsme_utils._get_value(user_ns.visibility) or 'private')
+ ns_obj.protected = (
+ wsme_utils._get_value(user_ns.protected) or False)
+ ns_obj.owner = (
+ wsme_utils._get_value(user_ns.owner) or req.context.owner)
+ updated_namespace = namespace_repo.save(ns_obj)
+ except exception.Forbidden as e:
+ raise webob.exc.HTTPForbidden(explanation=e.msg)
+ except exception.NotFound as e:
+ raise webob.exc.HTTPNotFound(explanation=e.msg)
+ except exception.Duplicate as e:
+ raise webob.exc.HTTPConflict(explanation=e.msg)
+ except Exception as e:
+ LOG.error(utils.exception_to_str(e))
+ raise webob.exc.HTTPInternalServerError()
+
+ return Namespace.to_wsme_model(updated_namespace,
+ get_namespace_href(updated_namespace),
+ self.ns_schema_link)
+
+ def delete(self, req, namespace):
+ namespace_repo = self.gateway.get_metadef_namespace_repo(req.context)
+ try:
+ namespace_obj = namespace_repo.get(namespace)
+ namespace_obj.delete()
+ namespace_repo.remove(namespace_obj)
+ except exception.Forbidden as e:
+ raise webob.exc.HTTPForbidden(explanation=e.msg)
+ except exception.NotFound as e:
+ raise webob.exc.HTTPNotFound(explanation=e.msg)
+ except Exception as e:
+ LOG.error(utils.exception_to_str(e))
+ raise webob.exc.HTTPInternalServerError()
+
+ def delete_objects(self, req, namespace):
+ ns_repo = self.gateway.get_metadef_namespace_repo(req.context)
+ try:
+ namespace_obj = ns_repo.get(namespace)
+ namespace_obj.delete()
+ ns_repo.remove_objects(namespace_obj)
+ except exception.Forbidden as e:
+ raise webob.exc.HTTPForbidden(explanation=e.msg)
+ except exception.NotFound as e:
+ raise webob.exc.HTTPNotFound(explanation=e.msg)
+ except Exception as e:
+ LOG.error(utils.exception_to_str(e))
+ raise webob.exc.HTTPInternalServerError()
+
+ def delete_properties(self, req, namespace):
+ ns_repo = self.gateway.get_metadef_namespace_repo(req.context)
+ try:
+ namespace_obj = ns_repo.get(namespace)
+ namespace_obj.delete()
+ ns_repo.remove_properties(namespace_obj)
+ except exception.Forbidden as e:
+ raise webob.exc.HTTPForbidden(explanation=e.msg)
+ except exception.NotFound as e:
+ raise webob.exc.HTTPNotFound(explanation=e.msg)
+ except Exception as e:
+ LOG.error(utils.exception_to_str(e))
+ raise webob.exc.HTTPInternalServerError()
+
+ def _prefix_property_name(self, namespace_detail, user_resource_type):
+ prefix = None
+ if user_resource_type and namespace_detail.resource_type_associations:
+ for resource_type in namespace_detail.resource_type_associations:
+ if resource_type.name == user_resource_type:
+ prefix = resource_type.prefix
+ break
+
+ if prefix:
+ if namespace_detail.properties:
+ new_property_dict = dict()
+ for (key, value) in namespace_detail.properties.items():
+ new_property_dict[prefix + key] = value
+ namespace_detail.properties = new_property_dict
+
+ if namespace_detail.objects:
+ for object in namespace_detail.objects:
+ new_object_property_dict = dict()
+ for (key, value) in object.properties.items():
+ new_object_property_dict[prefix + key] = value
+ object.properties = new_object_property_dict
+
+ if object.required and len(object.required) > 0:
+ required = [prefix + name for name in object.required]
+ object.required = required
+
+ return namespace_detail
+
+
+class RequestDeserializer(wsgi.JSONRequestDeserializer):
+ _disallowed_properties = ['self', 'schema', 'created_at', 'updated_at']
+
+ def __init__(self, schema=None):
+ super(RequestDeserializer, self).__init__()
+ self.schema = schema or get_schema()
+
+ def _get_request_body(self, request):
+ output = super(RequestDeserializer, self).default(request)
+ if 'body' not in output:
+ msg = _('Body expected in request.')
+ raise webob.exc.HTTPBadRequest(explanation=msg)
+ return output['body']
+
+ @classmethod
+ def _check_allowed(cls, image):
+ for key in cls._disallowed_properties:
+ if key in image:
+ msg = _("Attribute '%s' is read-only.") % key
+ raise webob.exc.HTTPForbidden(explanation=msg)
+
+ def index(self, request):
+ params = request.params.copy()
+ limit = params.pop('limit', None)
+ marker = params.pop('marker', None)
+ sort_dir = params.pop('sort_dir', 'desc')
+
+ if limit is None:
+ limit = CONF.limit_param_default
+ limit = min(CONF.api_limit_max, int(limit))
+
+ query_params = {
+ 'sort_key': params.pop('sort_key', 'created_at'),
+ 'sort_dir': self._validate_sort_dir(sort_dir),
+ 'filters': self._get_filters(params)
+ }
+
+ if marker is not None:
+ query_params['marker'] = marker
+
+ if limit is not None:
+ query_params['limit'] = self._validate_limit(limit)
+
+ return query_params
+
+ def _validate_sort_dir(self, sort_dir):
+ if sort_dir not in ['asc', 'desc']:
+ msg = _('Invalid sort direction: %s') % sort_dir
+ raise webob.exc.HTTPBadRequest(explanation=msg)
+
+ return sort_dir
+
+ def _get_filters(self, filters):
+ visibility = filters.get('visibility')
+ if visibility:
+ if visibility not in ['public', 'private']:
+ msg = _('Invalid visibility value: %s') % visibility
+ raise webob.exc.HTTPBadRequest(explanation=msg)
+
+ return filters
+
+ def _validate_limit(self, limit):
+ try:
+ limit = int(limit)
+ except ValueError:
+ msg = _("limit param must be an integer")
+ raise webob.exc.HTTPBadRequest(explanation=msg)
+
+ if limit < 0:
+ msg = _("limit param must be positive")
+ raise webob.exc.HTTPBadRequest(explanation=msg)
+
+ return limit
+
+ def show(self, request):
+ params = request.params.copy()
+ query_params = {
+ 'filters': self._get_filters(params)
+ }
+ return query_params
+
+ def create(self, request):
+ body = self._get_request_body(request)
+ self._check_allowed(body)
+ try:
+ self.schema.validate(body)
+ except exception.InvalidObject as e:
+ raise webob.exc.HTTPBadRequest(explanation=e.msg)
+ namespace = fromjson(Namespace, body)
+ return dict(namespace=namespace)
+
+ def update(self, request):
+ body = self._get_request_body(request)
+ self._check_allowed(body)
+ try:
+ self.schema.validate(body)
+ except exception.InvalidObject as e:
+ raise webob.exc.HTTPBadRequest(explanation=e.msg)
+ namespace = fromjson(Namespace, body)
+ return dict(user_ns=namespace)
+
+
+class ResponseSerializer(wsgi.JSONResponseSerializer):
+ def __init__(self, schema=None):
+ super(ResponseSerializer, self).__init__()
+ self.schema = schema
+
+ def create(self, response, namespace):
+ ns_json = tojson(Namespace, namespace)
+ response = self.__render(ns_json, response, 201)
+ response.location = get_namespace_href(namespace)
+
+ def show(self, response, namespace):
+ ns_json = tojson(Namespace, namespace)
+ response = self.__render(ns_json, response)
+
+ def index(self, response, result):
+ params = dict(response.request.params)
+ params.pop('marker', None)
+ query = urlparse.urlencode(params)
+ result.first = "/v2/metadefs/namespaces"
+ result.schema = "/v2/schemas/metadefs/namespaces"
+ if query:
+ result.first = '%s?%s' % (result.first, query)
+ if result.next:
+ params['marker'] = result.next
+ next_query = urlparse.urlencode(params)
+ result.next = '/v2/metadefs/namespaces?%s' % next_query
+
+ ns_json = tojson(Namespaces, result)
+ response = self.__render(ns_json, response)
+
+ def update(self, response, namespace):
+ ns_json = tojson(Namespace, namespace)
+ response = self.__render(ns_json, response, 200)
+
+ def delete(self, response, result):
+ response.status_int = 204
+
+ def delete_objects(self, response, result):
+ response.status_int = 204
+
+ def delete_properties(self, response, result):
+ response.status_int = 204
+
+ def __render(self, json_data, response, response_status=None):
+ body = json.dumps(json_data, ensure_ascii=False)
+ response.unicode_body = six.text_type(body)
+ response.content_type = 'application/json'
+ if response_status:
+ response.status_int = response_status
+ return response
+
+
+def _get_base_definitions():
+ return get_schema_definitions()
+
+
+def get_schema_definitions():
+ return {
+ "positiveInteger": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "positiveIntegerDefault0": {
+ "allOf": [
+ {"$ref": "#/definitions/positiveInteger"},
+ {"default": 0}
+ ]
+ },
+ "stringArray": {
+ "type": "array",
+ "items": {"type": "string"},
+ # "minItems": 1,
+ "uniqueItems": True
+ },
+ "property": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "object",
+ "required": ["title", "type"],
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "title": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string",
+ "enum": [
+ "array",
+ "boolean",
+ "integer",
+ "number",
+ "object",
+ "string",
+ None
+ ]
+ },
+ "required": {
+ "$ref": "#/definitions/stringArray"
+ },
+ "minimum": {
+ "type": "number"
+ },
+ "maximum": {
+ "type": "number"
+ },
+ "maxLength": {
+ "$ref": "#/definitions/positiveInteger"
+ },
+ "minLength": {
+ "$ref": "#/definitions/positiveIntegerDefault0"
+ },
+ "pattern": {
+ "type": "string",
+ "format": "regex"
+ },
+ "enum": {
+ "type": "array"
+ },
+ "readonly": {
+ "type": "boolean"
+ },
+ "default": {},
+ "items": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": [
+ "array",
+ "boolean",
+ "integer",
+ "number",
+ "object",
+ "string",
+ None
+ ]
+ },
+ "enum": {
+ "type": "array"
+ }
+ }
+ },
+ "maxItems": {
+ "$ref": "#/definitions/positiveInteger"
+ },
+ "minItems": {
+ "$ref": "#/definitions/positiveIntegerDefault0"
+ },
+ "uniqueItems": {
+ "type": "boolean",
+ "default": False
+ },
+ "additionalItems": {
+ "type": "boolean"
+ },
+ }
+ }
+ }
+ }
+
+
+def _get_base_properties():
+ return {
+ "namespace": {
+ "type": "string",
+ "description": _("The unique namespace text."),
+ "maxLength": 80,
+ },
+ "display_name": {
+ "type": "string",
+ "description": _("The user friendly name for the namespace. Used "
+ "by UI if available."),
+ "maxLength": 80,
+ },
+ "description": {
+ "type": "string",
+ "description": _("Provides a user friendly description of the "
+ "namespace."),
+ "maxLength": 500,
+ },
+ "visibility": {
+ "type": "string",
+ "description": _("Scope of namespace accessibility."),
+ "enum": ["public", "private"],
+ },
+ "protected": {
+ "type": "boolean",
+ "description": _("If true, namespace will not be deletable."),
+ },
+ "owner": {
+ "type": "string",
+ "description": _("Owner of the namespace."),
+ "maxLength": 255,
+ },
+ "created_at": {
+ "type": "string",
+ "description": _("Date and time of namespace creation"
+ " (READ-ONLY)"),
+ "format": "date-time"
+ },
+ "updated_at": {
+ "type": "string",
+ "description": _("Date and time of the last namespace modification"
+ " (READ-ONLY)"),
+ "format": "date-time"
+ },
+ "schema": {
+ "type": "string"
+ },
+ "self": {
+ "type": "string"
+ },
+ "resource_type_associations": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "prefix": {
+ "type": "string"
+ },
+ "properties_target": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "properties": {
+ "$ref": "#/definitions/property"
+ },
+ "objects": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "required": {
+ "$ref": "#/definitions/stringArray"
+ },
+ "properties": {
+ "$ref": "#/definitions/property"
+ },
+ }
+ }
+ }
+ }
+
+
+def get_schema():
+ properties = _get_base_properties()
+ definitions = _get_base_definitions()
+ mandatory_attrs = Namespace.get_mandatory_attrs()
+ schema = glance.schema.Schema(
+ 'namespace',
+ properties,
+ required=mandatory_attrs,
+ definitions=definitions
+ )
+ return schema
+
+
+def get_collection_schema():
+ namespace_schema = get_schema()
+ return glance.schema.CollectionSchema('namespaces', namespace_schema)
+
+
+def get_namespace_href(namespace):
+ base_href = '/v2/metadefs/namespaces/%s' % namespace.namespace
+ return base_href
+
+
+def get_object_href(namespace_name, metadef_object):
+ base_href = ('/v2/metadefs/namespaces/%s/objects/%s' %
+ (namespace_name, metadef_object.name))
+ return base_href
+
+
+def create_resource():
+ """Namespaces resource factory method"""
+ schema = get_schema()
+ deserializer = RequestDeserializer(schema)
+ serializer = ResponseSerializer(schema)
+ controller = NamespaceController()
+ return wsgi.Resource(controller, deserializer, serializer)
diff --git a/glance/api/v2/metadef_objects.py b/glance/api/v2/metadef_objects.py
new file mode 100644
index 0000000000..d86f26874e
--- /dev/null
+++ b/glance/api/v2/metadef_objects.py
@@ -0,0 +1,335 @@
+# Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
+#
+# 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.
+
+from oslo.config import cfg
+import six
+import webob.exc
+from wsme.rest.json import fromjson
+from wsme.rest.json import tojson
+
+from glance.api import policy
+from glance.api.v2 import metadef_namespaces as namespaces
+from glance.api.v2.model.metadef_object import MetadefObject
+from glance.api.v2.model.metadef_object import MetadefObjects
+from glance.common import exception
+from glance.common import utils
+from glance.common import wsgi
+from glance.common import wsme_utils
+import glance.db
+from glance import i18n
+import glance.notifier
+from glance.openstack.common import jsonutils as json
+import glance.openstack.common.log as logging
+import glance.schema
+
+LOG = logging.getLogger(__name__)
+_LE = i18n._LE
+_LI = i18n._LI
+
+CONF = cfg.CONF
+
+
+class MetadefObjectsController(object):
+ def __init__(self, db_api=None, policy_enforcer=None):
+ self.db_api = db_api or glance.db.get_api()
+ self.policy = policy_enforcer or policy.Enforcer()
+ self.gateway = glance.gateway.Gateway(db_api=self.db_api,
+ policy_enforcer=self.policy)
+ self.obj_schema_link = '/v2/schemas/metadefs/object'
+
+ def create(self, req, metadata_object, namespace):
+ object_factory = self.gateway.get_metadef_object_factory(req.context)
+ object_repo = self.gateway.get_metadef_object_repo(req.context)
+ try:
+ new_meta_object = object_factory.new_object(
+ namespace=namespace,
+ **metadata_object.to_dict())
+ object_repo.add(new_meta_object)
+
+ except exception.Forbidden as e:
+ raise webob.exc.HTTPForbidden(explanation=e.msg)
+ except exception.NotFound as e:
+ raise webob.exc.HTTPNotFound(explanation=e.msg)
+ except exception.Duplicate as e:
+ raise webob.exc.HTTPConflict(explanation=e.msg)
+ except Exception as e:
+ LOG.error(utils.exception_to_str(e))
+ raise webob.exc.HTTPInternalServerError()
+ return MetadefObject.to_wsme_model(
+ new_meta_object,
+ get_object_href(namespace, new_meta_object),
+ self.obj_schema_link)
+
+ def index(self, req, namespace, marker=None, limit=None,
+ sort_key='created_at', sort_dir='desc', filters=None):
+ try:
+ filters = filters or dict()
+ filters['namespace'] = namespace
+ object_repo = self.gateway.get_metadef_object_repo(req.context)
+ db_metaobject_list = object_repo.list(
+ marker=marker, limit=limit, sort_key=sort_key,
+ sort_dir=sort_dir, filters=filters)
+ object_list = [MetadefObject.to_wsme_model(
+ db_metaobject,
+ get_object_href(namespace, db_metaobject),
+ self.obj_schema_link) for db_metaobject in db_metaobject_list]
+ metadef_objects = MetadefObjects()
+ metadef_objects.objects = object_list
+ except exception.Forbidden as e:
+ raise webob.exc.HTTPForbidden(explanation=e.msg)
+ except exception.NotFound as e:
+ raise webob.exc.HTTPNotFound(explanation=e.msg)
+ except Exception as e:
+ LOG.error(utils.exception_to_str(e))
+ raise webob.exc.HTTPInternalServerError()
+ return metadef_objects
+
+ def show(self, req, namespace, object_name):
+ meta_object_repo = self.gateway.get_metadef_object_repo(
+ req.context)
+ try:
+ metadef_object = meta_object_repo.get(namespace, object_name)
+ return MetadefObject.to_wsme_model(
+ metadef_object,
+ get_object_href(namespace, metadef_object),
+ self.obj_schema_link)
+ except exception.Forbidden as e:
+ raise webob.exc.HTTPForbidden(explanation=e.msg)
+ except exception.NotFound as e:
+ raise webob.exc.HTTPNotFound(explanation=e.msg)
+ except Exception as e:
+ LOG.error(utils.exception_to_str(e))
+ raise webob.exc.HTTPInternalServerError()
+
+ def update(self, req, metadata_object, namespace, object_name):
+ meta_repo = self.gateway.get_metadef_object_repo(req.context)
+ try:
+ metadef_object = meta_repo.get(namespace, object_name)
+ metadef_object.name = wsme_utils._get_value(
+ metadata_object.name)
+ metadef_object.description = wsme_utils._get_value(
+ metadata_object.description)
+ metadef_object.required = wsme_utils._get_value(
+ metadata_object.required)
+ metadef_object.properties = wsme_utils._get_value(
+ metadata_object.properties)
+ updated_metadata_obj = meta_repo.save(metadef_object)
+ except exception.Forbidden as e:
+ raise webob.exc.HTTPForbidden(explanation=e.msg)
+ except exception.NotFound as e:
+ raise webob.exc.HTTPNotFound(explanation=e.msg)
+ except exception.Duplicate as e:
+ raise webob.exc.HTTPConflict(explanation=e.msg)
+ except Exception as e:
+ LOG.error(utils.exception_to_str(e))
+ raise webob.exc.HTTPInternalServerError()
+ return MetadefObject.to_wsme_model(
+ updated_metadata_obj,
+ get_object_href(namespace, updated_metadata_obj),
+ self.obj_schema_link)
+
+ def delete(self, req, namespace, object_name):
+ meta_repo = self.gateway.get_metadef_object_repo(req.context)
+ try:
+ metadef_object = meta_repo.get(namespace, object_name)
+ metadef_object.delete()
+ meta_repo.remove(metadef_object)
+ except exception.Forbidden as e:
+ raise webob.exc.HTTPForbidden(explanation=e.msg)
+ except exception.NotFound as e:
+ raise webob.exc.HTTPNotFound(explanation=e.msg)
+ except Exception as e:
+ LOG.error(utils.exception_to_str(e))
+ raise webob.exc.HTTPInternalServerError()
+
+
+def _get_base_definitions():
+ return namespaces.get_schema_definitions()
+
+
+def _get_base_properties():
+ return {
+ "name": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "required": {
+ "$ref": "#/definitions/stringArray"
+ },
+ "properties": {
+ "$ref": "#/definitions/property"
+ },
+ "schema": {
+ "type": "string"
+ },
+ "self": {
+ "type": "string"
+ },
+ "created_at": {
+ "type": "string",
+ "description": _("Date and time of object creation"
+ " (READ-ONLY)"),
+ "format": "date-time"
+ },
+ "updated_at": {
+ "type": "string",
+ "description": _("Date and time of the last object modification"
+ " (READ-ONLY)"),
+ "format": "date-time"
+ }
+ }
+
+
+def get_schema():
+ definitions = _get_base_definitions()
+ properties = _get_base_properties()
+ mandatory_attrs = MetadefObject.get_mandatory_attrs()
+ schema = glance.schema.Schema(
+ 'object',
+ properties,
+ required=mandatory_attrs,
+ definitions=definitions,
+ )
+ return schema
+
+
+def get_collection_schema():
+ object_schema = get_schema()
+ return glance.schema.CollectionSchema('objects', object_schema)
+
+
+class RequestDeserializer(wsgi.JSONRequestDeserializer):
+ _disallowed_properties = ['self', 'schema', 'created_at', 'updated_at']
+
+ def __init__(self, schema=None):
+ super(RequestDeserializer, self).__init__()
+ self.schema = schema or get_schema()
+
+ def _get_request_body(self, request):
+ output = super(RequestDeserializer, self).default(request)
+ if 'body' not in output:
+ msg = _('Body expected in request.')
+ raise webob.exc.HTTPBadRequest(explanation=msg)
+ return output['body']
+
+ def create(self, request):
+ body = self._get_request_body(request)
+ self._check_allowed(body)
+ try:
+ self.schema.validate(body)
+ except exception.InvalidObject as e:
+ raise webob.exc.HTTPBadRequest(explanation=e.msg)
+ metadata_object = fromjson(MetadefObject, body)
+ return dict(metadata_object=metadata_object)
+
+ def update(self, request):
+ body = self._get_request_body(request)
+ self._check_allowed(body)
+ try:
+ self.schema.validate(body)
+ except exception.InvalidObject as e:
+ raise webob.exc.HTTPBadRequest(explanation=e.msg)
+ metadata_object = fromjson(MetadefObject, body)
+ return dict(metadata_object=metadata_object)
+
+ def index(self, request):
+ params = request.params.copy()
+ limit = params.pop('limit', None)
+ marker = params.pop('marker', None)
+ sort_dir = params.pop('sort_dir', 'desc')
+
+ query_params = {
+ 'sort_key': params.pop('sort_key', 'created_at'),
+ 'sort_dir': self._validate_sort_dir(sort_dir),
+ 'filters': self._get_filters(params)
+ }
+
+ if marker is not None:
+ query_params['marker'] = marker
+
+ if limit is not None:
+ query_params['limit'] = self._validate_limit(limit)
+
+ return query_params
+
+ def _validate_sort_dir(self, sort_dir):
+ if sort_dir not in ['asc', 'desc']:
+ msg = _('Invalid sort direction: %s') % sort_dir
+ raise webob.exc.HTTPBadRequest(explanation=msg)
+
+ return sort_dir
+
+ def _get_filters(self, filters):
+ visibility = filters.get('visibility')
+ if visibility:
+ if visibility not in ['public', 'private', 'shared']:
+ msg = _('Invalid visibility value: %s') % visibility
+ raise webob.exc.HTTPBadRequest(explanation=msg)
+
+ return filters
+
+ @classmethod
+ def _check_allowed(cls, image):
+ for key in cls._disallowed_properties:
+ if key in image:
+ msg = _("Attribute '%s' is read-only.") % key
+ raise webob.exc.HTTPForbidden(explanation=msg)
+
+
+class ResponseSerializer(wsgi.JSONResponseSerializer):
+ def __init__(self, schema=None):
+ super(ResponseSerializer, self).__init__()
+ self.schema = schema or get_schema()
+
+ def create(self, response, metadata_object):
+ response.status_int = 201
+ self.show(response, metadata_object)
+
+ def show(self, response, metadata_object):
+ metadata_object_json = tojson(MetadefObject, metadata_object)
+ body = json.dumps(metadata_object_json, ensure_ascii=False)
+ response.unicode_body = six.text_type(body)
+ response.content_type = 'application/json'
+
+ def update(self, response, metadata_object):
+ response.status_int = 200
+ self.show(response, metadata_object)
+
+ def index(self, response, result):
+ result.schema = "v2/schemas/metadefs/objects"
+ metadata_objects_json = tojson(MetadefObjects, result)
+ body = json.dumps(metadata_objects_json, ensure_ascii=False)
+ response.unicode_body = six.text_type(body)
+ response.content_type = 'application/json'
+
+ def delete(self, response, result):
+ response.status_int = 204
+
+
+def get_object_href(namespace_name, metadef_object):
+ base_href = ('/v2/metadefs/namespaces/%s/objects/%s' %
+ (namespace_name, metadef_object.name))
+ return base_href
+
+
+def create_resource():
+ """Metadef objects resource factory method"""
+ schema = get_schema()
+ deserializer = RequestDeserializer(schema)
+ serializer = ResponseSerializer(schema)
+ controller = MetadefObjectsController()
+ return wsgi.Resource(controller, deserializer, serializer)
diff --git a/glance/api/v2/metadef_properties.py b/glance/api/v2/metadef_properties.py
new file mode 100644
index 0000000000..33c2d41c4e
--- /dev/null
+++ b/glance/api/v2/metadef_properties.py
@@ -0,0 +1,275 @@
+# Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
+#
+# 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 six
+import webob.exc
+from wsme.rest.json import fromjson
+from wsme.rest.json import tojson
+
+from glance.api import policy
+from glance.api.v2 import metadef_namespaces as namespaces
+from glance.api.v2.model.metadef_namespace import Namespace
+from glance.api.v2.model.metadef_property_type import PropertyType
+from glance.api.v2.model.metadef_property_type import PropertyTypes
+from glance.common import exception
+from glance.common import utils
+from glance.common import wsgi
+import glance.db
+import glance.gateway
+from glance import i18n
+import glance.notifier
+from glance.openstack.common import jsonutils as json
+import glance.openstack.common.log as logging
+import glance.schema
+import glance.store
+
+LOG = logging.getLogger(__name__)
+_LE = i18n._LE
+_LI = i18n._LI
+
+
+class NamespacePropertiesController(object):
+ def __init__(self, db_api=None, policy_enforcer=None):
+ self.db_api = db_api or glance.db.get_api()
+ self.policy = policy_enforcer or policy.Enforcer()
+ self.gateway = glance.gateway.Gateway(db_api=self.db_api,
+ policy_enforcer=self.policy)
+
+ def _to_dict(self, model_property_type):
+ # Convert the model PropertyTypes dict to a JSON string
+ json_data = tojson(PropertyType, model_property_type)
+ db_property_type_dict = dict()
+ db_property_type_dict['schema'] = json.dumps(json_data)
+ db_property_type_dict['name'] = model_property_type.name
+ return db_property_type_dict
+
+ def _to_model(self, db_property_type):
+ # Convert the persisted json schema to a dict of PropertyTypes
+ json_props = json.loads(db_property_type.schema)
+ property_type = fromjson(PropertyType, json_props)
+ property_type.name = db_property_type.name
+ return property_type
+
+ def index(self, req, namespace):
+ try:
+ filters = dict()
+ filters['namespace'] = namespace
+ prop_repo = self.gateway.get_metadef_property_repo(req.context)
+ db_properties = prop_repo.list(filters=filters)
+ property_list = Namespace.to_model_properties(db_properties)
+ namespace_properties = PropertyTypes()
+ namespace_properties.properties = property_list
+ except exception.Forbidden as e:
+ raise webob.exc.HTTPForbidden(explanation=e.msg)
+ except exception.NotFound as e:
+ raise webob.exc.HTTPNotFound(explanation=e.msg)
+ except Exception as e:
+ LOG.error(utils.exception_to_str(e))
+ raise webob.exc.HTTPInternalServerError()
+ return namespace_properties
+
+ def show(self, req, namespace, property_name):
+ try:
+ prop_repo = self.gateway.get_metadef_property_repo(req.context)
+ db_property = prop_repo.get(namespace, property_name)
+ property = self._to_model(db_property)
+ except exception.Forbidden as e:
+ raise webob.exc.HTTPForbidden(explanation=e.msg)
+ except exception.NotFound as e:
+ raise webob.exc.HTTPNotFound(explanation=e.msg)
+ except Exception as e:
+ LOG.error(utils.exception_to_str(e))
+ raise webob.exc.HTTPInternalServerError()
+ return property
+
+ def create(self, req, namespace, property_type):
+ prop_factory = self.gateway.get_metadef_property_factory(req.context)
+ prop_repo = self.gateway.get_metadef_property_repo(req.context)
+ try:
+ new_property_type = prop_factory.new_namespace_property(
+ namespace=namespace, **self._to_dict(property_type))
+ prop_repo.add(new_property_type)
+ except exception.Forbidden as e:
+ raise webob.exc.HTTPForbidden(explanation=e.msg)
+ except exception.NotFound as e:
+ raise webob.exc.HTTPNotFound(explanation=e.msg)
+ except exception.Duplicate as e:
+ raise webob.exc.HTTPConflict(explanation=e.msg)
+ except Exception as e:
+ LOG.error(utils.exception_to_str(e))
+ raise webob.exc.HTTPInternalServerError()
+ return self._to_model(new_property_type)
+
+ def update(self, req, namespace, property_name, property_type):
+ prop_repo = self.gateway.get_metadef_property_repo(req.context)
+ try:
+ db_property_type = prop_repo.get(namespace, property_name)
+ db_property_type.name = property_type.name
+ db_property_type.schema = (self._to_dict(property_type))['schema']
+ updated_property_type = prop_repo.save(db_property_type)
+ except exception.Forbidden as e:
+ raise webob.exc.HTTPForbidden(explanation=e.msg)
+ except exception.NotFound as e:
+ raise webob.exc.HTTPNotFound(explanation=e.msg)
+ except exception.Duplicate as e:
+ raise webob.exc.HTTPConflict(explanation=e.msg)
+ except Exception as e:
+ LOG.error(utils.exception_to_str(e))
+ raise webob.exc.HTTPInternalServerError()
+ return self._to_model(updated_property_type)
+
+ def delete(self, req, namespace, property_name):
+ prop_repo = self.gateway.get_metadef_property_repo(req.context)
+ try:
+ property_type = prop_repo.get(namespace, property_name)
+ property_type.delete()
+ prop_repo.remove(property_type)
+ except exception.Forbidden as e:
+ raise webob.exc.HTTPForbidden(explanation=e.msg)
+ except exception.NotFound as e:
+ raise webob.exc.HTTPNotFound(explanation=e.msg)
+ except Exception as e:
+ LOG.error(utils.exception_to_str(e))
+ raise webob.exc.HTTPInternalServerError()
+
+
+class RequestDeserializer(wsgi.JSONRequestDeserializer):
+ _disallowed_properties = ['created_at', 'updated_at']
+
+ def __init__(self, schema=None):
+ super(RequestDeserializer, self).__init__()
+ self.schema = schema or get_schema()
+
+ def _get_request_body(self, request):
+ output = super(RequestDeserializer, self).default(request)
+ if 'body' not in output:
+ msg = _('Body expected in request.')
+ raise webob.exc.HTTPBadRequest(explanation=msg)
+ return output['body']
+
+ @classmethod
+ def _check_allowed(cls, image):
+ for key in cls._disallowed_properties:
+ if key in image:
+ msg = _("Attribute '%s' is read-only.") % key
+ raise webob.exc.HTTPForbidden(explanation=msg)
+
+ def create(self, request):
+ body = self._get_request_body(request)
+ self._check_allowed(body)
+ try:
+ self.schema.validate(body)
+ except exception.InvalidObject as e:
+ raise webob.exc.HTTPBadRequest(explanation=e.msg)
+ property_type = fromjson(PropertyType, body)
+ return dict(property_type=property_type)
+
+ def update(self, request):
+ body = self._get_request_body(request)
+ self._check_allowed(body)
+ try:
+ self.schema.validate(body)
+ except exception.InvalidObject as e:
+ raise webob.exc.HTTPBadRequest(explanation=e.msg)
+ property_type = fromjson(PropertyType, body)
+ return dict(property_type=property_type)
+
+
+class ResponseSerializer(wsgi.JSONResponseSerializer):
+ def __init__(self, schema=None):
+ super(ResponseSerializer, self).__init__()
+ self.schema = schema
+
+ def show(self, response, result):
+ property_type_json = tojson(PropertyType, result)
+ body = json.dumps(property_type_json, ensure_ascii=False)
+ response.unicode_body = six.text_type(body)
+ response.content_type = 'application/json'
+
+ def index(self, response, result):
+ property_type_json = tojson(PropertyTypes, result)
+ body = json.dumps(property_type_json, ensure_ascii=False)
+ response.unicode_body = six.text_type(body)
+ response.content_type = 'application/json'
+
+ def create(self, response, result):
+ response.status_int = 201
+ self.show(response, result)
+
+ def update(self, response, result):
+ response.status_int = 200
+ self.show(response, result)
+
+ def delete(self, response, result):
+ response.status_int = 204
+
+
+def _get_base_definitions():
+ return {
+ "positiveInteger": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "positiveIntegerDefault0": {
+ "allOf": [
+ {"$ref": "#/definitions/positiveInteger"},
+ {"default": 0}
+ ]
+ },
+ "stringArray": {
+ "type": "array",
+ "items": {"type": "string"},
+ "minItems": 1,
+ "uniqueItems": True
+ }
+ }
+
+
+def _get_base_properties():
+ base_def = namespaces.get_schema_definitions()
+ return base_def['property']['additionalProperties']['properties']
+
+
+def get_schema():
+ definitions = _get_base_definitions()
+ properties = _get_base_properties()
+ mandatory_attrs = PropertyType.get_mandatory_attrs()
+ # name is required attribute when use as single property type
+ mandatory_attrs.append('name')
+ schema = glance.schema.Schema(
+ 'property',
+ properties,
+ required=mandatory_attrs,
+ definitions=definitions
+ )
+ return schema
+
+
+def get_collection_schema():
+ namespace_properties_schema = get_schema()
+ # Property name is a dict key and not a required attribute in
+ # individual property schema inside property collections
+ namespace_properties_schema.required.remove('name')
+ return glance.schema.DictCollectionSchema('properties',
+ namespace_properties_schema)
+
+
+def create_resource():
+ """NamespaceProperties resource factory method"""
+ schema = get_schema()
+ deserializer = RequestDeserializer(schema)
+ serializer = ResponseSerializer(schema)
+ controller = NamespacePropertiesController()
+ return wsgi.Resource(controller, deserializer, serializer)
diff --git a/glance/api/v2/metadef_resource_types.py b/glance/api/v2/metadef_resource_types.py
new file mode 100644
index 0000000000..f6be343895
--- /dev/null
+++ b/glance/api/v2/metadef_resource_types.py
@@ -0,0 +1,264 @@
+# Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
+#
+# 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 six
+import webob.exc
+from wsme.rest.json import fromjson
+from wsme.rest.json import tojson
+
+from glance.api import policy
+from glance.api.v2.model.metadef_resource_type import ResourceType
+from glance.api.v2.model.metadef_resource_type import ResourceTypeAssociation
+from glance.api.v2.model.metadef_resource_type import ResourceTypeAssociations
+from glance.api.v2.model.metadef_resource_type import ResourceTypes
+from glance.common import exception
+from glance.common import utils
+from glance.common import wsgi
+import glance.db
+import glance.gateway
+from glance import i18n
+import glance.notifier
+from glance.openstack.common import jsonutils as json
+import glance.openstack.common.log as logging
+import glance.schema
+import glance.store
+
+LOG = logging.getLogger(__name__)
+_LE = i18n._LE
+_LI = i18n._LI
+
+
+class ResourceTypeController(object):
+ def __init__(self, db_api=None, policy_enforcer=None):
+ self.db_api = db_api or glance.db.get_api()
+ self.policy = policy_enforcer or policy.Enforcer()
+ self.gateway = glance.gateway.Gateway(db_api=self.db_api,
+ policy_enforcer=self.policy)
+
+ def index(self, req):
+ try:
+ filters = {}
+ filters['namespace'] = None
+ rs_type_repo = self.gateway.get_metadef_resource_type_repo(
+ req.context)
+ db_resource_type_list = rs_type_repo.list(filters=filters)
+ resource_type_list = [ResourceType.to_wsme_model(
+ resource_type) for resource_type in db_resource_type_list]
+ resource_types = ResourceTypes()
+ resource_types.resource_types = resource_type_list
+ except exception.Forbidden as e:
+ raise webob.exc.HTTPForbidden(explanation=e.msg)
+ except exception.NotFound as e:
+ raise webob.exc.HTTPNotFound(explanation=e.msg)
+ except Exception as e:
+ LOG.error(utils.exception_to_str(e))
+ raise webob.exc.HTTPInternalServerError(e)
+ return resource_types
+
+ def show(self, req, namespace):
+ try:
+ filters = {}
+ filters['namespace'] = namespace
+ rs_type_repo = self.gateway.get_metadef_resource_type_repo(
+ req.context)
+ db_resource_type_list = rs_type_repo.list(filters=filters)
+ resource_type_list = [ResourceTypeAssociation.to_wsme_model(
+ resource_type) for resource_type in db_resource_type_list]
+ resource_types = ResourceTypeAssociations()
+ resource_types.resource_type_associations = resource_type_list
+ except exception.Forbidden as e:
+ raise webob.exc.HTTPForbidden(explanation=e.msg)
+ except exception.NotFound as e:
+ raise webob.exc.HTTPNotFound(explanation=e.msg)
+ except Exception as e:
+ LOG.error(utils.exception_to_str(e))
+ raise webob.exc.HTTPInternalServerError(e)
+ return resource_types
+
+ def create(self, req, resource_type, namespace):
+ rs_type_factory = self.gateway.get_metadef_resource_type_factory(
+ req.context)
+ rs_type_repo = self.gateway.get_metadef_resource_type_repo(req.context)
+ try:
+ new_resource_type = rs_type_factory.new_resource_type(
+ namespace=namespace, **resource_type.to_dict())
+ rs_type_repo.add(new_resource_type)
+
+ except exception.Forbidden as e:
+ raise webob.exc.HTTPForbidden(explanation=e.msg)
+ except exception.NotFound as e:
+ raise webob.exc.HTTPNotFound(explanation=e.msg)
+ except exception.Duplicate as e:
+ raise webob.exc.HTTPConflict(explanation=e.msg)
+ except Exception as e:
+ LOG.error(utils.exception_to_str(e))
+ raise webob.exc.HTTPInternalServerError()
+ return ResourceTypeAssociation.to_wsme_model(new_resource_type)
+
+ def delete(self, req, namespace, resource_type):
+ rs_type_repo = self.gateway.get_metadef_resource_type_repo(req.context)
+ try:
+ filters = {}
+ found = False
+ filters['namespace'] = namespace
+ db_resource_type_list = rs_type_repo.list(filters=filters)
+ for db_resource_type in db_resource_type_list:
+ if db_resource_type.name == resource_type:
+ db_resource_type.delete()
+ rs_type_repo.remove(db_resource_type)
+ found = True
+ if not found:
+ raise exception.NotFound()
+ except exception.Forbidden as e:
+ raise webob.exc.HTTPForbidden(explanation=e.msg)
+ except exception.NotFound as e:
+ msg = (_LE("Failed to find resource type %(resourcetype)s to "
+ "delete") % {'resourcetype': resource_type})
+ LOG.error(msg)
+ raise webob.exc.HTTPNotFound(explanation=msg)
+ except Exception as e:
+ LOG.error(utils.exception_to_str(e))
+ raise webob.exc.HTTPInternalServerError()
+
+
+class RequestDeserializer(wsgi.JSONRequestDeserializer):
+ _disallowed_properties = ['created_at', 'updated_at']
+
+ def __init__(self, schema=None):
+ super(RequestDeserializer, self).__init__()
+ self.schema = schema or get_schema()
+
+ def _get_request_body(self, request):
+ output = super(RequestDeserializer, self).default(request)
+ if 'body' not in output:
+ msg = _('Body expected in request.')
+ raise webob.exc.HTTPBadRequest(explanation=msg)
+ return output['body']
+
+ @classmethod
+ def _check_allowed(cls, image):
+ for key in cls._disallowed_properties:
+ if key in image:
+ msg = _("Attribute '%s' is read-only.") % key
+ raise webob.exc.HTTPForbidden(explanation=msg)
+
+ def create(self, request):
+ body = self._get_request_body(request)
+ self._check_allowed(body)
+ try:
+ self.schema.validate(body)
+ except exception.InvalidObject as e:
+ raise webob.exc.HTTPBadRequest(explanation=e.msg)
+ resource_type = fromjson(ResourceTypeAssociation, body)
+ return dict(resource_type=resource_type)
+
+
+class ResponseSerializer(wsgi.JSONResponseSerializer):
+ def __init__(self, schema=None):
+ super(ResponseSerializer, self).__init__()
+ self.schema = schema
+
+ def show(self, response, result):
+ resource_type_json = tojson(ResourceTypeAssociations, result)
+ body = json.dumps(resource_type_json, ensure_ascii=False)
+ response.unicode_body = six.text_type(body)
+ response.content_type = 'application/json'
+
+ def index(self, response, result):
+ resource_type_json = tojson(ResourceTypes, result)
+ body = json.dumps(resource_type_json, ensure_ascii=False)
+ response.unicode_body = six.text_type(body)
+ response.content_type = 'application/json'
+
+ def create(self, response, result):
+ resource_type_json = tojson(ResourceTypeAssociation, result)
+ response.status_int = 201
+ body = json.dumps(resource_type_json, ensure_ascii=False)
+ response.unicode_body = six.text_type(body)
+ response.content_type = 'application/json'
+
+ def delete(self, response, result):
+ response.status_int = 204
+
+
+def _get_base_properties():
+ return {
+ 'name': {
+ 'type': 'string',
+ 'description': _('Resource type names should be aligned with Heat '
+ 'resource types whenever possible: '
+ 'http://docs.openstack.org/developer/heat/'
+ 'template_guide/openstack.html'),
+ 'maxLength': 80,
+ },
+ 'prefix': {
+ 'type': 'string',
+ 'description': _('Specifies the prefix to use for the given '
+ 'resource type. Any properties in the namespace '
+ 'should be prefixed with this prefix when being '
+ 'applied to the specified resource type. Must '
+ 'include prefix separator (e.g. a colon :).'),
+ 'maxLength': 80,
+ },
+ 'properties_target': {
+ 'type': 'string',
+ 'description': _('Some resource types allow more than one key / '
+ 'value pair per instance. For example, Cinder '
+ 'allows user and image metadata on volumes. Only '
+ 'the image properties metadata is evaluated by '
+ 'Nova (scheduling or drivers). This property '
+ 'allows a namespace target to remove the '
+ 'ambiguity.'),
+ 'maxLength': 80,
+ },
+ "created_at": {
+ "type": "string",
+ "description": _("Date and time of resource type association"
+ " (READ-ONLY)"),
+ "format": "date-time"
+ },
+ "updated_at": {
+ "type": "string",
+ "description": _("Date and time of the last resource type "
+ "association modification (READ-ONLY)"),
+ "format": "date-time"
+ }
+ }
+
+
+def get_schema():
+ properties = _get_base_properties()
+ mandatory_attrs = ResourceTypeAssociation.get_mandatory_attrs()
+ schema = glance.schema.Schema(
+ 'resource_type_association',
+ properties,
+ required=mandatory_attrs,
+ )
+ return schema
+
+
+def get_collection_schema():
+ resource_type_schema = get_schema()
+ return glance.schema.CollectionSchema('resource_type_associations',
+ resource_type_schema)
+
+
+def create_resource():
+ """ResourceTypeAssociation resource factory method"""
+ schema = get_schema()
+ deserializer = RequestDeserializer(schema)
+ serializer = ResponseSerializer(schema)
+ controller = ResourceTypeController()
+ return wsgi.Resource(controller, deserializer, serializer)
diff --git a/glance/api/v2/model/__init__.py b/glance/api/v2/model/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/glance/api/v2/model/metadef_namespace.py b/glance/api/v2/model/metadef_namespace.py
new file mode 100644
index 0000000000..b93ec568e8
--- /dev/null
+++ b/glance/api/v2/model/metadef_namespace.py
@@ -0,0 +1,79 @@
+# Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
+#
+# 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 wsme
+from wsme.rest.json import fromjson
+from wsme import types
+
+from glance.api.v2.model.metadef_object import MetadefObject
+from glance.api.v2.model.metadef_property_type import PropertyType
+from glance.api.v2.model.metadef_resource_type import ResourceTypeAssociation
+from glance.common.wsme_utils import WSMEModelTransformer
+from glance.openstack.common import jsonutils as json
+
+
+class Namespace(types.Base, WSMEModelTransformer):
+
+ #Base fields
+ namespace = wsme.wsattr(types.text, mandatory=True)
+ display_name = wsme.wsattr(types.text, mandatory=False)
+ description = wsme.wsattr(types.text, mandatory=False)
+ visibility = wsme.wsattr(types.text, mandatory=False)
+ protected = wsme.wsattr(bool, mandatory=False)
+ owner = wsme.wsattr(types.text, mandatory=False)
+
+ #Not using datetime since time format has to be
+ #in glance.openstack.common.timeutils.isotime() format
+ created_at = wsme.wsattr(types.text, mandatory=False)
+ updated_at = wsme.wsattr(types.text, mandatory=False)
+
+ #Contained fields
+ resource_type_associations = wsme.wsattr([ResourceTypeAssociation],
+ mandatory=False)
+ properties = wsme.wsattr({types.text: PropertyType}, mandatory=False)
+ objects = wsme.wsattr([MetadefObject], mandatory=False)
+
+ #Generated fields
+ self = wsme.wsattr(types.text, mandatory=False)
+ schema = wsme.wsattr(types.text, mandatory=False)
+
+ def __init__(cls, **kwargs):
+ super(Namespace, cls).__init__(**kwargs)
+
+ @staticmethod
+ def to_model_properties(db_property_types):
+ property_types = {}
+ for db_property_type in db_property_types:
+ # Convert the persisted json schema to a dict of PropertyTypes
+ json_props = json.loads(db_property_type.schema)
+ property_type = fromjson(PropertyType, json_props)
+
+ property_type_name = db_property_type.name
+ property_types[property_type_name] = property_type
+
+ return property_types
+
+
+class Namespaces(types.Base, WSMEModelTransformer):
+
+ namespaces = wsme.wsattr([Namespace], mandatory=False)
+
+ #Pagination
+ next = wsme.wsattr(types.text, mandatory=False)
+ schema = wsme.wsattr(types.text, mandatory=True)
+ first = wsme.wsattr(types.text, mandatory=True)
+
+ def __init__(self, **kwargs):
+ super(Namespaces, self).__init__(**kwargs)
diff --git a/glance/api/v2/model/metadef_object.py b/glance/api/v2/model/metadef_object.py
new file mode 100644
index 0000000000..ac2c2833cb
--- /dev/null
+++ b/glance/api/v2/model/metadef_object.py
@@ -0,0 +1,49 @@
+# Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
+#
+# 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 wsme
+from wsme import types
+
+from glance.api.v2.model.metadef_property_type import PropertyType
+from glance.common.wsme_utils import WSMEModelTransformer
+
+
+class MetadefObject(types.Base, WSMEModelTransformer):
+
+ name = wsme.wsattr(types.text, mandatory=True)
+ required = wsme.wsattr([types.text], mandatory=False)
+ description = wsme.wsattr(types.text, mandatory=False)
+ properties = wsme.wsattr({types.text: PropertyType}, mandatory=False)
+
+ #Not using datetime since time format has to be
+ #in glance.openstack.common.timeutils.isotime() format
+ created_at = wsme.wsattr(types.text, mandatory=False)
+ updated_at = wsme.wsattr(types.text, mandatory=False)
+
+ #Generated fields
+ self = wsme.wsattr(types.text, mandatory=False)
+ schema = wsme.wsattr(types.text, mandatory=False)
+
+ def __init__(cls, **kwargs):
+ super(MetadefObject, cls).__init__(**kwargs)
+
+
+class MetadefObjects(types.Base, WSMEModelTransformer):
+
+ objects = wsme.wsattr([MetadefObject], mandatory=False)
+ schema = wsme.wsattr(types.text, mandatory=True)
+
+ def __init__(self, **kwargs):
+ super(MetadefObjects, self).__init__(**kwargs)
diff --git a/glance/api/v2/model/metadef_property_item_type.py b/glance/api/v2/model/metadef_property_item_type.py
new file mode 100644
index 0000000000..228147a194
--- /dev/null
+++ b/glance/api/v2/model/metadef_property_item_type.py
@@ -0,0 +1,27 @@
+# Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
+#
+# 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 wsme
+from wsme import types
+
+
+class ItemType(types.Base):
+ type = wsme.wsattr(types.text, mandatory=True)
+ enum = wsme.wsattr([types.text], mandatory=False)
+
+ _wsme_attr_order = ('type', 'enum')
+
+ def __init__(self, **kwargs):
+ super(ItemType, self).__init__(**kwargs)
diff --git a/glance/api/v2/model/metadef_property_type.py b/glance/api/v2/model/metadef_property_type.py
new file mode 100644
index 0000000000..a1224eb7a5
--- /dev/null
+++ b/glance/api/v2/model/metadef_property_type.py
@@ -0,0 +1,59 @@
+# Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
+#
+# 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 wsme
+from wsme import types
+
+from glance.api.v2.model.metadef_property_item_type import ItemType
+from glance.common.wsme_utils import WSMEModelTransformer
+
+
+class PropertyType(types.Base, WSMEModelTransformer):
+ #When used in collection of PropertyTypes, name is a dictionary key
+ #and not included as separate field.
+ name = wsme.wsattr(types.text, mandatory=False)
+
+ type = wsme.wsattr(types.text, mandatory=True)
+ title = wsme.wsattr(types.text, mandatory=True)
+ description = wsme.wsattr(types.text, mandatory=False)
+ default = wsme.wsattr(types.bytes, mandatory=False)
+
+ # fields for type = string
+ minimum = wsme.wsattr(int, mandatory=False)
+ maximum = wsme.wsattr(int, mandatory=False)
+ enum = wsme.wsattr([types.text], mandatory=False)
+ pattern = wsme.wsattr(types.text, mandatory=False)
+
+ # fields for type = integer, number
+ minLength = wsme.wsattr(int, mandatory=False)
+ maxLength = wsme.wsattr(int, mandatory=False)
+ confidential = wsme.wsattr(bool, mandatory=False)
+
+ # fields for type = array
+ items = wsme.wsattr(ItemType, mandatory=False)
+ uniqueItems = wsme.wsattr(bool, mandatory=False)
+ minItems = wsme.wsattr(int, mandatory=False)
+ maxItems = wsme.wsattr(int, mandatory=False)
+ additionalItems = wsme.wsattr(bool, mandatory=False)
+
+ def __init__(self, **kwargs):
+ super(PropertyType, self).__init__(**kwargs)
+
+
+class PropertyTypes(types.Base, WSMEModelTransformer):
+ properties = wsme.wsattr({types.text: PropertyType}, mandatory=False)
+
+ def __init__(self, **kwargs):
+ super(PropertyTypes, self).__init__(**kwargs)
diff --git a/glance/api/v2/model/metadef_resource_type.py b/glance/api/v2/model/metadef_resource_type.py
new file mode 100644
index 0000000000..809a1527e5
--- /dev/null
+++ b/glance/api/v2/model/metadef_resource_type.py
@@ -0,0 +1,62 @@
+# Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
+#
+# 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 wsme
+from wsme import types
+
+from glance.common.wsme_utils import WSMEModelTransformer
+
+
+class ResourceTypeAssociation(types.Base, WSMEModelTransformer):
+ name = wsme.wsattr(types.text, mandatory=True)
+ prefix = wsme.wsattr(types.text, mandatory=False)
+ properties_target = wsme.wsattr(types.text, mandatory=False)
+
+ #Not using datetime since time format has to be
+ #in glance.openstack.common.timeutils.isotime() format
+ created_at = wsme.wsattr(types.text, mandatory=False)
+ updated_at = wsme.wsattr(types.text, mandatory=False)
+
+ def __init__(self, **kwargs):
+ super(ResourceTypeAssociation, self).__init__(**kwargs)
+
+
+class ResourceTypeAssociations(types.Base, WSMEModelTransformer):
+
+ resource_type_associations = wsme.wsattr([ResourceTypeAssociation],
+ mandatory=False)
+
+ def __init__(self, **kwargs):
+ super(ResourceTypeAssociations, self).__init__(**kwargs)
+
+
+class ResourceType(types.Base, WSMEModelTransformer):
+ name = wsme.wsattr(types.text, mandatory=True)
+
+ #Not using datetime since time format has to be
+ #in glance.openstack.common.timeutils.isotime() format
+ created_at = wsme.wsattr(types.text, mandatory=False)
+ updated_at = wsme.wsattr(types.text, mandatory=False)
+
+ def __init__(self, **kwargs):
+ super(ResourceType, self).__init__(**kwargs)
+
+
+class ResourceTypes(types.Base, WSMEModelTransformer):
+
+ resource_types = wsme.wsattr([ResourceType], mandatory=False)
+
+ def __init__(self, **kwargs):
+ super(ResourceTypes, self).__init__(**kwargs)
diff --git a/glance/api/v2/router.py b/glance/api/v2/router.py
index 3bfe3ed8bc..743537d11a 100644
--- a/glance/api/v2/router.py
+++ b/glance/api/v2/router.py
@@ -17,6 +17,10 @@ from glance.api.v2 import image_data
from glance.api.v2 import image_members
from glance.api.v2 import image_tags
from glance.api.v2 import images
+from glance.api.v2 import metadef_namespaces
+from glance.api.v2 import metadef_objects
+from glance.api.v2 import metadef_properties
+from glance.api.v2 import metadef_resource_types
from glance.api.v2 import schemas
from glance.api.v2 import tasks
from glance.common import wsgi
@@ -94,6 +98,256 @@ class API(wsgi.Router):
conditions={'method': ['POST', 'PUT', 'DELETE',
'PATCH', 'HEAD']})
+ mapper.connect('/schemas/metadefs/namespace',
+ controller=schemas_resource,
+ action='metadef_namespace',
+ conditions={'method': ['GET']})
+ mapper.connect('/schemas/metadefs/namespace',
+ controller=reject_method_resource,
+ action='reject',
+ allowed_methods='GET',
+ conditions={'method': ['POST', 'PUT', 'DELETE',
+ 'PATCH', 'HEAD']})
+
+ mapper.connect('/schemas/metadefs/namespaces',
+ controller=schemas_resource,
+ action='metadef_namespaces',
+ conditions={'method': ['GET']})
+ mapper.connect('/schemas/metadefs/namespaces',
+ controller=reject_method_resource,
+ action='reject',
+ allowed_methods='GET',
+ conditions={'method': ['POST', 'PUT', 'DELETE',
+ 'PATCH', 'HEAD']})
+
+ mapper.connect('/schemas/metadefs/resource_type',
+ controller=schemas_resource,
+ action='metadef_resource_type',
+ conditions={'method': ['GET']})
+ mapper.connect('/schemas/metadefs/resource_type',
+ controller=reject_method_resource,
+ action='reject',
+ allowed_methods='GET',
+ conditions={'method': ['POST', 'PUT', 'DELETE',
+ 'PATCH', 'HEAD']})
+
+ mapper.connect('/schemas/metadefs/resource_types',
+ controller=schemas_resource,
+ action='metadef_resource_types',
+ conditions={'method': ['GET']})
+ mapper.connect('/schemas/metadefs/resource_types',
+ controller=reject_method_resource,
+ action='reject',
+ allowed_methods='GET',
+ conditions={'method': ['POST', 'PUT', 'DELETE',
+ 'PATCH', 'HEAD']})
+
+ mapper.connect('/schemas/metadefs/property',
+ controller=schemas_resource,
+ action='metadef_property',
+ conditions={'method': ['GET']})
+ mapper.connect('/schemas/metadefs/property',
+ controller=reject_method_resource,
+ action='reject',
+ allowed_methods='GET',
+ conditions={'method': ['POST', 'PUT', 'DELETE',
+ 'PATCH', 'HEAD']})
+
+ mapper.connect('/schemas/metadefs/properties',
+ controller=schemas_resource,
+ action='metadef_properties',
+ conditions={'method': ['GET']})
+ mapper.connect('/schemas/metadefs/properties',
+ controller=reject_method_resource,
+ action='reject',
+ allowed_methods='GET',
+ conditions={'method': ['POST', 'PUT', 'DELETE',
+ 'PATCH', 'HEAD']})
+
+ mapper.connect('/schemas/metadefs/object',
+ controller=schemas_resource,
+ action='metadef_object',
+ conditions={'method': ['GET']})
+ mapper.connect('/schemas/metadefs/object',
+ controller=reject_method_resource,
+ action='reject',
+ allowed_methods='GET',
+ conditions={'method': ['POST', 'PUT', 'DELETE',
+ 'PATCH', 'HEAD']})
+
+ mapper.connect('/schemas/metadefs/objects',
+ controller=schemas_resource,
+ action='metadef_objects',
+ conditions={'method': ['GET']})
+ mapper.connect('/schemas/metadefs/objects',
+ controller=reject_method_resource,
+ action='reject',
+ allowed_methods='GET',
+ conditions={'method': ['POST', 'PUT', 'DELETE',
+ 'PATCH', 'HEAD']})
+
+ # Metadef resource types
+ metadef_resource_types_resource = (
+ metadef_resource_types.create_resource())
+
+ mapper.connect('/metadefs/resource_types',
+ controller=metadef_resource_types_resource,
+ action='index',
+ conditions={'method': ['GET']})
+ mapper.connect('/metadefs/resource_types',
+ controller=reject_method_resource,
+ action='reject',
+ allowed_methods='GET',
+ conditions={'method': ['POST', 'PUT', 'DELETE',
+ 'PATCH', 'HEAD']})
+
+ mapper.connect('/metadefs/namespaces/{namespace}/resource_types',
+ controller=metadef_resource_types_resource,
+ action='show',
+ conditions={'method': ['GET']})
+ mapper.connect('/metadefs/namespaces/{namespace}/resource_types',
+ controller=metadef_resource_types_resource,
+ action='create',
+ conditions={'method': ['POST']})
+ mapper.connect('/metadefs/namespaces/{namespace}/resource_types',
+ controller=reject_method_resource,
+ action='reject',
+ allowed_methods='GET, POST',
+ conditions={'method': ['PUT', 'DELETE',
+ 'PATCH', 'HEAD']})
+
+ mapper.connect('/metadefs/namespaces/{namespace}/resource_types/'
+ '{resource_type}',
+ controller=metadef_resource_types_resource,
+ action='delete',
+ conditions={'method': ['DELETE']})
+ mapper.connect('/metadefs/namespaces/{namespace}/resource_types/'
+ '{resource_type}',
+ controller=reject_method_resource,
+ action='reject',
+ allowed_methods='DELETE',
+ conditions={'method': ['GET', 'POST', 'PUT',
+ 'PATCH', 'HEAD']})
+
+ # Metadef Namespaces
+ metadef_namespace_resource = metadef_namespaces.create_resource()
+ mapper.connect('/metadefs/namespaces',
+ controller=metadef_namespace_resource,
+ action='index',
+ conditions={'method': ['GET']})
+ mapper.connect('/metadefs/namespaces',
+ controller=metadef_namespace_resource,
+ action='create',
+ conditions={'method': ['POST']})
+ mapper.connect('/metadefs/namespaces',
+ controller=reject_method_resource,
+ action='reject',
+ allowed_methods='GET, POST',
+ conditions={'method': ['PUT', 'DELETE',
+ 'PATCH', 'HEAD']})
+
+ mapper.connect('/metadefs/namespaces/{namespace}',
+ controller=metadef_namespace_resource,
+ action='show',
+ conditions={'method': ['GET']})
+ mapper.connect('/metadefs/namespaces/{namespace}',
+ controller=metadef_namespace_resource,
+ action='update',
+ conditions={'method': ['PUT']})
+ mapper.connect('/metadefs/namespaces/{namespace}',
+ controller=metadef_namespace_resource,
+ action='delete',
+ conditions={'method': ['DELETE']})
+ mapper.connect('/metadefs/namespaces/{namespace}',
+ controller=reject_method_resource,
+ action='reject',
+ allowed_methods='GET, PUT, DELETE',
+ conditions={'method': ['POST', 'PATCH', 'HEAD']})
+
+ # Metadef namespace properties
+ metadef_properties_resource = metadef_properties.create_resource()
+ mapper.connect('/metadefs/namespaces/{namespace}/properties',
+ controller=metadef_properties_resource,
+ action='index',
+ conditions={'method': ['GET']})
+ mapper.connect('/metadefs/namespaces/{namespace}/properties',
+ controller=metadef_properties_resource,
+ action='create',
+ conditions={'method': ['POST']})
+ mapper.connect('/metadefs/namespaces/{namespace}/properties',
+ controller=metadef_namespace_resource,
+ action='delete_properties',
+ conditions={'method': ['DELETE']})
+ mapper.connect('/metadefs/namespaces/{namespace}/properties',
+ controller=reject_method_resource,
+ action='reject',
+ allowed_methods='GET, POST, DELETE',
+ conditions={'method': ['PUT', 'PATCH', 'HEAD']})
+
+ mapper.connect('/metadefs/namespaces/{namespace}/properties/{'
+ 'property_name}',
+ controller=metadef_properties_resource,
+ action='show',
+ conditions={'method': ['GET']})
+ mapper.connect('/metadefs/namespaces/{namespace}/properties/{'
+ 'property_name}',
+ controller=metadef_properties_resource,
+ action='update',
+ conditions={'method': ['PUT']})
+ mapper.connect('/metadefs/namespaces/{namespace}/properties/{'
+ 'property_name}',
+ controller=metadef_properties_resource,
+ action='delete',
+ conditions={'method': ['DELETE']})
+ mapper.connect('/metadefs/namespaces/{namespace}/properties/{'
+ 'property_name}',
+ controller=reject_method_resource,
+ action='reject',
+ allowed_methods='GET, PUT, DELETE',
+ conditions={'method': ['POST', 'PATCH', 'HEAD']})
+
+ # Metadef objects
+ metadef_objects_resource = metadef_objects.create_resource()
+ mapper.connect('/metadefs/namespaces/{namespace}/objects',
+ controller=metadef_objects_resource,
+ action='index',
+ conditions={'method': ['GET']})
+ mapper.connect('/metadefs/namespaces/{namespace}/objects',
+ controller=metadef_objects_resource,
+ action='create',
+ conditions={'method': ['POST']})
+ mapper.connect('/metadefs/namespaces/{namespace}/objects',
+ controller=metadef_namespace_resource,
+ action='delete_objects',
+ conditions={'method': ['DELETE']})
+ mapper.connect('/metadefs/namespaces/{namespace}/objects',
+ controller=reject_method_resource,
+ action='reject',
+ allowed_methods='GET, POST, DELETE',
+ conditions={'method': ['PUT', 'PATCH', 'HEAD']})
+
+ mapper.connect('/metadefs/namespaces/{namespace}/objects/{'
+ 'object_name}',
+ controller=metadef_objects_resource,
+ action='show',
+ conditions={'method': ['GET']})
+ mapper.connect('/metadefs/namespaces/{namespace}/objects/{'
+ 'object_name}',
+ controller=metadef_objects_resource,
+ action='update',
+ conditions={'method': ['PUT']})
+ mapper.connect('/metadefs/namespaces/{namespace}/objects/{'
+ 'object_name}',
+ controller=metadef_objects_resource,
+ action='delete',
+ conditions={'method': ['DELETE']})
+ mapper.connect('/metadefs/namespaces/{namespace}/objects/{'
+ 'object_name}',
+ controller=reject_method_resource,
+ action='reject',
+ allowed_methods='GET, PUT, DELETE',
+ conditions={'method': ['POST', 'PATCH', 'HEAD']})
+
images_resource = images.create_resource(custom_image_properties)
mapper.connect('/images',
controller=images_resource,
diff --git a/glance/api/v2/schemas.py b/glance/api/v2/schemas.py
index ce771fe6a3..e02d4da4f6 100644
--- a/glance/api/v2/schemas.py
+++ b/glance/api/v2/schemas.py
@@ -15,6 +15,10 @@
from glance.api.v2 import image_members
from glance.api.v2 import images
+from glance.api.v2 import metadef_namespaces
+from glance.api.v2 import metadef_objects
+from glance.api.v2 import metadef_properties
+from glance.api.v2 import metadef_resource_types
from glance.api.v2 import tasks
from glance.common import wsgi
@@ -29,6 +33,23 @@ class Controller(object):
self.task_schema = tasks.get_task_schema()
self.task_collection_schema = tasks.get_collection_schema()
+ #Metadef schemas
+ self.metadef_namespace_schema = metadef_namespaces.get_schema()
+ self.metadef_namespace_collection_schema = \
+ metadef_namespaces.get_collection_schema()
+
+ self.metadef_resource_type_schema = metadef_resource_types.get_schema()
+ self.metadef_resource_type_collection_schema = \
+ metadef_resource_types.get_collection_schema()
+
+ self.metadef_property_schema = metadef_properties.get_schema()
+ self.metadef_property_collection_schema = \
+ metadef_properties.get_collection_schema()
+
+ self.metadef_object_schema = metadef_objects.get_schema()
+ self.metadef_object_collection_schema = \
+ metadef_objects.get_collection_schema()
+
def image(self, req):
return self.image_schema.raw()
@@ -47,6 +68,30 @@ class Controller(object):
def tasks(self, req):
return self.task_collection_schema.minimal()
+ def metadef_namespace(self, req):
+ return self.metadef_namespace_schema.raw()
+
+ def metadef_namespaces(self, req):
+ return self.metadef_namespace_collection_schema.raw()
+
+ def metadef_resource_type(self, req):
+ return self.metadef_resource_type_schema.raw()
+
+ def metadef_resource_types(self, req):
+ return self.metadef_resource_type_collection_schema.raw()
+
+ def metadef_property(self, req):
+ return self.metadef_property_schema.raw()
+
+ def metadef_properties(self, req):
+ return self.metadef_property_collection_schema.raw()
+
+ def metadef_object(self, req):
+ return self.metadef_object_schema.raw()
+
+ def metadef_objects(self, req):
+ return self.metadef_object_collection_schema.raw()
+
def create_resource(custom_image_properties=None):
controller = Controller(custom_image_properties)
diff --git a/glance/common/wsme_utils.py b/glance/common/wsme_utils.py
new file mode 100644
index 0000000000..cd79fc59b3
--- /dev/null
+++ b/glance/common/wsme_utils.py
@@ -0,0 +1,71 @@
+# Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
+#
+# 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.
+
+from datetime import datetime
+
+from wsme import types as wsme_types
+
+from glance.openstack.common import timeutils
+
+
+class WSMEModelTransformer():
+
+ def to_dict(self):
+ # Return the wsme_attributes names:values as a dict
+ my_dict = {}
+ for attribute in self._wsme_attributes:
+ value = getattr(self, attribute.name)
+ if value is not wsme_types.Unset:
+ my_dict.update({attribute.name: value})
+ return my_dict
+
+ @classmethod
+ def to_wsme_model(model, db_entity, self_link=None, schema=None):
+ # Return the wsme_attributes names:values as a dict
+ names = []
+ for attribute in model._wsme_attributes:
+ names.append(attribute.name)
+
+ values = {}
+ for name in names:
+ value = getattr(db_entity, name, None)
+ if value is not None:
+ if type(value) == datetime:
+ iso_datetime_value = timeutils.isotime(value)
+ values.update({name: iso_datetime_value})
+ else:
+ values.update({name: value})
+
+ if schema:
+ values['schema'] = schema
+
+ model_object = model(**values)
+
+ # 'self' kwarg is used in wsme.types.Base.__init__(self, ..) and
+ # conflicts during initialization. self_link is a proxy field to self.
+ if self_link:
+ model_object.self = self_link
+
+ return model_object
+
+ @classmethod
+ def get_mandatory_attrs(cls):
+ return [attr.name for attr in cls._wsme_attributes if attr.mandatory]
+
+
+def _get_value(obj):
+ if obj is not wsme_types.Unset:
+ return obj
+ else:
+ return None
diff --git a/glance/db/__init__.py b/glance/db/__init__.py
index 0a7e4ecd1f..f6e402b5ce 100644
--- a/glance/db/__init__.py
+++ b/glance/db/__init__.py
@@ -17,14 +17,17 @@
# under the License.
from oslo.config import cfg
+from wsme.rest.json import fromjson
+from wsme.rest.json import tojson
+from glance.api.v2.model.metadef_property_type import PropertyType
from glance.common import crypt
from glance.common import exception
from glance.common import location_strategy
import glance.domain
import glance.domain.proxy
from glance.openstack.common import importutils
-
+from glance.openstack.common import jsonutils as json
CONF = cfg.CONF
CONF.import_opt('image_size_cap', 'glance.common.config')
@@ -380,3 +383,365 @@ class TaskRepo(object):
raise exception.NotFound(msg)
task.updated_at = updated_values['updated_at']
task.deleted_at = updated_values['deleted_at']
+
+
+class MetadefNamespaceRepo(object):
+
+ def __init__(self, context, db_api):
+ self.context = context
+ self.db_api = db_api
+
+ def _format_namespace_from_db(self, namespace_obj):
+ return glance.domain.MetadefNamespace(
+ namespace_id=namespace_obj['id'],
+ namespace=namespace_obj['namespace'],
+ display_name=namespace_obj['display_name'],
+ description=namespace_obj['description'],
+ owner=namespace_obj['owner'],
+ visibility=namespace_obj['visibility'],
+ protected=namespace_obj['protected'],
+ created_at=namespace_obj['created_at'],
+ updated_at=namespace_obj['updated_at']
+ )
+
+ def _format_namespace_to_db(self, namespace_obj):
+ namespace = {
+ 'namespace': namespace_obj.namespace,
+ 'display_name': namespace_obj.display_name,
+ 'description': namespace_obj.description,
+ 'visibility': namespace_obj.visibility,
+ 'protected': namespace_obj.protected,
+ 'owner': namespace_obj.owner
+ }
+ return namespace
+
+ def add(self, namespace):
+ self.db_api.metadef_namespace_create(
+ self.context,
+ self._format_namespace_to_db(namespace)
+ )
+
+ def get(self, namespace):
+ try:
+ db_api_namespace = self.db_api.metadef_namespace_get(
+ self.context, namespace)
+ except (exception.NotFound, exception.Forbidden):
+ msg = _('Could not find namespace %s') % namespace
+ raise exception.NotFound(msg)
+ return self._format_namespace_from_db(db_api_namespace)
+
+ def list(self, marker=None, limit=None, sort_key='created_at',
+ sort_dir='desc', filters=None):
+ db_namespaces = self.db_api.metadef_namespace_get_all(
+ self.context,
+ marker=marker,
+ limit=limit,
+ sort_key=sort_key,
+ sort_dir=sort_dir,
+ filters=filters
+ )
+ return [self._format_namespace_from_db(namespace_obj)
+ for namespace_obj in db_namespaces]
+
+ def remove(self, namespace):
+ try:
+ self.db_api.metadef_namespace_delete(self.context,
+ namespace.namespace)
+ except (exception.NotFound, exception.Forbidden):
+ msg = _("The specified namespace %s could not be found")
+ raise exception.NotFound(msg % namespace.namespace)
+
+ def remove_objects(self, namespace):
+ try:
+ self.db_api.metadef_object_delete_namespace_content(
+ self.context,
+ namespace.namespace
+ )
+ except (exception.NotFound, exception.Forbidden):
+ msg = _("The specified namespace %s could not be found")
+ raise exception.NotFound(msg % namespace.namespace)
+
+ def remove_properties(self, namespace):
+ try:
+ self.db_api.metadef_property_delete_namespace_content(
+ self.context,
+ namespace.namespace
+ )
+ except (exception.NotFound, exception.Forbidden):
+ msg = _("The specified namespace %s could not be found")
+ raise exception.NotFound(msg % namespace.namespace)
+
+ def object_count(self, namespace_name):
+ return self.db_api.metadef_object_count(
+ self.context,
+ namespace_name
+ )
+
+ def property_count(self, namespace_name):
+ return self.db_api.metadef_property_count(
+ self.context,
+ namespace_name
+ )
+
+ def save(self, namespace):
+ try:
+ self.db_api.metadef_namespace_update(
+ self.context, namespace.namespace_id,
+ self._format_namespace_to_db(namespace)
+ )
+ except exception.NotFound as e:
+ raise exception.NotFound(explanation=e.msg)
+ return namespace
+
+
+class MetadefObjectRepo(object):
+
+ def __init__(self, context, db_api):
+ self.context = context
+ self.db_api = db_api
+ self.meta_namespace_repo = MetadefNamespaceRepo(context, db_api)
+
+ def _format_metadef_object_from_db(self, metadata_object,
+ namespace_entity):
+ required_str = metadata_object['required']
+ required_list = required_str.split(",") if required_str else []
+
+ # Convert the persisted json schema to a dict of PropertyTypes
+ property_types = {}
+ json_props = json.loads(metadata_object['schema'])
+ for id in json_props:
+ property_types[id] = fromjson(PropertyType, json_props[id])
+
+ return glance.domain.MetadefObject(
+ namespace=namespace_entity,
+ object_id=metadata_object['id'],
+ name=metadata_object['name'],
+ required=required_list,
+ description=metadata_object['description'],
+ properties=property_types,
+ created_at=metadata_object['created_at'],
+ updated_at=metadata_object['updated_at']
+ )
+
+ def _format_metadef_object_to_db(self, metadata_object):
+
+ required_str = (",".join(metadata_object.required) if
+ metadata_object.required else None)
+
+ # Convert the model PropertyTypes dict to a JSON string
+ properties = metadata_object.properties
+ db_schema = {}
+ if properties:
+ for k, v in properties.items():
+ json_data = tojson(PropertyType, v)
+ db_schema[k] = json_data
+ property_schema = json.dumps(db_schema)
+
+ db_metadata_object = {
+ 'name': metadata_object.name,
+ 'required': required_str,
+ 'description': metadata_object.description,
+ 'schema': property_schema
+ }
+ return db_metadata_object
+
+ def add(self, metadata_object):
+ self.db_api.metadef_object_create(
+ self.context,
+ metadata_object.namespace,
+ self._format_metadef_object_to_db(metadata_object)
+ )
+
+ def get(self, namespace, object_name):
+ try:
+ namespace_entity = self.meta_namespace_repo.get(namespace)
+ db_metadata_object = self.db_api.metadef_object_get(
+ self.context,
+ namespace,
+ object_name)
+ except (exception.NotFound, exception.Forbidden):
+ msg = _('Could not find metadata object %s') % object_name
+ raise exception.NotFound(msg)
+ return self._format_metadef_object_from_db(db_metadata_object,
+ namespace_entity)
+
+ def list(self, marker=None, limit=None, sort_key='created_at',
+ sort_dir='desc', filters=None):
+ namespace = filters['namespace']
+ namespace_entity = self.meta_namespace_repo.get(namespace)
+ db_metadata_objects = self.db_api.metadef_object_get_all(
+ self.context, namespace)
+ return [self._format_metadef_object_from_db(metadata_object,
+ namespace_entity)
+ for metadata_object in db_metadata_objects]
+
+ def remove(self, metadata_object):
+ try:
+ self.db_api.metadef_object_delete(
+ self.context,
+ metadata_object.namespace.namespace,
+ metadata_object.name
+ )
+ except (exception.NotFound, exception.Forbidden):
+ msg = _("The specified metadata object %s could not be found")
+ raise exception.NotFound(msg % metadata_object.name)
+
+ def save(self, metadata_object):
+ try:
+ self.db_api.metadef_object_update(
+ self.context, metadata_object.namespace.namespace,
+ metadata_object.object_id,
+ self._format_metadef_object_to_db(metadata_object))
+ except exception.NotFound as e:
+ raise exception.NotFound(explanation=e.msg)
+ return metadata_object
+
+
+class MetadefResourceTypeRepo(object):
+
+ def __init__(self, context, db_api):
+ self.context = context
+ self.db_api = db_api
+ self.meta_namespace_repo = MetadefNamespaceRepo(context, db_api)
+
+ def _format_resource_type_from_db(self, resource_type, namespace):
+ return glance.domain.MetadefResourceType(
+ namespace=namespace,
+ name=resource_type['name'],
+ prefix=resource_type['prefix'],
+ properties_target=resource_type['properties_target'],
+ created_at=resource_type['created_at'],
+ updated_at=resource_type['updated_at']
+ )
+
+ def _format_resource_type_to_db(self, resource_type):
+ db_resource_type = {
+ 'name': resource_type.name,
+ 'prefix': resource_type.prefix,
+ 'properties_target': resource_type.properties_target
+ }
+ return db_resource_type
+
+ def add(self, resource_type):
+ self.db_api.metadef_resource_type_association_create(
+ self.context, resource_type.namespace,
+ self._format_resource_type_to_db(resource_type)
+ )
+
+ def list(self, filters=None):
+ namespace = filters['namespace']
+ if namespace:
+ namespace_entity = self.meta_namespace_repo.get(namespace)
+ db_resource_types = (
+ self.db_api.
+ metadef_resource_type_association_get_all_by_namespace(
+ self.context,
+ namespace
+ )
+ )
+ return [self._format_resource_type_from_db(resource_type,
+ namespace_entity)
+ for resource_type in db_resource_types]
+ else:
+ db_resource_types = (
+ self.db_api.
+ metadef_resource_type_get_all(self.context)
+ )
+ return [glance.domain.MetadefResourceType(
+ namespace=None,
+ name=resource_type['name'],
+ prefix=None,
+ properties_target=None,
+ created_at=resource_type['created_at'],
+ updated_at=resource_type['updated_at']
+ ) for resource_type in db_resource_types]
+
+ def remove(self, resource_type):
+ try:
+ self.db_api.metadef_resource_type_association_delete(
+ self.context, resource_type.namespace.namespace,
+ resource_type.name)
+
+ except (exception.NotFound, exception.Forbidden):
+ msg = _("The specified resource type %s could not be found ")
+ raise exception.NotFound(msg % resource_type.name)
+
+
+class MetadefPropertyRepo(object):
+
+ def __init__(self, context, db_api):
+ self.context = context
+ self.db_api = db_api
+ self.meta_namespace_repo = MetadefNamespaceRepo(context, db_api)
+
+ def _format_metadef_property_from_db(
+ self,
+ property,
+ namespace_entity):
+
+ return glance.domain.MetadefProperty(
+ namespace=namespace_entity,
+ property_id=property['id'],
+ name=property['name'],
+ schema=property['schema']
+ )
+
+ def _format_metadef_property_to_db(self, property):
+
+ db_metadata_object = {
+ 'name': property.name,
+ 'schema': property.schema
+ }
+ return db_metadata_object
+
+ def add(self, property):
+ self.db_api.metadef_property_create(
+ self.context,
+ property.namespace,
+ self._format_metadef_property_to_db(property)
+ )
+
+ def get(self, namespace, property_name):
+ try:
+ namespace_entity = self.meta_namespace_repo.get(namespace)
+ db_property_type = self.db_api.metadef_property_get(
+ self.context,
+ namespace,
+ property_name
+ )
+ except (exception.NotFound, exception.Forbidden):
+ msg = _('Could not find property %s') % property_name
+ raise exception.NotFound(msg)
+ return self._format_metadef_property_from_db(
+ db_property_type, namespace_entity)
+
+ def list(self, marker=None, limit=None, sort_key='created_at',
+ sort_dir='desc', filters=None):
+ namespace = filters['namespace']
+ namespace_entity = self.meta_namespace_repo.get(namespace)
+
+ db_properties = self.db_api.metadef_property_get_all(
+ self.context, namespace)
+ return (
+ [self._format_metadef_property_from_db(
+ property, namespace_entity) for property in db_properties]
+ )
+
+ def remove(self, property):
+ try:
+ self.db_api.metadef_property_delete(
+ self.context, property.namespace.namespace, property.name)
+ except (exception.NotFound, exception.Forbidden):
+ msg = _("The specified property %s could not be found")
+ raise exception.NotFound(msg % property.name)
+
+ def save(self, property):
+ try:
+ self.db_api.metadef_property_update(
+ self.context, property.namespace.namespace,
+ property.property_id,
+ self._format_metadef_property_to_db(property)
+ )
+ except exception.NotFound as e:
+ raise exception.NotFound(explanation=e.msg)
+ return property
diff --git a/glance/domain/__init__.py b/glance/domain/__init__.py
index e30b4212ce..685fb9b69e 100644
--- a/glance/domain/__init__.py
+++ b/glance/domain/__init__.py
@@ -443,3 +443,136 @@ class TaskFactory(object):
kwargs.get('result'),
task_time_to_live
)
+
+
+class MetadefNamespace(object):
+
+ def __init__(self, namespace_id, namespace, display_name, description,
+ owner, visibility, protected, created_at, updated_at):
+ self.namespace_id = namespace_id
+ self.namespace = namespace
+ self.display_name = display_name
+ self.description = description
+ self.owner = owner
+ self.visibility = visibility or "private"
+ self.protected = protected or False
+ self.created_at = created_at
+ self.updated_at = updated_at
+
+ def delete(self):
+ if self.protected:
+ raise exception.ProtectedMetadefNamespaceDelete(
+ namespace=self.namespace)
+
+
+class MetadefNamespaceFactory(object):
+
+ def new_namespace(self, namespace, owner, **kwargs):
+ namespace_id = str(uuid.uuid4())
+ created_at = timeutils.utcnow()
+ updated_at = created_at
+ return MetadefNamespace(
+ namespace_id,
+ namespace,
+ kwargs.get('display_name'),
+ kwargs.get('description'),
+ owner,
+ kwargs.get('visibility'),
+ kwargs.get('protected'),
+ created_at,
+ updated_at
+ )
+
+
+class MetadefObject(object):
+
+ def __init__(self, namespace, object_id, name, created_at, updated_at,
+ required, description, properties):
+ self.namespace = namespace
+ self.object_id = object_id
+ self.name = name
+ self.created_at = created_at
+ self.updated_at = updated_at
+ self.required = required
+ self.description = description
+ self.properties = properties
+
+ def delete(self):
+ if self.namespace.protected:
+ raise exception.ProtectedMetadefObjectDelete(object_name=self.name)
+
+
+class MetadefObjectFactory(object):
+
+ def new_object(self, namespace, name, **kwargs):
+ object_id = str(uuid.uuid4())
+ created_at = timeutils.utcnow()
+ updated_at = created_at
+ return MetadefObject(
+ namespace,
+ object_id,
+ name,
+ created_at,
+ updated_at,
+ kwargs.get('required'),
+ kwargs.get('description'),
+ kwargs.get('properties')
+ )
+
+
+class MetadefResourceType(object):
+
+ def __init__(self, namespace, name, prefix, properties_target,
+ created_at, updated_at):
+ self.namespace = namespace
+ self.name = name
+ self.prefix = prefix
+ self.properties_target = properties_target
+ self.created_at = created_at
+ self.updated_at = updated_at
+
+ def delete(self):
+ if self.namespace.protected:
+ raise exception.ProtectedMetadefResourceTypeAssociationDelete(
+ resource_type=self.name)
+
+
+class MetadefResourceTypeFactory(object):
+
+ def new_resource_type(self, namespace, name, **kwargs):
+ created_at = timeutils.utcnow()
+ updated_at = created_at
+ return MetadefResourceType(
+ namespace,
+ name,
+ kwargs.get('prefix'),
+ kwargs.get('properties_target'),
+ created_at,
+ updated_at
+ )
+
+
+class MetadefProperty(object):
+
+ def __init__(self, namespace, property_id, name, schema):
+ self.namespace = namespace
+ self.property_id = property_id
+ self.name = name
+ self.schema = schema
+
+ def delete(self):
+ if self.namespace.protected:
+ raise exception.ProtectedMetadefNamespacePropDelete(
+ property_name=self.name)
+
+
+class MetadefPropertyFactory(object):
+
+ def new_namespace_property(self, namespace, name, schema, **kwargs):
+ property_id = str(uuid.uuid4())
+ return MetadefProperty(
+ namespace,
+ property_id,
+ name,
+ schema
+ )
diff --git a/glance/domain/proxy.py b/glance/domain/proxy.py
index 7b54ab8321..e59baf5b84 100644
--- a/glance/domain/proxy.py
+++ b/glance/domain/proxy.py
@@ -219,3 +219,245 @@ class TaskFactory(object):
def new_task(self, **kwargs):
t = self.base.new_task(**kwargs)
return self.task_helper.proxy(t)
+
+
+#Metadef Namespace classes
+class MetadefNamespaceRepo(object):
+ def __init__(self, base,
+ namespace_proxy_class=None, namespace_proxy_kwargs=None):
+ self.base = base
+ self.namespace_proxy_helper = Helper(namespace_proxy_class,
+ namespace_proxy_kwargs)
+
+ def get(self, namespace):
+ namespace_obj = self.base.get(namespace)
+ return self.namespace_proxy_helper.proxy(namespace_obj)
+
+ def add(self, namespace):
+ self.base.add(self.namespace_proxy_helper.unproxy(namespace))
+
+ def list(self, *args, **kwargs):
+ namespaces = self.base.list(*args, **kwargs)
+ return [self.namespace_proxy_helper.proxy(namespace) for namespace
+ in namespaces]
+
+ def remove(self, item):
+ base_item = self.namespace_proxy_helper.unproxy(item)
+ result = self.base.remove(base_item)
+ return self.namespace_proxy_helper.proxy(result)
+
+ def remove_objects(self, item):
+ base_item = self.namespace_proxy_helper.unproxy(item)
+ result = self.base.remove_objects(base_item)
+ return self.namespace_proxy_helper.proxy(result)
+
+ def remove_properties(self, item):
+ base_item = self.namespace_proxy_helper.unproxy(item)
+ result = self.base.remove_properties(base_item)
+ return self.namespace_proxy_helper.proxy(result)
+
+ def save(self, item):
+ base_item = self.namespace_proxy_helper.unproxy(item)
+ result = self.base.save(base_item)
+ return self.namespace_proxy_helper.proxy(result)
+
+
+class MetadefNamespace(object):
+ def __init__(self, base):
+ self.base = base
+
+ namespace_id = _proxy('base', 'namespace_id')
+ namespace = _proxy('base', 'namespace')
+ display_name = _proxy('base', 'display_name')
+ description = _proxy('base', 'description')
+ owner = _proxy('base', 'owner')
+ visibility = _proxy('base', 'visibility')
+ protected = _proxy('base', 'protected')
+ created_at = _proxy('base', 'created_at')
+ updated_at = _proxy('base', 'updated_at')
+
+ def delete(self):
+ self.base.delete()
+
+
+class MetadefNamespaceFactory(object):
+ def __init__(self,
+ base,
+ meta_namespace_proxy_class=None,
+ meta_namespace_proxy_kwargs=None):
+ self.meta_namespace_helper = Helper(meta_namespace_proxy_class,
+ meta_namespace_proxy_kwargs)
+ self.base = base
+
+ def new_namespace(self, **kwargs):
+ t = self.base.new_namespace(**kwargs)
+ return self.meta_namespace_helper.proxy(t)
+
+
+#Metadef object classes
+class MetadefObjectRepo(object):
+ def __init__(self, base,
+ object_proxy_class=None, object_proxy_kwargs=None):
+ self.base = base
+ self.object_proxy_helper = Helper(object_proxy_class,
+ object_proxy_kwargs)
+
+ def get(self, namespace, object_name):
+ meta_object = self.base.get(namespace, object_name)
+ return self.object_proxy_helper.proxy(meta_object)
+
+ def add(self, meta_object):
+ self.base.add(self.object_proxy_helper.unproxy(meta_object))
+
+ def list(self, *args, **kwargs):
+ objects = self.base.list(*args, **kwargs)
+ return [self.object_proxy_helper.proxy(meta_object) for meta_object
+ in objects]
+
+ def remove(self, item):
+ base_item = self.object_proxy_helper.unproxy(item)
+ result = self.base.remove(base_item)
+ return self.object_proxy_helper.proxy(result)
+
+ def save(self, item):
+ base_item = self.object_proxy_helper.unproxy(item)
+ result = self.base.save(base_item)
+ return self.object_proxy_helper.proxy(result)
+
+
+class MetadefObject(object):
+ def __init__(self, base):
+ self.base = base
+ namespace = _proxy('base', 'namespace')
+ object_id = _proxy('base', 'object_id')
+ name = _proxy('base', 'name')
+ required = _proxy('base', 'required')
+ description = _proxy('base', 'description')
+ properties = _proxy('base', 'properties')
+ created_at = _proxy('base', 'created_at')
+ updated_at = _proxy('base', 'updated_at')
+
+ def delete(self):
+ self.base.delete()
+
+
+class MetadefObjectFactory(object):
+ def __init__(self,
+ base,
+ meta_object_proxy_class=None,
+ meta_object_proxy_kwargs=None):
+ self.meta_object_helper = Helper(meta_object_proxy_class,
+ meta_object_proxy_kwargs)
+ self.base = base
+
+ def new_object(self, **kwargs):
+ t = self.base.new_object(**kwargs)
+ return self.meta_object_helper.proxy(t)
+
+
+#Metadef ResourceType classes
+class MetadefResourceTypeRepo(object):
+ def __init__(self, base, resource_type_proxy_class=None,
+ resource_type_proxy_kwargs=None):
+ self.base = base
+ self.resource_type_proxy_helper = Helper(resource_type_proxy_class,
+ resource_type_proxy_kwargs)
+
+ def add(self, meta_resource_type):
+ self.base.add(self.resource_type_proxy_helper.unproxy(
+ meta_resource_type))
+
+ def list(self, *args, **kwargs):
+ resource_types = self.base.list(*args, **kwargs)
+ return [self.resource_type_proxy_helper.proxy(resource_type)
+ for resource_type in resource_types]
+
+ def remove(self, item):
+ base_item = self.resource_type_proxy_helper.unproxy(item)
+ result = self.base.remove(base_item)
+ return self.resource_type_proxy_helper.proxy(result)
+
+
+class MetadefResourceType(object):
+ def __init__(self, base):
+ self.base = base
+ namespace = _proxy('base', 'namespace')
+ name = _proxy('base', 'name')
+ prefix = _proxy('base', 'prefix')
+ properties_target = _proxy('base', 'properties_target')
+ created_at = _proxy('base', 'created_at')
+ updated_at = _proxy('base', 'updated_at')
+
+ def delete(self):
+ self.base.delete()
+
+
+class MetadefResourceTypeFactory(object):
+ def __init__(self,
+ base,
+ resource_type_proxy_class=None,
+ resource_type_proxy_kwargs=None):
+ self.resource_type_helper = Helper(resource_type_proxy_class,
+ resource_type_proxy_kwargs)
+ self.base = base
+
+ def new_resource_type(self, **kwargs):
+ t = self.base.new_resource_type(**kwargs)
+ return self.resource_type_helper.proxy(t)
+
+
+#Metadef namespace property classes
+class MetadefPropertyRepo(object):
+ def __init__(self, base,
+ property_proxy_class=None, property_proxy_kwargs=None):
+ self.base = base
+ self.property_proxy_helper = Helper(property_proxy_class,
+ property_proxy_kwargs)
+
+ def get(self, namespace, property_name):
+ property = self.base.get(namespace, property_name)
+ return self.property_proxy_helper.proxy(property)
+
+ def add(self, property):
+ self.base.add(self.property_proxy_helper.unproxy(property))
+
+ def list(self, *args, **kwargs):
+ properties = self.base.list(*args, **kwargs)
+ return [self.property_proxy_helper.proxy(property) for property
+ in properties]
+
+ def remove(self, item):
+ base_item = self.property_proxy_helper.unproxy(item)
+ result = self.base.remove(base_item)
+ return self.property_proxy_helper.proxy(result)
+
+ def save(self, item):
+ base_item = self.property_proxy_helper.unproxy(item)
+ result = self.base.save(base_item)
+ return self.property_proxy_helper.proxy(result)
+
+
+class MetadefProperty(object):
+ def __init__(self, base):
+ self.base = base
+ namespace = _proxy('base', 'namespace')
+ property_id = _proxy('base', 'property_id')
+ name = _proxy('base', 'name')
+ schema = _proxy('base', 'schema')
+
+ def delete(self):
+ self.base.delete()
+
+
+class MetadefPropertyFactory(object):
+ def __init__(self,
+ base,
+ property_proxy_class=None,
+ property_proxy_kwargs=None):
+ self.meta_object_helper = Helper(property_proxy_class,
+ property_proxy_kwargs)
+ self.base = base
+
+ def new_namespace_property(self, **kwargs):
+ t = self.base.new_namespace_property(**kwargs)
+ return self.meta_object_helper.proxy(t)
diff --git a/glance/gateway.py b/glance/gateway.py
index cd128974f2..b0245b0ff4 100644
--- a/glance/gateway.py
+++ b/glance/gateway.py
@@ -120,3 +120,70 @@ class Gateway(object):
authorized_task_stub_repo = authorization.TaskStubRepoProxy(
notifier_task_stub_repo, context)
return authorized_task_stub_repo
+
+ def get_metadef_namespace_factory(self, context):
+ ns_factory = glance.domain.MetadefNamespaceFactory()
+ policy_ns_factory = policy.MetadefNamespaceFactoryProxy(
+ ns_factory, context, self.policy)
+ authorized_ns_factory = authorization.MetadefNamespaceFactoryProxy(
+ policy_ns_factory, context)
+ return authorized_ns_factory
+
+ def get_metadef_namespace_repo(self, context):
+ ns_repo = glance.db.MetadefNamespaceRepo(context, self.db_api)
+ policy_ns_repo = policy.MetadefNamespaceRepoProxy(
+ ns_repo, context, self.policy)
+ authorized_ns_repo = authorization.MetadefNamespaceRepoProxy(
+ policy_ns_repo, context)
+ return authorized_ns_repo
+
+ def get_metadef_object_factory(self, context):
+ object_factory = glance.domain.MetadefObjectFactory()
+ policy_object_factory = policy.MetadefObjectFactoryProxy(
+ object_factory, context, self.policy)
+ authorized_object_factory = authorization.MetadefObjectFactoryProxy(
+ policy_object_factory, context)
+ return authorized_object_factory
+
+ def get_metadef_object_repo(self, context):
+ object_repo = glance.db.MetadefObjectRepo(context, self.db_api)
+ policy_object_repo = policy.MetadefObjectRepoProxy(
+ object_repo, context, self.policy)
+ authorized_object_repo = authorization.MetadefObjectRepoProxy(
+ policy_object_repo, context)
+ return authorized_object_repo
+
+ def get_metadef_resource_type_factory(self, context):
+ resource_type_factory = glance.domain.MetadefResourceTypeFactory()
+ policy_resource_type_factory = policy.MetadefResourceTypeFactoryProxy(
+ resource_type_factory, context, self.policy)
+ authorized_resource_type_factory = \
+ authorization.MetadefResourceTypeFactoryProxy(
+ policy_resource_type_factory, context)
+ return authorized_resource_type_factory
+
+ def get_metadef_resource_type_repo(self, context):
+ resource_type_repo = glance.db.MetadefResourceTypeRepo(
+ context, self.db_api)
+ policy_object_repo = policy.MetadefResourceTypeRepoProxy(
+ resource_type_repo, context, self.policy)
+ authorized_resource_type_repo = \
+ authorization.MetadefResourceTypeRepoProxy(policy_object_repo,
+ context)
+ return authorized_resource_type_repo
+
+ def get_metadef_property_factory(self, context):
+ prop_factory = glance.domain.MetadefPropertyFactory()
+ policy_prop_factory = policy.MetadefPropertyFactoryProxy(
+ prop_factory, context, self.policy)
+ authorized_prop_factory = authorization.MetadefPropertyFactoryProxy(
+ policy_prop_factory, context)
+ return authorized_prop_factory
+
+ def get_metadef_property_repo(self, context):
+ prop_repo = glance.db.MetadefPropertyRepo(context, self.db_api)
+ policy_prop_repo = policy.MetadefPropertyRepoProxy(
+ prop_repo, context, self.policy)
+ authorized_prop_repo = authorization.MetadefPropertyRepoProxy(
+ policy_prop_repo, context)
+ return authorized_prop_repo
diff --git a/glance/schema.py b/glance/schema.py
index 64758aab41..ef6cdfb874 100644
--- a/glance/schema.py
+++ b/glance/schema.py
@@ -22,12 +22,15 @@ from glance.common import utils
class Schema(object):
- def __init__(self, name, properties=None, links=None):
+ def __init__(self, name, properties=None, links=None, required=None,
+ definitions=None):
self.name = name
if properties is None:
properties = {}
self.properties = properties
self.links = links
+ self.required = required
+ self.definitions = definitions
def validate(self, obj):
try:
@@ -68,6 +71,10 @@ class Schema(object):
'properties': self.properties,
'additionalProperties': False,
}
+ if self.definitions:
+ raw['definitions'] = self.definitions
+ if self.required:
+ raw['required'] = self.required
if self.links:
raw['links'] = self.links
return raw
@@ -77,6 +84,10 @@ class Schema(object):
'name': self.name,
'properties': self.properties
}
+ if self.definitions:
+ minimal['definitions'] = self.definitions
+ if self.required:
+ minimal['required'] = self.required
return minimal
@@ -102,7 +113,11 @@ class CollectionSchema(object):
self.item_schema = item_schema
def raw(self):
- return {
+ definitions = None
+ if self.item_schema.definitions:
+ definitions = self.item_schema.definitions
+ self.item_schema.definitions = None
+ raw = {
'name': self.name,
'properties': {
self.name: {
@@ -119,9 +134,18 @@ class CollectionSchema(object):
{'rel': 'describedby', 'href': '{schema}'},
],
}
+ if definitions:
+ raw['definitions'] = definitions
+ self.item_schema.definitions = definitions
+
+ return raw
def minimal(self):
- return {
+ definitions = None
+ if self.item_schema.definitions:
+ definitions = self.item_schema.definitions
+ self.item_schema.definitions = None
+ minimal = {
'name': self.name,
'properties': {
self.name: {
@@ -134,3 +158,66 @@ class CollectionSchema(object):
{'rel': 'describedby', 'href': '{schema}'},
],
}
+ if definitions:
+ minimal['definitions'] = definitions
+ self.item_schema.definitions = definitions
+
+ return minimal
+
+
+class DictCollectionSchema(Schema):
+ def __init__(self, name, item_schema):
+ self.name = name
+ self.item_schema = item_schema
+
+ def raw(self):
+ definitions = None
+ if self.item_schema.definitions:
+ definitions = self.item_schema.definitions
+ self.item_schema.definitions = None
+ raw = {
+ 'name': self.name,
+ 'properties': {
+ self.name: {
+ 'type': 'object',
+ 'additionalProperties': self.item_schema.raw(),
+ },
+ 'first': {'type': 'string'},
+ 'next': {'type': 'string'},
+ 'schema': {'type': 'string'},
+ },
+ 'links': [
+ {'rel': 'first', 'href': '{first}'},
+ {'rel': 'next', 'href': '{next}'},
+ {'rel': 'describedby', 'href': '{schema}'},
+ ],
+ }
+ if definitions:
+ raw['definitions'] = definitions
+ self.item_schema.definitions = definitions
+
+ return raw
+
+ def minimal(self):
+ definitions = None
+ if self.item_schema.definitions:
+ definitions = self.item_schema.definitions
+ self.item_schema.definitions = None
+ minimal = {
+ 'name': self.name,
+ 'properties': {
+ self.name: {
+ 'type': 'object',
+ 'additionalProperties': self.item_schema.minimal(),
+ },
+ 'schema': {'type': 'string'},
+ },
+ 'links': [
+ {'rel': 'describedby', 'href': '{schema}'},
+ ],
+ }
+ if definitions:
+ minimal['definitions'] = definitions
+ self.item_schema.definitions = definitions
+
+ return minimal
diff --git a/glance/tests/functional/v2/test_metadef_namespaces.py b/glance/tests/functional/v2/test_metadef_namespaces.py
new file mode 100644
index 0000000000..6ae7cfd992
--- /dev/null
+++ b/glance/tests/functional/v2/test_metadef_namespaces.py
@@ -0,0 +1,177 @@
+# Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
+#
+# 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 requests
+
+from glance.openstack.common import jsonutils
+from glance.tests import functional
+
+TENANT1 = str(uuid.uuid4())
+TENANT2 = str(uuid.uuid4())
+
+
+class TestNamespaces(functional.FunctionalTest):
+
+ def setUp(self):
+ super(TestNamespaces, self).setUp()
+ self.cleanup()
+ self.api_server.deployment_flavor = 'noauth'
+ self.start_servers(**self.__dict__.copy())
+
+ def _url(self, path):
+ return 'http://127.0.0.1:%d%s' % (self.api_port, path)
+
+ def _headers(self, custom_headers=None):
+ base_headers = {
+ 'X-Identity-Status': 'Confirmed',
+ 'X-Auth-Token': '932c5c84-02ac-4fe5-a9ba-620af0e2bb96',
+ 'X-User-Id': 'f9a41d13-0c13-47e9-bee2-ce4e8bfe958e',
+ 'X-Tenant-Id': TENANT1,
+ 'X-Roles': 'admin',
+ }
+ base_headers.update(custom_headers or {})
+ return base_headers
+
+ def test_namespace_lifecycle(self):
+ # Namespace should not exist
+ path = self._url('/v2/metadefs/namespaces/MyNamespace')
+ response = requests.get(path, headers=self._headers())
+ self.assertEqual(404, response.status_code)
+
+ # Create a namespace
+ path = self._url('/v2/metadefs/namespaces')
+ headers = self._headers({'content-type': 'application/json'})
+ namespace_name = 'MyNamespace'
+ data = jsonutils.dumps({
+ "namespace": namespace_name,
+ "display_name": "My User Friendly Namespace",
+ "description": "My description"
+ }
+ )
+ response = requests.post(path, headers=headers, data=data)
+ self.assertEqual(201, response.status_code)
+ namespace_loc_header = response.headers['Location']
+
+ # Returned namespace should match the created namespace with default
+ # values of visibility=private, protected=False and owner=Context
+ # Tenant
+ namespace = jsonutils.loads(response.text)
+ checked_keys = set([
+ u'namespace',
+ u'display_name',
+ u'description',
+ u'visibility',
+ u'self',
+ u'schema',
+ u'protected',
+ u'owner',
+ u'created_at',
+ u'updated_at'
+ ])
+ self.assertEqual(set(namespace.keys()), checked_keys)
+ expected_namespace = {
+ "namespace": namespace_name,
+ "display_name": "My User Friendly Namespace",
+ "description": "My description",
+ "visibility": "private",
+ "protected": False,
+ "owner": TENANT1,
+ "self": "/v2/metadefs/namespaces/%s" % namespace_name,
+ "schema": "/v2/schemas/metadefs/namespace"
+ }
+ for key, value in expected_namespace.items():
+ self.assertEqual(namespace[key], value, key)
+
+ # Get the namespace using the returned Location header
+ response = requests.get(namespace_loc_header, headers=self._headers())
+ self.assertEqual(200, response.status_code)
+ namespace = jsonutils.loads(response.text)
+ self.assertEqual(namespace_name, namespace['namespace'])
+ self.assertNotIn('object', namespace)
+ self.assertEqual(TENANT1, namespace['owner'])
+ self.assertEqual('private', namespace['visibility'])
+ self.assertEqual(False, namespace['protected'])
+
+ # The namespace should be mutable
+ path = self._url('/v2/metadefs/namespaces/%s' % namespace_name)
+ media_type = 'application/json'
+ headers = self._headers({'content-type': media_type})
+ namespace_name = "MyNamespace-UPDATED"
+ data = jsonutils.dumps(
+ {
+ "namespace": namespace_name,
+ "display_name": "display_name-UPDATED",
+ "description": "description-UPDATED",
+ "visibility": "private", # Not changed
+ "protected": True,
+ "owner": TENANT2
+ }
+ )
+ response = requests.put(path, headers=headers, data=data)
+ self.assertEqual(200, response.status_code, response.text)
+
+ # Returned namespace should reflect the changes
+ namespace = jsonutils.loads(response.text)
+ self.assertEqual('MyNamespace-UPDATED', namespace_name)
+ self.assertEqual('display_name-UPDATED', namespace['display_name'])
+ self.assertEqual('description-UPDATED', namespace['description'])
+ self.assertEqual('private', namespace['visibility'])
+ self.assertTrue(namespace['protected'])
+ self.assertEqual(TENANT2, namespace['owner'])
+
+ # Updates should persist across requests
+ path = self._url('/v2/metadefs/namespaces/%s' % namespace_name)
+ response = requests.get(path, headers=self._headers())
+ self.assertEqual(200, response.status_code)
+ namespace = jsonutils.loads(response.text)
+ self.assertEqual('MyNamespace-UPDATED', namespace['namespace'])
+ self.assertEqual('display_name-UPDATED', namespace['display_name'])
+ self.assertEqual('description-UPDATED', namespace['description'])
+ self.assertEqual('private', namespace['visibility'])
+ self.assertTrue(namespace['protected'])
+ self.assertEqual(TENANT2, namespace['owner'])
+
+ # Deletion should not work on protected namespaces
+ path = self._url('/v2/metadefs/namespaces/%s' % namespace_name)
+ response = requests.delete(path, headers=self._headers())
+ self.assertEqual(403, response.status_code)
+
+ # Unprotect namespace for deletion
+ path = self._url('/v2/metadefs/namespaces/%s' % namespace_name)
+ media_type = 'application/json'
+ headers = self._headers({'content-type': media_type})
+ doc = {
+ "namespace": namespace_name,
+ "display_name": "My User Friendly Namespace",
+ "description": "My description",
+ "visibility": "public",
+ "protected": False,
+ "owner": TENANT2
+ }
+ data = jsonutils.dumps(doc)
+ response = requests.put(path, headers=headers, data=data)
+ self.assertEqual(200, response.status_code, response.text)
+
+ # Deletion should work. Deleting namespace MyNamespace
+ path = self._url('/v2/metadefs/namespaces/%s' % namespace_name)
+ response = requests.delete(path, headers=self._headers())
+ self.assertEqual(204, response.status_code)
+
+ # Namespace should not exist
+ path = self._url('/v2/metadefs/namespaces/MyNamespace')
+ response = requests.get(path, headers=self._headers())
+ self.assertEqual(404, response.status_code)
diff --git a/glance/tests/functional/v2/test_metadef_objects.py b/glance/tests/functional/v2/test_metadef_objects.py
new file mode 100644
index 0000000000..4cc63c1791
--- /dev/null
+++ b/glance/tests/functional/v2/test_metadef_objects.py
@@ -0,0 +1,263 @@
+# Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
+#
+# 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 requests
+
+from glance.openstack.common import jsonutils
+from glance.tests import functional
+
+TENANT1 = str(uuid.uuid4())
+
+
+class TestMetadefObjects(functional.FunctionalTest):
+
+ def setUp(self):
+ super(TestMetadefObjects, self).setUp()
+ self.cleanup()
+ self.api_server.deployment_flavor = 'noauth'
+ self.start_servers(**self.__dict__.copy())
+
+ def _url(self, path):
+ return 'http://127.0.0.1:%d%s' % (self.api_port, path)
+
+ def _headers(self, custom_headers=None):
+ base_headers = {
+ 'X-Identity-Status': 'Confirmed',
+ 'X-Auth-Token': '932c5c84-02ac-4fe5-a9ba-620af0e2bb96',
+ 'X-User-Id': 'f9a41d13-0c13-47e9-bee2-ce4e8bfe958e',
+ 'X-Tenant-Id': TENANT1,
+ 'X-Roles': 'admin',
+ }
+ base_headers.update(custom_headers or {})
+ return base_headers
+
+ def test_metadata_objects_lifecycle(self):
+ # Namespace should not exist
+ path = self._url('/v2/metadefs/namespaces/MyNamespace')
+ response = requests.get(path, headers=self._headers())
+ self.assertEqual(404, response.status_code)
+
+ # Create a namespace
+ path = self._url('/v2/metadefs/namespaces')
+ headers = self._headers({'content-type': 'application/json'})
+ namespace_name = 'MyNamespace'
+ data = jsonutils.dumps({
+ "namespace": namespace_name,
+ "display_name": "My User Friendly Namespace",
+ "description": "My description",
+ "visibility": "public",
+ "protected": False,
+ "owner": "The Test Owner"
+ }
+ )
+ response = requests.post(path, headers=headers, data=data)
+ self.assertEqual(201, response.status_code)
+
+ # Metadata objects should not exist
+ path = self._url('/v2/metadefs/namespaces/MyNamespace/objects/object1')
+ response = requests.get(path, headers=self._headers())
+ self.assertEqual(404, response.status_code)
+
+ # Create a object
+ path = self._url('/v2/metadefs/namespaces/MyNamespace/objects')
+ headers = self._headers({'content-type': 'application/json'})
+ metadata_object_name = "object1"
+ data = jsonutils.dumps(
+ {
+ "name": metadata_object_name,
+ "description": "object1 description.",
+ "required": [
+ "property1"
+ ],
+ "properties": {
+ "property1": {
+ "type": "integer",
+ "title": "property1",
+ "description": "property1 description",
+ "default": 100,
+ "minimum": 100,
+ "maximum": 30000369
+ },
+ "property2": {
+ "type": "string",
+ "title": "property2",
+ "description": "property2 description ",
+ "default": "value2",
+ "minLength": 2,
+ "maxLength": 50
+ }
+ }
+ }
+ )
+ response = requests.post(path, headers=headers, data=data)
+ self.assertEqual(201, response.status_code)
+
+ # Get the metadata object created above
+ path = self._url('/v2/metadefs/namespaces/%s/objects/%s' %
+ (namespace_name, metadata_object_name))
+ response = requests.get(path,
+ headers=self._headers())
+ self.assertEqual(200, response.status_code)
+ metadata_object = jsonutils.loads(response.text)
+ self.assertEqual("object1", metadata_object['name'])
+
+ # Returned object should match the created object
+ metadata_object = jsonutils.loads(response.text)
+ checked_keys = set([
+ u'name',
+ u'description',
+ u'properties',
+ u'required',
+ u'self',
+ u'schema',
+ u'created_at',
+ u'updated_at'
+ ])
+ self.assertEqual(set(metadata_object.keys()), checked_keys)
+ expected_metadata_object = {
+ "name": metadata_object_name,
+ "description": "object1 description.",
+ "required": [
+ "property1"
+ ],
+ "properties": {
+ 'property1': {
+ 'type': 'integer',
+ "title": "property1",
+ 'description': 'property1 description',
+ 'default': 100,
+ 'minimum': 100,
+ 'maximum': 30000369
+ },
+ "property2": {
+ "type": "string",
+ "title": "property2",
+ "description": "property2 description ",
+ "default": "value2",
+ "minLength": 2,
+ "maxLength": 50
+ }
+ },
+ "self": "/v2/metadefs/namespaces/%("
+ "namespace)s/objects/%(object)s" %
+ {'namespace': namespace_name,
+ 'object': metadata_object_name},
+ "schema": "v2/schemas/metadefs/object"
+ }
+
+ #Simple key values
+ checked_values = set([
+ u'name',
+ u'description',
+ ])
+ for key, value in expected_metadata_object.items():
+ if(key in checked_values):
+ self.assertEqual(metadata_object[key], value, key)
+ #Complex key values - properties
+ for key, value in \
+ expected_metadata_object["properties"]['property2'].items():
+ self.assertEqual(
+ metadata_object["properties"]["property2"][key],
+ value, key
+ )
+
+ # The metadata_object should be mutable
+ path = self._url('/v2/metadefs/namespaces/%s/objects/%s' %
+ (namespace_name, metadata_object_name))
+ media_type = 'application/json'
+ headers = self._headers({'content-type': media_type})
+ metadata_object_name = "object1-UPDATED"
+ data = jsonutils.dumps(
+ {
+ "name": metadata_object_name,
+ "description": "desc-UPDATED",
+ "required": [
+ "property2"
+ ],
+ "properties": {
+ 'property1': {
+ 'type': 'integer',
+ "title": "property1",
+ 'description': 'p1 desc-UPDATED',
+ 'default': 500,
+ 'minimum': 500,
+ 'maximum': 1369
+ },
+ "property2": {
+ "type": "string",
+ "title": "property2",
+ "description": "p2 desc-UPDATED",
+ "default": "value2-UPDATED",
+ "minLength": 5,
+ "maxLength": 150
+ }
+ }
+ }
+ )
+ response = requests.put(path, headers=headers, data=data)
+ self.assertEqual(200, response.status_code, response.text)
+
+ # Returned metadata_object should reflect the changes
+ metadata_object = jsonutils.loads(response.text)
+ self.assertEqual('object1-UPDATED', metadata_object['name'])
+ self.assertEqual('desc-UPDATED', metadata_object['description'])
+ self.assertEqual('property2', metadata_object['required'][0])
+ updated_property1 = metadata_object['properties']['property1']
+ updated_property2 = metadata_object['properties']['property2']
+ self.assertEqual('integer', updated_property1['type'])
+ self.assertEqual('p1 desc-UPDATED', updated_property1['description'])
+ self.assertEqual('500', updated_property1['default'])
+ self.assertEqual(500, updated_property1['minimum'])
+ self.assertEqual(1369, updated_property1['maximum'])
+ self.assertEqual('string', updated_property2['type'])
+ self.assertEqual('p2 desc-UPDATED', updated_property2['description'])
+ self.assertEqual('value2-UPDATED', updated_property2['default'])
+ self.assertEqual(5, updated_property2['minLength'])
+ self.assertEqual(150, updated_property2['maxLength'])
+
+ # Updates should persist across requests
+ path = self._url('/v2/metadefs/namespaces/%s/objects/%s' %
+ (namespace_name, metadata_object_name))
+ response = requests.get(path, headers=self._headers())
+ self.assertEqual(200, response.status_code)
+ self.assertEqual('object1-UPDATED', metadata_object['name'])
+ self.assertEqual('desc-UPDATED', metadata_object['description'])
+ self.assertEqual('property2', metadata_object['required'][0])
+ updated_property1 = metadata_object['properties']['property1']
+ updated_property2 = metadata_object['properties']['property2']
+ self.assertEqual('integer', updated_property1['type'])
+ self.assertEqual('p1 desc-UPDATED', updated_property1['description'])
+ self.assertEqual('500', updated_property1['default'])
+ self.assertEqual(500, updated_property1['minimum'])
+ self.assertEqual(1369, updated_property1['maximum'])
+ self.assertEqual('string', updated_property2['type'])
+ self.assertEqual('p2 desc-UPDATED', updated_property2['description'])
+ self.assertEqual('value2-UPDATED', updated_property2['default'])
+ self.assertEqual(5, updated_property2['minLength'])
+ self.assertEqual(150, updated_property2['maxLength'])
+
+ # Deletion of metadata_object object1
+ path = self._url('/v2/metadefs/namespaces/%s/objects/%s' %
+ (namespace_name, metadata_object_name))
+ response = requests.delete(path, headers=self._headers())
+ self.assertEqual(204, response.status_code)
+
+ # metadata_object object1 should not exist
+ path = self._url('/v2/metadefs/namespaces/%s/objects/%s' %
+ (namespace_name, metadata_object_name))
+ response = requests.get(path, headers=self._headers())
+ self.assertEqual(404, response.status_code)
diff --git a/glance/tests/functional/v2/test_metadef_properties.py b/glance/tests/functional/v2/test_metadef_properties.py
new file mode 100644
index 0000000000..7983cf2532
--- /dev/null
+++ b/glance/tests/functional/v2/test_metadef_properties.py
@@ -0,0 +1,180 @@
+# Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
+#
+# 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 requests
+
+from glance.openstack.common import jsonutils
+from glance.tests import functional
+
+TENANT1 = str(uuid.uuid4())
+
+
+class TestNamespaceProperties(functional.FunctionalTest):
+
+ def setUp(self):
+ super(TestNamespaceProperties, self).setUp()
+ self.cleanup()
+ self.api_server.deployment_flavor = 'noauth'
+ self.start_servers(**self.__dict__.copy())
+
+ def _url(self, path):
+ return 'http://127.0.0.1:%d%s' % (self.api_port, path)
+
+ def _headers(self, custom_headers=None):
+ base_headers = {
+ 'X-Identity-Status': 'Confirmed',
+ 'X-Auth-Token': '932c5c84-02ac-4fe5-a9ba-620af0e2bb96',
+ 'X-User-Id': 'f9a41d13-0c13-47e9-bee2-ce4e8bfe958e',
+ 'X-Tenant-Id': TENANT1,
+ 'X-Roles': 'admin',
+ }
+ base_headers.update(custom_headers or {})
+ return base_headers
+
+ def test_properties_lifecycle(self):
+ # Namespace should not exist
+ path = self._url('/v2/metadefs/namespaces/MyNamespace')
+ response = requests.get(path, headers=self._headers())
+ self.assertEqual(404, response.status_code)
+
+ # Create a namespace
+ path = self._url('/v2/metadefs/namespaces')
+ headers = self._headers({'content-type': 'application/json'})
+ namespace_name = 'MyNamespace'
+ data = jsonutils.dumps({
+ "namespace": namespace_name,
+ "display_name": "My User Friendly Namespace",
+ "description": "My description",
+ "visibility": "public",
+ "protected": False,
+ "owner": "The Test Owner"
+ }
+ )
+ response = requests.post(path, headers=headers, data=data)
+ self.assertEqual(201, response.status_code)
+
+ # Property1 should not exist
+ path = self._url('/v2/metadefs/namespaces/MyNamespace/properties'
+ '/property1')
+ response = requests.get(path, headers=self._headers())
+ self.assertEqual(404, response.status_code)
+
+ # Create a property
+ path = self._url('/v2/metadefs/namespaces/MyNamespace/properties')
+ headers = self._headers({'content-type': 'application/json'})
+ property_name = "property1"
+ data = jsonutils.dumps(
+ {
+ "name": property_name,
+ "type": "integer",
+ "title": "property1",
+ "description": "property1 description",
+ "default": 100,
+ "minimum": 100,
+ "maximum": 30000369
+ }
+ )
+ response = requests.post(path, headers=headers, data=data)
+ self.assertEqual(201, response.status_code)
+
+ # Get the property created above
+ path = self._url('/v2/metadefs/namespaces/%s/properties/%s' %
+ (namespace_name, property_name))
+ response = requests.get(path, headers=self._headers())
+ self.assertEqual(200, response.status_code)
+ property_object = jsonutils.loads(response.text)
+ self.assertEqual("integer", property_object['type'])
+ self.assertEqual("property1", property_object['title'])
+ self.assertEqual("property1 description", property_object[
+ 'description'])
+ self.assertEqual('100', property_object['default'])
+ self.assertEqual(100, property_object['minimum'])
+ self.assertEqual(30000369, property_object['maximum'])
+
+ # Returned property should match the created property
+ property_object = jsonutils.loads(response.text)
+ checked_keys = set([
+ u'name',
+ u'type',
+ u'title',
+ u'description',
+ u'default',
+ u'minimum',
+ u'maximum'
+ ])
+ self.assertEqual(set(property_object.keys()), checked_keys)
+ expected_metadata_property = {
+ "type": "integer",
+ "title": "property1",
+ "description": "property1 description",
+ "default": '100',
+ "minimum": 100,
+ "maximum": 30000369
+ }
+
+ for key, value in expected_metadata_property.items():
+ self.assertEqual(property_object[key], value, key)
+
+ # The property should be mutable
+ path = self._url('/v2/metadefs/namespaces/%s/properties/%s' %
+ (namespace_name, property_name))
+ media_type = 'application/json'
+ headers = self._headers({'content-type': media_type})
+ property_name = "property1-UPDATED"
+ data = jsonutils.dumps(
+ {
+ "name": property_name,
+ "type": "string",
+ "title": "string property",
+ "description": "desc-UPDATED",
+ "default": "value-UPDATED",
+ "minLength": 5,
+ "maxLength": 10
+ }
+ )
+ response = requests.put(path, headers=headers, data=data)
+ self.assertEqual(200, response.status_code, response.text)
+
+ # Returned property should reflect the changes
+ property_object = jsonutils.loads(response.text)
+ self.assertEqual('string', property_object['type'])
+ self.assertEqual('desc-UPDATED', property_object['description'])
+ self.assertEqual('value-UPDATED', property_object['default'])
+ self.assertEqual(5, property_object['minLength'])
+ self.assertEqual(10, property_object['maxLength'])
+
+ # Updates should persist across requests
+ path = self._url('/v2/metadefs/namespaces/%s/properties/%s' %
+ (namespace_name, property_name))
+ response = requests.get(path, headers=self._headers())
+ self.assertEqual('string', property_object['type'])
+ self.assertEqual('desc-UPDATED', property_object['description'])
+ self.assertEqual('value-UPDATED', property_object['default'])
+ self.assertEqual(5, property_object['minLength'])
+ self.assertEqual(10, property_object['maxLength'])
+
+ # Deletion of property property1
+ path = self._url('/v2/metadefs/namespaces/%s/properties/%s' %
+ (namespace_name, property_name))
+ response = requests.delete(path, headers=self._headers())
+ self.assertEqual(204, response.status_code)
+
+ # property1 should not exist
+ path = self._url('/v2/metadefs/namespaces/%s/properties/%s' %
+ (namespace_name, property_name))
+ response = requests.get(path, headers=self._headers())
+ self.assertEqual(404, response.status_code)
diff --git a/glance/tests/functional/v2/test_metadef_resourcetypes.py b/glance/tests/functional/v2/test_metadef_resourcetypes.py
new file mode 100644
index 0000000000..45c74da6f1
--- /dev/null
+++ b/glance/tests/functional/v2/test_metadef_resourcetypes.py
@@ -0,0 +1,268 @@
+# Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
+#
+# 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 six
+import webob.exc
+from wsme.rest.json import fromjson
+from wsme.rest.json import tojson
+
+from glance.api import policy
+from glance.api.v2.model.metadef_resource_type import ResourceType
+from glance.api.v2.model.metadef_resource_type import ResourceTypeAssociation
+from glance.api.v2.model.metadef_resource_type import ResourceTypeAssociations
+from glance.api.v2.model.metadef_resource_type import ResourceTypes
+from glance.common import exception
+from glance.common import utils
+from glance.common import wsgi
+import glance.db
+import glance.gateway
+from glance import i18n
+import glance.notifier
+from glance.openstack.common import jsonutils as json
+import glance.openstack.common.log as logging
+import glance.schema
+import glance.store
+
+LOG = logging.getLogger(__name__)
+_LE = i18n._LE
+_LI = i18n._LI
+
+
+class ResourceTypeController(object):
+ def __init__(self, db_api=None, policy_enforcer=None):
+ self.db_api = db_api or glance.db.get_api()
+ self.policy = policy_enforcer or policy.Enforcer()
+ self.gateway = glance.gateway.Gateway(db_api=self.db_api,
+ policy_enforcer=self.policy)
+
+ def index(self, req):
+ try:
+ filters = {}
+ filters['namespace'] = None
+ rs_type_repo = self.gateway.get_metadef_resource_type_repo(
+ req.context)
+ db_resource_type_list = rs_type_repo.list(filters=filters)
+ resource_type_list = [ResourceType.to_wsme_model(
+ resource_type) for resource_type in db_resource_type_list]
+ resource_types = ResourceTypes()
+ resource_types.resource_types = resource_type_list
+ except exception.Forbidden as e:
+ raise webob.exc.HTTPForbidden(explanation=e.msg)
+ except exception.NotFound as e:
+ raise webob.exc.HTTPNotFound(explanation=e.msg)
+ except Exception as e:
+ LOG.error(e)
+ raise webob.exc.HTTPInternalServerError(e)
+ return resource_types
+
+ def show(self, req, namespace):
+ try:
+ filters = {}
+ filters['namespace'] = namespace
+ rs_type_repo = self.gateway.get_metadef_resource_type_repo(
+ req.context)
+ db_resource_type_list = rs_type_repo.list(filters=filters)
+ resource_type_list = [ResourceTypeAssociation.to_wsme_model(
+ resource_type) for resource_type in db_resource_type_list]
+ resource_types = ResourceTypeAssociations()
+ resource_types.resource_type_associations = resource_type_list
+ except exception.Forbidden as e:
+ raise webob.exc.HTTPForbidden(explanation=e.msg)
+ except exception.NotFound as e:
+ raise webob.exc.HTTPNotFound(explanation=e.msg)
+ except Exception as e:
+ LOG.error(e)
+ raise webob.exc.HTTPInternalServerError(e)
+ return resource_types
+
+ def create(self, req, resource_type, namespace):
+ rs_type_factory = self.gateway.get_metadef_resource_type_factory(
+ req.context)
+ rs_type_repo = self.gateway.get_metadef_resource_type_repo(req.context)
+ try:
+ new_resource_type = rs_type_factory.new_resource_type(
+ namespace=namespace, **resource_type.to_dict())
+ rs_type_repo.add(new_resource_type)
+
+ except exception.Forbidden as e:
+ msg = (_LE("Forbidden to create resource type. Reason: %("
+ "reason)s") % {'reason': utils.exception_to_str(e)})
+ LOG.error(msg)
+ raise webob.exc.HTTPForbidden(explanation=e.msg)
+ except exception.NotFound as e:
+ raise webob.exc.HTTPNotFound(explanation=e.msg)
+ except exception.Duplicate as e:
+ raise webob.exc.HTTPConflict(explanation=e.msg)
+ except Exception as e:
+ LOG.error(e)
+ raise webob.exc.HTTPInternalServerError()
+ return ResourceTypeAssociation.to_wsme_model(new_resource_type)
+
+ def delete(self, req, namespace, resource_type):
+ rs_type_repo = self.gateway.get_metadef_resource_type_repo(req.context)
+ try:
+ filters = {}
+ found = False
+ filters['namespace'] = namespace
+ db_resource_type_list = rs_type_repo.list(filters=filters)
+ for db_resource_type in db_resource_type_list:
+ if db_resource_type.name == resource_type:
+ db_resource_type.delete()
+ rs_type_repo.remove(db_resource_type)
+ found = True
+ if not found:
+ raise exception.NotFound()
+ except exception.Forbidden as e:
+ raise webob.exc.HTTPForbidden(explanation=e.msg)
+ except exception.NotFound as e:
+ msg = (_LE("Failed to find resource type %(resourcetype)s to "
+ "delete") % {'resourcetype': resource_type})
+ LOG.error(msg)
+ raise webob.exc.HTTPNotFound(explanation=msg)
+ except Exception as e:
+ LOG.error(e)
+ raise webob.exc.HTTPInternalServerError()
+
+
+class RequestDeserializer(wsgi.JSONRequestDeserializer):
+ _disallowed_properties = ['created_at', 'updated_at']
+
+ def __init__(self, schema=None):
+ super(RequestDeserializer, self).__init__()
+ self.schema = schema or get_schema()
+
+ def _get_request_body(self, request):
+ output = super(RequestDeserializer, self).default(request)
+ if 'body' not in output:
+ msg = _('Body expected in request.')
+ raise webob.exc.HTTPBadRequest(explanation=msg)
+ return output['body']
+
+ @classmethod
+ def _check_allowed(cls, image):
+ for key in cls._disallowed_properties:
+ if key in image:
+ msg = _("Attribute '%s' is read-only.") % key
+ raise webob.exc.HTTPForbidden(explanation=
+ utils.exception_to_str(msg))
+
+ def create(self, request):
+ body = self._get_request_body(request)
+ self._check_allowed(body)
+ try:
+ self.schema.validate(body)
+ except exception.InvalidObject as e:
+ raise webob.exc.HTTPBadRequest(explanation=e.msg)
+ resource_type = fromjson(ResourceTypeAssociation, body)
+ return dict(resource_type=resource_type)
+
+
+class ResponseSerializer(wsgi.JSONResponseSerializer):
+ def __init__(self, schema=None):
+ super(ResponseSerializer, self).__init__()
+ self.schema = schema
+
+ def show(self, response, result):
+ resource_type_json = tojson(ResourceTypeAssociations, result)
+ body = json.dumps(resource_type_json, ensure_ascii=False)
+ response.unicode_body = six.text_type(body)
+ response.content_type = 'application/json'
+
+ def index(self, response, result):
+ resource_type_json = tojson(ResourceTypes, result)
+ body = json.dumps(resource_type_json, ensure_ascii=False)
+ response.unicode_body = six.text_type(body)
+ response.content_type = 'application/json'
+
+ def create(self, response, result):
+ resource_type_json = tojson(ResourceTypeAssociation, result)
+ response.status_int = 201
+ body = json.dumps(resource_type_json, ensure_ascii=False)
+ response.unicode_body = six.text_type(body)
+ response.content_type = 'application/json'
+
+ def delete(self, response, result):
+ response.status_int = 204
+
+
+def _get_base_properties():
+ return {
+ 'name': {
+ 'type': 'string',
+ 'description': _('Resource type names should be aligned with Heat '
+ 'resource types whenever possible: '
+ 'http://docs.openstack.org/developer/heat/'
+ 'template_guide/openstack.html'),
+ 'maxLength': 80,
+ },
+ 'prefix': {
+ 'type': 'string',
+ 'description': _('Specifies the prefix to use for the given '
+ 'resource type. Any properties in the namespace '
+ 'should be prefixed with this prefix when being '
+ 'applied to the specified resource type. Must '
+ 'include prefix separator (e.g. a colon :).'),
+ 'maxLength': 80,
+ },
+ 'properties_target': {
+ 'type': 'string',
+ 'description': _('Some resource types allow more than one key / '
+ 'value pair per instance. For example, Cinder '
+ 'allows user and image metadata on volumes. Only '
+ 'the image properties metadata is evaluated by '
+ 'Nova (scheduling or drivers). This property '
+ 'allows a namespace target to remove the '
+ 'ambiguity.'),
+ 'maxLength': 80,
+ },
+ "created_at": {
+ "type": "string",
+ "description": _("Date and time of resource type association"
+ " (READ-ONLY)"),
+ "format": "date-time"
+ },
+ "updated_at": {
+ "type": "string",
+ "description": _("Date and time of the last resource type "
+ "association modification (READ-ONLY)"),
+ "format": "date-time"
+ }
+ }
+
+
+def get_schema():
+ properties = _get_base_properties()
+ mandatory_attrs = ResourceTypeAssociation.get_mandatory_attrs()
+ schema = glance.schema.Schema(
+ 'resource_type_association',
+ properties,
+ required=mandatory_attrs,
+ )
+ return schema
+
+
+def get_collection_schema():
+ resource_type_schema = get_schema()
+ return glance.schema.CollectionSchema('resource_type_associations',
+ resource_type_schema)
+
+
+def create_resource():
+ """ResourceTypeAssociation resource factory method"""
+ schema = get_schema()
+ deserializer = RequestDeserializer(schema)
+ serializer = ResponseSerializer(schema)
+ controller = ResourceTypeController()
+ return wsgi.Resource(controller, deserializer, serializer)
diff --git a/glance/tests/unit/test_db_metadef.py b/glance/tests/unit/test_db_metadef.py
new file mode 100644
index 0000000000..f75e0985d6
--- /dev/null
+++ b/glance/tests/unit/test_db_metadef.py
@@ -0,0 +1,428 @@
+# Copyright 2012 OpenStack Foundation.
+# Copyright 2014 Intel Corporation
+# 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.
+
+from glance.common import exception
+from glance.common import utils
+import glance.context
+import glance.db
+import glance.tests.unit.utils as unit_test_utils
+import glance.tests.utils as test_utils
+
+TENANT1 = '6838eb7b-6ded-434a-882c-b344c77fe8df'
+TENANT2 = '2c014f32-55eb-467d-8fcb-4bd706012f81'
+TENANT3 = '5a3e60e8-cfa9-4a9e-a90a-62b42cea92b8'
+TENANT4 = 'c6c87f25-8a94-47ed-8c83-053c25f42df4'
+
+USER1 = '54492ba0-f4df-4e4e-be62-27f4d76b29cf'
+
+NAMESPACE1 = 'namespace1'
+NAMESPACE2 = 'namespace2'
+NAMESPACE3 = 'namespace3'
+NAMESPACE4 = 'namespace4'
+
+PROPERTY1 = 'Property1'
+PROPERTY2 = 'Property2'
+PROPERTY3 = 'Property3'
+
+OBJECT1 = 'Object1'
+OBJECT2 = 'Object2'
+OBJECT3 = 'Object3'
+
+RESOURCE_TYPE1 = 'ResourceType1'
+RESOURCE_TYPE2 = 'ResourceType2'
+RESOURCE_TYPE3 = 'ResourceType3'
+
+
+def _db_namespace_fixture(**kwargs):
+ namespace = {
+ 'namespace': None,
+ 'display_name': None,
+ 'description': None,
+ 'visibility': True,
+ 'protected': False,
+ 'owner': None
+ }
+ namespace.update(kwargs)
+ return namespace
+
+
+def _db_property_fixture(name, **kwargs):
+ property = {
+ 'name': name,
+ 'schema': '{"type": "string", "title": "title"}',
+ }
+ property.update(kwargs)
+ return property
+
+
+def _db_object_fixture(name, **kwargs):
+ obj = {
+ 'name': name,
+ 'description': None,
+ 'schema': '{}',
+ 'required': '[]',
+ }
+ obj.update(kwargs)
+ return obj
+
+
+def _db_resource_type_fixture(name, **kwargs):
+ obj = {
+ 'name': name,
+ 'protected': False,
+ }
+ obj.update(kwargs)
+ return obj
+
+
+def _db_namespace_resource_type_fixture(name, **kwargs):
+ obj = {
+ 'name': name,
+ 'properties_target': None,
+ 'prefix': None,
+ }
+ obj.update(kwargs)
+ return obj
+
+
+class TestMetadefRepo(test_utils.BaseTestCase):
+
+ def setUp(self):
+ super(TestMetadefRepo, self).setUp()
+ self.db = unit_test_utils.FakeDB()
+ self.db.reset()
+ self.context = glance.context.RequestContext(user=USER1,
+ tenant=TENANT1)
+ self.namespace_repo = glance.db.MetadefNamespaceRepo(self.context,
+ self.db)
+ self.property_repo = glance.db.MetadefPropertyRepo(self.context,
+ self.db)
+ self.object_repo = glance.db.MetadefObjectRepo(self.context,
+ self.db)
+ self.resource_type_repo = glance.db.\
+ MetadefResourceTypeRepo(self.context, self.db)
+ self.namespace_factory = glance.domain.MetadefNamespaceFactory()
+ self.property_factory = glance.domain.MetadefPropertyFactory()
+ self.object_factory = glance.domain.MetadefObjectFactory()
+ self.resource_type_factory = glance.domain.MetadefResourceTypeFactory()
+ self._create_namespaces()
+ self._create_properties()
+ self._create_objects()
+ self._create_resource_types()
+
+ def _create_namespaces(self):
+ self.db.reset()
+ self.namespaces = [
+ _db_namespace_fixture(namespace=NAMESPACE1,
+ display_name='1',
+ description='desc1',
+ visibility='private',
+ protected=True,
+ owner=TENANT1),
+ _db_namespace_fixture(namespace=NAMESPACE2,
+ display_name='2',
+ description='desc2',
+ visibility='public',
+ protected=False,
+ owner=TENANT1),
+ _db_namespace_fixture(namespace=NAMESPACE3,
+ display_name='3',
+ description='desc3',
+ visibility='private',
+ protected=True,
+ owner=TENANT3),
+ _db_namespace_fixture(namespace=NAMESPACE4,
+ display_name='4',
+ description='desc4',
+ visibility='public',
+ protected=True,
+ owner=TENANT3)
+ ]
+ [self.db.metadef_namespace_create(None, namespace)
+ for namespace in self.namespaces]
+
+ def _create_properties(self):
+ self.properties = [
+ _db_property_fixture(name=PROPERTY1),
+ _db_property_fixture(name=PROPERTY2),
+ _db_property_fixture(name=PROPERTY3)
+ ]
+ [self.db.metadef_property_create(self.context, NAMESPACE1, property)
+ for property in self.properties]
+ [self.db.metadef_property_create(self.context, NAMESPACE4, property)
+ for property in self.properties]
+
+ def _create_objects(self):
+ self.objects = [
+ _db_object_fixture(name=OBJECT1,
+ description='desc1'),
+ _db_object_fixture(name=OBJECT2,
+ description='desc2'),
+ _db_object_fixture(name=OBJECT3,
+ description='desc3'),
+ ]
+ [self.db.metadef_object_create(self.context, NAMESPACE1, object)
+ for object in self.objects]
+ [self.db.metadef_object_create(self.context, NAMESPACE4, object)
+ for object in self.objects]
+
+ def _create_resource_types(self):
+ self.resource_types = [
+ _db_resource_type_fixture(name=RESOURCE_TYPE1,
+ protected=False),
+ _db_resource_type_fixture(name=RESOURCE_TYPE2,
+ protected=False),
+ _db_resource_type_fixture(name=RESOURCE_TYPE3,
+ protected=True),
+ ]
+ [self.db.metadef_resource_type_create(self.context, resource_type)
+ for resource_type in self.resource_types]
+
+ def test_get_namespace(self):
+ namespace = self.namespace_repo.get(NAMESPACE1)
+ self.assertEqual(namespace.namespace, NAMESPACE1)
+ self.assertEqual(namespace.description, 'desc1')
+ self.assertEqual(namespace.display_name, '1')
+ self.assertEqual(namespace.owner, TENANT1)
+ self.assertEqual(namespace.protected, True)
+ self.assertEqual(namespace.visibility, 'private')
+
+ def test_get_namespace_not_found(self):
+ fake_namespace = "fake_namespace"
+ exc = self.assertRaises(exception.NotFound,
+ self.namespace_repo.get,
+ fake_namespace)
+ self.assertIn(fake_namespace, utils.exception_to_str(exc))
+
+ def test_get_namespace_forbidden(self):
+ self.assertRaises(exception.NotFound,
+ self.namespace_repo.get,
+ NAMESPACE3)
+
+ def test_list_namespace(self):
+ namespaces = self.namespace_repo.list()
+ namespace_names = set([n.namespace for n in namespaces])
+ self.assertEqual(set([NAMESPACE1, NAMESPACE2, NAMESPACE4]),
+ namespace_names)
+
+ def test_list_private_namespaces(self):
+ filters = {'visibility': 'private'}
+ namespaces = self.namespace_repo.list(filters=filters)
+ namespace_names = set([n.namespace for n in namespaces])
+ self.assertEqual(set([NAMESPACE1]), namespace_names)
+
+ def test_add_namespace(self):
+ # NOTE(pawel-koniszewski): Change db_namespace_fixture to
+ # namespace_factory when namespace primary key in DB
+ # will be changed from Integer to UUID
+ namespace = _db_namespace_fixture(namespace='added_namespace',
+ display_name='fake',
+ description='fake_desc',
+ visibility='public',
+ protected=True,
+ owner=TENANT1)
+ self.assertEqual(namespace['namespace'], 'added_namespace')
+ self.db.metadef_namespace_create(None, namespace)
+ retrieved_namespace = self.namespace_repo.get(namespace['namespace'])
+ self.assertEqual(retrieved_namespace.namespace, 'added_namespace')
+
+ def test_save_namespace(self):
+ namespace = self.namespace_repo.get(NAMESPACE1)
+ namespace.display_name = 'save_name'
+ namespace.description = 'save_desc'
+ self.namespace_repo.save(namespace)
+ namespace = self.namespace_repo.get(NAMESPACE1)
+ self.assertEqual(namespace.display_name, 'save_name')
+ self.assertEqual(namespace.description, 'save_desc')
+
+ def test_remove_namespace(self):
+ namespace = self.namespace_repo.get(NAMESPACE1)
+ self.namespace_repo.remove(namespace)
+ self.assertRaises(exception.NotFound, self.namespace_repo.get,
+ NAMESPACE1)
+
+ def test_remove_namespace_not_found(self):
+ fake_name = 'fake_name'
+ namespace = self.namespace_repo.get(NAMESPACE1)
+ namespace.namespace = fake_name
+ exc = self.assertRaises(exception.NotFound, self.namespace_repo.remove,
+ namespace)
+ self.assertIn(fake_name, utils.exception_to_str(exc))
+
+ def test_get_property(self):
+ property = self.property_repo.get(NAMESPACE1, PROPERTY1)
+ namespace = self.namespace_repo.get(NAMESPACE1)
+ self.assertEqual(property.name, PROPERTY1)
+ self.assertEqual(property.namespace.namespace, namespace.namespace)
+
+ def test_get_property_not_found(self):
+ exc = self.assertRaises(exception.NotFound,
+ self.property_repo.get,
+ NAMESPACE2, PROPERTY1)
+ self.assertIn(PROPERTY1, utils.exception_to_str(exc))
+
+ def test_list_property(self):
+ properties = self.property_repo.list(filters={'namespace': NAMESPACE1})
+ property_names = set([p.name for p in properties])
+ self.assertEqual(set([PROPERTY1, PROPERTY2, PROPERTY3]),
+ property_names)
+
+ def test_list_property_empty_result(self):
+ properties = self.property_repo.list(filters={'namespace': NAMESPACE2})
+ property_names = set([p.name for p in properties])
+ self.assertEqual(set([]),
+ property_names)
+
+ def test_list_property_namespace_not_found(self):
+ exc = self.assertRaises(exception.NotFound, self.property_repo.list,
+ filters={'namespace': 'not-a-namespace'})
+ self.assertIn('not-a-namespace', utils.exception_to_str(exc))
+
+ def test_add_property(self):
+ # NOTE(pawel-koniszewski): Change db_property_fixture to
+ # property_factory when property primary key in DB
+ # will be changed from Integer to UUID
+ property = _db_property_fixture(name='added_property')
+ self.assertEqual(property['name'], 'added_property')
+ self.db.metadef_property_create(self.context, NAMESPACE1, property)
+ retrieved_property = self.property_repo.get(NAMESPACE1,
+ 'added_property')
+ self.assertEqual(retrieved_property.name, 'added_property')
+
+ def test_add_property_namespace_forbidden(self):
+ # NOTE(pawel-koniszewski): Change db_property_fixture to
+ # property_factory when property primary key in DB
+ # will be changed from Integer to UUID
+ property = _db_property_fixture(name='added_property')
+ self.assertEqual(property['name'], 'added_property')
+ self.assertRaises(exception.Forbidden, self.db.metadef_property_create,
+ self.context, NAMESPACE3, property)
+
+ def test_add_property_namespace_not_found(self):
+ # NOTE(pawel-koniszewski): Change db_property_fixture to
+ # property_factory when property primary key in DB
+ # will be changed from Integer to UUID
+ property = _db_property_fixture(name='added_property')
+ self.assertEqual(property['name'], 'added_property')
+ self.assertRaises(exception.NotFound, self.db.metadef_property_create,
+ self.context, 'not_a_namespace', property)
+
+ def test_save_property(self):
+ property = self.property_repo.get(NAMESPACE1, PROPERTY1)
+ property.schema = '{"save": "schema"}'
+ self.property_repo.save(property)
+ property = self.property_repo.get(NAMESPACE1, PROPERTY1)
+ self.assertEqual(property.name, PROPERTY1)
+ self.assertEqual(property.schema, '{"save": "schema"}')
+
+ def test_remove_property(self):
+ property = self.property_repo.get(NAMESPACE1, PROPERTY1)
+ self.property_repo.remove(property)
+ self.assertRaises(exception.NotFound, self.property_repo.get,
+ NAMESPACE1, PROPERTY1)
+
+ def test_remove_property_not_found(self):
+ fake_name = 'fake_name'
+ property = self.property_repo.get(NAMESPACE1, PROPERTY1)
+ property.name = fake_name
+ self.assertRaises(exception.NotFound, self.property_repo.remove,
+ property)
+
+ def test_get_object(self):
+ object = self.object_repo.get(NAMESPACE1, OBJECT1)
+ namespace = self.namespace_repo.get(NAMESPACE1)
+ self.assertEqual(object.name, OBJECT1)
+ self.assertEqual(object.description, 'desc1')
+ self.assertEqual(object.required, ['[]'])
+ self.assertEqual(object.properties, {})
+ self.assertEqual(object.namespace.namespace, namespace.namespace)
+
+ def test_get_object_not_found(self):
+ exc = self.assertRaises(exception.NotFound, self.object_repo.get,
+ NAMESPACE2, OBJECT1)
+ self.assertIn(OBJECT1, utils.exception_to_str(exc))
+
+ def test_list_object(self):
+ objects = self.object_repo.list(filters={'namespace': NAMESPACE1})
+ object_names = set([o.name for o in objects])
+ self.assertEqual(set([OBJECT1, OBJECT2, OBJECT3]), object_names)
+
+ def test_list_object_empty_result(self):
+ objects = self.object_repo.list(filters={'namespace': NAMESPACE2})
+ object_names = set([o.name for o in objects])
+ self.assertEqual(set([]), object_names)
+
+ def test_list_object_namespace_not_found(self):
+ exc = self.assertRaises(exception.NotFound, self.object_repo.list,
+ filters={'namespace': 'not-a-namespace'})
+ self.assertIn('not-a-namespace', utils.exception_to_str(exc))
+
+ def test_add_object(self):
+ # NOTE(pawel-koniszewski): Change db_object_fixture to
+ # object_factory when object primary key in DB
+ # will be changed from Integer to UUID
+ object = _db_object_fixture(name='added_object')
+ self.assertEqual(object['name'], 'added_object')
+ self.db.metadef_object_create(self.context, NAMESPACE1, object)
+ retrieved_object = self.object_repo.get(NAMESPACE1,
+ 'added_object')
+ self.assertEqual(retrieved_object.name, 'added_object')
+
+ def test_add_object_namespace_forbidden(self):
+ # NOTE(pawel-koniszewski): Change db_object_fixture to
+ # object_factory when object primary key in DB
+ # will be changed from Integer to UUID
+ object = _db_object_fixture(name='added_object')
+ self.assertEqual(object['name'], 'added_object')
+ self.assertRaises(exception.Forbidden, self.db.metadef_object_create,
+ self.context, NAMESPACE3, object)
+
+ def test_add_object_namespace_not_found(self):
+ # NOTE(pawel-koniszewski): Change db_object_fixture to
+ # object_factory when object primary key in DB
+ # will be changed from Integer to UUID
+ object = _db_object_fixture(name='added_object')
+ self.assertEqual(object['name'], 'added_object')
+ self.assertRaises(exception.NotFound, self.db.metadef_object_create,
+ self.context, 'not-a-namespace', object)
+
+ def test_save_object(self):
+ object = self.object_repo.get(NAMESPACE1, OBJECT1)
+ object.required = ['save_req']
+ object.description = 'save_desc'
+ self.object_repo.save(object)
+ object = self.object_repo.get(NAMESPACE1, OBJECT1)
+ self.assertEqual(object.name, OBJECT1)
+ self.assertEqual(object.required, ['save_req'])
+ self.assertEqual(object.description, 'save_desc')
+
+ def test_remove_object(self):
+ object = self.object_repo.get(NAMESPACE1, OBJECT1)
+ self.object_repo.remove(object)
+ self.assertRaises(exception.NotFound, self.object_repo.get,
+ NAMESPACE1, OBJECT1)
+
+ def test_remove_object_not_found(self):
+ fake_name = 'fake_name'
+ object = self.object_repo.get(NAMESPACE1, OBJECT1)
+ object.name = fake_name
+ self.assertRaises(exception.NotFound, self.object_repo.remove,
+ object)
+
+ def test_list_resource_type(self):
+ resource_type = self.resource_type_repo.list(filters=
+ {'namespace': NAMESPACE1})
+ self.assertEqual(len(resource_type), 0)
diff --git a/glance/tests/unit/v2/test_metadef_resources.py b/glance/tests/unit/v2/test_metadef_resources.py
new file mode 100644
index 0000000000..61b3fd8403
--- /dev/null
+++ b/glance/tests/unit/v2/test_metadef_resources.py
@@ -0,0 +1,1116 @@
+# Copyright 2012 OpenStack Foundation.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import datetime
+
+import webob
+
+from glance.api.v2 import metadef_namespaces as namespaces
+from glance.api.v2 import metadef_objects as objects
+from glance.api.v2 import metadef_properties as properties
+from glance.api.v2 import metadef_resource_types as resource_types
+import glance.api.v2.model.metadef_namespace
+from glance.tests.unit import base
+import glance.tests.unit.utils as unit_test_utils
+
+DATETIME = datetime.datetime(2012, 5, 16, 15, 27, 36, 325355)
+ISOTIME = '2012-05-16T15:27:36Z'
+
+NAMESPACE1 = 'Namespace1'
+NAMESPACE2 = 'Namespace2'
+NAMESPACE3 = 'Namespace3'
+NAMESPACE4 = 'Namespace4'
+NAMESPACE5 = 'Namespace5'
+NAMESPACE6 = 'Namespace6'
+
+PROPERTY1 = 'Property1'
+PROPERTY2 = 'Property2'
+PROPERTY3 = 'Property3'
+
+OBJECT1 = 'Object1'
+OBJECT2 = 'Object2'
+OBJECT3 = 'Object3'
+
+RESOURCE_TYPE1 = 'ResourceType1'
+RESOURCE_TYPE2 = 'ResourceType2'
+RESOURCE_TYPE3 = 'ResourceType3'
+
+TENANT1 = '6838eb7b-6ded-434a-882c-b344c77fe8df'
+TENANT2 = '2c014f32-55eb-467d-8fcb-4bd706012f81'
+TENANT3 = '5a3e60e8-cfa9-4a9e-a90a-62b42cea92b8'
+TENANT4 = 'c6c87f25-8a94-47ed-8c83-053c25f42df4'
+
+
+def _db_namespace_fixture(namespace, **kwargs):
+ obj = {
+ 'namespace': namespace,
+ 'display_name': None,
+ 'description': None,
+ 'visibility': 'public',
+ 'protected': False,
+ 'owner': None,
+ }
+ obj.update(kwargs)
+ return obj
+
+
+def _db_property_fixture(name, **kwargs):
+ obj = {
+ 'name': name,
+ 'schema': '{"type": "string", "title": "title"}',
+ }
+ obj.update(kwargs)
+ return obj
+
+
+def _db_object_fixture(name, **kwargs):
+ obj = {
+ 'name': name,
+ 'description': None,
+ 'schema': '{}',
+ 'required': '[]',
+ }
+ obj.update(kwargs)
+ return obj
+
+
+def _db_resource_type_fixture(name, **kwargs):
+ obj = {
+ 'name': name,
+ 'protected': False,
+ }
+ obj.update(kwargs)
+ return obj
+
+
+def _db_namespace_resource_type_fixture(name, **kwargs):
+ obj = {
+ 'name': name,
+ 'properties_target': None,
+ 'prefix': None,
+ }
+ obj.update(kwargs)
+ return obj
+
+
+class TestMetadefsControllers(base.IsolatedUnitTest):
+
+ def setUp(self):
+ super(TestMetadefsControllers, self).setUp()
+ self.db = unit_test_utils.FakeDB()
+ self.policy = unit_test_utils.FakePolicyEnforcer()
+ self._create_namespaces()
+ self._create_properties()
+ self._create_objects()
+ self._create_resource_types()
+ self._create_namespaces_resource_types()
+ self.namespace_controller = namespaces.NamespaceController(self.db,
+ self.policy)
+ self.property_controller = \
+ properties.NamespacePropertiesController(self.db, self.policy)
+ self.object_controller = objects.MetadefObjectsController(self.db,
+ self.policy)
+ self.rt_controller = resource_types.ResourceTypeController(self.db,
+ self.policy)
+
+ def _create_namespaces(self):
+ self.db.reset()
+ req = unit_test_utils.get_fake_request()
+ self.namespaces = [
+ _db_namespace_fixture(NAMESPACE1, owner=TENANT1,
+ visibility='private', protected=True),
+ _db_namespace_fixture(NAMESPACE2, owner=TENANT2,
+ visibility='private'),
+ _db_namespace_fixture(NAMESPACE3, owner=TENANT3),
+ _db_namespace_fixture(NAMESPACE5, owner=TENANT4),
+ _db_namespace_fixture(NAMESPACE6, owner=TENANT4),
+ ]
+ [self.db.metadef_namespace_create(req.context, namespace)
+ for namespace in self.namespaces]
+
+ def _create_properties(self):
+ req = unit_test_utils.get_fake_request()
+ self.properties = [
+ (NAMESPACE3, _db_property_fixture(PROPERTY1)),
+ (NAMESPACE3, _db_property_fixture(PROPERTY2)),
+ (NAMESPACE1, _db_property_fixture(PROPERTY1)),
+ ]
+ [self.db.metadef_property_create(req.context, namespace, property)
+ for namespace, property in self.properties]
+
+ def _create_objects(self):
+ req = unit_test_utils.get_fake_request()
+ self.objects = [
+ (NAMESPACE3, _db_object_fixture(OBJECT1)),
+ (NAMESPACE3, _db_object_fixture(OBJECT2)),
+ (NAMESPACE1, _db_object_fixture(OBJECT1)),
+ ]
+ [self.db.metadef_object_create(req.context, namespace, object)
+ for namespace, object in self.objects]
+
+ def _create_resource_types(self):
+ req = unit_test_utils.get_fake_request()
+ self.resource_types = [
+ _db_resource_type_fixture(RESOURCE_TYPE1),
+ _db_resource_type_fixture(RESOURCE_TYPE2),
+ ]
+ [self.db.metadef_resource_type_create(req.context, resource_type)
+ for resource_type in self.resource_types]
+
+ def _create_namespaces_resource_types(self):
+ req = unit_test_utils.get_fake_request(is_admin=True)
+ self.ns_resource_types = [
+ (NAMESPACE1, _db_namespace_resource_type_fixture(RESOURCE_TYPE1)),
+ (NAMESPACE3, _db_namespace_resource_type_fixture(RESOURCE_TYPE1)),
+ (NAMESPACE2, _db_namespace_resource_type_fixture(RESOURCE_TYPE1)),
+ (NAMESPACE2, _db_namespace_resource_type_fixture(RESOURCE_TYPE2)),
+ ]
+ [self.db.metadef_resource_type_association_create(req.context,
+ namespace,
+ ns_resource_type)
+ for namespace, ns_resource_type in self.ns_resource_types]
+
+ def test_namespace_index(self):
+ request = unit_test_utils.get_fake_request()
+ output = self.namespace_controller.index(request)
+ output = output.to_dict()
+ self.assertEqual(4, len(output['namespaces']))
+ actual = set([namespace.namespace for
+ namespace in output['namespaces']])
+ expected = set([NAMESPACE1, NAMESPACE3, NAMESPACE5, NAMESPACE6])
+ self.assertEqual(actual, expected)
+
+ def test_namespace_index_admin(self):
+ request = unit_test_utils.get_fake_request(is_admin=True)
+ output = self.namespace_controller.index(request)
+ output = output.to_dict()
+ self.assertEqual(5, len(output['namespaces']))
+ actual = set([namespace.namespace for
+ namespace in output['namespaces']])
+ expected = set([NAMESPACE1, NAMESPACE2, NAMESPACE3, NAMESPACE5,
+ NAMESPACE6])
+ self.assertEqual(actual, expected)
+
+ def test_namespace_index_visibility_public(self):
+ request = unit_test_utils.get_fake_request(tenant=TENANT3)
+ filters = {'visibility': 'public'}
+ output = self.namespace_controller.index(request, filters=filters)
+ output = output.to_dict()
+ self.assertEqual(3, len(output['namespaces']))
+ actual = set([namespace.namespace for namespace
+ in output['namespaces']])
+ expected = set([NAMESPACE3, NAMESPACE5, NAMESPACE6])
+ self.assertEqual(actual, expected)
+
+ def test_namespace_index_resource_type(self):
+ request = unit_test_utils.get_fake_request()
+ filters = {'resource_types': [RESOURCE_TYPE1]}
+ output = self.namespace_controller.index(request, filters=filters)
+ output = output.to_dict()
+ self.assertEqual(2, len(output['namespaces']))
+ actual = set([namespace.namespace for namespace
+ in output['namespaces']])
+ expected = set([NAMESPACE1, NAMESPACE3])
+ self.assertEqual(actual, expected)
+
+ def test_namespace_show(self):
+ request = unit_test_utils.get_fake_request()
+ output = self.namespace_controller.show(request, NAMESPACE1)
+ output = output.to_dict()
+ self.assertEqual(output['namespace'], NAMESPACE1)
+ self.assertEqual(output['owner'], TENANT1)
+ self.assertEqual(output['protected'], True)
+ self.assertEqual(output['visibility'], 'private')
+
+ def test_namespace_show_with_related_resources(self):
+ request = unit_test_utils.get_fake_request()
+ output = self.namespace_controller.show(request, NAMESPACE3)
+ output = output.to_dict()
+ self.assertEqual(output['namespace'], NAMESPACE3)
+ self.assertEqual(output['owner'], TENANT3)
+ self.assertEqual(output['protected'], False)
+ self.assertEqual(output['visibility'], 'public')
+
+ self.assertEqual(2, len(output['properties']))
+ actual = set([property for property in output['properties']])
+ expected = set([PROPERTY1, PROPERTY2])
+ self.assertEqual(actual, expected)
+
+ self.assertEqual(2, len(output['objects']))
+ actual = set([object.name for object in output['objects']])
+ expected = set([OBJECT1, OBJECT2])
+ self.assertEqual(actual, expected)
+
+ self.assertEqual(1, len(output['resource_type_associations']))
+ actual = set([rt.name for rt in output['resource_type_associations']])
+ expected = set([RESOURCE_TYPE1])
+ self.assertEqual(actual, expected)
+
+ def test_namespace_show_with_property_prefix(self):
+ request = unit_test_utils.get_fake_request()
+ rt = glance.api.v2.model.metadef_resource_type.\
+ ResourceTypeAssociation()
+ rt.name = RESOURCE_TYPE2
+ rt.prefix = 'pref'
+ rt = self.rt_controller.create(request, rt, NAMESPACE3)
+
+ object = glance.api.v2.model.metadef_object.MetadefObject()
+ object.name = OBJECT3
+ object.required = []
+
+ property = glance.api.v2.model.metadef_property_type.PropertyType()
+ property.name = PROPERTY2
+ property.type = 'string'
+ property.title = 'title'
+ object.properties = {'prop1': property}
+ object = self.object_controller.create(request, object, NAMESPACE3)
+
+ filters = {'resource_type': RESOURCE_TYPE2}
+ output = self.namespace_controller.show(request, NAMESPACE3, filters)
+ output = output.to_dict()
+
+ [self.assertTrue(property_name.startswith(rt.prefix)) for
+ property_name in output['properties'].keys()]
+
+ for object in output['objects']:
+ [self.assertTrue(property_name.startswith(rt.prefix)) for
+ property_name in object.properties.keys()]
+
+ def test_namespace_show_non_existing(self):
+ request = unit_test_utils.get_fake_request()
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.namespace_controller.show, request, 'FakeName')
+
+ def test_namespace_show_non_visible(self):
+ request = unit_test_utils.get_fake_request()
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.namespace_controller.show, request, NAMESPACE2)
+
+ def test_namespace_delete(self):
+ request = unit_test_utils.get_fake_request(tenant=TENANT2)
+ self.namespace_controller.delete(request, NAMESPACE2)
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.namespace_controller.show, request, NAMESPACE2)
+
+ def test_namespace_delete_non_existing(self):
+ request = unit_test_utils.get_fake_request()
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.namespace_controller.delete, request,
+ 'FakeName')
+
+ def test_namespace_delete_non_visible(self):
+ request = unit_test_utils.get_fake_request()
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.namespace_controller.delete, request,
+ NAMESPACE2)
+
+ def test_namespace_delete_non_visible_admin(self):
+ request = unit_test_utils.get_fake_request(is_admin=True)
+ self.namespace_controller.delete(request, NAMESPACE2)
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.namespace_controller.show, request, NAMESPACE2)
+
+ def test_namespace_delete_protected(self):
+ request = unit_test_utils.get_fake_request()
+ self.assertRaises(webob.exc.HTTPForbidden,
+ self.namespace_controller.delete, request,
+ NAMESPACE1)
+
+ def test_namespace_delete_protected_admin(self):
+ request = unit_test_utils.get_fake_request(is_admin=True)
+ self.assertRaises(webob.exc.HTTPForbidden,
+ self.namespace_controller.delete, request,
+ NAMESPACE1)
+
+ def test_namespace_delete_with_contents(self):
+ request = unit_test_utils.get_fake_request(tenant=TENANT3)
+ self.namespace_controller.delete(request, NAMESPACE3)
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.namespace_controller.show, request, NAMESPACE3)
+ self.assertRaises(webob.exc.HTTPNotFound, self.object_controller.show,
+ request, NAMESPACE3, OBJECT1)
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.property_controller.show, request, NAMESPACE3,
+ OBJECT1)
+
+ def test_namespace_create(self):
+ request = unit_test_utils.get_fake_request()
+
+ namespace = glance.api.v2.model.metadef_namespace.Namespace()
+ namespace.namespace = NAMESPACE4
+ namespace = self.namespace_controller.create(request, namespace)
+ self.assertEqual(namespace.namespace, NAMESPACE4)
+
+ namespace = self.namespace_controller.show(request, NAMESPACE4)
+ self.assertEqual(namespace.namespace, NAMESPACE4)
+
+ def test_namespace_create_different_owner(self):
+ request = unit_test_utils.get_fake_request()
+
+ namespace = glance.api.v2.model.metadef_namespace.Namespace()
+ namespace.namespace = NAMESPACE4
+ namespace.owner = TENANT4
+ self.assertRaises(webob.exc.HTTPForbidden,
+ self.namespace_controller.create, request, namespace)
+
+ def test_namespace_create_different_owner_admin(self):
+ request = unit_test_utils.get_fake_request(is_admin=True)
+
+ namespace = glance.api.v2.model.metadef_namespace.Namespace()
+ namespace.namespace = NAMESPACE4
+ namespace.owner = TENANT4
+ namespace = self.namespace_controller.create(request, namespace)
+ self.assertEqual(namespace.namespace, NAMESPACE4)
+
+ namespace = self.namespace_controller.show(request, NAMESPACE4)
+ self.assertEqual(namespace.namespace, NAMESPACE4)
+
+ def test_namespace_create_with_related_resources(self):
+ request = unit_test_utils.get_fake_request()
+
+ namespace = glance.api.v2.model.metadef_namespace.Namespace()
+ namespace.namespace = NAMESPACE4
+
+ prop1 = glance.api.v2.model.metadef_property_type.PropertyType()
+ prop1.type = 'string'
+ prop1.title = 'title'
+ prop2 = glance.api.v2.model.metadef_property_type.PropertyType()
+ prop2.type = 'string'
+ prop2.title = 'title'
+ namespace.properties = {PROPERTY1: prop1, PROPERTY2: prop2}
+
+ object1 = glance.api.v2.model.metadef_object.MetadefObject()
+ object1.name = OBJECT1
+ object1.required = []
+ object1.properties = {}
+ object2 = glance.api.v2.model.metadef_object.MetadefObject()
+ object2.name = OBJECT2
+ object2.required = []
+ object2.properties = {}
+ namespace.objects = [object1, object2]
+
+ output = self.namespace_controller.create(request, namespace)
+ self.assertEqual(namespace.namespace, NAMESPACE4)
+ output = output.to_dict()
+
+ self.assertEqual(2, len(output['properties']))
+ actual = set([property for property in output['properties']])
+ expected = set([PROPERTY1, PROPERTY2])
+ self.assertEqual(actual, expected)
+
+ self.assertEqual(2, len(output['objects']))
+ actual = set([object.name for object in output['objects']])
+ expected = set([OBJECT1, OBJECT2])
+ self.assertEqual(actual, expected)
+
+ output = self.namespace_controller.show(request, NAMESPACE4)
+ self.assertEqual(namespace.namespace, NAMESPACE4)
+ output = output.to_dict()
+
+ self.assertEqual(2, len(output['properties']))
+ actual = set([property for property in output['properties']])
+ expected = set([PROPERTY1, PROPERTY2])
+ self.assertEqual(actual, expected)
+
+ self.assertEqual(2, len(output['objects']))
+ actual = set([object.name for object in output['objects']])
+ expected = set([OBJECT1, OBJECT2])
+ self.assertEqual(actual, expected)
+
+ def test_namespace_create_conflict(self):
+ request = unit_test_utils.get_fake_request()
+
+ namespace = glance.api.v2.model.metadef_namespace.Namespace()
+ namespace.namespace = NAMESPACE1
+
+ self.assertRaises(webob.exc.HTTPConflict,
+ self.namespace_controller.create, request, namespace)
+
+ def test_namespace_update(self):
+ request = unit_test_utils.get_fake_request()
+ namespace = self.namespace_controller.show(request, NAMESPACE1)
+
+ namespace.protected = False
+ namespace = self.namespace_controller.update(request, namespace,
+ NAMESPACE1)
+ self.assertEqual(namespace.protected, False)
+
+ namespace = self.namespace_controller.show(request, NAMESPACE1)
+ self.assertEqual(namespace.protected, False)
+
+ def test_namespace_update_non_existing(self):
+ request = unit_test_utils.get_fake_request()
+
+ namespace = glance.api.v2.model.metadef_namespace.Namespace()
+ namespace.namespace = NAMESPACE4
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.namespace_controller.update, request, namespace,
+ NAMESPACE4)
+
+ def test_namespace_update_non_visible(self):
+ request = unit_test_utils.get_fake_request()
+
+ namespace = glance.api.v2.model.metadef_namespace.Namespace()
+ namespace.namespace = NAMESPACE2
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.namespace_controller.update, request, namespace,
+ NAMESPACE2)
+
+ def test_namespace_update_non_visible_admin(self):
+ request = unit_test_utils.get_fake_request(is_admin=True)
+ namespace = self.namespace_controller.show(request, NAMESPACE2)
+
+ namespace.protected = False
+ namespace = self.namespace_controller.update(request, namespace,
+ NAMESPACE2)
+ self.assertEqual(namespace.protected, False)
+
+ namespace = self.namespace_controller.show(request, NAMESPACE2)
+ self.assertEqual(namespace.protected, False)
+
+ def test_namespace_update_name(self):
+ request = unit_test_utils.get_fake_request()
+ namespace = self.namespace_controller.show(request, NAMESPACE1)
+
+ namespace.namespace = NAMESPACE4
+ namespace = self.namespace_controller.update(request, namespace,
+ NAMESPACE1)
+ self.assertEqual(namespace.namespace, NAMESPACE4)
+
+ namespace = self.namespace_controller.show(request, NAMESPACE4)
+ self.assertEqual(namespace.namespace, NAMESPACE4)
+
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.namespace_controller.show, request, NAMESPACE1)
+
+ def test_namespace_update_name_conflict(self):
+ request = unit_test_utils.get_fake_request()
+ namespace = self.namespace_controller.show(request, NAMESPACE1)
+ namespace.namespace = NAMESPACE2
+ self.assertRaises(webob.exc.HTTPConflict,
+ self.namespace_controller.update, request, namespace,
+ NAMESPACE1)
+
+ def test_property_index(self):
+ request = unit_test_utils.get_fake_request()
+ output = self.property_controller.index(request, NAMESPACE3)
+ self.assertEqual(2, len(output.properties))
+ actual = set([property for property in output.properties])
+ expected = set([PROPERTY1, PROPERTY2])
+ self.assertEqual(actual, expected)
+
+ def test_property_index_empty(self):
+ request = unit_test_utils.get_fake_request(tenant=TENANT2)
+ output = self.property_controller.index(request, NAMESPACE2)
+ self.assertEqual(0, len(output.properties))
+
+ def test_property_index_non_existing_namespace(self):
+ request = unit_test_utils.get_fake_request(tenant=TENANT2)
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.property_controller.index, request, NAMESPACE4)
+
+ def test_property_show(self):
+ request = unit_test_utils.get_fake_request()
+ output = self.property_controller.show(request, NAMESPACE3, PROPERTY1)
+ self.assertEqual(output.name, PROPERTY1)
+
+ def test_property_show_non_existing(self):
+ request = unit_test_utils.get_fake_request()
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.property_controller.show, request, NAMESPACE2,
+ PROPERTY1)
+
+ def test_property_show_non_visible(self):
+ request = unit_test_utils.get_fake_request(tenant=TENANT2)
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.property_controller.show, request, NAMESPACE1,
+ PROPERTY1)
+
+ def test_property_show_non_visible_admin(self):
+ request = unit_test_utils.get_fake_request(tenant=TENANT2,
+ is_admin=True)
+ output = self.property_controller.show(request, NAMESPACE1, PROPERTY1)
+ self.assertEqual(output.name, PROPERTY1)
+
+ def test_property_delete(self):
+ request = unit_test_utils.get_fake_request(tenant=TENANT3)
+ self.property_controller.delete(request, NAMESPACE3, PROPERTY1)
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.property_controller.show, request, NAMESPACE3,
+ PROPERTY1)
+
+ def test_property_delete_other_owner(self):
+ request = unit_test_utils.get_fake_request()
+ self.assertRaises(webob.exc.HTTPForbidden,
+ self.property_controller.delete, request, NAMESPACE3,
+ PROPERTY1)
+
+ def test_property_delete_other_owner_admin(self):
+ request = unit_test_utils.get_fake_request(is_admin=True)
+ self.property_controller.delete(request, NAMESPACE3, PROPERTY1)
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.property_controller.show, request, NAMESPACE3,
+ PROPERTY1)
+
+ def test_property_delete_non_existing(self):
+ request = unit_test_utils.get_fake_request()
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.property_controller.delete, request, NAMESPACE5,
+ PROPERTY2)
+
+ def test_property_delete_non_existing_namespace(self):
+ request = unit_test_utils.get_fake_request()
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.property_controller.delete, request, NAMESPACE4,
+ PROPERTY1)
+
+ def test_property_delete_non_visible(self):
+ request = unit_test_utils.get_fake_request(tenant=TENANT2)
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.property_controller.delete, request, NAMESPACE1,
+ PROPERTY1)
+
+ def test_property_delete_admin_protected(self):
+ request = unit_test_utils.get_fake_request(is_admin=True)
+ self.assertRaises(webob.exc.HTTPForbidden,
+ self.property_controller.delete, request, NAMESPACE1,
+ PROPERTY1)
+
+ def test_property_create(self):
+ request = unit_test_utils.get_fake_request()
+
+ property = glance.api.v2.model.metadef_property_type.PropertyType()
+ property.name = PROPERTY2
+ property.type = 'string'
+ property.title = 'title'
+ property = self.property_controller.create(request, NAMESPACE1,
+ property)
+ self.assertEqual(property.name, PROPERTY2)
+ self.assertEqual(property.type, 'string')
+ self.assertEqual(property.title, 'title')
+
+ property = self.property_controller.show(request, NAMESPACE1,
+ PROPERTY2)
+ self.assertEqual(property.name, PROPERTY2)
+ self.assertEqual(property.type, 'string')
+ self.assertEqual(property.title, 'title')
+
+ def test_property_create_conflict(self):
+ request = unit_test_utils.get_fake_request()
+
+ property = glance.api.v2.model.metadef_property_type.PropertyType()
+ property.name = PROPERTY1
+ property.type = 'string'
+ property.title = 'title'
+
+ self.assertRaises(webob.exc.HTTPConflict,
+ self.property_controller.create, request, NAMESPACE1,
+ property)
+
+ def test_property_create_non_visible_namespace(self):
+ request = unit_test_utils.get_fake_request(tenant=TENANT2)
+
+ property = glance.api.v2.model.metadef_property_type.PropertyType()
+ property.name = PROPERTY1
+ property.type = 'string'
+ property.title = 'title'
+
+ self.assertRaises(webob.exc.HTTPForbidden,
+ self.property_controller.create, request, NAMESPACE1,
+ property)
+
+ def test_property_create_non_visible_namespace_admin(self):
+ request = unit_test_utils.get_fake_request(tenant=TENANT2,
+ is_admin=True)
+
+ property = glance.api.v2.model.metadef_property_type.PropertyType()
+ property.name = PROPERTY2
+ property.type = 'string'
+ property.title = 'title'
+ property = self.property_controller.create(request, NAMESPACE1,
+ property)
+ self.assertEqual(property.name, PROPERTY2)
+ self.assertEqual(property.type, 'string')
+ self.assertEqual(property.title, 'title')
+
+ property = self.property_controller.show(request, NAMESPACE1,
+ PROPERTY2)
+ self.assertEqual(property.name, PROPERTY2)
+ self.assertEqual(property.type, 'string')
+ self.assertEqual(property.title, 'title')
+
+ def test_property_create_non_existing_namespace(self):
+ request = unit_test_utils.get_fake_request()
+
+ property = glance.api.v2.model.metadef_property_type.PropertyType()
+ property.name = PROPERTY1
+ property.type = 'string'
+ property.title = 'title'
+
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.property_controller.create, request, NAMESPACE4,
+ property)
+
+ def test_property_update(self):
+ request = unit_test_utils.get_fake_request(tenant=TENANT3)
+
+ property = self.property_controller.show(request, NAMESPACE3,
+ PROPERTY1)
+ property.name = PROPERTY1
+ property.type = 'string123'
+ property.title = 'title123'
+ property = self.property_controller.update(request, NAMESPACE3,
+ PROPERTY1, property)
+ self.assertEqual(property.name, PROPERTY1)
+ self.assertEqual(property.type, 'string123')
+ self.assertEqual(property.title, 'title123')
+
+ property = self.property_controller.show(request, NAMESPACE3,
+ PROPERTY1)
+ self.assertEqual(property.name, PROPERTY1)
+ self.assertEqual(property.type, 'string123')
+ self.assertEqual(property.title, 'title123')
+
+ def test_property_update_name(self):
+ request = unit_test_utils.get_fake_request(tenant=TENANT3)
+
+ property = self.property_controller.show(request, NAMESPACE3,
+ PROPERTY1)
+ property.name = PROPERTY3
+ property.type = 'string'
+ property.title = 'title'
+ property = self.property_controller.update(request, NAMESPACE3,
+ PROPERTY1, property)
+ self.assertEqual(property.name, PROPERTY3)
+ self.assertEqual(property.type, 'string')
+ self.assertEqual(property.title, 'title')
+
+ property = self.property_controller.show(request, NAMESPACE3,
+ PROPERTY2)
+ self.assertEqual(property.name, PROPERTY2)
+ self.assertEqual(property.type, 'string')
+ self.assertEqual(property.title, 'title')
+
+ def test_property_update_conflict(self):
+ request = unit_test_utils.get_fake_request(tenant=TENANT3)
+
+ property = self.property_controller.show(request, NAMESPACE3,
+ PROPERTY1)
+ property.name = PROPERTY2
+ property.type = 'string'
+ property.title = 'title'
+ self.assertRaises(webob.exc.HTTPConflict,
+ self.property_controller.update, request, NAMESPACE3,
+ PROPERTY1, property)
+
+ def test_property_update_non_existing(self):
+ request = unit_test_utils.get_fake_request(tenant=TENANT3)
+
+ property = glance.api.v2.model.metadef_property_type.PropertyType()
+ property.name = PROPERTY1
+ property.type = 'string'
+ property.title = 'title'
+
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.property_controller.update, request, NAMESPACE5,
+ PROPERTY1, property)
+
+ def test_property_update_namespace_non_existing(self):
+ request = unit_test_utils.get_fake_request(tenant=TENANT3)
+
+ property = glance.api.v2.model.metadef_property_type.PropertyType()
+ property.name = PROPERTY1
+ property.type = 'string'
+ property.title = 'title'
+
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.property_controller.update, request, NAMESPACE4,
+ PROPERTY1, property)
+
+ def test_object_index(self):
+ request = unit_test_utils.get_fake_request()
+ output = self.object_controller.index(request, NAMESPACE3)
+ output = output.to_dict()
+ self.assertEqual(2, len(output['objects']))
+ actual = set([object.name for object in output['objects']])
+ expected = set([OBJECT1, OBJECT2])
+ self.assertEqual(actual, expected)
+
+ def test_object_index_empty(self):
+ request = unit_test_utils.get_fake_request()
+ output = self.object_controller.index(request, NAMESPACE5)
+ output = output.to_dict()
+ self.assertEqual(0, len(output['objects']))
+
+ def test_object_index_non_existing_namespace(self):
+ request = unit_test_utils.get_fake_request()
+ self.assertRaises(webob.exc.HTTPNotFound, self.object_controller.index,
+ request, NAMESPACE4)
+
+ def test_object_show(self):
+ request = unit_test_utils.get_fake_request()
+ output = self.object_controller.show(request, NAMESPACE3, OBJECT1)
+ self.assertEqual(output.name, OBJECT1)
+
+ def test_object_show_non_existing(self):
+ request = unit_test_utils.get_fake_request()
+ self.assertRaises(webob.exc.HTTPNotFound, self.object_controller.show,
+ request, NAMESPACE5, OBJECT1)
+
+ def test_object_show_non_visible(self):
+ request = unit_test_utils.get_fake_request(tenant=TENANT2)
+ self.assertRaises(webob.exc.HTTPNotFound, self.object_controller.show,
+ request, NAMESPACE1, OBJECT1)
+
+ def test_object_show_non_visible_admin(self):
+ request = unit_test_utils.get_fake_request(tenant=TENANT2,
+ is_admin=True)
+
+ output = self.object_controller.show(request, NAMESPACE1, OBJECT1)
+ self.assertEqual(output.name, OBJECT1)
+
+ def test_object_delete(self):
+ request = unit_test_utils.get_fake_request(tenant=TENANT3)
+ self.object_controller.delete(request, NAMESPACE3, OBJECT1)
+ self.assertRaises(webob.exc.HTTPNotFound, self.object_controller.show,
+ request, NAMESPACE3, OBJECT1)
+
+ def test_object_delete_other_owner(self):
+ request = unit_test_utils.get_fake_request()
+ self.assertRaises(webob.exc.HTTPForbidden,
+ self.object_controller.delete, request, NAMESPACE3,
+ OBJECT1)
+
+ def test_object_delete_other_owner_admin(self):
+ request = unit_test_utils.get_fake_request(is_admin=True)
+ self.object_controller.delete(request, NAMESPACE3, OBJECT1)
+ self.assertRaises(webob.exc.HTTPNotFound, self.object_controller.show,
+ request, NAMESPACE3, OBJECT1)
+
+ def test_object_delete_non_existing(self):
+ request = unit_test_utils.get_fake_request()
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.object_controller.delete, request, NAMESPACE5,
+ OBJECT1)
+
+ def test_object_delete_non_existing_namespace(self):
+ request = unit_test_utils.get_fake_request()
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.object_controller.delete, request, NAMESPACE4,
+ OBJECT1)
+
+ def test_object_delete_non_visible(self):
+ request = unit_test_utils.get_fake_request(tenant=TENANT2)
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.object_controller.delete, request, NAMESPACE1,
+ OBJECT1)
+
+ def test_object_delete_admin_protected(self):
+ request = unit_test_utils.get_fake_request(is_admin=True)
+ self.assertRaises(webob.exc.HTTPForbidden,
+ self.object_controller.delete, request, NAMESPACE1,
+ OBJECT1)
+
+ def test_object_create(self):
+ request = unit_test_utils.get_fake_request()
+
+ object = glance.api.v2.model.metadef_object.MetadefObject()
+ object.name = OBJECT2
+ object.required = []
+ object.properties = {}
+ object = self.object_controller.create(request, object, NAMESPACE1)
+ self.assertEqual(object.name, OBJECT2)
+ self.assertEqual(object.required, [])
+ self.assertEqual(object.properties, {})
+
+ object = self.object_controller.show(request, NAMESPACE1, OBJECT2)
+ self.assertEqual(object.name, OBJECT2)
+ self.assertEqual(object.required, [])
+ self.assertEqual(object.properties, {})
+
+ def test_object_create_conflict(self):
+ request = unit_test_utils.get_fake_request()
+
+ object = glance.api.v2.model.metadef_object.MetadefObject()
+ object.name = OBJECT1
+ object.required = []
+ object.properties = {}
+
+ self.assertRaises(webob.exc.HTTPConflict,
+ self.object_controller.create, request, object,
+ NAMESPACE1)
+
+ def test_object_create_non_existing_namespace(self):
+ request = unit_test_utils.get_fake_request()
+
+ object = glance.api.v2.model.metadef_object.MetadefObject()
+ object.name = PROPERTY1
+ object.required = []
+ object.properties = {}
+
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.object_controller.create, request, object,
+ NAMESPACE4)
+
+ def test_object_create_non_visible_namespace(self):
+ request = unit_test_utils.get_fake_request(tenant=TENANT2)
+
+ object = glance.api.v2.model.metadef_object.MetadefObject()
+ object.name = OBJECT1
+ object.required = []
+ object.properties = {}
+
+ self.assertRaises(webob.exc.HTTPForbidden,
+ self.object_controller.create, request, object,
+ NAMESPACE1)
+
+ def test_object_create_non_visible_namespace_admin(self):
+ request = unit_test_utils.get_fake_request(tenant=TENANT2,
+ is_admin=True)
+
+ object = glance.api.v2.model.metadef_object.MetadefObject()
+ object.name = OBJECT2
+ object.required = []
+ object.properties = {}
+ object = self.object_controller.create(request, object, NAMESPACE1)
+ self.assertEqual(object.name, OBJECT2)
+ self.assertEqual(object.required, [])
+ self.assertEqual(object.properties, {})
+
+ object = self.object_controller.show(request, NAMESPACE1, OBJECT2)
+ self.assertEqual(object.name, OBJECT2)
+ self.assertEqual(object.required, [])
+ self.assertEqual(object.properties, {})
+
+ def test_object_update(self):
+ request = unit_test_utils.get_fake_request(tenant=TENANT3)
+
+ object = self.object_controller.show(request, NAMESPACE3, OBJECT1)
+ object.name = OBJECT1
+ object.description = 'description'
+ object = self.object_controller.update(request, object, NAMESPACE3,
+ OBJECT1)
+ self.assertEqual(object.name, OBJECT1)
+ self.assertEqual(object.description, 'description')
+
+ property = self.object_controller.show(request, NAMESPACE3, OBJECT1)
+ self.assertEqual(property.name, OBJECT1)
+ self.assertEqual(object.description, 'description')
+
+ def test_object_update_name(self):
+ request = unit_test_utils.get_fake_request()
+
+ object = self.object_controller.show(request, NAMESPACE1, OBJECT1)
+ object.name = OBJECT2
+ object = self.object_controller.update(request, object, NAMESPACE1,
+ OBJECT1)
+ self.assertEqual(object.name, OBJECT2)
+
+ object = self.object_controller.show(request, NAMESPACE1, OBJECT2)
+ self.assertEqual(object.name, OBJECT2)
+
+ def test_object_update_conflict(self):
+ request = unit_test_utils.get_fake_request(tenant=TENANT3)
+
+ object = self.object_controller.show(request, NAMESPACE3, OBJECT1)
+ object.name = OBJECT2
+ self.assertRaises(webob.exc.HTTPConflict,
+ self.object_controller.update, request, object,
+ NAMESPACE3, OBJECT1)
+
+ def test_object_update_non_existing(self):
+ request = unit_test_utils.get_fake_request(tenant=TENANT3)
+
+ object = glance.api.v2.model.metadef_object.MetadefObject()
+ object.name = OBJECT1
+ object.required = []
+ object.properties = {}
+
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.object_controller.update, request, object,
+ NAMESPACE5, OBJECT1)
+
+ def test_object_update_namespace_non_existing(self):
+ request = unit_test_utils.get_fake_request(tenant=TENANT3)
+
+ object = glance.api.v2.model.metadef_object.MetadefObject()
+ object.name = OBJECT1
+ object.required = []
+ object.properties = {}
+
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.object_controller.update, request, object,
+ NAMESPACE4, OBJECT1)
+
+ def test_resource_type_index(self):
+ request = unit_test_utils.get_fake_request()
+ output = self.rt_controller.index(request)
+
+ self.assertEqual(2, len(output.resource_types))
+ actual = set([type.name for type in
+ output.resource_types])
+ expected = set([RESOURCE_TYPE1, RESOURCE_TYPE2])
+ self.assertEqual(actual, expected)
+
+ def test_resource_type_show(self):
+ request = unit_test_utils.get_fake_request()
+ output = self.rt_controller.show(request, NAMESPACE3)
+
+ self.assertEqual(1, len(output.resource_type_associations))
+ actual = set([rt.name for rt in output.resource_type_associations])
+ expected = set([RESOURCE_TYPE1])
+ self.assertEqual(actual, expected)
+
+ def test_resource_type_show_empty(self):
+ request = unit_test_utils.get_fake_request()
+ output = self.rt_controller.show(request, NAMESPACE5)
+
+ self.assertEqual(0, len(output.resource_type_associations))
+
+ def test_resource_type_show_non_visible(self):
+ request = unit_test_utils.get_fake_request()
+
+ self.assertRaises(webob.exc.HTTPNotFound, self.rt_controller.show,
+ request, NAMESPACE2)
+
+ def test_resource_type_show_non_visible_admin(self):
+ request = unit_test_utils.get_fake_request(tenant=TENANT2,
+ is_admin=True)
+
+ output = self.rt_controller.show(request, NAMESPACE2)
+ self.assertEqual(2, len(output.resource_type_associations))
+ actual = set([rt.name for rt in output.resource_type_associations])
+ expected = set([RESOURCE_TYPE1, RESOURCE_TYPE2])
+ self.assertEqual(actual, expected)
+
+ def test_resource_type_show_non_existing_namespace(self):
+ request = unit_test_utils.get_fake_request()
+
+ self.assertRaises(webob.exc.HTTPNotFound, self.rt_controller.show,
+ request, NAMESPACE4)
+
+ def test_resource_type_association_delete(self):
+ request = unit_test_utils.get_fake_request(tenant=TENANT3)
+ self.rt_controller.delete(request, NAMESPACE3, RESOURCE_TYPE1)
+
+ output = self.rt_controller.show(request, NAMESPACE3)
+ self.assertEqual(0, len(output.resource_type_associations))
+
+ def test_resource_type_association_delete_other_owner(self):
+ request = unit_test_utils.get_fake_request()
+ self.assertRaises(webob.exc.HTTPForbidden, self.rt_controller.delete,
+ request, NAMESPACE3, RESOURCE_TYPE1)
+
+ def test_resource_type_association_delete_other_owner_admin(self):
+ request = unit_test_utils.get_fake_request(is_admin=True)
+ self.rt_controller.delete(request, NAMESPACE3, RESOURCE_TYPE1)
+
+ output = self.rt_controller.show(request, NAMESPACE3)
+ self.assertEqual(0, len(output.resource_type_associations))
+
+ def test_resource_type_association_delete_non_existing(self):
+ request = unit_test_utils.get_fake_request()
+ self.assertRaises(webob.exc.HTTPNotFound, self.rt_controller.delete,
+ request, NAMESPACE1, RESOURCE_TYPE2)
+
+ def test_resource_type_association_delete_non_existing_namespace(self):
+ request = unit_test_utils.get_fake_request()
+ self.assertRaises(webob.exc.HTTPNotFound, self.rt_controller.delete,
+ request, NAMESPACE4, RESOURCE_TYPE1)
+
+ def test_resource_type_association_delete_non_visible(self):
+ request = unit_test_utils.get_fake_request(tenant=TENANT3)
+ self.assertRaises(webob.exc.HTTPNotFound, self.rt_controller.delete,
+ request, NAMESPACE1, RESOURCE_TYPE1)
+
+ def test_resource_type_association_delete_protected_admin(self):
+ request = unit_test_utils.get_fake_request(is_admin=True)
+ self.assertRaises(webob.exc.HTTPForbidden, self.rt_controller.delete,
+ request, NAMESPACE1, RESOURCE_TYPE1)
+
+ def test_resource_type_association_create(self):
+ request = unit_test_utils.get_fake_request()
+
+ rt = glance.api.v2.model.metadef_resource_type.\
+ ResourceTypeAssociation()
+ rt.name = RESOURCE_TYPE2
+ rt.prefix = 'pref'
+ rt = self.rt_controller.create(request, rt, NAMESPACE1)
+ self.assertEqual(rt.name, RESOURCE_TYPE2)
+ self.assertEqual(rt.prefix, 'pref')
+
+ output = self.rt_controller.show(request, NAMESPACE1)
+ self.assertEqual(2, len(output.resource_type_associations))
+ actual = set([x.name for x in output.resource_type_associations])
+ expected = set([RESOURCE_TYPE1, RESOURCE_TYPE2])
+ self.assertEqual(actual, expected)
+
+ def test_resource_type_association_create_conflict(self):
+ request = unit_test_utils.get_fake_request()
+
+ rt = glance.api.v2.model.metadef_resource_type.\
+ ResourceTypeAssociation()
+ rt.name = RESOURCE_TYPE1
+ rt.prefix = 'pref'
+ self.assertRaises(webob.exc.HTTPConflict, self.rt_controller.create,
+ request, rt, NAMESPACE1)
+
+ def test_resource_type_association_create_non_existing_namespace(self):
+ request = unit_test_utils.get_fake_request()
+
+ rt = glance.api.v2.model.metadef_resource_type.\
+ ResourceTypeAssociation()
+ rt.name = RESOURCE_TYPE1
+ rt.prefix = 'pref'
+ self.assertRaises(webob.exc.HTTPNotFound, self.rt_controller.create,
+ request, rt, NAMESPACE4)
+
+ def test_resource_type_association_create_non_existing_resource_type(self):
+ request = unit_test_utils.get_fake_request()
+
+ rt = glance.api.v2.model.metadef_resource_type.\
+ ResourceTypeAssociation()
+ rt.name = RESOURCE_TYPE3
+ rt.prefix = 'pref'
+ self.assertRaises(webob.exc.HTTPNotFound, self.rt_controller.create,
+ request, rt, NAMESPACE1)
+
+ def test_resource_type_association_create_non_visible_namespace(self):
+ request = unit_test_utils.get_fake_request(tenant=TENANT2)
+
+ rt = glance.api.v2.model.metadef_resource_type.\
+ ResourceTypeAssociation()
+ rt.name = RESOURCE_TYPE2
+ rt.prefix = 'pref'
+ self.assertRaises(webob.exc.HTTPForbidden, self.rt_controller.create,
+ request, rt, NAMESPACE1)
+
+ def test_resource_type_association_create_non_visible_namesp_admin(self):
+ request = unit_test_utils.get_fake_request(tenant=TENANT2,
+ is_admin=True)
+
+ rt = glance.api.v2.model.metadef_resource_type.\
+ ResourceTypeAssociation()
+ rt.name = RESOURCE_TYPE2
+ rt.prefix = 'pref'
+ rt = self.rt_controller.create(request, rt, NAMESPACE1)
+ self.assertEqual(rt.name, RESOURCE_TYPE2)
+ self.assertEqual(rt.prefix, 'pref')
+
+ output = self.rt_controller.show(request, NAMESPACE1)
+ self.assertEqual(2, len(output.resource_type_associations))
+ actual = set([x.name for x in output.resource_type_associations])
+ expected = set([RESOURCE_TYPE1, RESOURCE_TYPE2])
+ self.assertEqual(actual, expected)
diff --git a/requirements.txt b/requirements.txt
index e5287b94ae..ca97170ec9 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -24,6 +24,7 @@ oslo.config>=1.4.0.0a3
stevedore>=0.14
netaddr>=0.7.6
keystonemiddleware>=1.0.0
+WSME>=0.6
# For openstack/common/lockutils
posix_ipc