From 2e7de07c5a7c8f9d11c00499f7e85ac30f71d025 Mon Sep 17 00:00:00 2001 From: Wayne Okuma Date: Tue, 26 Aug 2014 03:22:58 -0400 Subject: [PATCH] Glance Metadata Definitions Catalog - API Implements: blueprint metadata-schema-catalog 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 catalogue will not store the values for specific instance properties. - REST API for CRUD on metadef namespace - REST API for CRUD on metadef objects - REST API for CRUD on metadef properites - REST API for CRUD on metadef resource types - REST API for JSON schemas on metadef API's Change-Id: I8e6d88ffee9a9337bf82b1da85648ba638a154ab DocImpact Co-Authored-By: Lakshmi N Sampath Co-Authored-By: Wayne Okuma Co-Authored-By: Travis Tripp Co-Authored-By: Pawel Koniszewski Co-Authored-By: Michal Jastrzebski Co-Authored-By: Michal Dulko --- doc/source/glancemetadefcatalogapi.rst | 510 ++++++++ doc/source/index.rst | 1 + etc/policy.json | 21 +- glance/api/authorization.py | 353 ++++++ glance/api/policy.py | 208 +++ glance/api/v2/metadef_namespaces.py | 744 +++++++++++ glance/api/v2/metadef_objects.py | 335 +++++ glance/api/v2/metadef_properties.py | 275 ++++ glance/api/v2/metadef_resource_types.py | 264 ++++ glance/api/v2/model/__init__.py | 0 glance/api/v2/model/metadef_namespace.py | 79 ++ glance/api/v2/model/metadef_object.py | 49 + .../v2/model/metadef_property_item_type.py | 27 + glance/api/v2/model/metadef_property_type.py | 59 + glance/api/v2/model/metadef_resource_type.py | 62 + glance/api/v2/router.py | 254 ++++ glance/api/v2/schemas.py | 45 + glance/common/wsme_utils.py | 71 ++ glance/db/__init__.py | 367 +++++- glance/domain/__init__.py | 133 ++ glance/domain/proxy.py | 242 ++++ glance/gateway.py | 67 + glance/schema.py | 93 +- .../functional/v2/test_metadef_namespaces.py | 177 +++ .../functional/v2/test_metadef_objects.py | 263 ++++ .../functional/v2/test_metadef_properties.py | 180 +++ .../v2/test_metadef_resourcetypes.py | 268 ++++ glance/tests/unit/test_db_metadef.py | 428 +++++++ .../tests/unit/v2/test_metadef_resources.py | 1116 +++++++++++++++++ requirements.txt | 1 + 30 files changed, 6687 insertions(+), 5 deletions(-) create mode 100644 doc/source/glancemetadefcatalogapi.rst create mode 100644 glance/api/v2/metadef_namespaces.py create mode 100644 glance/api/v2/metadef_objects.py create mode 100644 glance/api/v2/metadef_properties.py create mode 100644 glance/api/v2/metadef_resource_types.py create mode 100644 glance/api/v2/model/__init__.py create mode 100644 glance/api/v2/model/metadef_namespace.py create mode 100644 glance/api/v2/model/metadef_object.py create mode 100644 glance/api/v2/model/metadef_property_item_type.py create mode 100644 glance/api/v2/model/metadef_property_type.py create mode 100644 glance/api/v2/model/metadef_resource_type.py create mode 100644 glance/common/wsme_utils.py create mode 100644 glance/tests/functional/v2/test_metadef_namespaces.py create mode 100644 glance/tests/functional/v2/test_metadef_objects.py create mode 100644 glance/tests/functional/v2/test_metadef_properties.py create mode 100644 glance/tests/functional/v2/test_metadef_resourcetypes.py create mode 100644 glance/tests/unit/test_db_metadef.py create mode 100644 glance/tests/unit/v2/test_metadef_resources.py 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