diff --git a/novaclient/v1_1/images.py b/novaclient/v1_1/images.py index e25c237e7..37b61cb5d 100644 --- a/novaclient/v1_1/images.py +++ b/novaclient/v1_1/images.py @@ -56,3 +56,24 @@ class ImageManager(base.ManagerWithFind): :param image: The :class:`Image` (or its ID) to delete. """ self._delete("/images/%s" % base.getid(image)) + + def set_meta(self, image, metadata): + """ + Set an images metadata + + :param image: The :class:`Image` to add metadata to + :param metadata: A dict of metadata to add to the image + """ + body = {'metadata': metadata} + return self._create("/images/%s/metadata" % base.getid(image), body, + "metadata") + + def delete_meta(self, image, keys): + """ + Delete metadata from an image + + :param image: The :class:`Image` to add metadata to + :param keys: A list of metadata keys to delete from the image + """ + for k in keys: + self._delete("/images/%s/metadata/%s" % (base.getid(image), k)) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 2c7a06917..7e0c62d28 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -238,6 +238,52 @@ def do_image_list(cs, args): """Print a list of available images to boot from.""" utils.print_list(cs.images.list(), ['ID', 'Name', 'Status']) +@utils.arg('image', + metavar='', + help="Name or ID of image") +@utils.arg('action', + metavar='', + choices=['set', 'delete'], + help="Actions: 'set' or 'delete'") +@utils.arg('metadata', + metavar='', + nargs='+', + action='append', + default=[], + help='Metadata to add/update or delete (only key is necessary on delete)') +def do_image_meta(cs, args): + """Set or Delete metadata on an image.""" + image = _find_image(cs, args.image) + metadata = {} + for metadatum in args.metadata[0]: + # Can only pass the key in on 'delete' + # So this doesn't have to have '=' + if metadatum.find('=') > -1: + (key, value) = metadatum.split('=',1) + else: + key = metadatum + value = None + + metadata[key] = value + + if args.action == 'set': + cs.images.set_meta(image, metadata) + elif args.action == 'delete': + cs.images.delete_meta(image, metadata.keys()) + +def _print_image(image): + links = image.links + info = image._info.copy() + info.pop('links') + utils.print_dict(info) + +@utils.arg('image', + metavar='', + help="Name or ID of image") +def do_image_show(cs, args): + """Show details about the given image.""" + image = _find_image(cs, args.image) + _print_image(image) @utils.arg('image', metavar='', help='Name or ID of image.') def do_image_delete(cs, args): diff --git a/tests/v1_1/fakes.py b/tests/v1_1/fakes.py index 1f711778a..27788ae8a 100644 --- a/tests/v1_1/fakes.py +++ b/tests/v1_1/fakes.py @@ -338,7 +338,11 @@ class FakeHTTPClient(base_client.HTTPClient): 'name': 'CentOS 5.2', "updated": "2010-10-10T12:00:00Z", "created": "2010-08-10T12:00:00Z", - "status": "ACTIVE" + "status": "ACTIVE", + "metadata": { + "test_key": "test_value", + }, + "links": {}, }, { "id": 743, @@ -347,7 +351,8 @@ class FakeHTTPClient(base_client.HTTPClient): "updated": "2010-10-10T12:00:00Z", "created": "2010-08-10T12:00:00Z", "status": "SAVING", - "progress": 80 + "progress": 80, + "links": {}, } ]}) @@ -362,9 +367,19 @@ class FakeHTTPClient(base_client.HTTPClient): fakes.assert_has_keys(body['image'], required=['serverId', 'name']) return (202, self.get_images_1()[1]) + def post_images_1_metadata(self, body, **kw): + assert body.keys() == ['metadata'] + fakes.assert_has_keys(body['metadata'], + required=['test_key']) + return (200, + {'metadata': self.get_images_1()[1]['image']['metadata']}) + def delete_images_1(self, **kw): return (204, None) + def delete_images_1_metadata_test_key(self, **kw): + return (204, None) + # # Zones # diff --git a/tests/v1_1/test_images.py b/tests/v1_1/test_images.py index bf61a3385..07609af75 100644 --- a/tests/v1_1/test_images.py +++ b/tests/v1_1/test_images.py @@ -29,6 +29,15 @@ class ImagesTest(utils.TestCase): cs.images.delete(1) cs.assert_called('DELETE', '/images/1') + def test_delete_meta(self): + cs.images.delete_meta(1, {'test_key': 'test_value'}) + cs.assert_called('DELETE', '/images/1/metadata/test_key') + + def test_set_meta(self): + cs.images.set_meta(1, {'test_key': 'test_value'}) + cs.assert_called('POST', '/images/1/metadata', + {"metadata": {'test_key': 'test_value'}}) + def test_find(self): i = cs.images.find(name="CentOS 5.2") self.assertEqual(i.id, 1) diff --git a/tests/v1_1/test_shell.py b/tests/v1_1/test_shell.py index a0a4dbe3d..b9946ee5d 100644 --- a/tests/v1_1/test_shell.py +++ b/tests/v1_1/test_shell.py @@ -1,5 +1,7 @@ import os import mock +import sys +import tempfile from novaclient.shell import OpenStackComputeShell from novaclient import exceptions @@ -157,6 +159,32 @@ class ShellTest(utils.TestCase): self.run_command('flavor-list') self.assert_called_anytime('GET', '/flavors/detail') + def test_image_show(self): + self.run_command('image-show 1') + self.assert_called('GET', '/images/1') + + def test_image_meta_set(self): + self.run_command('image-meta 1 set test_key=test_value') + self.assert_called('POST', '/images/1/metadata', + {'metadata': {'test_key': 'test_value'}}) + + def test_image_meta_del(self): + self.run_command('image-meta 1 delete test_key=test_value') + self.assert_called('DELETE', '/images/1/metadata/test_key') + + def test_image_meta_bad_action(self): + tmp = tempfile.TemporaryFile() + + # Suppress stdout and stderr + (stdout, stderr) = (sys.stdout, sys.stderr) + (sys.stdout, sys.stderr) = (tmp, tmp) + + self.assertRaises(SystemExit, self.run_command, + 'image-meta 1 BAD_ACTION test_key=test_value') + + # Put stdout and stderr back + sys.stdout, sys.stderr = (stdout, stderr) + def test_image_list(self): self.run_command('image-list') self.assert_called('GET', '/images/detail')