diff --git a/doc/source/command-objects/image.rst b/doc/source/command-objects/image.rst index 8dce3662f3..c3fe77a10b 100644 --- a/doc/source/command-objects/image.rst +++ b/doc/source/command-objects/image.rst @@ -238,6 +238,7 @@ Set image properties [--checksum ] [--stdin] [--property [...] ] + [--tag [...] ] [--architecture ] [--instance-id ] [--kernel-id ] @@ -344,6 +345,14 @@ Set image properties Set a property on this image (repeat option to set multiple properties) + .. versionadded:: 2 + +.. option:: --tag + + Set a tag on this image (repeat for multiple values) + + .. versionadded:: 2 + .. option:: --architecture Operating system architecture diff --git a/openstackclient/image/v2/image.py b/openstackclient/image/v2/image.py index 11c7483bdb..7ef1f7806a 100644 --- a/openstackclient/image/v2/image.py +++ b/openstackclient/image/v2/image.py @@ -57,8 +57,7 @@ def _format_image(image): properties[key] = image.get(key) # format the tags if they are there - if image.get('tags'): - info['tags'] = utils.format_list(image.get('tags')) + info['tags'] = utils.format_list(image.get('tags')) # add properties back into the dictionary as a top-level key if properties: @@ -540,7 +539,6 @@ class SetImage(show.ShowOne): # --force - needs adding # --checksum - maybe could be done client side # --stdin - could be implemented - # --tags - needs adding parser.add_argument( "image", metavar="", @@ -610,6 +608,15 @@ class SetImage(show.ShowOne): help="Set a property on this image " "(repeat option to set multiple properties)", ) + parser.add_argument( + "--tag", + dest="tags", + metavar="", + default=[], + action='append', + help="Set a tag on this image " + "(repeat option to set multiple tags)", + ) parser.add_argument( "--architecture", metavar="", @@ -669,7 +676,7 @@ class SetImage(show.ShowOne): copy_attrs = ('architecture', 'container_format', 'disk_format', 'file', 'instance_id', 'kernel_id', 'locations', 'min_disk', 'min_ram', 'name', 'os_distro', 'os_version', - 'owner', 'prefix', 'progress', 'ramdisk_id') + 'owner', 'prefix', 'progress', 'ramdisk_id', 'tags') for attr in copy_attrs: if attr in parsed_args: val = getattr(parsed_args, attr, None) @@ -705,6 +712,10 @@ class SetImage(show.ShowOne): image = utils.find_resource( image_client.images, parsed_args.image) + if parsed_args.tags: + # Tags should be extended, but duplicates removed + kwargs['tags'] = list(set(image.tags).union(set(parsed_args.tags))) + image = image_client.images.update(image.id, **kwargs) info = {} info.update(image) diff --git a/openstackclient/tests/compute/v2/test_server.py b/openstackclient/tests/compute/v2/test_server.py index 4df18f0523..1e99bcd0ba 100644 --- a/openstackclient/tests/compute/v2/test_server.py +++ b/openstackclient/tests/compute/v2/test_server.py @@ -410,13 +410,14 @@ class TestServerImageCreate(TestServer): compute_fakes.server_name, ) - collist = ('id', 'name', 'owner', 'protected', 'visibility') + collist = ('id', 'name', 'owner', 'protected', 'tags', 'visibility') self.assertEqual(collist, columns) datalist = ( image_fakes.image_id, image_fakes.image_name, image_fakes.image_owner, image_fakes.image_protected, + image_fakes.image_tags, image_fakes.image_visibility, ) self.assertEqual(datalist, data) @@ -441,13 +442,14 @@ class TestServerImageCreate(TestServer): 'img-nam', ) - collist = ('id', 'name', 'owner', 'protected', 'visibility') + collist = ('id', 'name', 'owner', 'protected', 'tags', 'visibility') self.assertEqual(collist, columns) datalist = ( image_fakes.image_id, image_fakes.image_name, image_fakes.image_owner, image_fakes.image_protected, + image_fakes.image_tags, image_fakes.image_visibility, ) self.assertEqual(datalist, data) diff --git a/openstackclient/tests/image/v2/fakes.py b/openstackclient/tests/image/v2/fakes.py index 1a9e301a01..11ad455df2 100644 --- a/openstackclient/tests/image/v2/fakes.py +++ b/openstackclient/tests/image/v2/fakes.py @@ -13,6 +13,7 @@ # under the License. # +import copy import mock from openstackclient.tests import fakes @@ -25,6 +26,7 @@ image_name = 'graven' image_owner = 'baal' image_protected = False image_visibility = 'public' +image_tags = [] IMAGE = { 'id': image_id, @@ -32,11 +34,16 @@ IMAGE = { 'owner': image_owner, 'protected': image_protected, 'visibility': image_visibility, + 'tags': image_tags } IMAGE_columns = tuple(sorted(IMAGE)) IMAGE_data = tuple((IMAGE[x] for x in sorted(IMAGE))) +IMAGE_SHOW = copy.copy(IMAGE) +IMAGE_SHOW['tags'] = '' +IMAGE_SHOW_data = tuple((IMAGE_SHOW[x] for x in sorted(IMAGE_SHOW))) + member_status = 'pending' MEMBER = { 'member_id': identity_fakes.project_id, @@ -117,6 +124,14 @@ IMAGE_schema = { "type": "string", "description": "Status of the image (READ-ONLY)" }, + "tags": { + "items": { + "type": "string", + "maxLength": 255 + }, + "type": "array", + "description": "List of strings related to the image" + }, "visibility": { "enum": [ "public", diff --git a/openstackclient/tests/image/v2/test_image.py b/openstackclient/tests/image/v2/test_image.py index ce2974d692..46da9c6893 100644 --- a/openstackclient/tests/image/v2/test_image.py +++ b/openstackclient/tests/image/v2/test_image.py @@ -96,7 +96,7 @@ class TestImageCreate(TestImage): ) self.assertEqual(image_fakes.IMAGE_columns, columns) - self.assertEqual(image_fakes.IMAGE_data, data) + self.assertEqual(image_fakes.IMAGE_SHOW_data, data) @mock.patch('glanceclient.common.utils.get_data_file', name='Open') def test_image_reserve_options(self, mock_open): @@ -151,7 +151,7 @@ class TestImageCreate(TestImage): ) self.assertEqual(image_fakes.IMAGE_columns, columns) - self.assertEqual(image_fakes.IMAGE_data, data) + self.assertEqual(image_fakes.IMAGE_SHOW_data, data) @mock.patch('glanceclient.common.utils.get_data_file', name='Open') def test_image_create_file(self, mock_open): @@ -208,7 +208,7 @@ class TestImageCreate(TestImage): ) self.assertEqual(image_fakes.IMAGE_columns, columns) - self.assertEqual(image_fakes.IMAGE_data, data) + self.assertEqual(image_fakes.IMAGE_SHOW_data, data) def test_image_create_dead_options(self): @@ -812,6 +812,81 @@ class TestImageSet(TestImage): **kwargs ) + def test_image_set_tag(self): + arglist = [ + '--tag', 'test-tag', + image_fakes.image_name, + ] + verifylist = [ + ('tags', ['test-tag']), + ('image', image_fakes.image_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + self.cmd.take_action(parsed_args) + + kwargs = { + 'tags': ['test-tag'], + } + # ImageManager.update(image, **kwargs) + self.images_mock.update.assert_called_with( + image_fakes.image_id, + **kwargs + ) + + def test_image_set_tag_merge(self): + old_image = copy.copy(image_fakes.IMAGE) + old_image['tags'] = ['old1', 'new2'] + self.images_mock.get.return_value = self.model(**old_image) + arglist = [ + '--tag', 'test-tag', + image_fakes.image_name, + ] + verifylist = [ + ('tags', ['test-tag']), + ('image', image_fakes.image_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + self.cmd.take_action(parsed_args) + + kwargs = { + 'tags': ['old1', 'new2', 'test-tag'], + } + # ImageManager.update(image, **kwargs) + a, k = self.images_mock.update.call_args + self.assertEqual(image_fakes.image_id, a[0]) + self.assertTrue('tags' in k) + self.assertEqual(set(kwargs['tags']), set(k['tags'])) + + def test_image_set_tag_merge_dupe(self): + old_image = copy.copy(image_fakes.IMAGE) + old_image['tags'] = ['old1', 'new2'] + self.images_mock.get.return_value = self.model(**old_image) + arglist = [ + '--tag', 'old1', + image_fakes.image_name, + ] + verifylist = [ + ('tags', ['old1']), + ('image', image_fakes.image_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + self.cmd.take_action(parsed_args) + + kwargs = { + 'tags': ['new2', 'old1'], + } + # ImageManager.update(image, **kwargs) + a, k = self.images_mock.update.call_args + self.assertEqual(image_fakes.image_id, a[0]) + self.assertTrue('tags' in k) + self.assertEqual(set(kwargs['tags']), set(k['tags'])) + def test_image_set_dead_options(self): arglist = [ @@ -861,4 +936,4 @@ class TestImageShow(TestImage): ) self.assertEqual(image_fakes.IMAGE_columns, columns) - self.assertEqual(image_fakes.IMAGE_data, data) + self.assertEqual(image_fakes.IMAGE_SHOW_data, data)