From b68ab98d6718d5a7237f5620e8caffc770dfe822 Mon Sep 17 00:00:00 2001 From: Todd Willey Date: Tue, 21 Sep 2010 19:24:19 -0400 Subject: User updatable name & description for images. --- nova/endpoint/cloud.py | 12 ++++++++++++ nova/endpoint/images.py | 6 ++++++ nova/objectstore/handler.py | 19 +++++++++++++++---- nova/objectstore/image.py | 12 ++++++++++++ nova/tests/cloud_unittest.py | 26 +++++++++++++++++++++++++- nova/tests/objectstore_unittest.py | 6 ++++++ 6 files changed, 76 insertions(+), 5 deletions(-) diff --git a/nova/endpoint/cloud.py b/nova/endpoint/cloud.py index e29d09a89..b1678ecce 100644 --- a/nova/endpoint/cloud.py +++ b/nova/endpoint/cloud.py @@ -752,3 +752,15 @@ class CloudController(object): raise exception.ApiError('operation_type must be add or remove') result = images.modify(context, image_id, operation_type) return defer.succeed(result) + + @rbac.allow('projectmanager', 'sysadmin') + def set_image_name(self, context, image_id, name): + result = images.update_user_editable_field(context, image_id, + 'displayName', name) + return defer.succeed(result) + + @rbac.allow('projectmanager', 'sysadmin') + def set_image_description(self, context, image_id, name): + result = images.update_user_editable_field(context, image_id, + 'displayDescription', name) + return defer.succeed(result) diff --git a/nova/endpoint/images.py b/nova/endpoint/images.py index 4579cd81a..a63c059bb 100644 --- a/nova/endpoint/images.py +++ b/nova/endpoint/images.py @@ -43,6 +43,12 @@ def modify(context, image_id, operation): return True +def update_user_editable_field(context, image_id, field, value): + conn(context).make_request( + method='POST', + bucket='_images', + query_args=qs({'image_id': image_id, 'field': field, 'value': value})) + return True def register(context, image_location): """ rpc call to register a new image based from a manifest """ diff --git a/nova/objectstore/handler.py b/nova/objectstore/handler.py index 5c3dc286b..f4596e982 100644 --- a/nova/objectstore/handler.py +++ b/nova/objectstore/handler.py @@ -382,15 +382,26 @@ class ImagesResource(resource.Resource): def render_POST(self, request): # pylint: disable-msg=R0201 """Update image attributes: public/private""" + # image_id required for all requests image_id = get_argument(request, 'image_id', u'') - operation = get_argument(request, 'operation', u'') - image_object = image.Image(image_id) - if not image_object.is_authorized(request.context): + logging.debug("not authorized for handle_POST in images") raise exception.NotAuthorized - image_object.set_public(operation=='add') + operation = get_argument(request, 'operation', u'') + field = get_argument(request, 'field', u'') + value = get_argument(request, 'value', u'') + if operation: + # operation implies publicity toggle + logging.debug("handling publicity toggle") + image_object.set_public(operation=='add') + elif field: + # field implies user field editing (value can be blank) + logging.debug("update user field") + image_object.update_user_editable_field(field, value) + else: + logging.debug("unknown action for handle_POST in images") return '' diff --git a/nova/objectstore/image.py b/nova/objectstore/image.py index f3c02a425..ad1745af6 100644 --- a/nova/objectstore/image.py +++ b/nova/objectstore/image.py @@ -82,6 +82,18 @@ class Image(object): with open(os.path.join(self.path, 'info.json'), 'w') as f: json.dump(md, f) + def update_user_editable_field(self, field, value): + fields = ['displayName', 'displayDescription'] + if field not in fields: + raise KeyError("Invalid field: %s" % field) + info = self.metadata + if value: + info[field] = value + elif field in info: + del info[field] + with open(os.path.join(self.path, 'info.json'), 'w') as f: + json.dump(info, f) + @staticmethod def all(): images = [] diff --git a/nova/tests/cloud_unittest.py b/nova/tests/cloud_unittest.py index c36d5a34f..2e97180be 100644 --- a/nova/tests/cloud_unittest.py +++ b/nova/tests/cloud_unittest.py @@ -16,9 +16,13 @@ # License for the specific language governing permissions and limitations # under the License. +import json import logging +import os import StringIO +import tempfile import time + from tornado import ioloop from twisted.internet import defer import unittest @@ -32,15 +36,22 @@ from nova.auth import manager from nova.compute import power_state from nova.endpoint import api from nova.endpoint import cloud +from nova.objectstore import image FLAGS = flags.FLAGS +# Temp dirs for working with image attributes through the cloud controller +# (stole this from objectstore_unittest.py) +OSS_TEMPDIR = tempfile.mkdtemp(prefix='test_oss-') +IMAGES_PATH = os.path.join(OSS_TEMPDIR, 'images') +os.makedirs(IMAGES_PATH) + class CloudTestCase(test.BaseTestCase): def setUp(self): super(CloudTestCase, self).setUp() - self.flags(connection_type='fake') + self.flags(connection_type='fake', images_path=IMAGES_PATH) self.conn = rpc.Connection.instance() logging.getLogger().setLevel(logging.DEBUG) @@ -156,3 +167,16 @@ class CloudTestCase(test.BaseTestCase): #for i in xrange(4): # data = self.cloud.get_metadata(instance(i)['private_dns_name']) # self.assert_(data['meta-data']['ami-id'] == 'ami-%s' % i) + + def test_user_editable_endpoint(self): + pathdir = os.path.join(FLAGS.images_path, 'i-testing') + os.mkdir(pathdir) + info = {} + with open(os.path.join(pathdir, 'info.json'), 'w') as f: + json.dump(info, f) + yield self.cloud.set_image_description(self.context, 'i-testing', + 'Foo Img') + img = image.Image('i-testing') + self.assertEqual('Foo Img', img.metadata['displayDescription']) + self.cloud.set_image_description(self.context, 'i-testing', '') + self.assert_(not 'displayDescription' in img.metadata) diff --git a/nova/tests/objectstore_unittest.py b/nova/tests/objectstore_unittest.py index dece4b5d5..82c6e2c49 100644 --- a/nova/tests/objectstore_unittest.py +++ b/nova/tests/objectstore_unittest.py @@ -164,6 +164,12 @@ class ObjectStoreTestCase(test.BaseTestCase): self.context.project = self.auth_manager.get_project('proj2') self.assertFalse(my_img.is_authorized(self.context)) + # change user-editable fields + my_img.update_user_editable_field('displayName', 'my cool image') + self.assertEqual('my cool image', my_img.metadata['displayName']) + my_img.update_user_editable_field('displayName', '') + self.assert_(not 'displayName' in my_img.metadata) + class TestHTTPChannel(http.HTTPChannel): """Dummy site required for twisted.web""" -- cgit From 71c41338e4aac98ec03eec1c90ee99e43d51bcb7 Mon Sep 17 00:00:00 2001 From: Todd Willey Date: Wed, 22 Sep 2010 16:42:35 -0400 Subject: Add user display fields to instances & volumes. --- nova/db/sqlalchemy/models.py | 9 ++++++++- nova/endpoint/cloud.py | 32 ++++++++++++++++++++++++++++++++ nova/tests/cloud_unittest.py | 43 ++++++++++++++++++++++++++++++++++++++----- 3 files changed, 78 insertions(+), 6 deletions(-) diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index 41013f41b..359087db0 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -233,7 +233,6 @@ class Instance(BASE, NovaBase): vcpus = Column(Integer) local_gb = Column(Integer) - hostname = Column(String(255)) host = Column(String(255)) # , ForeignKey('hosts.id')) @@ -247,6 +246,10 @@ class Instance(BASE, NovaBase): scheduled_at = Column(DateTime) launched_at = Column(DateTime) terminated_at = Column(DateTime) + + display_name = Column(String(255)) + display_description = Column(String(255)) + # TODO(vish): see Ewan's email about state improvements, probably # should be in a driver base class or some such # vmstate_state = running, halted, suspended, paused @@ -282,6 +285,10 @@ class Volume(BASE, NovaBase): launched_at = Column(DateTime) terminated_at = Column(DateTime) + display_name = Column(String(255)) + display_description = Column(String(255)) + + class Quota(BASE, NovaBase): """Represents quota overrides for a project""" __tablename__ = 'quotas' diff --git a/nova/endpoint/cloud.py b/nova/endpoint/cloud.py index b1678ecce..50fdee60b 100644 --- a/nova/endpoint/cloud.py +++ b/nova/endpoint/cloud.py @@ -280,6 +280,10 @@ class CloudController(object): 'volume_id': volume['str_id']}] else: v['attachmentSet'] = [{}] + if 'display_name' in volume: + v['display_name'] = volume['display_name'] + if 'display_description' in volume: + v['display_description'] = volume['display_description'] return v @rbac.allow('projectmanager', 'sysadmin') @@ -299,6 +303,8 @@ class CloudController(object): vol['availability_zone'] = FLAGS.storage_availability_zone vol['status'] = "creating" vol['attach_status'] = "detached" + vol['display_name'] = kwargs.get('display_name') + vol['display_description'] = kwargs.get('display_description') volume_ref = db.volume_create(context, vol) rpc.cast(FLAGS.scheduler_topic, @@ -367,6 +373,17 @@ class CloudController(object): lst = [lst] return [{label: x} for x in lst] + @rbac.allow('projectmanager', 'sysadmin') + def update_volume(self, context, volume_id, **kwargs): + updatable_fields = ['display_name', 'display_description'] + changes = {} + for field in updatable_fields: + if field in kwargs: + changes[field] = kwargs[field] + if changes: + db.volume_update(context, volume_id, kwargs) + return defer.succeed(True) + @rbac.allow('all') def describe_instances(self, context, **kwargs): return defer.succeed(self._format_describe_instances(context)) @@ -420,6 +437,8 @@ class CloudController(object): i['instanceType'] = instance['instance_type'] i['launchTime'] = instance['created_at'] i['amiLaunchIndex'] = instance['launch_index'] + i['displayName'] = instance['display_name'] + i['displayDescription'] = instance['display_description'] if not reservations.has_key(instance['reservation_id']): r = {} r['reservationId'] = instance['reservation_id'] @@ -589,6 +608,8 @@ class CloudController(object): base_options['user_data'] = kwargs.get('user_data', '') base_options['security_group'] = security_group base_options['instance_type'] = instance_type + base_options['display_name'] = kwargs.get('display_name') + base_options['display_description'] = kwargs.get('display_description') type_data = INSTANCE_TYPES[instance_type] base_options['memory_mb'] = type_data['memory_mb'] @@ -689,6 +710,17 @@ class CloudController(object): "instance_id": instance_ref['id']}}) return defer.succeed(True) + @rbac.allow('projectmanager', 'sysadmin') + def update_instance(self, context, instance_id, **kwargs): + updatable_fields = ['display_name', 'display_description'] + changes = {} + for field in updatable_fields: + if field in kwargs: + changes[field] = kwargs[field] + if changes: + db.instance_update(context, instance_id, kwargs) + return defer.succeed(True) + @rbac.allow('projectmanager', 'sysadmin') def delete_volume(self, context, volume_id, **kwargs): # TODO: return error if not authorized diff --git a/nova/tests/cloud_unittest.py b/nova/tests/cloud_unittest.py index 2e97180be..18ad19ede 100644 --- a/nova/tests/cloud_unittest.py +++ b/nova/tests/cloud_unittest.py @@ -28,6 +28,7 @@ from twisted.internet import defer import unittest from xml.etree import ElementTree +from nova import db from nova import flags from nova import rpc from nova import test @@ -168,15 +169,47 @@ class CloudTestCase(test.BaseTestCase): # data = self.cloud.get_metadata(instance(i)['private_dns_name']) # self.assert_(data['meta-data']['ami-id'] == 'ami-%s' % i) - def test_user_editable_endpoint(self): - pathdir = os.path.join(FLAGS.images_path, 'i-testing') + def test_user_editable_image_endpoint(self): + pathdir = os.path.join(FLAGS.images_path, 'ami-testing') os.mkdir(pathdir) info = {} with open(os.path.join(pathdir, 'info.json'), 'w') as f: json.dump(info, f) - yield self.cloud.set_image_description(self.context, 'i-testing', + yield self.cloud.set_image_description(self.context, 'ami-testing', 'Foo Img') - img = image.Image('i-testing') + img = image.Image('ami-testing') self.assertEqual('Foo Img', img.metadata['displayDescription']) - self.cloud.set_image_description(self.context, 'i-testing', '') + self.cloud.set_image_description(self.context, 'ami-testing', '') self.assert_(not 'displayDescription' in img.metadata) + + def test_update_of_instance_display_fields(self): + inst = db.instance_create({}, {}) + self.cloud.update_instance(self.context, inst['id'], + display_name='c00l 1m4g3') + inst = db.instance_get({}, inst['id']) + self.assertEqual('c00l 1m4g3', inst['display_name']) + db.instance_destroy({}, inst['id']) + + def test_update_of_instance_wont_update_private_fields(self): + inst = db.instance_create({}, {}) + self.cloud.update_instance(self.context, inst['id'], + mac_address='DE:AD:BE:EF') + inst = db.instance_get({}, inst['id']) + self.assertEqual(None, inst['mac_address']) + db.instance_destroy({}, inst['id']) + + def test_update_of_volume_display_fields(self): + vol = db.volume_create({}, {}) + self.cloud.update_volume(self.context, vol['id'], + display_name='c00l v0lum3') + vol = db.volume_get({}, vol['id']) + self.assertEqual('c00l v0lum3', vol['display_name']) + db.volume_destroy({}, vol['id']) + + def test_update_of_volume_wont_update_private_fields(self): + vol = db.volume_create({}, {}) + self.cloud.update_volume(self.context, vol['id'], + mountpoint='/not/here') + vol = db.volume_get({}, vol['id']) + self.assertEqual(None, vol['mountpoint']) + db.volume_destroy({}, vol['id']) -- cgit From 4b26e60e3c84d4535fbd4ba7a3b2bf29f2121072 Mon Sep 17 00:00:00 2001 From: Todd Willey Date: Tue, 28 Sep 2010 14:09:53 -0400 Subject: Instance & Image renaming fixes. --- nova/endpoint/cloud.py | 24 ++++++++++-------------- nova/endpoint/images.py | 6 ++++-- nova/objectstore/handler.py | 18 +++++++++--------- nova/objectstore/image.py | 14 ++++++-------- nova/tests/cloud_unittest.py | 2 +- nova/tests/objectstore_unittest.py | 6 +++--- 6 files changed, 33 insertions(+), 37 deletions(-) diff --git a/nova/endpoint/cloud.py b/nova/endpoint/cloud.py index 2c174bb81..9c96ced9c 100644 --- a/nova/endpoint/cloud.py +++ b/nova/endpoint/cloud.py @@ -310,10 +310,11 @@ class CloudController(object): 'volume_id': volume['str_id']}] else: v['attachmentSet'] = [{}] - if 'display_name' in volume: - v['display_name'] = volume['display_name'] - if 'display_description' in volume: - v['display_description'] = volume['display_description'] + + # TODO(todd): check api version and only pass back to nova-aware + # clients + v['display_name'] = volume['display_name'] + v['display_description'] = volume['display_description'] return v @rbac.allow('projectmanager', 'sysadmin') @@ -746,7 +747,9 @@ class CloudController(object): if field in kwargs: changes[field] = kwargs[field] if changes: - db.instance_update(context, instance_id, kwargs) + db_context = {} + inst = db.instance_get_by_str(db_context, instance_id) + db.instance_update(db_context, inst['id'], kwargs) return defer.succeed(True) @rbac.allow('projectmanager', 'sysadmin') @@ -814,13 +817,6 @@ class CloudController(object): return defer.succeed(result) @rbac.allow('projectmanager', 'sysadmin') - def set_image_name(self, context, image_id, name): - result = images.update_user_editable_field(context, image_id, - 'displayName', name) - return defer.succeed(result) - - @rbac.allow('projectmanager', 'sysadmin') - def set_image_description(self, context, image_id, name): - result = images.update_user_editable_field(context, image_id, - 'displayDescription', name) + def update_image(self, context, image_id, **kwargs): + result = images.update(context, image_id, dict(kwargs)) return defer.succeed(result) diff --git a/nova/endpoint/images.py b/nova/endpoint/images.py index a63c059bb..cb54cdda2 100644 --- a/nova/endpoint/images.py +++ b/nova/endpoint/images.py @@ -43,11 +43,13 @@ def modify(context, image_id, operation): return True -def update_user_editable_field(context, image_id, field, value): +def update(context, image_id, attributes): + """update an image's attributes / info.json""" + attributes.update({"image_id": image_id}) conn(context).make_request( method='POST', bucket='_images', - query_args=qs({'image_id': image_id, 'field': field, 'value': value})) + query_args=qs(attributes)) return True def register(context, image_location): diff --git a/nova/objectstore/handler.py b/nova/objectstore/handler.py index f4596e982..daf633ac2 100644 --- a/nova/objectstore/handler.py +++ b/nova/objectstore/handler.py @@ -352,6 +352,8 @@ class ImagesResource(resource.Resource): m[u'imageType'] = m['type'] elif 'imageType' in m: m[u'type'] = m['imageType'] + if 'displayName' not in m: + m[u'displayName'] = u'' return m request.write(json.dumps([decorate(i.metadata) for i in images])) @@ -386,23 +388,21 @@ class ImagesResource(resource.Resource): image_id = get_argument(request, 'image_id', u'') image_object = image.Image(image_id) if not image_object.is_authorized(request.context): - logging.debug("not authorized for handle_POST in images") + logging.debug("not authorized for render_POST in images") raise exception.NotAuthorized operation = get_argument(request, 'operation', u'') - field = get_argument(request, 'field', u'') - value = get_argument(request, 'value', u'') if operation: # operation implies publicity toggle logging.debug("handling publicity toggle") image_object.set_public(operation=='add') - elif field: - # field implies user field editing (value can be blank) - logging.debug("update user field") - image_object.update_user_editable_field(field, value) else: - logging.debug("unknown action for handle_POST in images") - + # other attributes imply update + logging.debug("update user fields") + clean_args = {} + for arg in request.args.keys(): + clean_args[arg] = request.args[arg][0] + image_object.update_user_editable_fields(clean_args) return '' def render_DELETE(self, request): # pylint: disable-msg=R0201 diff --git a/nova/objectstore/image.py b/nova/objectstore/image.py index ad1745af6..def1b8167 100644 --- a/nova/objectstore/image.py +++ b/nova/objectstore/image.py @@ -82,15 +82,13 @@ class Image(object): with open(os.path.join(self.path, 'info.json'), 'w') as f: json.dump(md, f) - def update_user_editable_field(self, field, value): - fields = ['displayName', 'displayDescription'] - if field not in fields: - raise KeyError("Invalid field: %s" % field) + def update_user_editable_fields(self, args): + """args is from the request parameters, so requires extra cleaning""" + fields = {'display_name': 'displayName', 'description': 'description'} info = self.metadata - if value: - info[field] = value - elif field in info: - del info[field] + for field in fields.keys(): + if field in args: + info[fields[field]] = args[field] with open(os.path.join(self.path, 'info.json'), 'w') as f: json.dump(info, f) diff --git a/nova/tests/cloud_unittest.py b/nova/tests/cloud_unittest.py index cea2a67ce..f7340f12d 100644 --- a/nova/tests/cloud_unittest.py +++ b/nova/tests/cloud_unittest.py @@ -219,7 +219,7 @@ class CloudTestCase(test.BaseTestCase): def test_update_of_instance_display_fields(self): inst = db.instance_create({}, {}) - self.cloud.update_instance(self.context, inst['id'], + self.cloud.update_instance(self.context, inst['str_id'], display_name='c00l 1m4g3') inst = db.instance_get({}, inst['id']) self.assertEqual('c00l 1m4g3', inst['display_name']) diff --git a/nova/tests/objectstore_unittest.py b/nova/tests/objectstore_unittest.py index 82c6e2c49..4216cf190 100644 --- a/nova/tests/objectstore_unittest.py +++ b/nova/tests/objectstore_unittest.py @@ -165,10 +165,10 @@ class ObjectStoreTestCase(test.BaseTestCase): self.assertFalse(my_img.is_authorized(self.context)) # change user-editable fields - my_img.update_user_editable_field('displayName', 'my cool image') + my_img.update_user_editable_fields({'display_name': 'my cool image'}) self.assertEqual('my cool image', my_img.metadata['displayName']) - my_img.update_user_editable_field('displayName', '') - self.assert_(not 'displayName' in my_img.metadata) + my_img.update_user_editable_fields({'display_name': ''}) + self.assert_(not my_img.metadata['displayName']) class TestHTTPChannel(http.HTTPChannel): -- cgit From c1f7914f9d8c4f7687c67de37c5eda5a95245a0d Mon Sep 17 00:00:00 2001 From: Todd Willey Date: Tue, 28 Sep 2010 16:41:39 -0400 Subject: Remove TODO, since apparently newer boto doesn't die on extra fields. --- nova/api/ec2/cloud.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 4defef87e..528380f0f 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -286,8 +286,6 @@ class CloudController(object): else: v['attachmentSet'] = [{}] - # TODO(todd): check api version and only pass back to nova-aware - # clients v['display_name'] = volume['display_name'] v['display_description'] = volume['display_description'] return v -- cgit From c80c0786baadf521c86ceff21288e3760aaea5bd Mon Sep 17 00:00:00 2001 From: Todd Willey Date: Tue, 28 Sep 2010 16:47:29 -0400 Subject: Add authorization info for cloud endpoints. --- nova/api/ec2/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py index f0aa57ee4..7a958f841 100644 --- a/nova/api/ec2/__init__.py +++ b/nova/api/ec2/__init__.py @@ -158,12 +158,14 @@ class Authorizer(wsgi.Middleware): 'RunInstances': ['projectmanager', 'sysadmin'], 'TerminateInstances': ['projectmanager', 'sysadmin'], 'RebootInstances': ['projectmanager', 'sysadmin'], + 'UpdateInstance': ['projectmanager', 'sysadmin'], 'DeleteVolume': ['projectmanager', 'sysadmin'], 'DescribeImages': ['all'], 'DeregisterImage': ['projectmanager', 'sysadmin'], 'RegisterImage': ['projectmanager', 'sysadmin'], 'DescribeImageAttribute': ['all'], 'ModifyImageAttribute': ['projectmanager', 'sysadmin'], + 'UpdateImage': ['projectmanager', 'sysadmin'], }, 'AdminController': { # All actions have the same permission: ['none'] (the default) -- cgit From c4df3d63c83c073664a9ed0abaefe2adbe4cd061 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Tue, 28 Sep 2010 14:59:32 -0700 Subject: fix test for editable image --- nova/tests/cloud_unittest.py | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/nova/tests/cloud_unittest.py b/nova/tests/cloud_unittest.py index 8db4aeb77..cce4a5568 100644 --- a/nova/tests/cloud_unittest.py +++ b/nova/tests/cloud_unittest.py @@ -202,18 +202,37 @@ class CloudTestCase(test.BaseTestCase): # data = self.cloud.get_metadata(instance(i)['private_dns_name']) # self.assert_(data['meta-data']['ami-id'] == 'ami-%s' % i) + @staticmethod + def _fake_set_image_description(ctxt, image_id, description): + from nova.objectstore import handler + class req: + pass + request = req() + request.context = ctxt + request.args = {'image_id': [image_id], + 'description': [description]} + + resource = handler.ImagesResource() + resource.render_POST(request) + def test_user_editable_image_endpoint(self): pathdir = os.path.join(FLAGS.images_path, 'ami-testing') os.mkdir(pathdir) - info = {} + info = {'isPublic': False} with open(os.path.join(pathdir, 'info.json'), 'w') as f: json.dump(info, f) - yield self.cloud.set_image_description(self.context, 'ami-testing', - 'Foo Img') img = image.Image('ami-testing') - self.assertEqual('Foo Img', img.metadata['displayDescription']) - self.cloud.set_image_description(self.context, 'ami-testing', '') - self.assert_(not 'displayDescription' in img.metadata) + # self.cloud.set_image_description(self.context, 'ami-testing', + # 'Foo Img') + # NOTE(vish): Above won't work unless we start objectstore or create + # a fake version of api/ec2/images.py conn that can + # call methods directly instead of going through boto. + # for now, just cheat and call the method directly + self._fake_set_image_description(self.context, 'ami-testing', + 'Foo Img') + self.assertEqual('Foo Img', img.metadata['description']) + self._fake_set_image_description(self.context, 'ami-testing', '') + self.assertEqual('', img.metadata['description']) def test_update_of_instance_display_fields(self): inst = db.instance_create({}, {}) -- cgit