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