diff --git a/etc/policy.json b/etc/policy.json index 325f00b21f..511fe8ec35 100644 --- a/etc/policy.json +++ b/etc/policy.json @@ -47,6 +47,12 @@ "get_metadef_property":"", "get_metadef_properties":"", "modify_metadef_property":"", - "add_metadef_property":"" + "add_metadef_property":"", + + "get_metadef_tag":"", + "get_metadef_tags":"", + "modify_metadef_tag":"", + "add_metadef_tag":"", + "add_metadef_tags":"" } diff --git a/glance/api/authorization.py b/glance/api/authorization.py index 6ae30085b7..811c97af48 100644 --- a/glance/api/authorization.py +++ b/glance/api/authorization.py @@ -815,3 +815,88 @@ class MetadefPropertyRepoProxy(glance.domain.proxy.MetadefPropertyRepo): *args, **kwargs) return [proxy_namespace_property(self.context, namespace_property) for namespace_property in namespace_properties] + + +# Metadef Tag classes +def is_tag_mutable(context, tag): + """Return True if the tag is mutable in this context.""" + if context.is_admin: + return True + + if context.owner is None: + return False + + return tag.namespace.owner == context.owner + + +def proxy_tag(context, tag): + if is_tag_mutable(context, tag): + return tag + else: + return ImmutableMetadefTagProxy(tag) + + +class ImmutableMetadefTagProxy(object): + + def __init__(self, base): + self.base = base + self.resource_name = 'tag' + + tag_id = _immutable_attr('base', 'tag_id') + name = _immutable_attr('base', 'name') + 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 tag.") + raise exception.Forbidden(message) + + def save(self): + message = _("You are not permitted to update this tag.") + raise exception.Forbidden(message) + + +class MetadefTagProxy(glance.domain.proxy.MetadefTag): + + def __init__(self, meta_tag): + super(MetadefTagProxy, self).__init__(meta_tag) + + +class MetadefTagFactoryProxy(glance.domain.proxy.MetadefTagFactory): + + def __init__(self, meta_tag_factory, context): + self.meta_tag_factory = meta_tag_factory + self.context = context + super(MetadefTagFactoryProxy, self).__init__( + meta_tag_factory, + meta_tag_proxy_class=MetadefTagProxy) + + def new_tag(self, **kwargs): + owner = kwargs.pop('owner', self.context.owner) + if not self.context.is_admin: + if owner is None: + message = _("Owner must be specified to create a tag.") + raise exception.Forbidden(message) + elif owner != self.context.owner: + message = _("You are not permitted to create a tag" + " in the namespace owned by '%s'") + raise exception.Forbidden(message % (owner)) + + return super(MetadefTagFactoryProxy, self).new_tag(**kwargs) + + +class MetadefTagRepoProxy(glance.domain.proxy.MetadefTagRepo): + + def __init__(self, tag_repo, context): + self.tag_repo = tag_repo + self.context = context + super(MetadefTagRepoProxy, self).__init__(tag_repo) + + def get(self, namespace, tag_name): + meta_tag = self.tag_repo.get(namespace, tag_name) + return proxy_tag(self.context, meta_tag) + + def list(self, *args, **kwargs): + tags = self.tag_repo.list(*args, **kwargs) + return [proxy_tag(self.context, meta_tag) for + meta_tag in tags] diff --git a/glance/api/policy.py b/glance/api/policy.py index 6bc7655509..c6257e5a81 100755 --- a/glance/api/policy.py +++ b/glance/api/policy.py @@ -594,3 +594,58 @@ class MetadefPropertyFactoryProxy(glance.domain.proxy.MetadefPropertyFactory): namespace_property_factory, property_proxy_class=MetadefPropertyProxy, property_proxy_kwargs=proxy_kwargs) + + +# Metadef Tag classes +class MetadefTagProxy(glance.domain.proxy.MetadefTag): + + def __init__(self, meta_tag, context, policy): + self.context = context + self.policy = policy + super(MetadefTagProxy, self).__init__(meta_tag) + + +class MetadefTagRepoProxy(glance.domain.proxy.MetadefTagRepo): + + def __init__(self, tag_repo, context, tag_policy): + self.context = context + self.policy = tag_policy + self.tag_repo = tag_repo + proxy_kwargs = {'context': self.context, 'policy': self.policy} + super(MetadefTagRepoProxy, + self).__init__(tag_repo, + tag_proxy_class=MetadefTagProxy, + tag_proxy_kwargs=proxy_kwargs) + + def get(self, namespace, tag_name): + self.policy.enforce(self.context, 'get_metadef_tag', {}) + return super(MetadefTagRepoProxy, self).get(namespace, tag_name) + + def list(self, *args, **kwargs): + self.policy.enforce(self.context, 'get_metadef_tags', {}) + return super(MetadefTagRepoProxy, self).list(*args, **kwargs) + + def save(self, meta_tag): + self.policy.enforce(self.context, 'modify_metadef_tag', {}) + return super(MetadefTagRepoProxy, self).save(meta_tag) + + def add(self, meta_tag): + self.policy.enforce(self.context, 'add_metadef_tag', {}) + return super(MetadefTagRepoProxy, self).add(meta_tag) + + def add_tags(self, meta_tags): + self.policy.enforce(self.context, 'add_metadef_tags', {}) + return super(MetadefTagRepoProxy, self).add_tags(meta_tags) + + +class MetadefTagFactoryProxy(glance.domain.proxy.MetadefTagFactory): + + def __init__(self, meta_tag_factory, context, policy): + self.meta_tag_factory = meta_tag_factory + self.context = context + self.policy = policy + proxy_kwargs = {'context': self.context, 'policy': self.policy} + super(MetadefTagFactoryProxy, self).__init__( + meta_tag_factory, + meta_tag_proxy_class=MetadefTagProxy, + meta_tag_proxy_kwargs=proxy_kwargs) diff --git a/glance/api/v2/metadef_namespaces.py b/glance/api/v2/metadef_namespaces.py index 3fdffbefa0..2cb1bacc5e 100644 --- a/glance/api/v2/metadef_namespaces.py +++ b/glance/api/v2/metadef_namespaces.py @@ -26,6 +26,7 @@ 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.api.v2.model.metadef_tag import MetadefTag from glance.common import exception from glance.common import utils from glance.common import wsgi @@ -54,6 +55,7 @@ class NamespaceController(object): policy_enforcer=self.policy) self.ns_schema_link = '/v2/schemas/metadefs/namespace' self.obj_schema_link = '/v2/schemas/metadefs/object' + self.tag_schema_link = '/v2/schemas/metadefs/tag' def index(self, req, marker=None, limit=None, sort_key='created_at', sort_dir='desc', filters=None): @@ -112,10 +114,11 @@ class NamespaceController(object): 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: + rs_factory = (self.gateway.get_metadef_resource_type_factory( + req.context)) + rs_repo = self.gateway.get_metadef_resource_type_repo( + req.context) for resource_type in namespace.resource_type_associations: new_resource = rs_factory.new_resource_type( namespace=namespace.namespace, @@ -123,22 +126,34 @@ class NamespaceController(object): 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: + object_factory = self.gateway.get_metadef_object_factory( + req.context) + object_repo = self.gateway.get_metadef_object_repo( + req.context) 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 Tags + if namespace.tags: + tag_factory = self.gateway.get_metadef_tag_factory( + req.context) + tag_repo = self.gateway.get_metadef_tag_repo(req.context) + for metadata_tag in namespace.tags: + new_meta_tag = tag_factory.new_tag( + namespace=namespace.namespace, + **metadata_tag.to_dict()) + tag_repo.add(new_meta_tag) + # 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: + prop_factory = (self.gateway.get_metadef_property_factory( + req.context)) + prop_repo = self.gateway.get_metadef_property_repo( + req.context) for (name, value) in namespace.properties.items(): new_property_type = ( prop_factory.new_namespace_property( @@ -165,6 +180,7 @@ class NamespaceController(object): new_namespace.objects = namespace.objects new_namespace.resource_type_associations = ( namespace.resource_type_associations) + new_namespace.tags = namespace.tags return Namespace.to_wsme_model(new_namespace, get_namespace_href(new_namespace), self.ns_schema_link) @@ -232,6 +248,14 @@ class NamespaceController(object): namespace_detail = self._prefix_property_name( namespace_detail, filters['resource_type']) + # Get tags + tag_repo = self.gateway.get_metadef_tag_repo(req.context) + db_metatag_list = tag_repo.list(filters=ns_filters) + tag_list = [MetadefTag(**{'name': db_metatag.name}) + for db_metatag in db_metatag_list] + if tag_list: + namespace_detail.tags = tag_list + except exception.Forbidden as e: raise webob.exc.HTTPForbidden(explanation=e.msg) except exception.NotFound as e: @@ -299,6 +323,20 @@ class NamespaceController(object): LOG.error(utils.exception_to_str(e)) raise webob.exc.HTTPInternalServerError() + def delete_tags(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_tags(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: @@ -700,7 +738,18 @@ def _get_base_properties(): }, } } - } + }, + "tags": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + } + } + }, } @@ -733,6 +782,12 @@ def get_object_href(namespace_name, metadef_object): return base_href +def get_tag_href(namespace_name, metadef_tag): + base_href = ('/v2/metadefs/namespaces/%s/tags/%s' % + (namespace_name, metadef_tag.name)) + return base_href + + def create_resource(): """Namespaces resource factory method""" schema = get_schema() diff --git a/glance/api/v2/metadef_tags.py b/glance/api/v2/metadef_tags.py new file mode 100644 index 0000000000..bdd363c6da --- /dev/null +++ b/glance/api/v2/metadef_tags.py @@ -0,0 +1,391 @@ +# 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 +from oslo.serialization import jsonutils +import six +import webob.exc +from wsme.rest import json + +from glance.api import policy +from glance.api.v2.model.metadef_tag import MetadefTag +from glance.api.v2.model.metadef_tag import MetadefTags +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 +import glance.openstack.common.log as logging +import glance.schema + +LOG = logging.getLogger(__name__) +_ = i18n._ +_LE = i18n._LE +_LI = i18n._LI + +CONF = cfg.CONF + + +class TagsController(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.tag_schema_link = '/v2/schemas/metadefs/tag' + + def create(self, req, metadata_tag, namespace): + tag_factory = self.gateway.get_metadef_tag_factory(req.context) + tag_repo = self.gateway.get_metadef_tag_repo(req.context) + try: + new_meta_tag = tag_factory.new_tag( + namespace=namespace, + **metadata_tag.to_dict()) + tag_repo.add(new_meta_tag) + 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 MetadefTag.to_wsme_model(new_meta_tag) + + def create_tags(self, req, metadata_tags, namespace): + tag_factory = self.gateway.get_metadef_tag_factory(req.context) + tag_repo = self.gateway.get_metadef_tag_repo(req.context) + try: + tag_list = [] + for metadata_tag in metadata_tags.tags: + tag_list.append(tag_factory.new_tag( + namespace=namespace, **metadata_tag.to_dict())) + tag_repo.add_tags(tag_list) + tag_list_out = [MetadefTag(**{'name': db_metatag.name}) + for db_metatag in tag_list] + metadef_tags = MetadefTags() + metadef_tags.tags = tag_list_out + 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 metadef_tags + + 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 + + tag_repo = self.gateway.get_metadef_tag_repo(req.context) + if marker: + metadef_tag = tag_repo.get(namespace, marker) + marker = metadef_tag.tag_id + + db_metatag_list = tag_repo.list( + marker=marker, limit=limit, sort_key=sort_key, + sort_dir=sort_dir, filters=filters) + + tag_list = [MetadefTag(**{'name': db_metatag.name}) + for db_metatag in db_metatag_list] + + metadef_tags = MetadefTags() + metadef_tags.tags = tag_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_tags + + def show(self, req, namespace, tag_name): + meta_tag_repo = self.gateway.get_metadef_tag_repo(req.context) + try: + metadef_tag = meta_tag_repo.get(namespace, tag_name) + return MetadefTag.to_wsme_model(metadef_tag) + 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_tag, namespace, tag_name): + meta_repo = self.gateway.get_metadef_tag_repo(req.context) + try: + metadef_tag = meta_repo.get(namespace, tag_name) + metadef_tag.name = wsme_utils._get_value( + metadata_tag.name) + updated_metadata_tag = meta_repo.save(metadef_tag) + 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 MetadefTag.to_wsme_model(updated_metadata_tag) + + def delete(self, req, namespace, tag_name): + meta_repo = self.gateway.get_metadef_tag_repo(req.context) + try: + metadef_tag = meta_repo.get(namespace, tag_name) + metadef_tag.delete() + meta_repo.remove(metadef_tag) + 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 None + + +def _get_base_properties(): + return { + "name": { + "type": "string" + }, + "created_at": { + "type": "string", + "description": _("Date and time of tag creation" + " (READ-ONLY)"), + "format": "date-time" + }, + "updated_at": { + "type": "string", + "description": _("Date and time of the last tag modification" + " (READ-ONLY)"), + "format": "date-time" + } + } + + +def _get_base_properties_for_list(): + return { + "tags": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + 'required': ['name'], + "additionalProperties": False + } + }, + } + + +def get_schema(): + definitions = _get_base_definitions() + properties = _get_base_properties() + mandatory_attrs = MetadefTag.get_mandatory_attrs() + schema = glance.schema.Schema( + 'tag', + properties, + required=mandatory_attrs, + definitions=definitions, + ) + return schema + + +def get_schema_for_list(): + definitions = _get_base_definitions() + properties = _get_base_properties_for_list() + schema = glance.schema.Schema( + 'tags', + properties, + required=None, + definitions=definitions, + ) + return schema + + +def get_collection_schema(): + tag_schema = get_schema() + return glance.schema.CollectionSchema('tags', tag_schema) + + +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() + self.schema_for_list = get_schema_for_list() + + 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 _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 + + 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 _create_or_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_tag = json.fromjson(MetadefTag, body) + return dict(metadata_tag=metadata_tag) + + 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: + query_params['marker'] = marker + + if limit: + query_params['limit'] = self._validate_limit(limit) + + return query_params + + def create(self, request): + return self._create_or_update(request) + + def create_tags(self, request): + body = self._get_request_body(request) + self._check_allowed(body) + try: + self.schema_for_list.validate(body) + except exception.InvalidObject as e: + raise webob.exc.HTTPBadRequest(explanation=e.msg) + metadata_tags = json.fromjson(MetadefTags, body) + return dict(metadata_tags=metadata_tags) + + def update(self, request): + return self._create_or_update(request) + + @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_tag): + response.status_int = 201 + self.show(response, metadata_tag) + + def create_tags(self, response, result): + response.status_int = 201 + metadata_tags_json = json.tojson(MetadefTags, result) + body = jsonutils.dumps(metadata_tags_json, ensure_ascii=False) + response.unicode_body = six.text_type(body) + response.content_type = 'application/json' + + def show(self, response, metadata_tag): + metadata_tag_json = json.tojson(MetadefTag, metadata_tag) + body = jsonutils.dumps(metadata_tag_json, ensure_ascii=False) + response.unicode_body = six.text_type(body) + response.content_type = 'application/json' + + def update(self, response, metadata_tag): + response.status_int = 200 + self.show(response, metadata_tag) + + def index(self, response, result): + metadata_tags_json = json.tojson(MetadefTags, result) + body = jsonutils.dumps(metadata_tags_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_tag_href(namespace_name, metadef_tag): + base_href = ('/v2/metadefs/namespaces/%s/tags/%s' % + (namespace_name, metadef_tag.name)) + return base_href + + +def create_resource(): + """Metadef tags resource factory method""" + schema = get_schema() + deserializer = RequestDeserializer(schema) + serializer = ResponseSerializer(schema) + controller = TagsController() + return wsgi.Resource(controller, deserializer, serializer) diff --git a/glance/api/v2/model/metadef_namespace.py b/glance/api/v2/model/metadef_namespace.py index f951d40074..9544f369ac 100644 --- a/glance/api/v2/model/metadef_namespace.py +++ b/glance/api/v2/model/metadef_namespace.py @@ -20,6 +20,7 @@ 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.api.v2.model.metadef_tag import MetadefTag from glance.common.wsme_utils import WSMEModelTransformer @@ -43,6 +44,7 @@ class Namespace(types.Base, WSMEModelTransformer): mandatory=False) properties = wsme.wsattr({types.text: PropertyType}, mandatory=False) objects = wsme.wsattr([MetadefObject], mandatory=False) + tags = wsme.wsattr([MetadefTag], mandatory=False) # Generated fields self = wsme.wsattr(types.text, mandatory=False) diff --git a/glance/api/v2/model/metadef_tag.py b/glance/api/v2/model/metadef_tag.py new file mode 100644 index 0000000000..147b087db0 --- /dev/null +++ b/glance/api/v2/model/metadef_tag.py @@ -0,0 +1,34 @@ +# 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 import wsme_utils + + +class MetadefTag(types.Base, wsme_utils.WSMEModelTransformer): + + name = wsme.wsattr(types.text, mandatory=True) + + # Not using datetime since time format has to be + # in oslo.utils.timeutils.isotime() format + created_at = wsme.wsattr(types.text, mandatory=False) + updated_at = wsme.wsattr(types.text, mandatory=False) + + +class MetadefTags(types.Base, wsme_utils.WSMEModelTransformer): + + tags = wsme.wsattr([MetadefTag], mandatory=False) diff --git a/glance/api/v2/router.py b/glance/api/v2/router.py index 743537d11a..a5db75cbfe 100644 --- a/glance/api/v2/router.py +++ b/glance/api/v2/router.py @@ -21,6 +21,7 @@ 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 metadef_tags from glance.api.v2 import schemas from glance.api.v2 import tasks from glance.common import wsgi @@ -186,6 +187,28 @@ class API(wsgi.Router): conditions={'method': ['POST', 'PUT', 'DELETE', 'PATCH', 'HEAD']}) + mapper.connect('/schemas/metadefs/tag', + controller=schemas_resource, + action='metadef_tag', + conditions={'method': ['GET']}) + mapper.connect('/schemas/metadefs/tag', + controller=reject_method_resource, + action='reject', + allowed_methods='GET', + conditions={'method': ['POST', 'PUT', 'DELETE', + 'PATCH', 'HEAD']}) + + mapper.connect('/schemas/metadefs/tags', + controller=schemas_resource, + action='metadef_tags', + conditions={'method': ['GET']}) + mapper.connect('/schemas/metadefs/tags', + 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()) @@ -348,6 +371,48 @@ class API(wsgi.Router): allowed_methods='GET, PUT, DELETE', conditions={'method': ['POST', 'PATCH', 'HEAD']}) + # Metadef tags + metadef_tags_resource = metadef_tags.create_resource() + mapper.connect('/metadefs/namespaces/{namespace}/tags', + controller=metadef_tags_resource, + action='index', + conditions={'method': ['GET']}) + mapper.connect('/metadefs/namespaces/{namespace}/tags', + controller=metadef_tags_resource, + action='create', + conditions={'method': ['POST']}) + mapper.connect('/metadefs/namespaces/{namespace}/tags', + controller=metadef_tags_resource, + action='create_tags', + conditions={'method': ['PUT']}) + mapper.connect('/metadefs/namespaces/{namespace}/tags', + controller=metadef_namespace_resource, + action='delete_tags', + conditions={'method': ['DELETE']}) + mapper.connect('/metadefs/namespaces/{namespace}/tags', + controller=reject_method_resource, + action='reject', + allowed_methods='GET, POST, PUT, DELETE', + conditions={'method': ['PATCH', 'HEAD']}) + + mapper.connect('/metadefs/namespaces/{namespace}/tags/{tag_name}', + controller=metadef_tags_resource, + action='show', + conditions={'method': ['GET']}) + mapper.connect('/metadefs/namespaces/{namespace}/tags/{tag_name}', + controller=metadef_tags_resource, + action='update', + conditions={'method': ['PUT']}) + mapper.connect('/metadefs/namespaces/{namespace}/tags/{tag_name}', + controller=metadef_tags_resource, + action='delete', + conditions={'method': ['DELETE']}) + mapper.connect('/metadefs/namespaces/{namespace}/tags/{tag_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 fce79b392d..6f124e5643 100644 --- a/glance/api/v2/schemas.py +++ b/glance/api/v2/schemas.py @@ -19,6 +19,7 @@ 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 metadef_tags from glance.api.v2 import tasks from glance.common import wsgi @@ -50,6 +51,10 @@ class Controller(object): self.metadef_object_collection_schema = \ metadef_objects.get_collection_schema() + self.metadef_tag_schema = metadef_tags.get_schema() + self.metadef_tag_collection_schema = ( + metadef_tags.get_collection_schema()) + def image(self, req): return self.image_schema.raw() @@ -92,6 +97,12 @@ class Controller(object): def metadef_objects(self, req): return self.metadef_object_collection_schema.raw() + def metadef_tag(self, req): + return self.metadef_tag_schema.raw() + + def metadef_tags(self, req): + return self.metadef_tag_collection_schema.raw() + def create_resource(custom_image_properties=None): controller = Controller(custom_image_properties) diff --git a/glance/common/exception.py b/glance/common/exception.py index cb45a24775..9498ad3572 100644 --- a/glance/common/exception.py +++ b/glance/common/exception.py @@ -151,6 +151,11 @@ class ProtectedMetadefResourceTypeSystemDelete(Forbidden): " a seeded-system type and cannot be deleted.") +class ProtectedMetadefTagDelete(Forbidden): + message = _("Metadata definition tag %(tag_name)s is protected" + " and cannot be deleted.") + + class Invalid(GlanceException): message = _("Data supplied was not valid.") @@ -381,6 +386,11 @@ class MetadefDuplicateResourceTypeAssociation(Duplicate): " already exists.") +class MetadefDuplicateTag(Duplicate): + message = _("A metadata tag with name=%(name)s" + " already exists in namespace=%(namespace_name)s.") + + class MetadefForbidden(Forbidden): message = _("You are not authorized to complete this action.") @@ -418,3 +428,9 @@ class MetadefResourceTypeAssociationNotFound(NotFound): " resource-type=%(resource_type_name)s to" " namespace=%(namespace_name)s," " was not found.") + + +class MetadefTagNotFound(NotFound): + message = _("The metadata definition tag with" + " name=%(name)s was not found in" + " namespace=%(namespace_name)s.") diff --git a/glance/db/__init__.py b/glance/db/__init__.py index 16b7ddaa00..4cd7842265 100644 --- a/glance/db/__init__.py +++ b/glance/db/__init__.py @@ -472,6 +472,16 @@ class MetadefNamespaceRepo(object): msg = _("The specified namespace %s could not be found") raise exception.NotFound(msg % namespace.namespace) + def remove_tags(self, namespace): + try: + self.db_api.metadef_tag_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, @@ -758,3 +768,92 @@ class MetadefPropertyRepo(object): except exception.NotFound as e: raise exception.NotFound(explanation=e.msg) return property + + +class MetadefTagRepo(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_tag_from_db(self, metadata_tag, + namespace_entity): + return glance.domain.MetadefTag( + namespace=namespace_entity, + tag_id=metadata_tag['id'], + name=metadata_tag['name'], + created_at=metadata_tag['created_at'], + updated_at=metadata_tag['updated_at'] + ) + + def _format_metadef_tag_to_db(self, metadata_tag): + db_metadata_tag = { + 'name': metadata_tag.name + } + return db_metadata_tag + + def add(self, metadata_tag): + self.db_api.metadef_tag_create( + self.context, + metadata_tag.namespace, + self._format_metadef_tag_to_db(metadata_tag) + ) + + def add_tags(self, metadata_tags): + tag_list = [] + namespace = None + for metadata_tag in metadata_tags: + tag_list.append(self._format_metadef_tag_to_db(metadata_tag)) + if namespace is None: + namespace = metadata_tag.namespace + + self.db_api.metadef_tag_create_tags( + self.context, namespace, tag_list) + + def get(self, namespace, name): + try: + namespace_entity = self.meta_namespace_repo.get(namespace) + db_metadata_tag = self.db_api.metadef_tag_get( + self.context, + namespace, + name) + except (exception.NotFound, exception.Forbidden): + msg = _('Could not find metadata tag %s') % name + raise exception.NotFound(msg) + return self._format_metadef_tag_from_db(db_metadata_tag, + 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_tag = self.db_api.metadef_tag_get_all( + self.context, namespace, filters, marker, limit, sort_key, + sort_dir) + + return [self._format_metadef_tag_from_db(metadata_tag, + namespace_entity) + for metadata_tag in db_metadata_tag] + + def remove(self, metadata_tag): + try: + self.db_api.metadef_tag_delete( + self.context, + metadata_tag.namespace.namespace, + metadata_tag.name + ) + except (exception.NotFound, exception.Forbidden): + msg = _("The specified metadata tag %s could not be found") + raise exception.NotFound(msg % metadata_tag.name) + + def save(self, metadata_tag): + try: + self.db_api.metadef_tag_update( + self.context, metadata_tag.namespace.namespace, + metadata_tag.tag_id, + self._format_metadef_tag_to_db(metadata_tag)) + except exception.NotFound as e: + raise exception.NotFound(explanation=e.msg) + return metadata_tag diff --git a/glance/db/registry/api.py b/glance/db/registry/api.py index dc947d5341..43fe98acd6 100644 --- a/glance/db/registry/api.py +++ b/glance/db/registry/api.py @@ -499,3 +499,58 @@ def metadef_resource_type_association_get_all_by_namespace( namespace_name, session=None): return client.metadef_resource_type_association_get_all_by_namespace( namespace_name=namespace_name) + + +@_get_client +def metadef_tag_get_all(client, namespace_name, filters=None, marker=None, + limit=None, sort_key='created_at', sort_dir=None, + session=None): + return client.metadef_tag_get_all( + namespace_name=namespace_name, filters=filters, marker=marker, + limit=limit, sort_key=sort_key, sort_dir=sort_dir, session=session) + + +@_get_client +def metadef_tag_get(client, namespace_name, name, session=None): + return client.metadef_tag_get( + namespace_name=namespace_name, name=name) + + +@_get_client +def metadef_tag_create( + client, namespace_name, tag_dict, session=None): + return client.metadef_tag_create( + namespace_name=namespace_name, tag_dict=tag_dict) + + +@_get_client +def metadef_tag_create_tags( + client, namespace_name, tag_list, session=None): + return client.metadef_tag_create_tags( + namespace_name=namespace_name, tag_list=tag_list) + + +@_get_client +def metadef_tag_update( + client, namespace_name, id, tag_dict, session=None): + return client.metadef_tag_update( + namespace_name=namespace_name, id=id, tag_dict=tag_dict) + + +@_get_client +def metadef_tag_delete( + client, namespace_name, name, session=None): + return client.metadef_tag_delete( + namespace_name=namespace_name, name=name) + + +@_get_client +def metadef_tag_delete_namespace_content( + client, namespace_name, session=None): + return client.metadef_tag_delete_namespace_content( + namespace_name=namespace_name) + + +@_get_client +def metadef_tag_count(client, namespace_name, session=None): + return client.metadef_tag_count(namespace_name=namespace_name) diff --git a/glance/db/simple/api.py b/glance/db/simple/api.py index 2b3ddef486..fe27445abb 100644 --- a/glance/db/simple/api.py +++ b/glance/db/simple/api.py @@ -39,6 +39,7 @@ DATA = { 'metadef_objects': [], 'metadef_properties': [], 'metadef_resource_types': [], + 'metadef_tags': [], 'tags': {}, 'locations': [], 'tasks': {}, @@ -74,6 +75,7 @@ def reset(): 'metadef_objects': [], 'metadef_properties': [], 'metadef_resource_types': [], + 'metadef_tags': [], 'tags': {}, 'locations': [], 'tasks': {}, @@ -1668,6 +1670,200 @@ def metadef_resource_type_association_delete(context, namespace_name, return resource_type +@log_call +def metadef_tag_get(context, namespace_name, name): + """Get a metadef tag""" + namespace = metadef_namespace_get(context, namespace_name) + _check_namespace_visibility(context, namespace, namespace_name) + + for tag in DATA['metadef_tags']: + if (tag['namespace_id'] == namespace['id'] and + tag['name'] == name): + return tag + else: + msg = ("The metadata definition tag with name=%(name)s" + " was not found in namespace=%(namespace_name)s." + % {'name': name, 'namespace_name': namespace_name}) + LOG.debug(msg) + raise exception.MetadefTagNotFound(name=name, + namespace_name=namespace_name) + + +@log_call +def metadef_tag_get_by_id(context, namespace_name, id): + """Get a metadef tag""" + namespace = metadef_namespace_get(context, namespace_name) + _check_namespace_visibility(context, namespace, namespace_name) + + for tag in DATA['metadef_tags']: + if (tag['namespace_id'] == namespace['id'] and + tag['id'] == id): + return tag + else: + msg = (_("Metadata definition tag not found for id=%s") % id) + LOG.warn(msg) + raise exception.MetadefTagNotFound(msg) + + +@log_call +def metadef_tag_get_all(context, namespace_name, filters=None, marker=None, + limit=None, sort_key='created_at', sort_dir=None, + session=None): + """Get a metadef tags list""" + + namespace = metadef_namespace_get(context, namespace_name) + _check_namespace_visibility(context, namespace, namespace_name) + + tags = [] + for tag in DATA['metadef_tags']: + if tag['namespace_id'] == namespace['id']: + tags.append(tag) + + return tags + + +@log_call +def metadef_tag_create(context, namespace_name, values): + """Create a metadef tag""" + global DATA + + tag_values = copy.deepcopy(values) + tag_name = tag_values['name'] + required_attributes = ['name'] + allowed_attributes = ['name'] + + namespace = metadef_namespace_get(context, namespace_name) + + for tag in DATA['metadef_tags']: + if (tag['name'] == tag_name and + tag['namespace_id'] == namespace['id']): + msg = ("A metadata definition tag with name=%(name)s" + " in namespace=%(namespace_name)s already exists." + % {'name': tag_name, 'namespace_name': namespace_name}) + LOG.debug(msg) + raise exception.MetadefDuplicateTag( + name=tag_name, namespace_name=namespace_name) + + for key in required_attributes: + if key not in tag_values: + raise exception.Invalid('%s is a required attribute' % key) + + incorrect_keys = set(tag_values.keys()) - set(allowed_attributes) + if incorrect_keys: + raise exception.Invalid( + 'The keys %s are not valid' % str(incorrect_keys)) + + tag_values['namespace_id'] = namespace['id'] + + _check_namespace_visibility(context, namespace, namespace_name) + + tag = _format_tag(tag_values) + DATA['metadef_tags'].append(tag) + return tag + + +@log_call +def metadef_tag_create_tags(context, namespace_name, tag_list): + """Create a metadef tag""" + global DATA + + namespace = metadef_namespace_get(context, namespace_name) + _check_namespace_visibility(context, namespace, namespace_name) + + required_attributes = ['name'] + allowed_attributes = ['name'] + data_tag_list = [] + tag_name_list = [] + for tag_value in tag_list: + tag_values = copy.deepcopy(tag_value) + tag_name = tag_values['name'] + + for key in required_attributes: + if key not in tag_values: + raise exception.Invalid('%s is a required attribute' % key) + + incorrect_keys = set(tag_values.keys()) - set(allowed_attributes) + if incorrect_keys: + raise exception.Invalid( + 'The keys %s are not valid' % str(incorrect_keys)) + + if tag_name in tag_name_list: + msg = ("A metadata definition tag with name=%(name)s" + " in namespace=%(namespace_name)s already exists." + % {'name': tag_name, 'namespace_name': namespace_name}) + LOG.debug(msg) + raise exception.MetadefDuplicateTag( + name=tag_name, namespace_name=namespace_name) + else: + tag_name_list.append(tag_name) + + tag_values['namespace_id'] = namespace['id'] + data_tag_list.append(_format_tag(tag_values)) + + DATA['metadef_tags'] = [] + for tag in data_tag_list: + DATA['metadef_tags'].append(tag) + + return data_tag_list + + +@log_call +def metadef_tag_update(context, namespace_name, id, values): + """Update a metadef tag""" + global DATA + + namespace = metadef_namespace_get(context, namespace_name) + + _check_namespace_visibility(context, namespace, namespace_name) + + tag = metadef_tag_get_by_id(context, namespace_name, id) + if tag['name'] != values['name']: + for db_tag in DATA['metadef_tags']: + if (db_tag['name'] == values['name'] and + db_tag['namespace_id'] == namespace['id']): + msg = ("Invalid update. It would result in a duplicate" + " metadata definition tag with same name=%(name)s " + " in namespace=%(namespace_name)s." + % {'name': tag['name'], + 'namespace_name': namespace_name}) + LOG.debug(msg) + raise exception.MetadefDuplicateTag( + name=tag['name'], namespace_name=namespace_name) + + DATA['metadef_tags'].remove(tag) + + tag.update(values) + tag['updated_at'] = timeutils.utcnow() + DATA['metadef_tags'].append(tag) + return tag + + +@log_call +def metadef_tag_delete(context, namespace_name, name): + """Delete a metadef tag""" + global DATA + + tags = metadef_tag_get(context, namespace_name, name) + DATA['metadef_tags'].remove(tags) + + return tags + + +@log_call +def metadef_tag_count(context, namespace_name): + """Get metadef tag count in a namespace""" + namespace = metadef_namespace_get(context, namespace_name) + + _check_namespace_visibility(context, namespace, namespace_name) + + count = 0 + for tag in DATA['metadef_tags']: + if tag['namespace_id'] == namespace['id']: + count = count + 1 + + return count + + def _format_association(namespace, resource_type, association_values): association = { 'namespace_id': namespace['id'], @@ -1739,6 +1935,19 @@ def _format_object(values): return object +def _format_tag(values): + dt = timeutils.utcnow() + tag = { + 'id': _get_metadef_id(), + 'namespace_id': None, + 'name': None, + 'created_at': dt, + 'updated_at': dt + } + tag.update(values) + return tag + + def _is_namespace_visible(context, namespace): """Return true if namespace is visible in this context""" if context.is_admin: diff --git a/glance/db/sqlalchemy/api.py b/glance/db/sqlalchemy/api.py index 53a4443e68..9ce8f1fc62 100644 --- a/glance/db/sqlalchemy/api.py +++ b/glance/db/sqlalchemy/api.py @@ -43,6 +43,7 @@ from glance.db.sqlalchemy.metadef_api\ import resource_type as metadef_resource_type_api from glance.db.sqlalchemy.metadef_api\ import resource_type_association as metadef_association_api +from glance.db.sqlalchemy.metadef_api import tag as metadef_tag_api from glance.db.sqlalchemy import models from glance import i18n import glance.openstack.common.log as os_logging @@ -1627,3 +1628,66 @@ def metadef_resource_type_association_get_all_by_namespace( session = session or get_session() return metadef_association_api.\ get_all_by_namespace(context, namespace_name, session) + + +def metadef_tag_get_all( + context, namespace_name, filters=None, marker=None, limit=None, + sort_key=None, sort_dir=None, session=None): + """Get metadata-schema tags or raise if none exist.""" + session = session or get_session() + return metadef_tag_api.get_all( + context, namespace_name, session, + filters, marker, limit, sort_key, sort_dir) + + +def metadef_tag_get(context, namespace_name, name, session=None): + """Get a metadata-schema tag or raise if it does not exist.""" + session = session or get_session() + return metadef_tag_api.get( + context, namespace_name, name, session) + + +def metadef_tag_create(context, namespace_name, tag_dict, + session=None): + """Create a metadata-schema tag or raise if it already exists.""" + session = session or get_session() + return metadef_tag_api.create( + context, namespace_name, tag_dict, session) + + +def metadef_tag_create_tags(context, namespace_name, tag_list, + session=None): + """Create a metadata-schema tag or raise if it already exists.""" + session = get_session() + return metadef_tag_api.create_tags( + context, namespace_name, tag_list, session) + + +def metadef_tag_update(context, namespace_name, id, tag_dict, + session=None): + """Update an tag or raise if it does not exist or not visible.""" + session = session or get_session() + return metadef_tag_api.update( + context, namespace_name, id, tag_dict, session) + + +def metadef_tag_delete(context, namespace_name, name, + session=None): + """Delete an tag or raise if namespace or tag doesn't exist.""" + session = session or get_session() + return metadef_tag_api.delete( + context, namespace_name, name, session) + + +def metadef_tag_delete_namespace_content( + context, namespace_name, session=None): + """Delete an tag or raise if namespace or tag doesn't exist.""" + session = session or get_session() + return metadef_tag_api.delete_by_namespace_name( + context, namespace_name, session) + + +def metadef_tag_count(context, namespace_name, session=None): + """Get count of tags for a namespace, raise if ns doesn't exist.""" + session = session or get_session() + return metadef_tag_api.count(context, namespace_name, session) diff --git a/glance/db/sqlalchemy/metadata.py b/glance/db/sqlalchemy/metadata.py index 5ebc22453d..00162b5f5a 100644 --- a/glance/db/sqlalchemy/metadata.py +++ b/glance/db/sqlalchemy/metadata.py @@ -71,6 +71,10 @@ def get_metadef_objects_table(meta): return sqlalchemy.Table('metadef_objects', meta, autoload=True) +def get_metadef_tags_table(meta): + return sqlalchemy.Table('metadef_tags', meta, autoload=True) + + def _get_resource_type_id(meta, name): resource_types_table = get_metadef_resource_types_table(meta) return resource_types_table.select().\ @@ -106,6 +110,14 @@ def _get_objects(meta, namespace_id): execute().fetchall() +def _get_tags(meta, namespace_id): + tags_table = get_metadef_tags_table(meta) + return ( + tags_table.select(). + where(tags_table.c.namespace_id == namespace_id). + execute().fetchall()) + + def _populate_metadata(meta, metadata_path=None): if not metadata_path: metadata_path = CONF.metadata_source_path @@ -122,6 +134,7 @@ def _populate_metadata(meta, metadata_path=None): metadef_namespace_resource_types_tables =\ get_metadef_namespace_resource_types_table(meta) metadef_objects_table = get_metadef_objects_table(meta) + metadef_tags_table = get_metadef_tags_table(meta) metadef_properties_table = get_metadef_properties_table(meta) metadef_resource_types_table = get_metadef_resource_types_table(meta) @@ -208,7 +221,7 @@ def _populate_metadata(meta, metadata_path=None): for object in metadata.get('objects', []): values = { - 'name': object.get('name', None), + 'name': object.get('name'), 'description': object.get('description', None), 'namespace_id': namespace_id, 'json_schema': json.dumps(object.get('properties', None)), @@ -216,6 +229,16 @@ def _populate_metadata(meta, metadata_path=None): } _insert_data_to_db(metadef_objects_table, values) + for tag in metadata.get('tags', []): + timeutils_utcnow = timeutils.utcnow() + values = { + 'name': tag.get('name'), + 'namespace_id': namespace_id, + 'created_at': timeutils_utcnow, + 'updated_at': timeutils_utcnow + } + _insert_data_to_db(metadef_tags_table, values) + LOG.info(_LI("File %s loaded to database."), file) LOG.info(_LI("Metadata loading finished")) @@ -224,6 +247,7 @@ def _populate_metadata(meta, metadata_path=None): def _clear_metadata(meta): metadef_tables = [get_metadef_properties_table(meta), get_metadef_objects_table(meta), + get_metadef_tags_table(meta), get_metadef_namespace_resource_types_table(meta), get_metadef_namespaces_table(meta)] @@ -262,13 +286,15 @@ def _export_data_to_file(meta, path): 'owner': namespace['owner'], 'resource_type_associations': [], 'properties': {}, - 'objects': [] + 'objects': [], + 'tags': [] } namespace_resource_types = _get_namespace_resource_types(meta, namespace_id) db_objects = _get_objects(meta, namespace_id) db_properties = _get_properties(meta, namespace_id) + db_tags = _get_tags(meta, namespace_id) resource_types = [] for namespace_resource_type in namespace_resource_types: @@ -303,6 +329,15 @@ def _export_data_to_file(meta, path): 'properties': properties }) + tags = [] + for tag in db_tags: + tags.append({ + "name": tag['name'] + }) + values.update({ + 'tags': tags + }) + try: file_name = ''.join([path, namespace_file_name, '.json']) with open(file_name, 'w') as json_file: diff --git a/glance/db/sqlalchemy/metadef_api/namespace.py b/glance/db/sqlalchemy/metadef_api/namespace.py index 2525a7c851..36b8714c9e 100644 --- a/glance/db/sqlalchemy/metadef_api/namespace.py +++ b/glance/db/sqlalchemy/metadef_api/namespace.py @@ -287,6 +287,8 @@ def delete_cascade(context, name, session): namespace_rec = _get_by_name(context, name, session) with session.begin(): try: + metadef_api.tag.delete_namespace_content( + context, namespace_rec.id, session) metadef_api.object.delete_namespace_content( context, namespace_rec.id, session) metadef_api.property.delete_namespace_content( diff --git a/glance/db/sqlalchemy/metadef_api/tag.py b/glance/db/sqlalchemy/metadef_api/tag.py new file mode 100644 index 0000000000..4bec4072f0 --- /dev/null +++ b/glance/db/sqlalchemy/metadef_api/tag.py @@ -0,0 +1,204 @@ +# 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.db import exception as db_exc +from oslo.db.sqlalchemy.utils import paginate_query +from sqlalchemy import func +import sqlalchemy.orm as sa_orm + +from glance.common import exception as exc +from glance.db.sqlalchemy.metadef_api import namespace as namespace_api +import glance.db.sqlalchemy.metadef_api.utils as metadef_utils +from glance.db.sqlalchemy import models_metadef as models +from glance import i18n +import glance.openstack.common.log as os_logging + +LOG = os_logging.getLogger(__name__) +_LW = i18n._LW + + +def _get(context, id, session): + try: + query = (session.query(models.MetadefTag).filter_by(id=id)) + metadef_tag = query.one() + except sa_orm.exc.NoResultFound: + msg = (_LW("Metadata tag not found for id %s") % id) + LOG.warn(msg) + raise exc.MetadefTagNotFound(message=msg) + return metadef_tag + + +def _get_by_name(context, namespace_name, name, session): + namespace = namespace_api.get(context, namespace_name, session) + try: + query = (session.query(models.MetadefTag).filter_by( + name=name, namespace_id=namespace['id'])) + metadef_tag = query.one() + except sa_orm.exc.NoResultFound: + msg = ("The metadata tag with name=%(name)s" + " was not found in namespace=%(namespace_name)s." + % {'name': name, 'namespace_name': namespace_name}) + LOG.debug(msg) + raise exc.MetadefTagNotFound(name=name, + namespace_name=namespace_name) + return metadef_tag + + +def get_all(context, namespace_name, session, filters=None, marker=None, + limit=None, sort_key='created_at', sort_dir='desc'): + """Get all tags that match zero or more filters. + + :param filters: dict of filter keys and values. + :param marker: tag id after which to start page + :param limit: maximum number of namespaces to return + :param sort_key: namespace attribute by which results should be sorted + :param sort_dir: direction in which results should be sorted (asc, desc) + """ + + namespace = namespace_api.get(context, namespace_name, session) + query = (session.query(models.MetadefTag).filter_by( + namespace_id=namespace['id'])) + + marker_tag = None + if marker is not None: + marker_tag = _get(context, marker, session) + + sort_keys = ['created_at', 'id'] + sort_keys.insert(0, sort_key) if sort_key not in sort_keys else sort_keys + + query = paginate_query(query=query, + model=models.MetadefTag, + limit=limit, + sort_keys=sort_keys, + marker=marker_tag, sort_dir=sort_dir) + metadef_tag = query.all() + metadef_tag_list = [] + for tag in metadef_tag: + metadef_tag_list.append(tag.to_dict()) + + return metadef_tag_list + + +def create(context, namespace_name, values, session): + namespace = namespace_api.get(context, namespace_name, session) + values.update({'namespace_id': namespace['id']}) + + metadef_tag = models.MetadefTag() + metadef_utils.drop_protected_attrs(models.MetadefTag, values) + metadef_tag.update(values.copy()) + try: + metadef_tag.save(session=session) + except db_exc.DBDuplicateEntry: + msg = ("A metadata tag name=%(name)s" + " in namespace=%(namespace_name)s already exists." + % {'name': metadef_tag.name, + 'namespace_name': namespace_name}) + LOG.debug(msg) + raise exc.MetadefDuplicateTag( + name=metadef_tag.name, namespace_name=namespace_name) + + return metadef_tag.to_dict() + + +def create_tags(context, namespace_name, tag_list, session): + + metadef_tags_list = [] + if tag_list: + namespace = namespace_api.get(context, namespace_name, session) + + try: + with session.begin(): + query = (session.query(models.MetadefTag).filter_by( + namespace_id=namespace['id'])) + query.delete(synchronize_session='fetch') + + for value in tag_list: + value.update({'namespace_id': namespace['id']}) + metadef_utils.drop_protected_attrs( + models.MetadefTag, value) + metadef_tag = models.MetadefTag() + metadef_tag.update(value.copy()) + metadef_tag.save(session=session) + metadef_tags_list.append(metadef_tag.to_dict()) + except db_exc.DBDuplicateEntry: + msg = ("A metadata tag name=%(name)s" + " in namespace=%(namespace_name)s already exists." + % {'name': metadef_tag.name, + 'namespace_name': namespace_name}) + LOG.debug(msg) + raise exc.MetadefDuplicateTag( + name=metadef_tag.name, namespace_name=namespace_name) + + return metadef_tags_list + + +def get(context, namespace_name, name, session): + metadef_tag = _get_by_name(context, namespace_name, name, session) + return metadef_tag.to_dict() + + +def update(context, namespace_name, id, values, session): + """Update an tag, raise if ns not found/visible or duplicate result""" + namespace_api.get(context, namespace_name, session) + + metadata_tag = _get(context, id, session) + metadef_utils.drop_protected_attrs(models.MetadefTag, values) + # values['updated_at'] = timeutils.utcnow() - done by TS mixin + try: + metadata_tag.update(values.copy()) + metadata_tag.save(session=session) + except db_exc.DBDuplicateEntry: + msg = ("Invalid update. It would result in a duplicate" + " metadata tag with same name=%(name)s" + " in namespace=%(namespace_name)s." + % {'name': values['name'], + 'namespace_name': namespace_name}) + LOG.debug(msg) + raise exc.MetadefDuplicateTag( + name=values['name'], namespace_name=namespace_name) + + return metadata_tag.to_dict() + + +def delete(context, namespace_name, name, session): + namespace_api.get(context, namespace_name, session) + md_tag = _get_by_name(context, namespace_name, name, session) + + session.delete(md_tag) + session.flush() + + return md_tag.to_dict() + + +def delete_namespace_content(context, namespace_id, session): + """Use this def only if the ns for the id has been verified as visible""" + count = 0 + query = (session.query(models.MetadefTag).filter_by( + namespace_id=namespace_id)) + count = query.delete(synchronize_session='fetch') + return count + + +def delete_by_namespace_name(context, namespace_name, session): + namespace = namespace_api.get(context, namespace_name, session) + return delete_namespace_content(context, namespace['id'], session) + + +def count(context, namespace_name, session): + """Get the count of objects for a namespace, raise if ns not found""" + namespace = namespace_api.get(context, namespace_name, session) + query = (session.query(func.count(models.MetadefTag.id)).filter_by( + namespace_id=namespace['id'])) + return query.scalar() diff --git a/glance/db/sqlalchemy/migrate_repo/versions/038_add_metadef_tags_table.py b/glance/db/sqlalchemy/migrate_repo/versions/038_add_metadef_tags_table.py new file mode 100644 index 0000000000..f0a2610cb2 --- /dev/null +++ b/glance/db/sqlalchemy/migrate_repo/versions/038_add_metadef_tags_table.py @@ -0,0 +1,57 @@ +# 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 sqlalchemy.schema import ( + Column, Index, MetaData, Table, UniqueConstraint) # noqa + +from glance.db.sqlalchemy.migrate_repo.schema import ( + DateTime, Integer, String, create_tables, drop_tables) # noqa + + +def define_metadef_tags_table(meta): + _constr_kwargs = {} + metadef_tags = Table('metadef_tags', + meta, + Column('id', Integer(), primary_key=True, + nullable=False), + Column('namespace_id', Integer(), + nullable=False), + Column('name', String(80), nullable=False), + Column('created_at', DateTime(), nullable=False), + Column('updated_at', DateTime()), + UniqueConstraint('namespace_id', 'name', + **_constr_kwargs), + mysql_engine='InnoDB', + extend_existing=False) + + if meta.bind.name != 'ibm_db_sa': + Index('ix_tags_namespace_id_name', + metadef_tags.c.namespace_id, + metadef_tags.c.name) + + return metadef_tags + + +def upgrade(migrate_engine): + meta = MetaData() + meta.bind = migrate_engine + tables = [define_metadef_tags_table(meta)] + create_tables(tables) + + +def downgrade(migrate_engine): + meta = MetaData() + meta.bind = migrate_engine + tables = [define_metadef_tags_table(meta)] + drop_tables(tables) diff --git a/glance/db/sqlalchemy/models_metadef.py b/glance/db/sqlalchemy/models_metadef.py index 7b8985b4f8..81432de45a 100644 --- a/glance/db/sqlalchemy/models_metadef.py +++ b/glance/db/sqlalchemy/models_metadef.py @@ -137,9 +137,23 @@ class MetadefResourceType(BASE_DICT, GlanceMetadefBase): primaryjoin=id == MetadefNamespaceResourceType.resource_type_id) +class MetadefTag(BASE_DICT, GlanceMetadefBase): + """Represents a metadata-schema tag in the data store.""" + __tablename__ = 'metadef_tags' + __table_args__ = (Index('ix_metadef_tags_namespace_id', + 'namespace_id', 'name'), + Index('ix_metadef_tags_name', 'name')) + + id = Column(Integer, primary_key=True, nullable=False) + namespace_id = Column(Integer(), ForeignKey('metadef_namespaces.id'), + nullable=False) + name = Column(String(80), nullable=False) + + def register_models(engine): """Create database tables for all models with the given engine.""" models = (MetadefNamespace, MetadefObject, MetadefProperty, + MetadefTag, MetadefResourceType, MetadefNamespaceResourceType) for model in models: model.metadata.create_all(engine) @@ -148,6 +162,7 @@ def register_models(engine): def unregister_models(engine): """Drop database tables for all models with the given engine.""" models = (MetadefObject, MetadefProperty, MetadefNamespaceResourceType, + MetadefTag, MetadefNamespace, MetadefResourceType) for model in models: model.metadata.drop_all(engine) diff --git a/glance/domain/__init__.py b/glance/domain/__init__.py index 21d05ae182..86768660f7 100644 --- a/glance/domain/__init__.py +++ b/glance/domain/__init__.py @@ -606,3 +606,32 @@ class MetadefPropertyFactory(object): name, schema ) + + +class MetadefTag(object): + + def __init__(self, namespace, tag_id, name, created_at, updated_at): + self.namespace = namespace + self.tag_id = tag_id + self.name = name + self.created_at = created_at + self.updated_at = updated_at + + def delete(self): + if self.namespace.protected: + raise exception.ProtectedMetadefTagDelete(tag_name=self.name) + + +class MetadefTagFactory(object): + + def new_tag(self, namespace, name, **kwargs): + tag_id = str(uuid.uuid4()) + created_at = timeutils.utcnow() + updated_at = created_at + return MetadefTag( + namespace, + tag_id, + name, + created_at, + updated_at + ) diff --git a/glance/domain/proxy.py b/glance/domain/proxy.py index d8bbeb2149..c7b1b7cbfa 100644 --- a/glance/domain/proxy.py +++ b/glance/domain/proxy.py @@ -255,6 +255,11 @@ class MetadefNamespaceRepo(object): result = self.base.remove_properties(base_item) return self.namespace_proxy_helper.proxy(result) + def remove_tags(self, item): + base_item = self.namespace_proxy_helper.unproxy(item) + result = self.base.remove_tags(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) @@ -464,3 +469,68 @@ class MetadefPropertyFactory(object): def new_namespace_property(self, **kwargs): t = self.base.new_namespace_property(**kwargs) return self.meta_object_helper.proxy(t) + + +# Metadef tag classes +class MetadefTagRepo(object): + def __init__(self, base, + tag_proxy_class=None, tag_proxy_kwargs=None): + self.base = base + self.tag_proxy_helper = Helper(tag_proxy_class, + tag_proxy_kwargs) + + def get(self, namespace, name): + meta_tag = self.base.get(namespace, name) + return self.tag_proxy_helper.proxy(meta_tag) + + def add(self, meta_tag): + self.base.add(self.tag_proxy_helper.unproxy(meta_tag)) + + def add_tags(self, meta_tags): + tags_list = [] + for meta_tag in meta_tags: + tags_list.append(self.tag_proxy_helper.unproxy(meta_tag)) + self.base.add_tags(tags_list) + + def list(self, *args, **kwargs): + tags = self.base.list(*args, **kwargs) + return [self.tag_proxy_helper.proxy(meta_tag) for meta_tag + in tags] + + def remove(self, item): + base_item = self.tag_proxy_helper.unproxy(item) + result = self.base.remove(base_item) + return self.tag_proxy_helper.proxy(result) + + def save(self, item): + base_item = self.tag_proxy_helper.unproxy(item) + result = self.base.save(base_item) + return self.tag_proxy_helper.proxy(result) + + +class MetadefTag(object): + def __init__(self, base): + self.base = base + + namespace = _proxy('base', 'namespace') + tag_id = _proxy('base', 'tag_id') + name = _proxy('base', 'name') + created_at = _proxy('base', 'created_at') + updated_at = _proxy('base', 'updated_at') + + def delete(self): + self.base.delete() + + +class MetadefTagFactory(object): + def __init__(self, + base, + meta_tag_proxy_class=None, + meta_tag_proxy_kwargs=None): + self.meta_tag_helper = Helper(meta_tag_proxy_class, + meta_tag_proxy_kwargs) + self.base = base + + def new_tag(self, **kwargs): + t = self.base.new_tag(**kwargs) + return self.meta_tag_helper.proxy(t) diff --git a/glance/gateway.py b/glance/gateway.py index 133a5e166d..86a8aa9c78 100644 --- a/glance/gateway.py +++ b/glance/gateway.py @@ -189,3 +189,19 @@ class Gateway(object): authorized_prop_repo = authorization.MetadefPropertyRepoProxy( policy_prop_repo, context) return authorized_prop_repo + + def get_metadef_tag_factory(self, context): + tag_factory = glance.domain.MetadefTagFactory() + policy_tag_factory = policy.MetadefTagFactoryProxy( + tag_factory, context, self.policy) + authorized_tag_factory = authorization.MetadefTagFactoryProxy( + policy_tag_factory, context) + return authorized_tag_factory + + def get_metadef_tag_repo(self, context): + tag_repo = glance.db.MetadefTagRepo(context, self.db_api) + policy_tag_repo = policy.MetadefTagRepoProxy( + tag_repo, context, self.policy) + authorized_tag_repo = authorization.MetadefTagRepoProxy( + policy_tag_repo, context) + return authorized_tag_repo diff --git a/glance/tests/etc/policy.json b/glance/tests/etc/policy.json index c515b04e77..c5858eacee 100644 --- a/glance/tests/etc/policy.json +++ b/glance/tests/etc/policy.json @@ -48,5 +48,11 @@ "get_metadef_property":"", "get_metadef_properties":"", "modify_metadef_property":"", - "add_metadef_property":"" + "add_metadef_property":"", + + "get_metadef_tag":"", + "get_metadef_tags":"", + "modify_metadef_tag":"", + "add_metadef_tag":"", + "add_metadef_tags":"" } diff --git a/glance/tests/functional/db/base_metadef.py b/glance/tests/functional/db/base_metadef.py index bfd067d5c8..383543710e 100644 --- a/glance/tests/functional/db/base_metadef.py +++ b/glance/tests/functional/db/base_metadef.py @@ -77,6 +77,23 @@ def build_property_fixture(**kwargs): return property +def build_tag_fixture(**kwargs): + # Full testing of required and schema done via rest api tests + tag = { + 'namespace_id': 1, + 'name': u'test-tag-name', + } + tag.update(kwargs) + return tag + + +def build_tags_fixture(tag_name_list): + tag_list = [] + for tag_name in tag_name_list: + tag_list.append({'name': tag_name}) + return tag_list + + class TestMetadefDriver(test_utils.BaseTestCase): """Test Driver class for Metadef tests.""" @@ -469,10 +486,117 @@ class MetadefResourceTypeAssociationTests(object): self._assert_saved_fields(fixture, item) +class MetadefTagTests(object): + + def test_tag_create(self): + fixture = build_namespace_fixture() + created_ns = self.db_api.metadef_namespace_create(self.context, + fixture) + self.assertIsNotNone(created_ns) + self._assert_saved_fields(fixture, created_ns) + + fixture_tag = build_tag_fixture(namespace_id=created_ns['id']) + created_tag = self.db_api.metadef_tag_create( + self.context, created_ns['namespace'], fixture_tag) + self._assert_saved_fields(fixture_tag, created_tag) + + def test_tag_create_tags(self): + fixture = build_namespace_fixture() + created_ns = self.db_api.metadef_namespace_create(self.context, + fixture) + self.assertIsNotNone(created_ns) + self._assert_saved_fields(fixture, created_ns) + + tags = build_tags_fixture(['Tag1', 'Tag2', 'Tag3']) + created_tags = self.db_api.metadef_tag_create_tags( + self.context, created_ns['namespace'], tags) + actual = set([tag['name'] for tag in created_tags]) + expected = set(['Tag1', 'Tag2', 'Tag3']) + self.assertEqual(expected, actual) + + def test_tag_get(self): + fixture_ns = build_namespace_fixture() + created_ns = self.db_api.metadef_namespace_create(self.context, + fixture_ns) + self.assertIsNotNone(created_ns) + self._assert_saved_fields(fixture_ns, created_ns) + + fixture_tag = build_tag_fixture(namespace_id=created_ns['id']) + created_tag = self.db_api.metadef_tag_create( + self.context, created_ns['namespace'], fixture_tag) + + found_tag = self.db_api.metadef_tag_get( + self.context, created_ns['namespace'], created_tag['name']) + self._assert_saved_fields(fixture_tag, found_tag) + + def test_tag_get_all(self): + ns_fixture = build_namespace_fixture() + ns_created = self.db_api.metadef_namespace_create(self.context, + ns_fixture) + self.assertIsNotNone(ns_created, "Could not create a namespace.") + self._assert_saved_fields(ns_fixture, ns_created) + + fixture1 = build_tag_fixture(namespace_id=ns_created['id']) + created_tag1 = self.db_api.metadef_tag_create( + self.context, ns_created['namespace'], fixture1) + self.assertIsNotNone(created_tag1, "Could not create tag 1.") + + fixture2 = build_tag_fixture(namespace_id=ns_created['id'], + name='test-tag-2') + created_tag2 = self.db_api.metadef_tag_create( + self.context, ns_created['namespace'], fixture2) + self.assertIsNotNone(created_tag2, "Could not create tag 2.") + + found = self.db_api.metadef_tag_get_all( + self.context, ns_created['namespace'], sort_key='created_at') + self.assertEqual(2, len(found)) + + def test_tag_update(self): + delta = {'name': u'New-name'} + + fixture_ns = build_namespace_fixture() + created_ns = self.db_api.metadef_namespace_create(self.context, + fixture_ns) + self.assertIsNotNone(created_ns['namespace']) + + tag_fixture = build_tag_fixture(namespace_id=created_ns['id']) + created_tag = self.db_api.metadef_tag_create( + self.context, created_ns['namespace'], tag_fixture) + self.assertIsNotNone(created_tag, "Could not create a tag.") + + delta_dict = {} + delta_dict.update(delta.copy()) + + updated = self.db_api.metadef_tag_update( + self.context, created_ns['namespace'], + created_tag['id'], delta_dict) + self.assertEqual(delta['name'], updated['name']) + + def test_tag_delete(self): + fixture_ns = build_namespace_fixture() + created_ns = self.db_api.metadef_namespace_create( + self.context, fixture_ns) + self.assertIsNotNone(created_ns['namespace']) + + tag_fixture = build_tag_fixture(namespace_id=created_ns['id']) + created_tag = self.db_api.metadef_tag_create( + self.context, created_ns['namespace'], tag_fixture) + self.assertIsNotNone(created_tag, "Could not create a tag.") + + self.db_api.metadef_tag_delete( + self.context, created_ns['namespace'], created_tag['name']) + + self.assertRaises(exception.NotFound, + self.db_api.metadef_tag_get, + self.context, created_ns['namespace'], + created_tag['name']) + + class MetadefDriverTests(MetadefNamespaceTests, MetadefResourceTypeTests, MetadefResourceTypeAssociationTests, MetadefPropertyTests, - MetadefObjectTests): + MetadefObjectTests, + MetadefTagTests): # collection class pass diff --git a/glance/tests/functional/v2/test_metadef_tags.py b/glance/tests/functional/v2/test_metadef_tags.py new file mode 100644 index 0000000000..f51c512717 --- /dev/null +++ b/glance/tests/functional/v2/test_metadef_tags.py @@ -0,0 +1,179 @@ +# 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 + +from oslo.serialization import jsonutils +import requests + +from glance.tests import functional + +TENANT1 = str(uuid.uuid4()) + + +class TestMetadefTags(functional.FunctionalTest): + + def setUp(self): + super(TestMetadefTags, 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_tags_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 tags should not exist + path = self._url('/v2/metadefs/namespaces/MyNamespace/tags/tag1') + response = requests.get(path, headers=self._headers()) + self.assertEqual(404, response.status_code) + + # Create a tag + path = self._url('/v2/metadefs/namespaces/MyNamespace/tags') + headers = self._headers({'content-type': 'application/json'}) + metadata_tag_name = "tag1" + data = jsonutils.dumps( + { + "name": metadata_tag_name + } + ) + response = requests.post(path, headers=headers, data=data) + self.assertEqual(201, response.status_code) + + # Get the metadata tag created above + path = self._url('/v2/metadefs/namespaces/%s/tags/%s' % + (namespace_name, metadata_tag_name)) + response = requests.get(path, + headers=self._headers()) + self.assertEqual(200, response.status_code) + metadata_tag = jsonutils.loads(response.text) + self.assertEqual("tag1", metadata_tag['name']) + + # Returned tag should match the created tag + metadata_tag = jsonutils.loads(response.text) + checked_keys = set([ + u'name', + u'created_at', + u'updated_at' + ]) + self.assertEqual(checked_keys, set(metadata_tag.keys())) + expected_metadata_tag = { + "name": metadata_tag_name + } + + # Simple key values + checked_values = set([ + u'name' + ]) + for key, value in expected_metadata_tag.items(): + if(key in checked_values): + self.assertEqual(metadata_tag[key], value, key) + + # The metadata_tag should be mutable + path = self._url('/v2/metadefs/namespaces/%s/tags/%s' % + (namespace_name, metadata_tag_name)) + media_type = 'application/json' + headers = self._headers({'content-type': media_type}) + metadata_tag_name = "tag1-UPDATED" + data = jsonutils.dumps( + { + "name": metadata_tag_name + } + ) + response = requests.put(path, headers=headers, data=data) + self.assertEqual(200, response.status_code, response.text) + + # Returned metadata_tag should reflect the changes + metadata_tag = jsonutils.loads(response.text) + self.assertEqual('tag1-UPDATED', metadata_tag['name']) + + # Updates should persist across requests + path = self._url('/v2/metadefs/namespaces/%s/tags/%s' % + (namespace_name, metadata_tag_name)) + response = requests.get(path, headers=self._headers()) + self.assertEqual(200, response.status_code) + self.assertEqual('tag1-UPDATED', metadata_tag['name']) + + # Deletion of metadata_tag_name + path = self._url('/v2/metadefs/namespaces/%s/tags/%s' % + (namespace_name, metadata_tag_name)) + response = requests.delete(path, headers=self._headers()) + self.assertEqual(204, response.status_code) + + # metadata_tag_name should not exist + path = self._url('/v2/metadefs/namespaces/%s/tags/%s' % + (namespace_name, metadata_tag_name)) + response = requests.get(path, headers=self._headers()) + self.assertEqual(404, response.status_code) + + # Create multiple tags. + path = self._url('/v2/metadefs/namespaces/%s/tags' % + (namespace_name)) + headers = self._headers({'content-type': 'application/json'}) + data = jsonutils.dumps( + {"tags": [{"name": "tag1"}, {"name": "tag2"}, {"name": "tag3"}]} + ) + response = requests.put(path, headers=headers, data=data) + self.assertEqual(201, response.status_code) + + # List out the three new tags. + response = requests.get(path, headers=self._headers()) + self.assertEqual(200, response.status_code) + tags = jsonutils.loads(response.text)['tags'] + self.assertEqual(3, len(tags)) + + # Attempt to create bogus duplicate tag4 + data = jsonutils.dumps( + {"tags": [{"name": "tag4"}, {"name": "tag5"}, {"name": "tag4"}]} + ) + response = requests.put(path, headers=headers, data=data) + self.assertEqual(409, response.status_code) + + # Verify the previous 3 still exist + response = requests.get(path, headers=self._headers()) + self.assertEqual(200, response.status_code) + tags = jsonutils.loads(response.text)['tags'] + self.assertEqual(3, len(tags)) diff --git a/glance/tests/unit/test_db_metadef.py b/glance/tests/unit/test_db_metadef.py index c48a887654..bc43b90ac6 100644 --- a/glance/tests/unit/test_db_metadef.py +++ b/glance/tests/unit/test_db_metadef.py @@ -41,6 +41,12 @@ OBJECT1 = 'Object1' OBJECT2 = 'Object2' OBJECT3 = 'Object3' +TAG1 = 'Tag1' +TAG2 = 'Tag2' +TAG3 = 'Tag3' +TAG4 = 'Tag4' +TAG5 = 'Tag5' + RESOURCE_TYPE1 = 'ResourceType1' RESOURCE_TYPE2 = 'ResourceType2' RESOURCE_TYPE3 = 'ResourceType3' @@ -79,6 +85,26 @@ def _db_object_fixture(name, **kwargs): return obj +def _db_tag_fixture(name, **kwargs): + obj = { + 'name': name + } + obj.update(kwargs) + return obj + + +def _db_tags_fixture(names=None): + tags = [] + if names: + tag_name_list = names + else: + tag_name_list = [TAG1, TAG2, TAG3] + + for tag_name in tag_name_list: + tags.append(_db_tag_fixture(tag_name)) + return tags + + def _db_resource_type_fixture(name, **kwargs): obj = { 'name': name, @@ -112,15 +138,19 @@ class TestMetadefRepo(test_utils.BaseTestCase): self.db) self.object_repo = glance.db.MetadefObjectRepo(self.context, self.db) + self.tag_repo = glance.db.MetadefTagRepo(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.tag_factory = glance.domain.MetadefTagFactory() self.resource_type_factory = glance.domain.MetadefResourceTypeFactory() self._create_namespaces() self._create_properties() self._create_objects() + self._create_tags() self._create_resource_types() def _create_namespaces(self): @@ -179,6 +209,17 @@ class TestMetadefRepo(test_utils.BaseTestCase): [self.db.metadef_object_create(self.context, NAMESPACE4, object) for object in self.objects] + def _create_tags(self): + self.tags = [ + _db_tag_fixture(name=TAG1), + _db_tag_fixture(name=TAG2), + _db_tag_fixture(name=TAG3), + ] + [self.db.metadef_tag_create(self.context, NAMESPACE1, tag) + for tag in self.tags] + [self.db.metadef_tag_create(self.context, NAMESPACE4, tag) + for tag in self.tags] + def _create_resource_types(self): self.resource_types = [ _db_resource_type_fixture(name=RESOURCE_TYPE1, @@ -426,3 +467,101 @@ class TestMetadefRepo(test_utils.BaseTestCase): resource_type = self.resource_type_repo.list( filters={'namespace': NAMESPACE1}) self.assertEqual(0, len(resource_type)) + + def test_get_tag(self): + tag = self.tag_repo.get(NAMESPACE1, TAG1) + namespace = self.namespace_repo.get(NAMESPACE1) + self.assertEqual(TAG1, tag.name) + self.assertEqual(namespace.namespace, tag.namespace.namespace) + + def test_get_tag_not_found(self): + exc = self.assertRaises(exception.NotFound, self.tag_repo.get, + NAMESPACE2, TAG1) + self.assertIn(TAG1, utils.exception_to_str(exc)) + + def test_list_tag(self): + tags = self.tag_repo.list(filters={'namespace': NAMESPACE1}) + tag_names = set([t.name for t in tags]) + self.assertEqual(set([TAG1, TAG2, TAG3]), tag_names) + + def test_list_tag_empty_result(self): + tags = self.tag_repo.list(filters={'namespace': NAMESPACE2}) + tag_names = set([t.name for t in tags]) + self.assertEqual(set([]), tag_names) + + def test_list_tag_namespace_not_found(self): + exc = self.assertRaises(exception.NotFound, self.tag_repo.list, + filters={'namespace': 'not-a-namespace'}) + self.assertIn('not-a-namespace', utils.exception_to_str(exc)) + + def test_add_tag(self): + # NOTE(pawel-koniszewski): Change db_tag_fixture to + # tag_factory when tag primary key in DB + # will be changed from Integer to UUID + tag = _db_tag_fixture(name='added_tag') + self.assertEqual('added_tag', tag['name']) + self.db.metadef_tag_create(self.context, NAMESPACE1, tag) + retrieved_tag = self.tag_repo.get(NAMESPACE1, 'added_tag') + self.assertEqual('added_tag', retrieved_tag.name) + + def test_add_tags(self): + tags = self.tag_repo.list(filters={'namespace': NAMESPACE1}) + tag_names = set([t.name for t in tags]) + self.assertEqual(set([TAG1, TAG2, TAG3]), tag_names) + + tags = _db_tags_fixture([TAG3, TAG4, TAG5]) + self.db.metadef_tag_create_tags(self.context, NAMESPACE1, tags) + + tags = self.tag_repo.list(filters={'namespace': NAMESPACE1}) + tag_names = set([t.name for t in tags]) + self.assertEqual(set([TAG3, TAG4, TAG5]), tag_names) + + def test_add_duplicate_tags_with_pre_existing_tags(self): + tags = self.tag_repo.list(filters={'namespace': NAMESPACE1}) + tag_names = set([t.name for t in tags]) + self.assertEqual(set([TAG1, TAG2, TAG3]), tag_names) + + tags = _db_tags_fixture([TAG5, TAG4, TAG5]) + self.assertRaises(exception.Duplicate, + self.db.metadef_tag_create_tags, + self.context, NAMESPACE1, tags) + + tags = self.tag_repo.list(filters={'namespace': NAMESPACE1}) + tag_names = set([t.name for t in tags]) + self.assertEqual(set([TAG1, TAG2, TAG3]), tag_names) + + def test_add_tag_namespace_forbidden(self): + # NOTE(pawel-koniszewski): Change db_tag_fixture to + # tag_factory when tag primary key in DB + # will be changed from Integer to UUID + tag = _db_tag_fixture(name='added_tag') + self.assertEqual('added_tag', tag['name']) + self.assertRaises(exception.Forbidden, self.db.metadef_tag_create, + self.context, NAMESPACE3, tag) + + def test_add_tag_namespace_not_found(self): + # NOTE(pawel-koniszewski): Change db_tag_fixture to + # tag_factory when tag primary key in DB + # will be changed from Integer to UUID + tag = _db_tag_fixture(name='added_tag') + self.assertEqual('added_tag', tag['name']) + self.assertRaises(exception.NotFound, self.db.metadef_tag_create, + self.context, 'not-a-namespace', tag) + + def test_save_tag(self): + tag = self.tag_repo.get(NAMESPACE1, TAG1) + self.tag_repo.save(tag) + tag = self.tag_repo.get(NAMESPACE1, TAG1) + self.assertEqual(TAG1, tag.name) + + def test_remove_tag(self): + tag = self.tag_repo.get(NAMESPACE1, TAG1) + self.tag_repo.remove(tag) + self.assertRaises(exception.NotFound, self.tag_repo.get, + NAMESPACE1, TAG1) + + def test_remove_tag_not_found(self): + fake_name = 'fake_name' + tag = self.tag_repo.get(NAMESPACE1, TAG1) + tag.name = fake_name + self.assertRaises(exception.NotFound, self.tag_repo.remove, tag) diff --git a/glance/tests/unit/test_migrations.py b/glance/tests/unit/test_migrations.py index 1319d912e3..98dc3c6eb5 100644 --- a/glance/tests/unit/test_migrations.py +++ b/glance/tests/unit/test_migrations.py @@ -1336,6 +1336,28 @@ class MigrationsMixin(test_migrations.WalkVersionsMixin): self.assertIsNone(image_member['status']) + def _pre_upgrade_038(self, engine): + self.assertRaises(sqlalchemy.exc.NoSuchTableError, + db_utils.get_table, engine, 'metadef_tags') + + def _check_038(self, engine, data): + meta = sqlalchemy.MetaData() + meta.bind = engine + + # metadef_tags + table = sqlalchemy.Table("metadef_tags", meta, autoload=True) + expected_cols = [u'id', + u'namespace_id', + u'name', + u'created_at', + u'updated_at'] + col_data = [col.name for col in table.columns] + self.assertEqual(expected_cols, col_data) + + def _post_downgrade_038(self, engine): + self.assertRaises(sqlalchemy.exc.NoSuchTableError, + db_utils.get_table, engine, 'metadef_tags') + class TestMysqlMigrations(test_base.MySQLOpportunisticTestCase, MigrationsMixin): @@ -1392,6 +1414,7 @@ class ModelsMigrationSyncMixin(object): # (except 'migrate_version') if name in ['migrate_version', 'metadef_objects', 'metadef_namespaces', 'metadef_properties', 'metadef_resource_types', + 'metadef_tags', 'metadef_namespace_resource_types'] and type_ == 'table': return False return True diff --git a/glance/tests/unit/v2/test_metadef_resources.py b/glance/tests/unit/v2/test_metadef_resources.py index 93bc150f8f..3fefbffa98 100644 --- a/glance/tests/unit/v2/test_metadef_resources.py +++ b/glance/tests/unit/v2/test_metadef_resources.py @@ -21,6 +21,7 @@ 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 +from glance.api.v2 import metadef_tags as tags import glance.api.v2.model.metadef_namespace from glance.tests.unit import base import glance.tests.unit.utils as unit_test_utils @@ -49,6 +50,12 @@ RESOURCE_TYPE2 = 'ResourceType2' RESOURCE_TYPE3 = 'ResourceType3' RESOURCE_TYPE4 = 'ResourceType4' +TAG1 = 'Tag1' +TAG2 = 'Tag2' +TAG3 = 'Tag3' +TAG4 = 'Tag4' +TAG5 = 'Tag5' + TENANT1 = '6838eb7b-6ded-434a-882c-b344c77fe8df' TENANT2 = '2c014f32-55eb-467d-8fcb-4bd706012f81' TENANT3 = '5a3e60e8-cfa9-4a9e-a90a-62b42cea92b8' @@ -99,6 +106,26 @@ def _db_resource_type_fixture(name, **kwargs): return obj +def _db_tag_fixture(name, **kwargs): + obj = { + 'name': name + } + obj.update(kwargs) + return obj + + +def _db_tags_fixture(tag_names=None): + tag_list = [] + if not tag_names: + tag_names = [TAG1, TAG2, TAG3] + + for tag_name in tag_names: + tag = glance.api.v2.model.metadef_tag.MetadefTag() + tag.name = tag_name + tag_list.append(tag) + return tag_list + + def _db_namespace_resource_type_fixture(name, **kwargs): obj = { 'name': name, @@ -120,6 +147,7 @@ class TestMetadefsControllers(base.IsolatedUnitTest): self._create_objects() self._create_resource_types() self._create_namespaces_resource_types() + self._create_tags() self.namespace_controller = namespaces.NamespaceController(self.db, self.policy) self.property_controller = \ @@ -128,6 +156,7 @@ class TestMetadefsControllers(base.IsolatedUnitTest): self.policy) self.rt_controller = resource_types.ResourceTypeController(self.db, self.policy) + self.tag_controller = tags.TagsController(self.db, self.policy) def _create_namespaces(self): self.db.reset() @@ -175,6 +204,16 @@ class TestMetadefsControllers(base.IsolatedUnitTest): [self.db.metadef_resource_type_create(req.context, resource_type) for resource_type in self.resource_types] + def _create_tags(self): + req = unit_test_utils.get_fake_request() + self.tags = [ + (NAMESPACE3, _db_tag_fixture(TAG1)), + (NAMESPACE3, _db_tag_fixture(TAG2)), + (NAMESPACE1, _db_tag_fixture(TAG1)), + ] + [self.db.metadef_tag_create(req.context, namespace, tag) + for namespace, tag in self.tags] + def _create_namespaces_resource_types(self): req = unit_test_utils.get_fake_request(is_admin=True) self.ns_resource_types = [ @@ -1141,3 +1180,242 @@ class TestMetadefsControllers(base.IsolatedUnitTest): actual = set([x.name for x in output.resource_type_associations]) expected = set([RESOURCE_TYPE1, RESOURCE_TYPE2]) self.assertEqual(expected, actual) + + def test_tag_index(self): + request = unit_test_utils.get_fake_request() + output = self.tag_controller.index(request, NAMESPACE3) + output = output.to_dict() + self.assertEqual(2, len(output['tags'])) + actual = set([tag.name for tag in output['tags']]) + expected = set([TAG1, TAG2]) + self.assertEqual(expected, actual) + + def test_tag_index_empty(self): + request = unit_test_utils.get_fake_request() + output = self.tag_controller.index(request, NAMESPACE5) + output = output.to_dict() + self.assertEqual(0, len(output['tags'])) + + def test_tag_index_non_existing_namespace(self): + request = unit_test_utils.get_fake_request() + self.assertRaises(webob.exc.HTTPNotFound, self.tag_controller.index, + request, NAMESPACE4) + + def test_tag_show(self): + request = unit_test_utils.get_fake_request() + output = self.tag_controller.show(request, NAMESPACE3, TAG1) + self.assertEqual(TAG1, output.name) + + def test_tag_show_non_existing(self): + request = unit_test_utils.get_fake_request() + self.assertRaises(webob.exc.HTTPNotFound, self.tag_controller.show, + request, NAMESPACE5, TAG1) + + def test_tag_show_non_visible(self): + request = unit_test_utils.get_fake_request(tenant=TENANT2) + self.assertRaises(webob.exc.HTTPNotFound, self.tag_controller.show, + request, NAMESPACE1, TAG1) + + def test_tag_show_non_visible_admin(self): + request = unit_test_utils.get_fake_request(tenant=TENANT2, + is_admin=True) + + output = self.tag_controller.show(request, NAMESPACE1, TAG1) + self.assertEqual(TAG1, output.name) + + def test_tag_delete(self): + request = unit_test_utils.get_fake_request(tenant=TENANT3) + self.tag_controller.delete(request, NAMESPACE3, TAG1) + self.assertRaises(webob.exc.HTTPNotFound, self.tag_controller.show, + request, NAMESPACE3, TAG1) + + def test_tag_delete_other_owner(self): + request = unit_test_utils.get_fake_request() + self.assertRaises(webob.exc.HTTPForbidden, + self.tag_controller.delete, request, NAMESPACE3, + TAG1) + + def test_tag_delete_other_owner_admin(self): + request = unit_test_utils.get_fake_request(is_admin=True) + self.tag_controller.delete(request, NAMESPACE3, TAG1) + self.assertRaises(webob.exc.HTTPNotFound, self.tag_controller.show, + request, NAMESPACE3, TAG1) + + def test_tag_delete_non_existing(self): + request = unit_test_utils.get_fake_request() + self.assertRaises(webob.exc.HTTPNotFound, + self.tag_controller.delete, request, NAMESPACE5, + TAG1) + + def test_tag_delete_non_existing_namespace(self): + request = unit_test_utils.get_fake_request() + self.assertRaises(webob.exc.HTTPNotFound, + self.tag_controller.delete, request, NAMESPACE4, + TAG1) + + def test_tag_delete_non_visible(self): + request = unit_test_utils.get_fake_request(tenant=TENANT2) + self.assertRaises(webob.exc.HTTPNotFound, + self.tag_controller.delete, request, NAMESPACE1, + TAG1) + + def test_tag_delete_admin_protected(self): + request = unit_test_utils.get_fake_request(is_admin=True) + self.assertRaises(webob.exc.HTTPForbidden, + self.tag_controller.delete, request, NAMESPACE1, + TAG1) + + def test_tag_create(self): + request = unit_test_utils.get_fake_request() + + tag = glance.api.v2.model.metadef_tag.MetadefTag() + tag.name = TAG2 + tag = self.tag_controller.create(request, tag, NAMESPACE1) + self.assertEqual(TAG2, tag.name) + + tag = self.tag_controller.show(request, NAMESPACE1, TAG2) + self.assertEqual(TAG2, tag.name) + + def test_tag_create_tags(self): + request = unit_test_utils.get_fake_request() + + metadef_tags = glance.api.v2.model.metadef_tag.MetadefTags() + metadef_tags.tags = _db_tags_fixture() + output = self.tag_controller.create_tags( + request, metadef_tags, NAMESPACE1) + output = output.to_dict() + self.assertEqual(3, len(output['tags'])) + actual = set([tag.name for tag in output['tags']]) + expected = set([TAG1, TAG2, TAG3]) + self.assertEqual(expected, actual) + + def test_tag_create_duplicate_tags(self): + request = unit_test_utils.get_fake_request() + + metadef_tags = glance.api.v2.model.metadef_tag.MetadefTags() + metadef_tags.tags = _db_tags_fixture([TAG4, TAG5, TAG4]) + self.assertRaises( + webob.exc.HTTPConflict, + self.tag_controller.create_tags, + request, metadef_tags, NAMESPACE1) + + def test_tag_create_duplicate_with_pre_existing_tags(self): + request = unit_test_utils.get_fake_request() + + metadef_tags = glance.api.v2.model.metadef_tag.MetadefTags() + metadef_tags.tags = _db_tags_fixture([TAG1, TAG2, TAG3]) + output = self.tag_controller.create_tags( + request, metadef_tags, NAMESPACE1) + output = output.to_dict() + self.assertEqual(3, len(output['tags'])) + actual = set([tag.name for tag in output['tags']]) + expected = set([TAG1, TAG2, TAG3]) + self.assertEqual(expected, actual) + + metadef_tags = glance.api.v2.model.metadef_tag.MetadefTags() + metadef_tags.tags = _db_tags_fixture([TAG4, TAG5, TAG4]) + self.assertRaises( + webob.exc.HTTPConflict, + self.tag_controller.create_tags, + request, metadef_tags, NAMESPACE1) + + output = self.tag_controller.index(request, NAMESPACE1) + output = output.to_dict() + self.assertEqual(3, len(output['tags'])) + actual = set([tag.name for tag in output['tags']]) + expected = set([TAG1, TAG2, TAG3]) + self.assertEqual(expected, actual) + + def test_tag_create_conflict(self): + request = unit_test_utils.get_fake_request() + + tag = glance.api.v2.model.metadef_tag.MetadefTag() + tag.name = TAG1 + + self.assertRaises(webob.exc.HTTPConflict, + self.tag_controller.create, request, tag, + NAMESPACE1) + + def test_tag_create_non_existing_namespace(self): + request = unit_test_utils.get_fake_request() + + tag = glance.api.v2.model.metadef_tag.MetadefTag() + tag.name = TAG1 + + self.assertRaises(webob.exc.HTTPNotFound, + self.tag_controller.create, request, tag, + NAMESPACE4) + + def test_tag_create_non_visible_namespace(self): + request = unit_test_utils.get_fake_request(tenant=TENANT2) + + tag = glance.api.v2.model.metadef_tag.MetadefTag() + tag.name = TAG1 + + self.assertRaises(webob.exc.HTTPForbidden, + self.tag_controller.create, request, tag, + NAMESPACE1) + + def test_tag_create_non_visible_namespace_admin(self): + request = unit_test_utils.get_fake_request(tenant=TENANT2, + is_admin=True) + + tag = glance.api.v2.model.metadef_tag.MetadefTag() + tag.name = TAG2 + + tag = self.tag_controller.create(request, tag, NAMESPACE1) + self.assertEqual(TAG2, tag.name) + + tag = self.tag_controller.show(request, NAMESPACE1, TAG2) + self.assertEqual(TAG2, tag.name) + + def test_tag_update(self): + request = unit_test_utils.get_fake_request(tenant=TENANT3) + + tag = self.tag_controller.show(request, NAMESPACE3, TAG1) + tag.name = TAG3 + tag = self.tag_controller.update(request, tag, NAMESPACE3, TAG1) + self.assertEqual(TAG3, tag.name) + + property = self.tag_controller.show(request, NAMESPACE3, TAG3) + self.assertEqual(TAG3, property.name) + + def test_tag_update_name(self): + request = unit_test_utils.get_fake_request() + + tag = self.tag_controller.show(request, NAMESPACE1, TAG1) + tag.name = TAG2 + tag = self.tag_controller.update(request, tag, NAMESPACE1, TAG1) + self.assertEqual(TAG2, tag.name) + + tag = self.tag_controller.show(request, NAMESPACE1, TAG2) + self.assertEqual(TAG2, tag.name) + + def test_tag_update_conflict(self): + request = unit_test_utils.get_fake_request(tenant=TENANT3) + + tag = self.tag_controller.show(request, NAMESPACE3, TAG1) + tag.name = TAG2 + self.assertRaises(webob.exc.HTTPConflict, + self.tag_controller.update, request, tag, + NAMESPACE3, TAG1) + + def test_tag_update_non_existing(self): + request = unit_test_utils.get_fake_request(tenant=TENANT3) + + tag = glance.api.v2.model.metadef_tag.MetadefTag() + tag.name = TAG1 + + self.assertRaises(webob.exc.HTTPNotFound, + self.tag_controller.update, request, tag, + NAMESPACE5, TAG1) + + def test_tag_update_namespace_non_existing(self): + request = unit_test_utils.get_fake_request(tenant=TENANT3) + + tag = glance.api.v2.model.metadef_tag.MetadefTag() + tag.name = TAG1 + + self.assertRaises(webob.exc.HTTPNotFound, + self.tag_controller.update, request, tag, + NAMESPACE4, TAG1)