summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--nova/api/ec2/__init__.py2
-rw-r--r--nova/api/ec2/cloud.py35
-rw-r--r--nova/api/ec2/images.py8
-rw-r--r--nova/db/sqlalchemy/models.py9
-rw-r--r--nova/objectstore/handler.py21
-rw-r--r--nova/objectstore/image.py10
-rw-r--r--nova/tests/cloud_unittest.py76
-rw-r--r--nova/tests/objectstore_unittest.py6
8 files changed, 160 insertions, 7 deletions
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)
diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py
index f3bd4f9d9..d3f54367b 100644
--- a/nova/api/ec2/cloud.py
+++ b/nova/api/ec2/cloud.py
@@ -285,6 +285,9 @@ class CloudController(object):
'volume_id': volume['ec2_id']}]
else:
v['attachmentSet'] = [{}]
+
+ v['display_name'] = volume['display_name']
+ v['display_description'] = volume['display_description']
return v
def create_volume(self, context, size, **kwargs):
@@ -302,6 +305,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,
@@ -368,6 +373,16 @@ class CloudController(object):
lst = [lst]
return [{label: x} for x in lst]
+ 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 True
+
def describe_instances(self, context, **kwargs):
return self._format_describe_instances(context)
@@ -420,6 +435,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']
@@ -577,6 +594,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']
@@ -673,6 +692,18 @@ class CloudController(object):
"instance_id": instance_ref['id']}})
return True
+ 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_context = {}
+ inst = db.instance_get_by_ec2_id(db_context, instance_id)
+ db.instance_update(db_context, inst['id'], kwargs)
+ return True
+
def delete_volume(self, context, volume_id, **kwargs):
# TODO: return error if not authorized
volume_ref = db.volume_get_by_ec2_id(context, volume_id)
@@ -728,3 +759,7 @@ class CloudController(object):
if not operation_type in ['add', 'remove']:
raise exception.ApiError('operation_type must be add or remove')
return images.modify(context, image_id, operation_type)
+
+ def update_image(self, context, image_id, **kwargs):
+ result = images.update(context, image_id, dict(kwargs))
+ return result
diff --git a/nova/api/ec2/images.py b/nova/api/ec2/images.py
index 4579cd81a..cb54cdda2 100644
--- a/nova/api/ec2/images.py
+++ b/nova/api/ec2/images.py
@@ -43,6 +43,14 @@ def modify(context, image_id, operation):
return True
+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(attributes))
+ return True
def register(context, image_location):
""" rpc call to register a new image based from a manifest """
diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py
index 0067cedf4..01e58b05e 100644
--- a/nova/db/sqlalchemy/models.py
+++ b/nova/db/sqlalchemy/models.py
@@ -239,7 +239,6 @@ class Instance(BASE, NovaBase):
vcpus = Column(Integer)
local_gb = Column(Integer)
-
hostname = Column(String(255))
host = Column(String(255)) # , ForeignKey('hosts.id'))
@@ -253,6 +252,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
@@ -289,6 +292,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/objectstore/handler.py b/nova/objectstore/handler.py
index aabf6831f..dfee64aca 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]))
@@ -382,16 +384,25 @@ 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 render_POST in images")
raise exception.NotAuthorized
- image_object.set_public(operation=='add')
-
+ operation = get_argument(request, 'operation', u'')
+ if operation:
+ # operation implies publicity toggle
+ logging.debug("handling publicity toggle")
+ image_object.set_public(operation=='add')
+ else:
+ # 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 f3c02a425..def1b8167 100644
--- a/nova/objectstore/image.py
+++ b/nova/objectstore/image.py
@@ -82,6 +82,16 @@ class Image(object):
with open(os.path.join(self.path, 'info.json'), 'w') as f:
json.dump(md, f)
+ 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
+ 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)
+
@staticmethod
def all():
images = []
diff --git a/nova/tests/cloud_unittest.py b/nova/tests/cloud_unittest.py
index e8ff42fc5..ae7dea1db 100644
--- a/nova/tests/cloud_unittest.py
+++ b/nova/tests/cloud_unittest.py
@@ -16,10 +16,13 @@
# License for the specific language governing permissions and limitations
# under the License.
+import json
import logging
from M2Crypto import BIO
from M2Crypto import RSA
+import os
import StringIO
+import tempfile
import time
from twisted.internet import defer
@@ -36,15 +39,22 @@ from nova.auth import manager
from nova.compute import power_state
from nova.api.ec2 import context
from nova.api.ec2 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.TrialTestCase):
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)
@@ -191,3 +201,67 @@ class CloudTestCase(test.TrialTestCase):
#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)
+
+ @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 = {'isPublic': False}
+ with open(os.path.join(pathdir, 'info.json'), 'w') as f:
+ json.dump(info, f)
+ img = image.Image('ami-testing')
+ # 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({}, {})
+ self.cloud.update_instance(self.context, inst['ec2_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'])
diff --git a/nova/tests/objectstore_unittest.py b/nova/tests/objectstore_unittest.py
index b5970d405..5a599ff3a 100644
--- a/nova/tests/objectstore_unittest.py
+++ b/nova/tests/objectstore_unittest.py
@@ -164,6 +164,12 @@ class ObjectStoreTestCase(test.TrialTestCase):
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_fields({'display_name': 'my cool image'})
+ self.assertEqual('my cool image', my_img.metadata['displayName'])
+ my_img.update_user_editable_fields({'display_name': ''})
+ self.assert_(not my_img.metadata['displayName'])
+
class TestHTTPChannel(http.HTTPChannel):
"""Dummy site required for twisted.web"""