summaryrefslogtreecommitdiffstats
path: root/nova/api
diff options
context:
space:
mode:
authorIsaku Yamahata <yamahata@valinux.co.jp>2011-07-18 23:42:02 +0000
committerTarmac <>2011-07-18 23:42:02 +0000
commit77db06c908f9c08c80beb11241c0e23247129ad6 (patch)
treee59beff78136a5ffe9b7b9db3baf4c89112a2771 /nova/api
parentd101bb53cf310e1f093c46cdbdbe2c5b0207e49e (diff)
parentd5307a2e1575778fcbfcf3d8ad65733be7544a54 (diff)
This change adds the basic boot-from-volume support to the image service.
Specifically following API will supports --block-device-mapping with volume/snapshot and root device name - register image - describe image - create image(newly support) At the moment swap and ephemeral aren't supported yet. They will be supported with the next step Next step - describe instance attribute with euca command - get metadata for bundle volume - swap/ephemeral device support
Diffstat (limited to 'nova/api')
-rw-r--r--nova/api/ec2/__init__.py7
-rw-r--r--nova/api/ec2/cloud.py329
-rw-r--r--nova/api/ec2/ec2utils.py40
3 files changed, 340 insertions, 36 deletions
diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py
index 890d57fe7..cf1734281 100644
--- a/nova/api/ec2/__init__.py
+++ b/nova/api/ec2/__init__.py
@@ -262,6 +262,8 @@ class Authorizer(wsgi.Middleware):
'TerminateInstances': ['projectmanager', 'sysadmin'],
'RebootInstances': ['projectmanager', 'sysadmin'],
'UpdateInstance': ['projectmanager', 'sysadmin'],
+ 'StartInstances': ['projectmanager', 'sysadmin'],
+ 'StopInstances': ['projectmanager', 'sysadmin'],
'DeleteVolume': ['projectmanager', 'sysadmin'],
'DescribeImages': ['all'],
'DeregisterImage': ['projectmanager', 'sysadmin'],
@@ -269,6 +271,7 @@ class Authorizer(wsgi.Middleware):
'DescribeImageAttribute': ['all'],
'ModifyImageAttribute': ['projectmanager', 'sysadmin'],
'UpdateImage': ['projectmanager', 'sysadmin'],
+ 'CreateImage': ['projectmanager', 'sysadmin'],
},
'AdminController': {
# All actions have the same permission: ['none'] (the default)
@@ -325,13 +328,13 @@ class Executor(wsgi.Application):
except exception.VolumeNotFound as ex:
LOG.info(_('VolumeNotFound raised: %s'), unicode(ex),
context=context)
- ec2_id = ec2utils.id_to_ec2_id(ex.volume_id, 'vol-%08x')
+ ec2_id = ec2utils.id_to_ec2_vol_id(ex.volume_id)
message = _('Volume %s not found') % ec2_id
return self._error(req, context, type(ex).__name__, message)
except exception.SnapshotNotFound as ex:
LOG.info(_('SnapshotNotFound raised: %s'), unicode(ex),
context=context)
- ec2_id = ec2utils.id_to_ec2_id(ex.snapshot_id, 'snap-%08x')
+ ec2_id = ec2utils.id_to_ec2_snap_id(ex.snapshot_id)
message = _('Snapshot %s not found') % ec2_id
return self._error(req, context, type(ex).__name__, message)
except exception.NotFound as ex:
diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py
index acfd1361c..16ca1ed2a 100644
--- a/nova/api/ec2/cloud.py
+++ b/nova/api/ec2/cloud.py
@@ -27,6 +27,7 @@ import netaddr
import os
import urllib
import tempfile
+import time
import shutil
from nova import compute
@@ -75,6 +76,95 @@ def _gen_key(context, user_id, key_name):
return {'private_key': private_key, 'fingerprint': fingerprint}
+# TODO(yamahata): hypervisor dependent default device name
+_DEFAULT_ROOT_DEVICE_NAME = '/dev/sda1'
+
+
+def _parse_block_device_mapping(bdm):
+ """Parse BlockDeviceMappingItemType into flat hash
+ BlockDevicedMapping.<N>.DeviceName
+ BlockDevicedMapping.<N>.Ebs.SnapshotId
+ BlockDevicedMapping.<N>.Ebs.VolumeSize
+ BlockDevicedMapping.<N>.Ebs.DeleteOnTermination
+ BlockDevicedMapping.<N>.Ebs.NoDevice
+ BlockDevicedMapping.<N>.VirtualName
+ => remove .Ebs and allow volume id in SnapshotId
+ """
+ ebs = bdm.pop('ebs', None)
+ if ebs:
+ ec2_id = ebs.pop('snapshot_id', None)
+ if ec2_id:
+ id = ec2utils.ec2_id_to_id(ec2_id)
+ if ec2_id.startswith('snap-'):
+ bdm['snapshot_id'] = id
+ elif ec2_id.startswith('vol-'):
+ bdm['volume_id'] = id
+ ebs.setdefault('delete_on_termination', True)
+ bdm.update(ebs)
+ return bdm
+
+
+def _properties_get_mappings(properties):
+ return ec2utils.mappings_prepend_dev(properties.get('mappings', []))
+
+
+def _format_block_device_mapping(bdm):
+ """Contruct BlockDeviceMappingItemType
+ {'device_name': '...', 'snapshot_id': , ...}
+ => BlockDeviceMappingItemType
+ """
+ keys = (('deviceName', 'device_name'),
+ ('virtualName', 'virtual_name'))
+ item = {}
+ for name, k in keys:
+ if k in bdm:
+ item[name] = bdm[k]
+ if bdm.get('no_device'):
+ item['noDevice'] = True
+ if ('snapshot_id' in bdm) or ('volume_id' in bdm):
+ ebs_keys = (('snapshotId', 'snapshot_id'),
+ ('snapshotId', 'volume_id'), # snapshotId is abused
+ ('volumeSize', 'volume_size'),
+ ('deleteOnTermination', 'delete_on_termination'))
+ ebs = {}
+ for name, k in ebs_keys:
+ if k in bdm:
+ if k == 'snapshot_id':
+ ebs[name] = ec2utils.id_to_ec2_snap_id(bdm[k])
+ elif k == 'volume_id':
+ ebs[name] = ec2utils.id_to_ec2_vol_id(bdm[k])
+ else:
+ ebs[name] = bdm[k]
+ assert 'snapshotId' in ebs
+ item['ebs'] = ebs
+ return item
+
+
+def _format_mappings(properties, result):
+ """Format multiple BlockDeviceMappingItemType"""
+ mappings = [{'virtualName': m['virtual'], 'deviceName': m['device']}
+ for m in _properties_get_mappings(properties)
+ if (m['virtual'] == 'swap' or
+ m['virtual'].startswith('ephemeral'))]
+
+ block_device_mapping = [_format_block_device_mapping(bdm) for bdm in
+ properties.get('block_device_mapping', [])]
+
+ # NOTE(yamahata): overwrite mappings with block_device_mapping
+ for bdm in block_device_mapping:
+ for i in range(len(mappings)):
+ if bdm['deviceName'] == mappings[i]['deviceName']:
+ del mappings[i]
+ break
+ mappings.append(bdm)
+
+ # NOTE(yamahata): trim ebs.no_device == true. Is this necessary?
+ mappings = [bdm for bdm in mappings if not (bdm.get('noDevice', False))]
+
+ if mappings:
+ result['blockDeviceMapping'] = mappings
+
+
class CloudController(object):
""" CloudController provides the critical dispatch between
inbound API calls through the endpoint and messages
@@ -179,7 +269,7 @@ class CloudController(object):
# TODO(vish): replace with real data
'ami': 'sda1',
'ephemeral0': 'sda2',
- 'root': '/dev/sda1',
+ 'root': _DEFAULT_ROOT_DEVICE_NAME,
'swap': 'sda3'},
'hostname': hostname,
'instance-action': 'none',
@@ -307,9 +397,8 @@ class CloudController(object):
def _format_snapshot(self, context, snapshot):
s = {}
- s['snapshotId'] = ec2utils.id_to_ec2_id(snapshot['id'], 'snap-%08x')
- s['volumeId'] = ec2utils.id_to_ec2_id(snapshot['volume_id'],
- 'vol-%08x')
+ s['snapshotId'] = ec2utils.id_to_ec2_snap_id(snapshot['id'])
+ s['volumeId'] = ec2utils.id_to_ec2_vol_id(snapshot['volume_id'])
s['status'] = snapshot['status']
s['startTime'] = snapshot['created_at']
s['progress'] = snapshot['progress']
@@ -686,7 +775,7 @@ class CloudController(object):
instance_data = '%s[%s]' % (instance_ec2_id,
volume['instance']['host'])
v = {}
- v['volumeId'] = ec2utils.id_to_ec2_id(volume['id'], 'vol-%08x')
+ v['volumeId'] = ec2utils.id_to_ec2_vol_id(volume['id'])
v['status'] = volume['status']
v['size'] = volume['size']
v['availabilityZone'] = volume['availability_zone']
@@ -708,8 +797,7 @@ class CloudController(object):
else:
v['attachmentSet'] = [{}]
if volume.get('snapshot_id') != None:
- v['snapshotId'] = ec2utils.id_to_ec2_id(volume['snapshot_id'],
- 'snap-%08x')
+ v['snapshotId'] = ec2utils.id_to_ec2_snap_id(volume['snapshot_id'])
else:
v['snapshotId'] = None
@@ -772,7 +860,7 @@ class CloudController(object):
'instanceId': ec2utils.id_to_ec2_id(instance_id),
'requestId': context.request_id,
'status': volume['attach_status'],
- 'volumeId': ec2utils.id_to_ec2_id(volume_id, 'vol-%08x')}
+ 'volumeId': ec2utils.id_to_ec2_vol_id(volume_id)}
def detach_volume(self, context, volume_id, **kwargs):
volume_id = ec2utils.ec2_id_to_id(volume_id)
@@ -784,7 +872,7 @@ class CloudController(object):
'instanceId': ec2utils.id_to_ec2_id(instance['id']),
'requestId': context.request_id,
'status': volume['attach_status'],
- 'volumeId': ec2utils.id_to_ec2_id(volume_id, 'vol-%08x')}
+ 'volumeId': ec2utils.id_to_ec2_vol_id(volume_id)}
def _convert_to_set(self, lst, label):
if lst is None or lst == []:
@@ -808,6 +896,37 @@ class CloudController(object):
assert len(i) == 1
return i[0]
+ def _format_instance_bdm(self, context, instance_id, root_device_name,
+ result):
+ """Format InstanceBlockDeviceMappingResponseItemType"""
+ root_device_type = 'instance-store'
+ mapping = []
+ for bdm in db.block_device_mapping_get_all_by_instance(context,
+ instance_id):
+ volume_id = bdm['volume_id']
+ if (volume_id is None or bdm['no_device']):
+ continue
+
+ if (bdm['device_name'] == root_device_name and
+ (bdm['snapshot_id'] or bdm['volume_id'])):
+ assert not bdm['virtual_name']
+ root_device_type = 'ebs'
+
+ vol = self.volume_api.get(context, volume_id=volume_id)
+ LOG.debug(_("vol = %s\n"), vol)
+ # TODO(yamahata): volume attach time
+ ebs = {'volumeId': volume_id,
+ 'deleteOnTermination': bdm['delete_on_termination'],
+ 'attachTime': vol['attach_time'] or '-',
+ 'status': vol['status'], }
+ res = {'deviceName': bdm['device_name'],
+ 'ebs': ebs, }
+ mapping.append(res)
+
+ if mapping:
+ result['blockDeviceMapping'] = mapping
+ result['rootDeviceType'] = root_device_type
+
def _format_instances(self, context, instance_id=None, **kwargs):
# TODO(termie): this method is poorly named as its name does not imply
# that it will be making a variety of database calls
@@ -869,6 +988,10 @@ class CloudController(object):
i['amiLaunchIndex'] = instance['launch_index']
i['displayName'] = instance['display_name']
i['displayDescription'] = instance['display_description']
+ i['rootDeviceName'] = (instance.get('root_device_name') or
+ _DEFAULT_ROOT_DEVICE_NAME)
+ self._format_instance_bdm(context, instance_id,
+ i['rootDeviceName'], i)
host = instance['host']
zone = self._get_availability_zone_by_host(context, host)
i['placement'] = {'availabilityZone': zone}
@@ -956,23 +1079,7 @@ class CloudController(object):
ramdisk = self._get_image(context, kwargs['ramdisk_id'])
kwargs['ramdisk_id'] = ramdisk['id']
for bdm in kwargs.get('block_device_mapping', []):
- # NOTE(yamahata)
- # BlockDevicedMapping.<N>.DeviceName
- # BlockDevicedMapping.<N>.Ebs.SnapshotId
- # BlockDevicedMapping.<N>.Ebs.VolumeSize
- # BlockDevicedMapping.<N>.Ebs.DeleteOnTermination
- # BlockDevicedMapping.<N>.VirtualName
- # => remove .Ebs and allow volume id in SnapshotId
- ebs = bdm.pop('ebs', None)
- if ebs:
- ec2_id = ebs.pop('snapshot_id')
- id = ec2utils.ec2_id_to_id(ec2_id)
- if ec2_id.startswith('snap-'):
- bdm['snapshot_id'] = id
- elif ec2_id.startswith('vol-'):
- bdm['volume_id'] = id
- ebs.setdefault('delete_on_termination', True)
- bdm.update(ebs)
+ _parse_block_device_mapping(bdm)
image = self._get_image(context, kwargs['image_id'])
@@ -1131,6 +1238,20 @@ class CloudController(object):
i['imageType'] = display_mapping.get(image_type)
i['isPublic'] = image.get('is_public') == True
i['architecture'] = image['properties'].get('architecture')
+
+ properties = image['properties']
+ root_device_name = ec2utils.properties_root_device_name(properties)
+ root_device_type = 'instance-store'
+ for bdm in properties.get('block_device_mapping', []):
+ if (bdm.get('device_name') == root_device_name and
+ ('snapshot_id' in bdm or 'volume_id' in bdm) and
+ not bdm.get('no_device')):
+ root_device_type = 'ebs'
+ i['rootDeviceName'] = (root_device_name or _DEFAULT_ROOT_DEVICE_NAME)
+ i['rootDeviceType'] = root_device_type
+
+ _format_mappings(properties, i)
+
return i
def describe_images(self, context, image_id=None, **kwargs):
@@ -1155,30 +1276,64 @@ class CloudController(object):
self.image_service.delete(context, internal_id)
return {'imageId': image_id}
+ def _register_image(self, context, metadata):
+ image = self.image_service.create(context, metadata)
+ image_type = self._image_type(image.get('container_format'))
+ image_id = self.image_ec2_id(image['id'], image_type)
+ return image_id
+
def register_image(self, context, image_location=None, **kwargs):
if image_location is None and 'name' in kwargs:
image_location = kwargs['name']
metadata = {'properties': {'image_location': image_location}}
- image = self.image_service.create(context, metadata)
- image_type = self._image_type(image.get('container_format'))
- image_id = self.image_ec2_id(image['id'],
- image_type)
+
+ if 'root_device_name' in kwargs:
+ metadata['properties']['root_device_name'] = \
+ kwargs.get('root_device_name')
+
+ mappings = [_parse_block_device_mapping(bdm) for bdm in
+ kwargs.get('block_device_mapping', [])]
+ if mappings:
+ metadata['properties']['block_device_mapping'] = mappings
+
+ image_id = self._register_image(context, metadata)
msg = _("Registered image %(image_location)s with"
" id %(image_id)s") % locals()
LOG.audit(msg, context=context)
return {'imageId': image_id}
def describe_image_attribute(self, context, image_id, attribute, **kwargs):
- if attribute != 'launchPermission':
+ def _block_device_mapping_attribute(image, result):
+ _format_mappings(image['properties'], result)
+
+ def _launch_permission_attribute(image, result):
+ result['launchPermission'] = []
+ if image['is_public']:
+ result['launchPermission'].append({'group': 'all'})
+
+ def _root_device_name_attribute(image, result):
+ result['rootDeviceName'] = \
+ ec2utils.properties_root_device_name(image['properties'])
+ if result['rootDeviceName'] is None:
+ result['rootDeviceName'] = _DEFAULT_ROOT_DEVICE_NAME
+
+ supported_attributes = {
+ 'blockDeviceMapping': _block_device_mapping_attribute,
+ 'launchPermission': _launch_permission_attribute,
+ 'rootDeviceName': _root_device_name_attribute,
+ }
+
+ fn = supported_attributes.get(attribute)
+ if fn is None:
raise exception.ApiError(_('attribute not supported: %s')
% attribute)
try:
image = self._get_image(context, image_id)
except exception.NotFound:
raise exception.ImageNotFound(image_id=image_id)
- result = {'imageId': image_id, 'launchPermission': []}
- if image['is_public']:
- result['launchPermission'].append({'group': 'all'})
+
+ result = {'imageId': image_id}
+ fn(image, result)
return result
def modify_image_attribute(self, context, image_id, attribute,
@@ -1209,3 +1364,109 @@ class CloudController(object):
internal_id = ec2utils.ec2_id_to_id(image_id)
result = self.image_service.update(context, internal_id, dict(kwargs))
return result
+
+ # TODO(yamahata): race condition
+ # At the moment there is no way to prevent others from
+ # manipulating instances/volumes/snapshots.
+ # As other code doesn't take it into consideration, here we don't
+ # care of it for now. Ostrich algorithm
+ def create_image(self, context, instance_id, **kwargs):
+ # NOTE(yamahata): name/description are ignored by register_image(),
+ # do so here
+ no_reboot = kwargs.get('no_reboot', False)
+
+ ec2_instance_id = instance_id
+ instance_id = ec2utils.ec2_id_to_id(ec2_instance_id)
+ instance = self.compute_api.get(context, instance_id)
+
+ # stop the instance if necessary
+ restart_instance = False
+ if not no_reboot:
+ state_description = instance['state_description']
+
+ # if the instance is in subtle state, refuse to proceed.
+ if state_description not in ('running', 'stopping', 'stopped'):
+ raise exception.InstanceNotRunning(instance_id=ec2_instance_id)
+
+ if state_description == 'running':
+ restart_instance = True
+ self.compute_api.stop(context, instance_id=instance_id)
+
+ # wait instance for really stopped
+ start_time = time.time()
+ while state_description != 'stopped':
+ time.sleep(1)
+ instance = self.compute_api.get(context, instance_id)
+ state_description = instance['state_description']
+ # NOTE(yamahata): timeout and error. 1 hour for now for safety.
+ # Is it too short/long?
+ # Or is there any better way?
+ timeout = 1 * 60 * 60 * 60
+ if time.time() > start_time + timeout:
+ raise exception.ApiError(
+ _('Couldn\'t stop instance with in %d sec') % timeout)
+
+ src_image = self._get_image(context, instance['image_ref'])
+ properties = src_image['properties']
+ if instance['root_device_name']:
+ properties['root_device_name'] = instance['root_device_name']
+
+ mapping = []
+ bdms = db.block_device_mapping_get_all_by_instance(context,
+ instance_id)
+ for bdm in bdms:
+ if bdm.no_device:
+ continue
+ m = {}
+ for attr in ('device_name', 'snapshot_id', 'volume_id',
+ 'volume_size', 'delete_on_termination', 'no_device',
+ 'virtual_name'):
+ val = getattr(bdm, attr)
+ if val is not None:
+ m[attr] = val
+
+ volume_id = m.get('volume_id')
+ if m.get('snapshot_id') and volume_id:
+ # create snapshot based on volume_id
+ vol = self.volume_api.get(context, volume_id=volume_id)
+ # NOTE(yamahata): Should we wait for snapshot creation?
+ # Linux LVM snapshot creation completes in
+ # short time, it doesn't matter for now.
+ snapshot = self.volume_api.create_snapshot_force(
+ context, volume_id=volume_id, name=vol['display_name'],
+ description=vol['display_description'])
+ m['snapshot_id'] = snapshot['id']
+ del m['volume_id']
+
+ if m:
+ mapping.append(m)
+
+ for m in _properties_get_mappings(properties):
+ virtual_name = m['virtual']
+ if virtual_name in ('ami', 'root'):
+ continue
+
+ assert (virtual_name == 'swap' or
+ virtual_name.startswith('ephemeral'))
+ device_name = m['device']
+ if device_name in [b['device_name'] for b in mapping
+ if not b.get('no_device', False)]:
+ continue
+
+ # NOTE(yamahata): swap and ephemeral devices are specified in
+ # AMI, but disabled for this instance by user.
+ # So disable those device by no_device.
+ mapping.append({'device_name': device_name, 'no_device': True})
+
+ if mapping:
+ properties['block_device_mapping'] = mapping
+
+ for attr in ('status', 'location', 'id'):
+ src_image.pop(attr, None)
+
+ image_id = self._register_image(context, src_image)
+
+ if restart_instance:
+ self.compute_api.start(context, instance_id=instance_id)
+
+ return {'imageId': image_id}
diff --git a/nova/api/ec2/ec2utils.py b/nova/api/ec2/ec2utils.py
index 222e1de1e..bae1e0ee5 100644
--- a/nova/api/ec2/ec2utils.py
+++ b/nova/api/ec2/ec2utils.py
@@ -34,6 +34,17 @@ def id_to_ec2_id(instance_id, template='i-%08x'):
return template % instance_id
+def id_to_ec2_snap_id(instance_id):
+ """Convert an snapshot ID (int) to an ec2 snapshot ID
+ (snap-[base 16 number])"""
+ return id_to_ec2_id(instance_id, 'snap-%08x')
+
+
+def id_to_ec2_vol_id(instance_id):
+ """Convert an volume ID (int) to an ec2 volume ID (vol-[base 16 number])"""
+ return id_to_ec2_id(instance_id, 'vol-%08x')
+
+
_c2u = re.compile('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))')
@@ -124,3 +135,32 @@ def dict_from_dotted_str(items):
args[key] = value
return args
+
+
+def properties_root_device_name(properties):
+ """get root device name from image meta data.
+ If it isn't specified, return None.
+ """
+ root_device_name = None
+
+ # NOTE(yamahata): see image_service.s3.s3create()
+ for bdm in properties.get('mappings', []):
+ if bdm['virtual'] == 'root':
+ root_device_name = bdm['device']
+
+ # NOTE(yamahata): register_image's command line can override
+ # <machine>.manifest.xml
+ if 'root_device_name' in properties:
+ root_device_name = properties['root_device_name']
+
+ return root_device_name
+
+
+def mappings_prepend_dev(mappings):
+ """Prepend '/dev/' to 'device' entry of swap/ephemeral virtual type"""
+ for m in mappings:
+ virtual = m['virtual']
+ if ((virtual == 'swap' or virtual.startswith('ephemeral')) and
+ (not m['device'].startswith('/'))):
+ m['device'] = '/dev/' + m['device']
+ return mappings