Skip to content

Commit

Permalink
Boot from volume without image supplied
Browse files Browse the repository at this point in the history
This patch allows for booting instances without supplying an image if
there is block device mapping supplied. It makes changes to nova API
and
compute services to handle requests that do not have any image
supplied.

Also it makes rescue and rebuild work with instances started from
volume.

Finally the patch introduces tests to make sure the system acts as
expected, and in the process fixes and refactors some old tests to make
them test for cases this new functionality can introduce.

This patch is intended to be a proof of concept and a first step
towards
a more cleaner interface for booting from volumes, outlined in
https://etherpad.openstack.org/grizzly-boot-from-volumes.

This patch also introduces a slight modification of the nova API so I
am flagging it with DocImpact. The change is that if the os-volumes
extension is used ImageRef does not need to be supplied to the create
server API call provided there is block_device_mapping provided.

Also note that this is the first step towards introducing a 'volume'
parameter
for starting instances which will replace the somewhat unintuitive
block_device_mapping (they will still be used but not for the boot
device).

This patch is coupled with I5ba9b0f35a5084aa91eca260f46cac83b8b6591e
that provides changes to the nova client.

Implements: blueprint improve-boot-from-volume
Fixes bug #1008622

Change-Id: I530760cfaa5eb0cae590c7383e0840c6b3f896b9
  • Loading branch information
djipko committed Dec 3, 2012
1 parent ac7737d commit 1d00dfc
Show file tree
Hide file tree
Showing 6 changed files with 288 additions and 129 deletions.
23 changes: 20 additions & 3 deletions nova/api/openstack/compute/servers.py
Expand Up @@ -743,8 +743,7 @@ def create(self, req, body):
self._validate_server_name(name)
name = name.strip()

image_href = self._image_ref_from_req_data(body)
image_href = self._image_uuid_from_href(image_href)
image_uuid = self._image_from_req_data(body)

personality = server_dict.get('personality')
config_drive = None
Expand Down Expand Up @@ -855,7 +854,7 @@ def create(self, req, body):

(instances, resv_id) = self.compute_api.create(context,
inst_type,
image_href,
image_uuid,
display_name=name,
display_description=name,
key_name=key_name,
Expand Down Expand Up @@ -1108,6 +1107,24 @@ def _image_uuid_from_href(self, image_href):

return image_uuid

def _image_from_req_data(self, data):
"""
Get image data from the request or raise appropriate
exceptions
If no image is supplied - checks to see if there is
block devices set and proper extesions loaded.
"""
image_ref = data['server'].get('imageRef')
bdm = data['server'].get('block_device_mapping')

if not image_ref and bdm and self.ext_mgr.is_loaded('os-volumes'):
return ''
else:
image_href = self._image_ref_from_req_data(data)
image_uuid = self._image_uuid_from_href(image_href)
return image_uuid

def _flavor_id_from_req_data(self, data):
try:
flavor_ref = data['server']['flavorRef']
Expand Down
25 changes: 14 additions & 11 deletions nova/api/openstack/compute/views/servers.py
Expand Up @@ -164,17 +164,20 @@ def _get_addresses(self, request, instance):

def _get_image(self, request, instance):
image_ref = instance["image_ref"]
image_id = str(common.get_id_from_href(image_ref))
bookmark = self._image_builder._get_bookmark_link(request,
image_id,
"images")
return {
"id": image_id,
"links": [{
"rel": "bookmark",
"href": bookmark,
}],
}
if image_ref:
image_id = str(common.get_id_from_href(image_ref))
bookmark = self._image_builder._get_bookmark_link(request,
image_id,
"images")
return {
"id": image_id,
"links": [{
"rel": "bookmark",
"href": bookmark,
}],
}
else:
return ""

def _get_flavor(self, request, instance):
instance_type = instance["instance_type"]
Expand Down
82 changes: 50 additions & 32 deletions nova/compute/api.py
Expand Up @@ -317,8 +317,7 @@ def _check_requested_networks(self, context, requested_networks):
self.network_api.validate_networks(context, requested_networks)

@staticmethod
def _handle_kernel_and_ramdisk(context, kernel_id, ramdisk_id, image,
image_service):
def _handle_kernel_and_ramdisk(context, kernel_id, ramdisk_id, image):
"""Choose kernel and ramdisk appropriate for the instance.
The kernel and ramdisk can be chosen in one of three ways:
Expand All @@ -330,11 +329,13 @@ def _handle_kernel_and_ramdisk(context, kernel_id, ramdisk_id, image,
3. Forced to None by using `null_kernel` FLAG.
"""
# Inherit from image if not specified
image_properties = image.get('properties', {})

if kernel_id is None:
kernel_id = image['properties'].get('kernel_id')
kernel_id = image_properties.get('kernel_id')

if ramdisk_id is None:
ramdisk_id = image['properties'].get('ramdisk_id')
ramdisk_id = image_properties.get('ramdisk_id')

# Force to None if using null_kernel
if kernel_id == str(CONF.null_kernel):
Expand All @@ -343,9 +344,13 @@ def _handle_kernel_and_ramdisk(context, kernel_id, ramdisk_id, image,

# Verify kernel and ramdisk exist (fail-fast)
if kernel_id is not None:
image_service, kernel_id = glance.get_remote_image_service(
context, kernel_id)
image_service.show(context, kernel_id)

if ramdisk_id is not None:
image_service, ramdisk_id = glance.get_remote_image_service(
context, ramdisk_id)
image_service.show(context, ramdisk_id)

return kernel_id, ramdisk_id
Expand All @@ -367,9 +372,11 @@ def _handle_availability_zone(availability_zone):

@staticmethod
def _inherit_properties_from_image(image, auto_disk_config):
image_properties = image.get('properties', {})

def prop(prop_, prop_type=None):
"""Return the value of an image property."""
value = image['properties'].get(prop_)
value = image_properties.get(prop_)

if value is not None:
if prop_type == 'bool':
Expand Down Expand Up @@ -435,17 +442,23 @@ def _create_instance(self, context, instance_type,
self._check_injected_file_quota(context, injected_files)
self._check_requested_networks(context, requested_networks)

(image_service, image_id) = glance.get_remote_image_service(
context, image_href)
image = image_service.show(context, image_id)
if image['status'] != 'active':
raise exception.ImageNotActive(image_id=image_id)
if image_href:
(image_service, image_id) = glance.get_remote_image_service(
context, image_href)
image = image_service.show(context, image_id)
if image['status'] != 'active':
raise exception.ImageNotActive(image_id=image_id)
else:
image = {}

if instance_type['memory_mb'] < int(image.get('min_ram') or 0):
raise exception.InstanceTypeMemoryTooSmall()
if instance_type['root_gb'] < int(image.get('min_disk') or 0):
raise exception.InstanceTypeDiskTooSmall()

kernel_id, ramdisk_id = self._handle_kernel_and_ramdisk(
context, kernel_id, ramdisk_id, image)

# Handle config_drive
config_drive_id = None
if config_drive and config_drive is not True:
Expand All @@ -454,10 +467,9 @@ def _create_instance(self, context, instance_type,
config_drive = None

# Ensure config_drive image exists
image_service.show(context, config_drive_id)

kernel_id, ramdisk_id = self._handle_kernel_and_ramdisk(
context, kernel_id, ramdisk_id, image, image_service)
cd_image_service, config_drive_id = \
glance.get_remote_image_service(context, config_drive_id)
cd_image_service.show(context, config_drive_id)

if key_data is None and key_name:
key_pair = self.db.key_pair_get(context, context.user_id,
Expand All @@ -467,11 +479,8 @@ def _create_instance(self, context, instance_type,
if reservation_id is None:
reservation_id = utils.generate_uid('r')

# grab the architecture from glance
architecture = image['properties'].get('architecture', 'Unknown')

root_device_name = block_device.properties_root_device_name(
image['properties'])
image.get('properties', {}))

availability_zone, forced_host = self._handle_availability_zone(
availability_zone)
Expand Down Expand Up @@ -505,7 +514,6 @@ def _create_instance(self, context, instance_type,
'access_ip_v6': access_ip_v6,
'availability_zone': availability_zone,
'root_device_name': root_device_name,
'architecture': architecture,
'progress': 0}

if user_data:
Expand Down Expand Up @@ -663,12 +671,13 @@ def _populate_instance_for_bdm(self, context, instance, instance_type,
# require elevated context?
elevated = context.elevated()
instance_uuid = instance['uuid']
mappings = image['properties'].get('mappings', [])
image_properties = image.get('properties', {})
mappings = image_properties.get('mappings', [])
if mappings:
self._update_image_block_device_mapping(elevated,
instance_type, instance_uuid, mappings)

image_bdm = image['properties'].get('block_device_mapping', [])
image_bdm = image_properties.get('block_device_mapping', [])
for mapping in (image_bdm, block_device_mapping):
if not mapping:
continue
Expand All @@ -678,9 +687,10 @@ def _populate_instance_for_bdm(self, context, instance, instance_type,
def _populate_instance_shutdown_terminate(self, instance, image,
block_device_mapping):
"""Populate instance shutdown_terminate information."""
image_properties = image.get('properties', {})
if (block_device_mapping or
image['properties'].get('mappings') or
image['properties'].get('block_device_mapping')):
image_properties.get('mappings') or
image_properties.get('block_device_mapping')):
instance['shutdown_terminate'] = False

def _populate_instance_names(self, instance):
Expand All @@ -701,6 +711,7 @@ def _default_display_name(self, instance_uuid):
def _populate_instance_for_create(self, base_options, image,
security_groups):
"""Build the beginning of a new instance."""
image_properties = image.get('properties', {})

instance = base_options
if not instance.get('uuid'):
Expand All @@ -716,13 +727,13 @@ def _populate_instance_for_create(self, base_options, image,
# Store image properties so we can use them later
# (for notifications, etc). Only store what we can.
instance.setdefault('system_metadata', {})
for key, value in image['properties'].iteritems():
for key, value in image_properties.iteritems():
new_value = str(value)[:255]
instance['system_metadata']['image_%s' % key] = new_value

# Keep a record of the original base image that this
# image's instance is derived from:
base_image_ref = image['properties'].get('base_image_ref')
base_image_ref = image_properties.get('base_image_ref')
if not base_image_ref:
# base image ref property not previously set through a snapshot.
# default to using the image ref as the base:
Expand Down Expand Up @@ -1509,8 +1520,12 @@ def _get_image(self, context, image_href):
def rebuild(self, context, instance, image_href, admin_password, **kwargs):
"""Rebuild the given instance with the provided attributes."""

orig_image_ref = instance['image_ref']
image = self._get_image(context, image_href)
if instance['image_ref']:
orig_image_ref = instance['image_ref']
image = self._get_image(context, image_href)
else:
orig_image_ref = ''
image = {}

files_to_inject = kwargs.pop('files_to_inject', [])
self._check_injected_file_quota(context, files_to_inject)
Expand All @@ -1524,11 +1539,14 @@ def rebuild(self, context, instance, image_href, admin_password, **kwargs):
if instance_type['root_gb'] < int(image.get('min_disk') or 0):
raise exception.InstanceTypeDiskTooSmall()

(image_service, image_id) = glance.get_remote_image_service(context,
image_href)
image = image_service.show(context, image_id)
if image_href:
(image_service, image_id) = glance.get_remote_image_service(
context, image_href)
image = image_service.show(context, image_id)
else:
image = {}
kernel_id, ramdisk_id = self._handle_kernel_and_ramdisk(
context, None, None, image, image_service)
context, None, None, image)

def _reset_image_metadata():
"""
Expand All @@ -1551,7 +1569,7 @@ def _reset_image_metadata():
if key.startswith('image_'):
del sys_metadata[key]
# Add the new ones
for key, value in image['properties'].iteritems():
for key, value in image.get('properties', {}).iteritems():
new_value = str(value)[:255]
sys_metadata['image_%s' % key] = new_value
self.db.instance_system_metadata_update(context,
Expand Down
26 changes: 19 additions & 7 deletions nova/compute/manager.py
Expand Up @@ -585,7 +585,11 @@ def _run_instance(self, context, request_spec,
LOG.debug(_("No node specified, defaulting to %(node)s") %
locals())

extra_usage_info = {"image_name": image_meta['name']}
if image_meta:
extra_usage_info = {"image_name": image_meta['name']}
else:
extra_usage_info = {}

self._start_building(context, instance)
self._notify_about_instance_usage(
context, instance, "create.start",
Expand Down Expand Up @@ -766,7 +770,10 @@ def _check_image_size(self, context, instance):
image, but is accurate because it reflects the image's
actual size.
"""
image_meta = _get_image_meta(context, instance['image_ref'])
if instance['image_ref']:
image_meta = _get_image_meta(context, instance['image_ref'])
else: # Instance was started from volume - so no image ref
return {}

try:
size_bytes = image_meta['size']
Expand Down Expand Up @@ -1201,7 +1208,10 @@ def rebuild_instance(self, context, instance, orig_image_ref, image_ref,
LOG.audit(_("Rebuilding instance"), context=context,
instance=instance)

image_meta = _get_image_meta(context, image_ref)
if image_ref:
image_meta = _get_image_meta(context, image_ref)
else:
image_meta = {}

# This instance.exists message should contain the original
# image_ref, not the new one. Since the DB has been updated
Expand All @@ -1213,7 +1223,7 @@ def rebuild_instance(self, context, instance, orig_image_ref, image_ref,
extra_usage_info=extra_usage_info)

# This message should contain the new image_ref
extra_usage_info = {'image_name': image_meta['name']}
extra_usage_info = {'image_name': image_meta.get('name', '')}
self._notify_about_instance_usage(context, instance,
"rebuild.start", extra_usage_info=extra_usage_info)

Expand Down Expand Up @@ -1554,10 +1564,12 @@ def rescue_instance(self, context, instance, rescue_password=None):

network_info = self._get_instance_nw_info(context, instance)

# Boot the instance using the 'base' image instead of the user's
# current (possibly broken) image
rescue_image_ref = self._get_rescue_image_ref(context, instance)
rescue_image_meta = _get_image_meta(context, rescue_image_ref)

if rescue_image_ref:
rescue_image_meta = _get_image_meta(context, rescue_image_ref)
else:
rescue_image_meta = {}

with self._error_out_instance_on_exception(context, instance['uuid']):
self.driver.rescue(context, instance,
Expand Down

0 comments on commit 1d00dfc

Please sign in to comment.