cdea73bd9c
This change adds the `encrypted`, `encryption_secret_uuid`, `encryption_format` and `encryption_options` to the BlockDeviceMapping object and associated table in the database. Change-Id: I6178c9bc249ef4d448de375dc16d82b2d087fc90
696 lines
25 KiB
Python
696 lines
25 KiB
Python
# Copyright 2011 Isaku Yamahata <yamahata@valinux co jp>
|
|
# All Rights Reserved.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License. You may obtain
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
|
|
import copy
|
|
import re
|
|
|
|
from oslo_log import log as logging
|
|
from oslo_utils import strutils
|
|
from oslo_utils import units
|
|
|
|
import nova.conf
|
|
from nova import exception
|
|
from nova.i18n import _
|
|
from nova import utils
|
|
from nova.virt import driver
|
|
|
|
CONF = nova.conf.CONF
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
DEFAULT_ROOT_DEV_NAME = '/dev/sda1'
|
|
_DEFAULT_MAPPINGS = {'ami': 'sda1',
|
|
'ephemeral0': 'sda2',
|
|
'root': DEFAULT_ROOT_DEV_NAME,
|
|
'swap': 'sda3'}
|
|
|
|
# Image attributes which Cinder stores in volume image metadata
|
|
# as regular properties
|
|
VIM_IMAGE_ATTRIBUTES = (
|
|
'image_id', 'image_name', 'size', 'checksum',
|
|
'container_format', 'disk_format', 'min_ram', 'min_disk',
|
|
)
|
|
|
|
bdm_legacy_fields = set(['device_name', 'delete_on_termination',
|
|
'virtual_name', 'snapshot_id',
|
|
'volume_id', 'volume_size', 'no_device',
|
|
'connection_info'])
|
|
|
|
|
|
bdm_new_fields = set(['source_type', 'destination_type',
|
|
'guest_format', 'device_type', 'disk_bus', 'boot_index',
|
|
'device_name', 'delete_on_termination', 'snapshot_id',
|
|
'volume_id', 'volume_size', 'image_id', 'no_device',
|
|
'connection_info', 'tag', 'volume_type', 'encrypted',
|
|
'encryption_secret_uuid', 'encryption_format',
|
|
'encryption_options'])
|
|
|
|
|
|
bdm_db_only_fields = set(['id', 'instance_uuid', 'attachment_id', 'uuid'])
|
|
|
|
|
|
bdm_db_inherited_fields = set(['created_at', 'updated_at',
|
|
'deleted_at', 'deleted'])
|
|
|
|
|
|
class BlockDeviceDict(dict):
|
|
"""Represents a Block Device Mapping in Nova."""
|
|
|
|
_fields = bdm_new_fields
|
|
_db_only_fields = (bdm_db_only_fields |
|
|
bdm_db_inherited_fields)
|
|
|
|
_required_fields = set(['source_type'])
|
|
|
|
def __init__(self, bdm_dict=None, do_not_default=None, **kwargs):
|
|
super(BlockDeviceDict, self).__init__()
|
|
|
|
bdm_dict = bdm_dict or {}
|
|
bdm_dict.update(kwargs)
|
|
do_not_default = do_not_default or set()
|
|
|
|
self._validate(bdm_dict)
|
|
if bdm_dict.get('device_name'):
|
|
bdm_dict['device_name'] = prepend_dev(bdm_dict['device_name'])
|
|
bdm_dict['delete_on_termination'] = bool(
|
|
bdm_dict.get('delete_on_termination'))
|
|
# NOTE (ndipanov): Never default db fields
|
|
self.update({field: None for field in self._fields - do_not_default})
|
|
self.update(bdm_dict.items())
|
|
|
|
def _validate(self, bdm_dict):
|
|
"""Basic data format validations."""
|
|
dict_fields = set(key for key, _ in bdm_dict.items())
|
|
valid_fields = self._fields | self._db_only_fields
|
|
|
|
# Check that there are no bogus fields
|
|
if not (dict_fields <= valid_fields):
|
|
raise exception.InvalidBDMFormat(
|
|
details=("Following fields are invalid: %s" %
|
|
" ".join(dict_fields - valid_fields)))
|
|
|
|
if bdm_dict.get('no_device'):
|
|
return
|
|
|
|
# Check that all required fields are there
|
|
if (self._required_fields and
|
|
not ((dict_fields & self._required_fields) ==
|
|
self._required_fields)):
|
|
raise exception.InvalidBDMFormat(
|
|
details=_("Some required fields are missing"))
|
|
|
|
if 'delete_on_termination' in bdm_dict:
|
|
bdm_dict['delete_on_termination'] = strutils.bool_from_string(
|
|
bdm_dict['delete_on_termination'])
|
|
|
|
if bdm_dict.get('device_name') is not None:
|
|
validate_device_name(bdm_dict['device_name'])
|
|
|
|
validate_and_default_volume_size(bdm_dict)
|
|
|
|
if bdm_dict.get('boot_index'):
|
|
try:
|
|
bdm_dict['boot_index'] = int(bdm_dict['boot_index'])
|
|
except ValueError:
|
|
raise exception.InvalidBDMFormat(
|
|
details=_("Boot index is invalid."))
|
|
|
|
@classmethod
|
|
def from_legacy(cls, legacy_bdm):
|
|
|
|
copy_over_fields = bdm_legacy_fields & bdm_new_fields
|
|
copy_over_fields |= (bdm_db_only_fields |
|
|
bdm_db_inherited_fields)
|
|
# NOTE (ndipanov): These fields cannot be computed
|
|
# from legacy bdm, so do not default them
|
|
# to avoid overwriting meaningful values in the db
|
|
non_computable_fields = set(['boot_index', 'disk_bus',
|
|
'guest_format', 'device_type'])
|
|
|
|
new_bdm = {fld: val for fld, val in legacy_bdm.items()
|
|
if fld in copy_over_fields}
|
|
|
|
virt_name = legacy_bdm.get('virtual_name')
|
|
|
|
if is_swap_or_ephemeral(virt_name):
|
|
new_bdm['source_type'] = 'blank'
|
|
new_bdm['delete_on_termination'] = True
|
|
new_bdm['destination_type'] = 'local'
|
|
|
|
if virt_name == 'swap':
|
|
new_bdm['guest_format'] = 'swap'
|
|
else:
|
|
new_bdm['guest_format'] = CONF.default_ephemeral_format
|
|
|
|
elif legacy_bdm.get('snapshot_id'):
|
|
new_bdm['source_type'] = 'snapshot'
|
|
new_bdm['destination_type'] = 'volume'
|
|
|
|
elif legacy_bdm.get('volume_id'):
|
|
new_bdm['source_type'] = 'volume'
|
|
new_bdm['destination_type'] = 'volume'
|
|
|
|
elif legacy_bdm.get('no_device'):
|
|
# NOTE (ndipanov): Just keep the BDM for now,
|
|
pass
|
|
|
|
else:
|
|
raise exception.InvalidBDMFormat(
|
|
details=_("Unrecognized legacy format."))
|
|
|
|
return cls(new_bdm, non_computable_fields)
|
|
|
|
@classmethod
|
|
def from_api(cls, api_dict, image_uuid_specified):
|
|
"""Transform the API format of data to the internally used one.
|
|
|
|
Only validate if the source_type field makes sense.
|
|
"""
|
|
if not api_dict.get('no_device'):
|
|
|
|
source_type = api_dict.get('source_type')
|
|
device_uuid = api_dict.get('uuid')
|
|
destination_type = api_dict.get('destination_type')
|
|
volume_type = api_dict.get('volume_type')
|
|
|
|
if source_type == 'blank' and device_uuid:
|
|
raise exception.InvalidBDMFormat(
|
|
details=_("Invalid device UUID."))
|
|
elif source_type != 'blank':
|
|
if not device_uuid:
|
|
raise exception.InvalidBDMFormat(
|
|
details=_("Missing device UUID."))
|
|
api_dict[source_type + '_id'] = device_uuid
|
|
if source_type == 'image' and destination_type == 'local':
|
|
# NOTE(mriedem): boot_index can be None so we need to
|
|
# account for that to avoid a TypeError.
|
|
boot_index = api_dict.get('boot_index', -1)
|
|
if boot_index is None:
|
|
# boot_index=None is equivalent to -1.
|
|
boot_index = -1
|
|
boot_index = int(boot_index)
|
|
|
|
# if this bdm is generated from --image, then
|
|
# source_type = image and destination_type = local is allowed
|
|
if not (image_uuid_specified and boot_index == 0):
|
|
raise exception.InvalidBDMFormat(
|
|
details=_("Mapping image to local is not supported."))
|
|
|
|
if destination_type == 'local' and volume_type:
|
|
raise exception.InvalidBDMFormat(
|
|
details=_("Specifying a volume_type with destination_type="
|
|
"local is not supported."))
|
|
|
|
# Specifying a volume_type with a pre-existing source volume is
|
|
# not supported.
|
|
if source_type == 'volume' and volume_type:
|
|
raise exception.InvalidBDMFormat(
|
|
details=_("Specifying volume type to existing volume is "
|
|
"not supported."))
|
|
|
|
api_dict.pop('uuid', None)
|
|
return cls(api_dict)
|
|
|
|
def legacy(self):
|
|
copy_over_fields = bdm_legacy_fields - set(['virtual_name'])
|
|
copy_over_fields |= (bdm_db_only_fields |
|
|
bdm_db_inherited_fields)
|
|
|
|
legacy_block_device = {field: self.get(field)
|
|
for field in copy_over_fields if field in self}
|
|
|
|
source_type = self.get('source_type')
|
|
destination_type = self.get('destination_type')
|
|
no_device = self.get('no_device')
|
|
if source_type == 'blank':
|
|
if self['guest_format'] == 'swap':
|
|
legacy_block_device['virtual_name'] = 'swap'
|
|
else:
|
|
# NOTE (ndipanov): Always label as 0, it is up to
|
|
# the calling routine to re-enumerate them
|
|
legacy_block_device['virtual_name'] = 'ephemeral0'
|
|
elif source_type in ('volume', 'snapshot') or no_device:
|
|
legacy_block_device['virtual_name'] = None
|
|
elif source_type == 'image':
|
|
if destination_type != 'volume':
|
|
# NOTE(ndipanov): Image bdms with local destination
|
|
# have no meaning in the legacy format - raise
|
|
raise exception.InvalidBDMForLegacy()
|
|
legacy_block_device['virtual_name'] = None
|
|
|
|
return legacy_block_device
|
|
|
|
def get_image_mapping(self):
|
|
drop_fields = (set(['connection_info']) |
|
|
self._db_only_fields)
|
|
mapping_dict = dict(self)
|
|
for fld in drop_fields:
|
|
mapping_dict.pop(fld, None)
|
|
return mapping_dict
|
|
|
|
|
|
def is_safe_for_update(block_device_dict):
|
|
"""Determine if passed dict is a safe subset for update.
|
|
|
|
Safe subset in this case means a safe subset of both legacy
|
|
and new versions of data, that can be passed to an UPDATE query
|
|
without any transformation.
|
|
"""
|
|
fields = set(block_device_dict.keys())
|
|
return fields <= (bdm_new_fields |
|
|
bdm_db_inherited_fields |
|
|
bdm_db_only_fields)
|
|
|
|
|
|
def create_image_bdm(image_ref, boot_index=0):
|
|
"""Create a block device dict based on the image_ref.
|
|
|
|
This is useful in the API layer to keep the compatibility
|
|
with having an image_ref as a field in the instance requests
|
|
"""
|
|
return BlockDeviceDict(
|
|
{'source_type': 'image',
|
|
'image_id': image_ref,
|
|
'delete_on_termination': True,
|
|
'boot_index': boot_index,
|
|
'device_type': 'disk',
|
|
'destination_type': 'local'})
|
|
|
|
|
|
def create_blank_bdm(size, guest_format=None):
|
|
return BlockDeviceDict(
|
|
{'source_type': 'blank',
|
|
'delete_on_termination': True,
|
|
'device_type': 'disk',
|
|
'boot_index': -1,
|
|
'destination_type': 'local',
|
|
'guest_format': guest_format,
|
|
'volume_size': size})
|
|
|
|
|
|
def snapshot_from_bdm(snapshot_id, template):
|
|
"""Create a basic volume snapshot BDM from a given template bdm."""
|
|
|
|
copy_from_template = ('disk_bus', 'device_type', 'boot_index',
|
|
'delete_on_termination', 'volume_size',
|
|
'device_name')
|
|
snapshot_dict = {'source_type': 'snapshot',
|
|
'destination_type': 'volume',
|
|
'snapshot_id': snapshot_id}
|
|
for key in copy_from_template:
|
|
snapshot_dict[key] = template.get(key)
|
|
return BlockDeviceDict(snapshot_dict)
|
|
|
|
|
|
def from_legacy_mapping(legacy_block_device_mapping, image_uuid='',
|
|
root_device_name=None, no_root=False):
|
|
"""Transform a legacy list of block devices to the new data format."""
|
|
|
|
new_bdms = [BlockDeviceDict.from_legacy(legacy_bdm)
|
|
for legacy_bdm in legacy_block_device_mapping]
|
|
# NOTE (ndipanov): We will not decide which device is root here - we assume
|
|
# that it will be supplied later. This is useful for having the root device
|
|
# as part of the image defined mappings that are already in the v2 format.
|
|
if no_root:
|
|
for bdm in new_bdms:
|
|
bdm['boot_index'] = -1
|
|
return new_bdms
|
|
|
|
image_bdm = None
|
|
volume_backed = False
|
|
|
|
# Try to assign boot_device
|
|
if not root_device_name and not image_uuid:
|
|
# NOTE (ndipanov): If there is no root_device, pick the first non
|
|
# blank one.
|
|
non_blank = [bdm for bdm in new_bdms if bdm['source_type'] != 'blank']
|
|
if non_blank:
|
|
non_blank[0]['boot_index'] = 0
|
|
else:
|
|
for bdm in new_bdms:
|
|
if (bdm['source_type'] in ('volume', 'snapshot', 'image') and
|
|
root_device_name is not None and
|
|
(strip_dev(bdm.get('device_name')) ==
|
|
strip_dev(root_device_name))):
|
|
bdm['boot_index'] = 0
|
|
volume_backed = True
|
|
elif not bdm['no_device']:
|
|
bdm['boot_index'] = -1
|
|
else:
|
|
bdm['boot_index'] = None
|
|
|
|
if not volume_backed and image_uuid:
|
|
image_bdm = create_image_bdm(image_uuid, boot_index=0)
|
|
|
|
return ([image_bdm] if image_bdm else []) + new_bdms
|
|
|
|
|
|
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 validate_device_name(value):
|
|
try:
|
|
# NOTE (ndipanov): Do not allow empty device names
|
|
# until assigning default values
|
|
# are supported by nova.compute
|
|
utils.check_string_length(value, 'Device name',
|
|
min_length=1, max_length=255)
|
|
except exception.InvalidInput:
|
|
raise exception.InvalidBDMFormat(
|
|
details=_("Device name empty or too long."))
|
|
|
|
if ' ' in value:
|
|
raise exception.InvalidBDMFormat(
|
|
details=_("Device name contains spaces."))
|
|
|
|
|
|
def validate_and_default_volume_size(bdm):
|
|
if bdm.get('volume_size'):
|
|
try:
|
|
bdm['volume_size'] = utils.validate_integer(
|
|
bdm['volume_size'], 'volume_size', min_value=0)
|
|
except exception.InvalidInput:
|
|
# NOTE: We can remove this validation code after removing
|
|
# Nova v2.0 API code, because v2.1 API validates this case
|
|
# already at its REST API layer.
|
|
raise exception.InvalidBDMFormat(
|
|
details=_("Invalid volume_size."))
|
|
|
|
|
|
_ephemeral = re.compile(r'^ephemeral(\d|[1-9]\d+)$')
|
|
|
|
|
|
def is_ephemeral(device_name):
|
|
return _ephemeral.match(device_name) is not None
|
|
|
|
|
|
def ephemeral_num(ephemeral_name):
|
|
assert is_ephemeral(ephemeral_name)
|
|
return int(_ephemeral.sub('\\1', ephemeral_name))
|
|
|
|
|
|
def is_swap_or_ephemeral(device_name):
|
|
return (device_name and
|
|
(device_name == 'swap' or is_ephemeral(device_name)))
|
|
|
|
|
|
def new_format_is_swap(bdm):
|
|
if (bdm.get('source_type') == 'blank' and
|
|
bdm.get('destination_type') == 'local' and
|
|
bdm.get('guest_format') == 'swap'):
|
|
return True
|
|
return False
|
|
|
|
|
|
def new_format_is_ephemeral(bdm):
|
|
if (bdm.get('source_type') == 'blank' and
|
|
bdm.get('destination_type') == 'local' and
|
|
bdm.get('guest_format') != 'swap'):
|
|
return True
|
|
return False
|
|
|
|
|
|
def get_root_bdm(bdms):
|
|
try:
|
|
return next(bdm for bdm in bdms if bdm.get('boot_index', -1) == 0)
|
|
except StopIteration:
|
|
return None
|
|
|
|
|
|
def get_bdms_to_connect(bdms, exclude_root_mapping=False):
|
|
"""Will return non-root mappings, when exclude_root_mapping is true.
|
|
Otherwise all mappings will be returned.
|
|
"""
|
|
return (bdm for bdm in bdms if bdm.get('boot_index', -1) != 0 or
|
|
not exclude_root_mapping)
|
|
|
|
|
|
def mappings_prepend_dev(mappings):
|
|
"""Prepend '/dev/' to 'device' entry of swap/ephemeral virtual type."""
|
|
for m in mappings:
|
|
virtual = m['virtual']
|
|
if (is_swap_or_ephemeral(virtual) and
|
|
(not m['device'].startswith('/'))):
|
|
m['device'] = '/dev/' + m['device']
|
|
return mappings
|
|
|
|
|
|
_dev = re.compile('^/dev/')
|
|
|
|
|
|
def strip_dev(device_name):
|
|
"""remove leading '/dev/'."""
|
|
return _dev.sub('', device_name) if device_name else device_name
|
|
|
|
|
|
def prepend_dev(device_name):
|
|
"""Make sure there is a leading '/dev/'."""
|
|
return device_name and '/dev/' + strip_dev(device_name)
|
|
|
|
|
|
_pref = re.compile('^((x?v|s|h)d)')
|
|
|
|
|
|
def strip_prefix(device_name):
|
|
"""remove both leading /dev/ and xvd or sd or vd or hd."""
|
|
device_name = strip_dev(device_name)
|
|
return _pref.sub('', device_name) if device_name else device_name
|
|
|
|
|
|
_nums = re.compile(r'\d+')
|
|
|
|
|
|
def get_device_letter(device_name):
|
|
letter = strip_prefix(device_name)
|
|
# NOTE(vish): delete numbers in case we have something like
|
|
# /dev/sda1
|
|
return _nums.sub('', letter) if device_name else device_name
|
|
|
|
|
|
def generate_device_letter(index):
|
|
"""Returns device letter by index (starts by zero)
|
|
i.e.
|
|
index = 0, 1,..., 18277
|
|
results = a, b,..., zzz
|
|
"""
|
|
base = ord('z') - ord('a') + 1
|
|
unit_dev_name = ""
|
|
while index >= 0:
|
|
letter = chr(ord('a') + (index % base))
|
|
unit_dev_name = letter + unit_dev_name
|
|
index = int(index / base) - 1
|
|
|
|
return unit_dev_name
|
|
|
|
|
|
def generate_device_name(prefix, index):
|
|
"""Returns device unit name by index (starts by zero)
|
|
i.e.
|
|
prefix = vd
|
|
index = 0, 1,..., 18277
|
|
results = vda, vdb,..., vdzzz
|
|
"""
|
|
return prefix + generate_device_letter(index)
|
|
|
|
|
|
def instance_block_mapping(instance, bdms):
|
|
root_device_name = instance['root_device_name']
|
|
if root_device_name is None:
|
|
return _DEFAULT_MAPPINGS
|
|
|
|
mappings = {}
|
|
mappings['ami'] = strip_dev(root_device_name)
|
|
mappings['root'] = root_device_name
|
|
default_ephemeral_device = instance.get('default_ephemeral_device')
|
|
if default_ephemeral_device:
|
|
mappings['ephemeral0'] = default_ephemeral_device
|
|
default_swap_device = instance.get('default_swap_device')
|
|
if default_swap_device:
|
|
mappings['swap'] = default_swap_device
|
|
ebs_devices = []
|
|
blanks = []
|
|
|
|
# 'ephemeralN', 'swap' and ebs
|
|
for bdm in bdms:
|
|
# ebs volume case
|
|
if bdm.destination_type == 'volume':
|
|
ebs_devices.append(bdm.device_name)
|
|
continue
|
|
|
|
if bdm.source_type == 'blank':
|
|
blanks.append(bdm)
|
|
|
|
# NOTE(yamahata): I'm not sure how ebs device should be numbered.
|
|
# Right now sort by device name for deterministic
|
|
# result.
|
|
if ebs_devices:
|
|
# NOTE(claudiub): python2.7 sort places None values first.
|
|
# this sort will maintain the same behaviour for both py27 and py34.
|
|
ebs_devices = sorted(ebs_devices, key=lambda x: (x is not None, x))
|
|
for nebs, ebs in enumerate(ebs_devices):
|
|
mappings['ebs%d' % nebs] = ebs
|
|
|
|
swap = [bdm for bdm in blanks if bdm.guest_format == 'swap']
|
|
if swap:
|
|
mappings['swap'] = swap.pop().device_name
|
|
|
|
ephemerals = [bdm for bdm in blanks if bdm.guest_format != 'swap']
|
|
if ephemerals:
|
|
for num, eph in enumerate(ephemerals):
|
|
mappings['ephemeral%d' % num] = eph.device_name
|
|
|
|
return mappings
|
|
|
|
|
|
def match_device(device):
|
|
"""Matches device name and returns prefix, suffix."""
|
|
match = re.match("(^/dev/x{0,1}[a-z]{0,1}d{0,1})([a-z]+)[0-9]*$", device)
|
|
if not match:
|
|
return None
|
|
return match.groups()
|
|
|
|
|
|
def volume_in_mapping(mount_device, block_device_info):
|
|
block_device_list = [strip_dev(vol['mount_device'])
|
|
for vol in
|
|
driver.block_device_info_get_mapping(
|
|
block_device_info)]
|
|
|
|
swap = driver.block_device_info_get_swap(block_device_info)
|
|
if driver.swap_is_usable(swap):
|
|
block_device_list.append(strip_dev(swap['device_name']))
|
|
|
|
block_device_list += [strip_dev(ephemeral['device_name'])
|
|
for ephemeral in
|
|
driver.block_device_info_get_ephemerals(
|
|
block_device_info)]
|
|
|
|
LOG.debug("block_device_list %s", sorted(filter(None, block_device_list)))
|
|
return strip_dev(mount_device) in block_device_list
|
|
|
|
|
|
def get_bdm_ephemeral_disk_size(block_device_mappings):
|
|
return sum(bdm.get('volume_size', 0)
|
|
for bdm in block_device_mappings
|
|
if new_format_is_ephemeral(bdm))
|
|
|
|
|
|
def get_bdm_swap_list(block_device_mappings):
|
|
return [bdm for bdm in block_device_mappings
|
|
if new_format_is_swap(bdm)]
|
|
|
|
|
|
def get_bdm_local_disk_num(block_device_mappings):
|
|
return len([bdm for bdm in block_device_mappings
|
|
if bdm.get('destination_type') == 'local'])
|
|
|
|
|
|
def get_bdm_image_metadata(context, image_api, volume_api,
|
|
block_device_mapping, legacy_bdm=True):
|
|
"""Attempt to retrieve image metadata from a given block_device_mapping.
|
|
|
|
If we are booting from a volume, we need to get the volume details from
|
|
Cinder and make sure we pass the metadata back accordingly.
|
|
|
|
:param context: request context
|
|
:param image_api: Image API
|
|
:param volume_api: Volume API
|
|
:param block_device_mapping:
|
|
:param legacy_bdm:
|
|
"""
|
|
if not block_device_mapping:
|
|
return {}
|
|
|
|
for bdm in block_device_mapping:
|
|
if (legacy_bdm and
|
|
get_device_letter(
|
|
bdm.get('device_name', '')) != 'a'):
|
|
continue
|
|
elif not legacy_bdm and bdm.get('boot_index') != 0:
|
|
continue
|
|
|
|
volume_id = bdm.get('volume_id')
|
|
snapshot_id = bdm.get('snapshot_id')
|
|
if snapshot_id:
|
|
# NOTE(alaski): A volume snapshot inherits metadata from the
|
|
# originating volume, but the API does not expose metadata
|
|
# on the snapshot itself. So we query the volume for it below.
|
|
snapshot = volume_api.get_snapshot(context, snapshot_id)
|
|
volume_id = snapshot['volume_id']
|
|
|
|
if bdm.get('image_id'):
|
|
try:
|
|
image_id = bdm['image_id']
|
|
image_meta = image_api.get(context, image_id)
|
|
return image_meta
|
|
except Exception:
|
|
raise exception.InvalidBDMImage(id=image_id)
|
|
elif volume_id:
|
|
try:
|
|
volume = volume_api.get(context, volume_id)
|
|
except exception.CinderConnectionFailed:
|
|
raise
|
|
except Exception:
|
|
raise exception.InvalidBDMVolume(id=volume_id)
|
|
|
|
if not volume.get('bootable', True):
|
|
raise exception.InvalidBDMVolumeNotBootable(id=volume_id)
|
|
|
|
return get_image_metadata_from_volume(volume)
|
|
return {}
|
|
|
|
|
|
def get_image_metadata_from_volume(volume):
|
|
properties = copy.copy(volume.get('volume_image_metadata', {}))
|
|
image_meta = {'properties': properties}
|
|
# Volume size is no longer related to the original image size,
|
|
# so we take it from the volume directly. Cinder creates
|
|
# volumes in Gb increments, and stores size in Gb, whereas
|
|
# glance reports size in bytes. As we're returning glance
|
|
# metadata here, we need to convert it.
|
|
image_meta['size'] = volume.get('size', 0) * units.Gi
|
|
# NOTE(yjiang5): restore the basic attributes
|
|
# NOTE(mdbooth): These values come from volume_glance_metadata
|
|
# in cinder. This is a simple key/value table, and all values
|
|
# are strings. We need to convert them to ints to avoid
|
|
# unexpected type errors.
|
|
for attr in VIM_IMAGE_ATTRIBUTES:
|
|
val = properties.pop(attr, None)
|
|
if attr in ('min_ram', 'min_disk'):
|
|
image_meta[attr] = int(val or 0)
|
|
# NOTE(mriedem): Set the status to 'active' as a really old hack
|
|
# from when this method was in the compute API class and is
|
|
# needed for _validate_flavor_image which makes sure the image
|
|
# is 'active'. For volume-backed servers, if the volume is not
|
|
# available because the image backing the volume is not active,
|
|
# then the compute API trying to reserve the volume should fail.
|
|
image_meta['status'] = 'active'
|
|
return image_meta
|