From b9c1df8dfc6e6520b1a9ba407b4321bd199b134c Mon Sep 17 00:00:00 2001 From: Venkatesh Sampath Date: Tue, 25 Jun 2013 17:58:42 +0530 Subject: [PATCH] Enable client V2 to update/delete tags for a given image. Added the CLI option image-tag-update to associate a tag to an image via API V2. Added the CLI option image-tag-delete to delete a tag associated with an image via API V2. Related to bp glance-client-v2 Change-Id: I76060e1982223770a6c2c0bd9376d568af0df456 --- glanceclient/v2/client.py | 5 ++- glanceclient/v2/image_tags.py | 40 ++++++++++++++++++ glanceclient/v2/shell.py | 28 +++++++++++++ tests/v2/test_shell_v2.py | 61 ++++++++++++++++++++++++++-- tests/v2/test_tags.py | 76 +++++++++++++++++++++++++++++++++++ 5 files changed, 206 insertions(+), 4 deletions(-) create mode 100644 glanceclient/v2/image_tags.py create mode 100644 tests/v2/test_tags.py diff --git a/glanceclient/v2/client.py b/glanceclient/v2/client.py index 741a59e6..99285c4f 100644 --- a/glanceclient/v2/client.py +++ b/glanceclient/v2/client.py @@ -18,6 +18,7 @@ import warlock from glanceclient.common import http from glanceclient.v2 import images from glanceclient.v2 import image_members +from glanceclient.v2 import image_tags from glanceclient.v2 import schemas @@ -34,8 +35,10 @@ class Client(object): def __init__(self, *args, **kwargs): self.http_client = http.HTTPClient(*args, **kwargs) self.schemas = schemas.Controller(self.http_client) + image_model = self._get_image_model() self.images = images.Controller(self.http_client, - self._get_image_model()) + image_model) + self.image_tags = image_tags.Controller(self.http_client, image_model) self.image_members = image_members.Controller(self.http_client, self._get_member_model()) diff --git a/glanceclient/v2/image_tags.py b/glanceclient/v2/image_tags.py new file mode 100644 index 00000000..a943d6a5 --- /dev/null +++ b/glanceclient/v2/image_tags.py @@ -0,0 +1,40 @@ +# Copyright 2013 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. + + +class Controller(object): + def __init__(self, http_client, model): + self.http_client = http_client + self.model = model + + def update(self, image_id, tag_value): + """ + Update an image with the given tag. + + :param image_id: image to be updated with the given tag. + :param tag_value: value of the tag. + """ + url = '/v2/images/%s/tags/%s' % (image_id, tag_value) + self.http_client.json_request('PUT', url) + + def delete(self, image_id, tag_value): + """ + Delete the tag associated with the given image. + + :param image_id: Image whose tag to be deleted. + :param tag_value: tag value to be deleted. + """ + url = '/v2/images/%s/tags/%s' % (image_id, tag_value) + self.http_client.json_request('DELETE', url) diff --git a/glanceclient/v2/shell.py b/glanceclient/v2/shell.py index d7e0b3f7..7427e731 100644 --- a/glanceclient/v2/shell.py +++ b/glanceclient/v2/shell.py @@ -133,3 +133,31 @@ def do_image_download(gc, args): def do_image_delete(gc, args): """Delete specified image.""" gc.images.delete(args.id) + + +@utils.arg('image_id', metavar='', + help='Image to be updated with the given tag') +@utils.arg('tag_value', metavar='', + help='Value of the tag') +def do_image_tag_update(gc, args): + """Update an image with the given tag.""" + if not (args.image_id and args.tag_value): + utils.exit('Unable to update tag. Specify image_id and tag_value') + else: + gc.image_tags.update(args.image_id, args.tag_value) + image = gc.images.get(args.image_id) + image = [image] + columns = ['ID', 'Tags'] + utils.print_list(image, columns) + + +@utils.arg('image_id', metavar='', + help='Image whose tag to be deleted') +@utils.arg('tag_value', metavar='', + help='Value of the tag') +def do_image_tag_delete(gc, args): + """Delete the tag associated with the given image.""" + if not (args.image_id and args.tag_value): + utils.exit('Unable to delete tag. Specify image_id and tag_value') + else: + gc.image_tags.delete(args.image_id, args.tag_value) diff --git a/tests/v2/test_shell_v2.py b/tests/v2/test_shell_v2.py index 1f3cc0b4..354ca971 100644 --- a/tests/v2/test_shell_v2.py +++ b/tests/v2/test_shell_v2.py @@ -24,6 +24,12 @@ from glanceclient.v2 import shell as test_shell class LegacyShellV1Test(testtools.TestCase): + def _mock_glance_client(self): + my_mocked_gc = mock.Mock() + my_mocked_gc.schemas.return_value = 'test' + my_mocked_gc.get.return_value = {} + return my_mocked_gc + def test_do_image_list(self): gc = client.Client('1', 'http://no.where') @@ -51,9 +57,7 @@ class LegacyShellV1Test(testtools.TestCase): actual = test_shell.do_image_show(gc, Fake()) def test_do_explain(self): - my_mocked_gc = mock.Mock() - my_mocked_gc.schemas.return_value = 'test' - my_mocked_gc.get.return_value = {} + my_mocked_gc = self._mock_glance_client() class Fake(): def __init__(self): @@ -84,3 +88,54 @@ class LegacyShellV1Test(testtools.TestCase): with mock.patch.object(gc.images, 'delete') as mocked_delete: mocked_delete.return_value = 0 test_shell.do_image_delete(gc, Fake()) + + def test_image_tag_update(self): + class Fake(): + image_id = 'IMG-01' + tag_value = 'tag01' + + gc = self._mock_glance_client() + + with mock.patch.object(gc.image_tags, 'update') as mocked_update: + gc.images.get = mock.Mock(return_value={}) + mocked_update.return_value = None + test_shell.do_image_tag_update(gc, Fake()) + mocked_update.assert_called_once_with('IMG-01', 'tag01') + + def test_image_tag_update_with_few_arguments(self): + class Fake(): + image_id = None + tag_value = 'tag01' + + gc = self._mock_glance_client() + + with mock.patch.object(utils, 'exit') as mocked_utils_exit: + err_msg = 'Unable to update tag. Specify image_id and tag_value' + mocked_utils_exit.return_value = '%s' % err_msg + test_shell.do_image_tag_update(gc, Fake()) + mocked_utils_exit.assert_called_once_with(err_msg) + + def test_image_tag_delete(self): + class Fake(): + image_id = 'IMG-01' + tag_value = 'tag01' + + gc = self._mock_glance_client() + + with mock.patch.object(gc.image_tags, 'delete') as mocked_delete: + mocked_delete.return_value = None + test_shell.do_image_tag_delete(gc, Fake()) + mocked_delete.assert_called_once_with('IMG-01', 'tag01') + + def test_image_tag_delete_with_few_arguments(self): + class Fake(): + image_id = 'IMG-01' + tag_value = None + + gc = self._mock_glance_client() + + with mock.patch.object(utils, 'exit') as mocked_utils_exit: + err_msg = 'Unable to delete tag. Specify image_id and tag_value' + mocked_utils_exit.return_value = '%s' % err_msg + test_shell.do_image_tag_delete(gc, Fake()) + mocked_utils_exit.assert_called_once_with(err_msg) diff --git a/tests/v2/test_tags.py b/tests/v2/test_tags.py new file mode 100644 index 00000000..2d763d43 --- /dev/null +++ b/tests/v2/test_tags.py @@ -0,0 +1,76 @@ +# Copyright 2013 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 +import warlock + +from glanceclient.v2 import image_tags +from tests import utils + + +IMAGE = '3a4560a1-e585-443e-9b39-553b46ec92d1' +TAG = 'tag01' + + +fixtures = { + '/v2/images/{image}/tags/{tag_value}'.format(image=IMAGE, tag_value=TAG): { + 'DELETE': ( + {}, + None, + ), + 'PUT': ( + {}, + { + 'image_id': IMAGE, + 'tag_value': TAG + } + ), + }, +} + + +fake_schema = {'name': 'image', 'properties': {'image_id': {}, 'tags': {}}} +FakeModel = warlock.model_factory(fake_schema) + + +class TestController(testtools.TestCase): + def setUp(self): + super(TestController, self).setUp() + self.api = utils.FakeAPI(fixtures) + self.controller = image_tags.Controller(self.api, FakeModel) + + def test_update_image_tag(self): + image_id = IMAGE + tag_value = TAG + self.controller.update(image_id, tag_value) + expect = [ + ('PUT', + '/v2/images/{image}/tags/{tag_value}'.format(image=IMAGE, + tag_value=TAG), + {}, + None)] + self.assertEqual(self.api.calls, expect) + + def test_delete_image_tag(self): + image_id = IMAGE + tag_value = TAG + self.controller.delete(image_id, tag_value) + expect = [ + ('DELETE', + '/v2/images/{image}/tags/{tag_value}'.format(image=IMAGE, + tag_value=TAG), + {}, + None)] + self.assertEqual(self.api.calls, expect)