From c0e90fa2bdaa58249f01acbc6eb3edc36f83a740 Mon Sep 17 00:00:00 2001 From: Wayne Okuma Date: Tue, 2 Dec 2014 14:32:58 -0800 Subject: [PATCH] Support for Metadata Definition Catalog for Tags This set provides API and shell commands support for: - CRUD on metadef_tags; Change-Id: I09bdf43edee6fff615d223f1a6df7c15a1e40565 Implements: blueprint metadefs-tags-cli DocImpact --- .../tests/unit/v2/test_metadefs_tags.py | 183 ++++++++++++++++++ glanceclient/tests/unit/v2/test_shell_v2.py | 104 ++++++++++ glanceclient/v2/client.py | 3 + glanceclient/v2/metadefs.py | 104 ++++++++++ glanceclient/v2/shell.py | 114 +++++++++++ 5 files changed, 508 insertions(+) create mode 100644 glanceclient/tests/unit/v2/test_metadefs_tags.py diff --git a/glanceclient/tests/unit/v2/test_metadefs_tags.py b/glanceclient/tests/unit/v2/test_metadefs_tags.py new file mode 100644 index 00000000..10822ef8 --- /dev/null +++ b/glanceclient/tests/unit/v2/test_metadefs_tags.py @@ -0,0 +1,183 @@ +# Copyright 2015 OpenStack Foundation. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import testtools + +from glanceclient.tests import utils +from glanceclient.v2 import metadefs + +NAMESPACE1 = 'Namespace1' +TAG1 = 'Tag1' +TAG2 = 'Tag2' +TAGNEW1 = 'TagNew1' +TAGNEW2 = 'TagNew2' +TAGNEW3 = 'TagNew3' + + +def _get_tag_fixture(tag_name, **kwargs): + tag = { + "name": tag_name + } + tag.update(kwargs) + return tag + + +data_fixtures = { + "/v2/metadefs/namespaces/%s/tags" % NAMESPACE1: { + "GET": ( + {}, + { + "tags": [ + _get_tag_fixture(TAG1), + _get_tag_fixture(TAG2) + ] + } + ), + "POST": ( + {}, + { + 'tags': [ + _get_tag_fixture(TAGNEW2), + _get_tag_fixture(TAGNEW3) + ] + } + ), + "DELETE": ( + {}, + {} + ) + }, + "/v2/metadefs/namespaces/%s/tags/%s" % (NAMESPACE1, TAGNEW1): { + "POST": ( + {}, + _get_tag_fixture(TAGNEW1) + ) + }, + "/v2/metadefs/namespaces/%s/tags/%s" % (NAMESPACE1, TAG1): { + "GET": ( + {}, + _get_tag_fixture(TAG1) + ), + "PUT": ( + {}, + _get_tag_fixture(TAG2) + ), + "DELETE": ( + {}, + {} + ) + }, + "/v2/metadefs/namespaces/%s/tags/%s" % (NAMESPACE1, TAG2): { + "GET": ( + {}, + _get_tag_fixture(TAG2) + ), + }, + "/v2/metadefs/namespaces/%s/tags/%s" % (NAMESPACE1, TAGNEW2): { + "GET": ( + {}, + _get_tag_fixture(TAGNEW2) + ), + }, + "/v2/metadefs/namespaces/%s/tags/%s" % (NAMESPACE1, TAGNEW3): { + "GET": ( + {}, + _get_tag_fixture(TAGNEW3) + ), + } + +} + +schema_fixtures = { + "metadefs/tag": { + "GET": ( + {}, + { + "additionalProperties": True, + "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" + }, + 'properties': {} + } + ) + } +} + + +class TestTagController(testtools.TestCase): + def setUp(self): + super(TestTagController, self).setUp() + self.api = utils.FakeAPI(data_fixtures) + self.schema_api = utils.FakeSchemaAPI(schema_fixtures) + self.controller = metadefs.TagController(self.api, self.schema_api) + + def test_list_tag(self): + tags = list(self.controller.list(NAMESPACE1)) + + actual = [tag.name for tag in tags] + self.assertEqual([TAG1, TAG2], actual) + + def test_get_tag(self): + tag = self.controller.get(NAMESPACE1, TAG1) + self.assertEqual(TAG1, tag.name) + + def test_create_tag(self): + tag = self.controller.create(NAMESPACE1, TAGNEW1) + self.assertEqual(TAGNEW1, tag.name) + + def test_create_multiple_tags(self): + properties = { + 'tags': [TAGNEW2, TAGNEW3] + } + tags = self.controller.create_multiple(NAMESPACE1, **properties) + actual = [tag.name for tag in tags] + self.assertEqual([TAGNEW2, TAGNEW3], actual) + + def test_update_tag(self): + properties = { + 'name': TAG2 + } + tag = self.controller.update(NAMESPACE1, TAG1, **properties) + self.assertEqual(TAG2, tag.name) + + def test_delete_tag(self): + self.controller.delete(NAMESPACE1, TAG1) + expect = [ + ('DELETE', + '/v2/metadefs/namespaces/%s/tags/%s' % (NAMESPACE1, TAG1), + {}, + None)] + self.assertEqual(expect, self.api.calls) + + def test_delete_all_tags(self): + self.controller.delete_all(NAMESPACE1) + expect = [ + ('DELETE', + '/v2/metadefs/namespaces/%s/tags' % NAMESPACE1, + {}, + None)] + self.assertEqual(expect, self.api.calls) diff --git a/glanceclient/tests/unit/v2/test_shell_v2.py b/glanceclient/tests/unit/v2/test_shell_v2.py index 5967b004..2e51b960 100644 --- a/glanceclient/tests/unit/v2/test_shell_v2.py +++ b/glanceclient/tests/unit/v2/test_shell_v2.py @@ -1096,3 +1096,107 @@ class ShellV2Test(testtools.TestCase): ['name', 'description'], field_settings={ 'description': {'align': 'l', 'max_width': 50}}) + + def test_do_md_tag_create(self): + args = self._make_args({'namespace': 'MyNamespace', + 'name': 'MyTag'}) + with mock.patch.object(self.gc.metadefs_tag, + 'create') as mocked_create: + expect_tag = {} + expect_tag['namespace'] = 'MyNamespace' + expect_tag['name'] = 'MyTag' + + mocked_create.return_value = expect_tag + + test_shell.do_md_tag_create(self.gc, args) + + mocked_create.assert_called_once_with('MyNamespace', 'MyTag') + utils.print_dict.assert_called_once_with(expect_tag) + + def test_do_md_tag_update(self): + args = self._make_args({'namespace': 'MyNamespace', + 'tag': 'MyTag', + 'name': 'NewTag'}) + with mock.patch.object(self.gc.metadefs_tag, + 'update') as mocked_update: + expect_tag = {} + expect_tag['namespace'] = 'MyNamespace' + expect_tag['name'] = 'NewTag' + + mocked_update.return_value = expect_tag + + test_shell.do_md_tag_update(self.gc, args) + + mocked_update.assert_called_once_with('MyNamespace', 'MyTag', + name='NewTag') + utils.print_dict.assert_called_once_with(expect_tag) + + def test_do_md_tag_show(self): + args = self._make_args({'namespace': 'MyNamespace', + 'tag': 'MyTag', + 'sort_dir': 'desc'}) + with mock.patch.object(self.gc.metadefs_tag, 'get') as mocked_get: + expect_tag = {} + expect_tag['namespace'] = 'MyNamespace' + expect_tag['tag'] = 'MyTag' + + mocked_get.return_value = expect_tag + + test_shell.do_md_tag_show(self.gc, args) + + mocked_get.assert_called_once_with('MyNamespace', 'MyTag') + utils.print_dict.assert_called_once_with(expect_tag) + + def test_do_md_tag_delete(self): + args = self._make_args({'namespace': 'MyNamespace', + 'tag': 'MyTag'}) + with mock.patch.object(self.gc.metadefs_tag, + 'delete') as mocked_delete: + test_shell.do_md_tag_delete(self.gc, args) + + mocked_delete.assert_called_once_with('MyNamespace', 'MyTag') + + def test_do_md_namespace_tags_delete(self): + args = self._make_args({'namespace': 'MyNamespace'}) + with mock.patch.object(self.gc.metadefs_tag, + 'delete_all') as mocked_delete_all: + test_shell.do_md_namespace_tags_delete(self.gc, args) + + mocked_delete_all.assert_called_once_with('MyNamespace') + + def test_do_md_tag_list(self): + args = self._make_args({'namespace': 'MyNamespace'}) + with mock.patch.object(self.gc.metadefs_tag, 'list') as mocked_list: + expect_tags = [{'namespace': 'MyNamespace', + 'tag': 'MyTag'}] + + mocked_list.return_value = expect_tags + + test_shell.do_md_tag_list(self.gc, args) + + mocked_list.assert_called_once_with('MyNamespace') + utils.print_list.assert_called_once_with( + expect_tags, + ['name'], + field_settings={ + 'description': {'align': 'l', 'max_width': 50}}) + + def test_do_md_tag_create_multiple(self): + args = self._make_args({'namespace': 'MyNamespace', + 'delim': ',', + 'names': 'MyTag1, MyTag2'}) + with mock.patch.object( + self.gc.metadefs_tag, 'create_multiple') as mocked_create_tags: + expect_tags = [{'tags': [{'name': 'MyTag1'}, {'name': 'MyTag2'}]}] + + mocked_create_tags.return_value = expect_tags + + test_shell.do_md_tag_create_multiple(self.gc, args) + + mocked_create_tags.assert_called_once_with( + 'MyNamespace', tags=['MyTag1', 'MyTag2']) + utils.print_list.assert_called_once_with( + expect_tags, + ['name'], + field_settings={ + 'description': {'align': 'l', 'max_width': 50}}) diff --git a/glanceclient/v2/client.py b/glanceclient/v2/client.py index 803673ba..e8f280ca 100644 --- a/glanceclient/v2/client.py +++ b/glanceclient/v2/client.py @@ -56,5 +56,8 @@ class Client(object): self.metadefs_object = ( metadefs.ObjectController(self.http_client, self.schemas)) + self.metadefs_tag = ( + metadefs.TagController(self.http_client, self.schemas)) + self.metadefs_namespace = ( metadefs.NamespaceController(self.http_client, self.schemas)) diff --git a/glanceclient/v2/metadefs.py b/glanceclient/v2/metadefs.py index 84d83df7..0cca6f8d 100644 --- a/glanceclient/v2/metadefs.py +++ b/glanceclient/v2/metadefs.py @@ -385,3 +385,107 @@ class ObjectController(object): """Delete all objects in a namespace.""" url = '/v2/metadefs/namespaces/{0}/objects'.format(namespace) self.http_client.delete(url) + + +class TagController(object): + def __init__(self, http_client, schema_client): + self.http_client = http_client + self.schema_client = schema_client + + @utils.memoized_property + def model(self): + schema = self.schema_client.get('metadefs/tag') + return warlock.model_factory(schema.raw(), schemas.SchemaBasedModel) + + def create(self, namespace, tag_name): + """Create a tag. + + :param namespace: Name of a namespace the Tag belongs. + :param tag_name: The name of the new tag to create. + """ + + url = ('/v2/metadefs/namespaces/{0}/tags/{1}'.format(namespace, + tag_name)) + + resp, body = self.http_client.post(url) + body.pop('self', None) + return self.model(**body) + + def create_multiple(self, namespace, **kwargs): + """Create the list of tags. + + :param namespace: Name of a namespace to which the Tags belong. + :param kwargs: list of tags. + """ + + tag_names = kwargs.pop('tags', []) + md_tag_list = [] + + for tag_name in tag_names: + try: + md_tag_list.append(self.model(name=tag_name)) + except (warlock.InvalidOperation) as e: + raise TypeError(utils.exception_to_str(e)) + tags = {'tags': md_tag_list} + + url = '/v2/metadefs/namespaces/{0}/tags'.format(namespace) + + resp, body = self.http_client.post(url, data=tags) + body.pop('self', None) + for tag in body['tags']: + yield self.model(tag) + + def update(self, namespace, tag_name, **kwargs): + """Update a tag. + + :param namespace: Name of a namespace the Tag belongs. + :param prop_name: Name of the Tag (old one). + :param kwargs: Unpacked tag. + """ + tag = self.get(namespace, tag_name) + for (key, value) in kwargs.items(): + try: + setattr(tag, key, value) + except warlock.InvalidOperation as e: + raise TypeError(utils.exception_to_str(e)) + + # Remove read-only parameters. + read_only = ['updated_at', 'created_at'] + for elem in read_only: + if elem in tag: + del tag[elem] + + url = '/v2/metadefs/namespaces/{0}/tags/{1}'.format(namespace, + tag_name) + self.http_client.put(url, data=tag) + + return self.get(namespace, tag.name) + + def get(self, namespace, tag_name): + url = '/v2/metadefs/namespaces/{0}/tags/{1}'.format(namespace, + tag_name) + resp, body = self.http_client.get(url) + body.pop('self', None) + return self.model(**body) + + def list(self, namespace, **kwargs): + """Retrieve a listing of metadata tags. + + :returns generator over list of tags. + """ + url = '/v2/metadefs/namespaces/{0}/tags'.format(namespace) + resp, body = self.http_client.get(url) + + for tag in body['tags']: + yield self.model(tag) + + def delete(self, namespace, tag_name): + """Delete a tag.""" + url = '/v2/metadefs/namespaces/{0}/tags/{1}'.format(namespace, + tag_name) + self.http_client.delete(url) + + def delete_all(self, namespace): + """Delete all tags in a namespace.""" + url = '/v2/metadefs/namespaces/{0}/tags'.format(namespace) + self.http_client.delete(url) diff --git a/glanceclient/v2/shell.py b/glanceclient/v2/shell.py index a56e5529..5da44449 100644 --- a/glanceclient/v2/shell.py +++ b/glanceclient/v2/shell.py @@ -417,6 +417,10 @@ def _namespace_show(namespace, max_column_width=None): objects = [obj['name'] for obj in namespace['objects']] namespace['objects'] = objects + if 'tags' in namespace: + tags = [tag['name'] for tag in namespace['tags']] + namespace['tags'] = tags + if max_column_width: utils.print_dict(namespace, max_column_width) else: @@ -775,6 +779,116 @@ def do_md_object_list(gc, args): utils.print_list(objects, columns, field_settings=column_settings) +def _tag_show(tag, max_column_width=None): + tag = dict(tag) # Warlock objects are compatible with dicts + if max_column_width: + utils.print_dict(tag, max_column_width) + else: + utils.print_dict(tag) + + +@utils.arg('namespace', metavar='', + help='Name of the namespace the tag will belong to.') +@utils.arg('--name', metavar='', required=True, + help='The name of the new tag to add.') +def do_md_tag_create(gc, args): + """Add a new metadata definitions tag inside a namespace.""" + name = args.name.strip() + if name: + new_tag = gc.metadefs_tag.create(args.namespace, name) + _tag_show(new_tag) + else: + utils.exit('Please supply at least one non-blank tag name.') + + +@utils.arg('namespace', metavar='', + help='Name of the namespace the tags will belong to.') +@utils.arg('--names', metavar='', required=True, + help='A comma separated list of tag names.') +@utils.arg('--delim', metavar='', required=False, + help='The delimiter used to separate the names' + ' (if none is provided then the default is a comma).') +def do_md_tag_create_multiple(gc, args): + """Create new metadata definitions tags inside a namespace.""" + delim = args.delim or ',' + + tags = [] + names_list = args.names.split(delim) + for name in names_list: + name = name.strip() + if name: + tags.append(name) + + if not tags: + utils.exit('Please supply at least one tag name. For example: ' + '--names Tag1') + + fields = {'tags': tags} + new_tags = gc.metadefs_tag.create_multiple(args.namespace, **fields) + columns = ['name'] + column_settings = { + "description": { + "max_width": 50, + "align": "l" + } + } + utils.print_list(new_tags, columns, field_settings=column_settings) + + +@utils.arg('namespace', metavar='', + help='Name of the namespace to which the tag belongs.') +@utils.arg('tag', metavar='', help='Name of the old tag.') +@utils.arg('--name', metavar='', default=None, required=True, + help='New name of the new tag.') +def do_md_tag_update(gc, args): + """Rename a metadata definitions tag inside a namespace.""" + name = args.name.strip() + if name: + fields = {'name': name} + new_tag = gc.metadefs_tag.update(args.namespace, args.tag, + **fields) + _tag_show(new_tag) + else: + utils.exit('Please supply at least one non-blank tag name.') + + +@utils.arg('namespace', metavar='', + help='Name of the namespace to which the tag belongs.') +@utils.arg('tag', metavar='', help='Name of the tag.') +def do_md_tag_show(gc, args): + """Describe a specific metadata definitions tag inside a namespace.""" + tag = gc.metadefs_tag.get(args.namespace, args.tag) + _tag_show(tag) + + +@utils.arg('namespace', metavar='', + help='Name of the namespace to which the tag belongs.') +@utils.arg('tag', metavar='', help='Name of the tag.') +def do_md_tag_delete(gc, args): + """Delete a specific metadata definitions tag inside a namespace.""" + gc.metadefs_tag.delete(args.namespace, args.tag) + + +@utils.arg('namespace', metavar='', help='Name of namespace.') +def do_md_namespace_tags_delete(gc, args): + """Delete all metadata definitions tags inside a specific namespace.""" + gc.metadefs_tag.delete_all(args.namespace) + + +@utils.arg('namespace', metavar='', help='Name of namespace.') +def do_md_tag_list(gc, args): + """List metadata definitions tags inside a specific namespace.""" + tags = gc.metadefs_tag.list(args.namespace) + columns = ['name'] + column_settings = { + "description": { + "max_width": 50, + "align": "l" + } + } + utils.print_list(tags, columns, field_settings=column_settings) + + @utils.arg('--sort-key', default='status', choices=tasks.SORT_KEY_VALUES, help='Sort task list by specified field.')