summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorWayne Okuma <wayne.okuma@hp.com>2014-12-02 14:32:58 -0800
committerWayne Okuma <wayne.okuma@hp.com>2015-07-16 14:00:50 -0700
commitc0e90fa2bdaa58249f01acbc6eb3edc36f83a740 (patch)
treed51436af67ee0ea9b556d79bb77b963703db517d
parent16ab18b64d44a3eb1767308b4b25ac7a6a6c1aee (diff)
downloadpython-glanceclient-c0e90fa2bdaa58249f01acbc6eb3edc36f83a740.tar.gz
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
-rw-r--r--glanceclient/tests/unit/v2/test_metadefs_tags.py183
-rw-r--r--glanceclient/tests/unit/v2/test_shell_v2.py104
-rw-r--r--glanceclient/v2/client.py3
-rw-r--r--glanceclient/v2/metadefs.py104
-rw-r--r--glanceclient/v2/shell.py114
5 files changed, 508 insertions, 0 deletions
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 0000000..10822ef
--- /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 5967b00..2e51b96 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 803673b..e8f280c 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 84d83df..0cca6f8 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 a56e552..5da4444 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='<NAMESPACE>',
+ help='Name of the namespace the tag will belong to.')
+@utils.arg('--name', metavar='<NAME>', 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='<NAMESPACE>',
+ help='Name of the namespace the tags will belong to.')
+@utils.arg('--names', metavar='<NAMES>', required=True,
+ help='A comma separated list of tag names.')
+@utils.arg('--delim', metavar='<DELIM>', 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='<NAMESPACE>',
+ help='Name of the namespace to which the tag belongs.')
+@utils.arg('tag', metavar='<TAG>', help='Name of the old tag.')
+@utils.arg('--name', metavar='<NAME>', 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='<NAMESPACE>',
+ help='Name of the namespace to which the tag belongs.')
+@utils.arg('tag', metavar='<TAG>', 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='<NAMESPACE>',
+ help='Name of the namespace to which the tag belongs.')
+@utils.arg('tag', metavar='<TAG>', 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='<NAMESPACE>', 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='<NAMESPACE>', 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.')