BDM class and transformation functions
This patch adds the utility functions that will help us switch between versions of block device mapping data. In order to accomplish this, and to have a more structured approach to dealing with block device mapping data in the future, this patch introduces a BlockDeviceDict class which is a thin wrapper around a standard dict that makes it easier to reason about which fields are present in the dictionary, and introduces said conversion routines. blueprint: improve-block-device-handling Change-Id: I9370333059b8c9aaf92010470b8475a913d329b2
This commit is contained in:
		@@ -17,9 +17,14 @@
 | 
			
		||||
 | 
			
		||||
import re
 | 
			
		||||
 | 
			
		||||
from oslo.config import cfg
 | 
			
		||||
 | 
			
		||||
from nova import exception
 | 
			
		||||
from nova.openstack.common import log as logging
 | 
			
		||||
from nova.virt import driver
 | 
			
		||||
 | 
			
		||||
CONF = cfg.CONF
 | 
			
		||||
CONF.import_opt('default_ephemeral_format', 'nova.virt.driver')
 | 
			
		||||
LOG = logging.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
DEFAULT_ROOT_DEV_NAME = '/dev/sda1'
 | 
			
		||||
@@ -29,6 +34,161 @@ _DEFAULT_MAPPINGS = {'ami': 'sda1',
 | 
			
		||||
                     'swap': 'sda3'}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
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'])
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
bdm_db_only_fields = set(['id', 'instance_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)
 | 
			
		||||
 | 
			
		||||
    def __init__(self, bdm_dict=None, do_not_default=None):
 | 
			
		||||
        super(BlockDeviceDict, self).__init__()
 | 
			
		||||
 | 
			
		||||
        bdm_dict = bdm_dict or {}
 | 
			
		||||
        do_not_default = do_not_default or set()
 | 
			
		||||
 | 
			
		||||
        self._validate(bdm_dict)
 | 
			
		||||
        # NOTE (ndipanov): Never default db fields
 | 
			
		||||
        self.update(
 | 
			
		||||
            dict((field, None)
 | 
			
		||||
                 for field in self._fields - do_not_default))
 | 
			
		||||
        self.update(bdm_dict)
 | 
			
		||||
 | 
			
		||||
    def _validate(self, bdm_dict):
 | 
			
		||||
        """Basic data format validations."""
 | 
			
		||||
        if (not set(key for key, _ in bdm_dict.iteritems()) <=
 | 
			
		||||
                (self._fields | self._db_only_fields)):
 | 
			
		||||
            raise exception.InvalidBDMFormat()
 | 
			
		||||
        # TODO(ndipanov): Validate must-have fields!
 | 
			
		||||
 | 
			
		||||
    @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 = dict((fld, val) for fld, val in legacy_bdm.iteritems()
 | 
			
		||||
                        if fld in copy_over_fields)
 | 
			
		||||
 | 
			
		||||
        virt_name = legacy_bdm.get('virtual_name')
 | 
			
		||||
        volume_size = legacy_bdm.get('volume_size')
 | 
			
		||||
 | 
			
		||||
        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()
 | 
			
		||||
 | 
			
		||||
        return cls(new_bdm, non_computable_fields)
 | 
			
		||||
 | 
			
		||||
    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 = dict((field, self.get(field))
 | 
			
		||||
            for field in copy_over_fields if field in self)
 | 
			
		||||
 | 
			
		||||
        source_type = self.get('source_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':
 | 
			
		||||
            # NOTE(ndipanov): Image bdms have no meaning in
 | 
			
		||||
            # the legacy format - raise
 | 
			
		||||
            raise exception.InvalidBDMForLegacy()
 | 
			
		||||
 | 
			
		||||
        return legacy_block_device
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
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 legacy_mapping(block_device_mapping):
 | 
			
		||||
    """Transform a list of block devices of an instance back to the
 | 
			
		||||
    legacy data format."""
 | 
			
		||||
 | 
			
		||||
    legacy_block_device_mapping = []
 | 
			
		||||
 | 
			
		||||
    for bdm in block_device_mapping:
 | 
			
		||||
        try:
 | 
			
		||||
            legacy_block_device = BlockDeviceDict(bdm).legacy()
 | 
			
		||||
        except exception.InvalidBDMForLegacy:
 | 
			
		||||
            continue
 | 
			
		||||
 | 
			
		||||
        legacy_block_device_mapping.append(legacy_block_device)
 | 
			
		||||
 | 
			
		||||
    # Re-enumerate the ephemeral devices
 | 
			
		||||
    for i, dev in enumerate(dev for dev in legacy_block_device_mapping
 | 
			
		||||
                            if dev['virtual_name'] and
 | 
			
		||||
                            is_ephemeral(dev['virtual_name'])):
 | 
			
		||||
        dev['virtual_name'] = dev['virtual_name'][:-1] + str(i)
 | 
			
		||||
 | 
			
		||||
    return legacy_block_device_mapping
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def properties_root_device_name(properties):
 | 
			
		||||
    """get root device name from image meta data.
 | 
			
		||||
    If it isn't specified, return None.
 | 
			
		||||
@@ -61,7 +221,8 @@ def ephemeral_num(ephemeral_name):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def is_swap_or_ephemeral(device_name):
 | 
			
		||||
    return device_name == 'swap' or is_ephemeral(device_name)
 | 
			
		||||
    return (device_name and
 | 
			
		||||
            (device_name == 'swap' or is_ephemeral(device_name)))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def mappings_prepend_dev(mappings):
 | 
			
		||||
 
 | 
			
		||||
@@ -221,6 +221,17 @@ class InvalidBDMVolume(InvalidBDM):
 | 
			
		||||
                "failed to get volume %(id)s.")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class InvalidBDMFormat(InvalidBDM):
 | 
			
		||||
    message = _("Block Device Mapping is Invalid: "
 | 
			
		||||
                "some fields are not recognized, "
 | 
			
		||||
                "or have invalid values.")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class InvalidBDMForLegacy(InvalidBDM):
 | 
			
		||||
    message = _("Block Device Mapping cannot "
 | 
			
		||||
                "be converted to legacy format. ")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class VolumeUnattached(Invalid):
 | 
			
		||||
    message = _("Volume %(volume_id)s is not attached to anything")
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -20,7 +20,9 @@ Tests for Block Device utility functions.
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
from nova import block_device
 | 
			
		||||
from nova import exception
 | 
			
		||||
from nova import test
 | 
			
		||||
from nova.tests import matchers
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class BlockDeviceTestCase(test.TestCase):
 | 
			
		||||
@@ -126,3 +128,124 @@ class BlockDeviceTestCase(test.TestCase):
 | 
			
		||||
        _assert_volume_in_mapping('sdf', True)
 | 
			
		||||
        _assert_volume_in_mapping('sdg', False)
 | 
			
		||||
        _assert_volume_in_mapping('sdh1', False)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestBlockDeviceDict(test.TestCase):
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        super(TestBlockDeviceDict, self).setUp()
 | 
			
		||||
 | 
			
		||||
        BDM = block_device.BlockDeviceDict
 | 
			
		||||
 | 
			
		||||
        self.new_mapping = [
 | 
			
		||||
            BDM({'id': 1, 'instance_uuid': 'fake-instance',
 | 
			
		||||
                 'device_name': '/dev/sdb1',
 | 
			
		||||
                 'source_type': 'blank',
 | 
			
		||||
                 'destination_type': 'local',
 | 
			
		||||
                 'delete_on_termination': True,
 | 
			
		||||
                 'guest_format': 'swap',
 | 
			
		||||
                 'boot_index': -1}),
 | 
			
		||||
            BDM({'id': 2, 'instance_uuid': 'fake-instance',
 | 
			
		||||
                 'device_name': '/dev/sdc1',
 | 
			
		||||
                 'source_type': 'blank',
 | 
			
		||||
                 'destination_type': 'local',
 | 
			
		||||
                 'delete_on_termination': True,
 | 
			
		||||
                 'boot_index': -1}),
 | 
			
		||||
            BDM({'id': 3, 'instance_uuid': 'fake-instance',
 | 
			
		||||
                 'device_name': '/dev/sda1',
 | 
			
		||||
                 'source_type': 'volume',
 | 
			
		||||
                 'destination_type': 'volume',
 | 
			
		||||
                 'volume_id': 'fake-folume-id-1',
 | 
			
		||||
                 'connection_info': "{'fake': 'connection_info'}",
 | 
			
		||||
                 'boot_index': -1}),
 | 
			
		||||
            BDM({'id': 4, 'instance_uuid': 'fake-instance',
 | 
			
		||||
                 'device_name': '/dev/sda2',
 | 
			
		||||
                 'source_type': 'snapshot',
 | 
			
		||||
                 'destination_type': 'volume',
 | 
			
		||||
                 'connection_info': "{'fake': 'connection_info'}",
 | 
			
		||||
                 'snapshot_id': 'fake-snapshot-id-1',
 | 
			
		||||
                 'volume_id': 'fake-volume-id-2',
 | 
			
		||||
                 'boot_index': -1}),
 | 
			
		||||
            BDM({'id': 5, 'instance_uuid': 'fake-instance',
 | 
			
		||||
                 'no_device': True,
 | 
			
		||||
                 'device_name': '/dev/vdc'}),
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
        self.legacy_mapping = [
 | 
			
		||||
            {'id': 1, 'instance_uuid': 'fake-instance',
 | 
			
		||||
             'device_name': '/dev/sdb1',
 | 
			
		||||
             'delete_on_termination': True,
 | 
			
		||||
             'virtual_name': 'swap'},
 | 
			
		||||
            {'id': 2, 'instance_uuid': 'fake-instance',
 | 
			
		||||
             'device_name': '/dev/sdc1',
 | 
			
		||||
             'delete_on_termination': True,
 | 
			
		||||
             'virtual_name': 'ephemeral0'},
 | 
			
		||||
            {'id': 3, 'instance_uuid': 'fake-instance',
 | 
			
		||||
             'device_name': '/dev/sda1',
 | 
			
		||||
             'volume_id': 'fake-folume-id-1',
 | 
			
		||||
             'connection_info': "{'fake': 'connection_info'}"},
 | 
			
		||||
            {'id': 4, 'instance_uuid': 'fake-instance',
 | 
			
		||||
             'device_name': '/dev/sda2',
 | 
			
		||||
             'connection_info': "{'fake': 'connection_info'}",
 | 
			
		||||
             'snapshot_id': 'fake-snapshot-id-1',
 | 
			
		||||
             'volume_id': 'fake-volume-id-2'},
 | 
			
		||||
            {'id': 5, 'instance_uuid': 'fake-instance',
 | 
			
		||||
             'no_device': True,
 | 
			
		||||
             'device_name': '/dev/vdc'},
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
    def test_init(self):
 | 
			
		||||
        self.stubs.Set(block_device.BlockDeviceDict, '_fields',
 | 
			
		||||
                       set(['field1', 'field2']))
 | 
			
		||||
        self.stubs.Set(block_device.BlockDeviceDict, '_db_only_fields',
 | 
			
		||||
                       set(['db_field1', 'db_field2']))
 | 
			
		||||
 | 
			
		||||
        # Make sure db fields are not picked up if they are not
 | 
			
		||||
        # in the original dict
 | 
			
		||||
        dev_dict = block_device.BlockDeviceDict({'field1': 'foo',
 | 
			
		||||
                                                 'field2': 'bar',
 | 
			
		||||
                                                 'db_field1': 'baz'})
 | 
			
		||||
        self.assertTrue('field1' in dev_dict)
 | 
			
		||||
        self.assertTrue('field2' in dev_dict)
 | 
			
		||||
        self.assertTrue('db_field1' in dev_dict)
 | 
			
		||||
        self.assertFalse('db_field2'in dev_dict)
 | 
			
		||||
 | 
			
		||||
        # Make sure all expected fields are defaulted
 | 
			
		||||
        dev_dict = block_device.BlockDeviceDict({'field1': 'foo'})
 | 
			
		||||
        self.assertTrue('field1' in dev_dict)
 | 
			
		||||
        self.assertTrue('field2' in dev_dict)
 | 
			
		||||
        self.assertTrue(dev_dict['field2'] is None)
 | 
			
		||||
        self.assertFalse('db_field1' in dev_dict)
 | 
			
		||||
        self.assertFalse('db_field2'in dev_dict)
 | 
			
		||||
 | 
			
		||||
        # Unless they are not meant to be
 | 
			
		||||
        dev_dict = block_device.BlockDeviceDict({'field1': 'foo'},
 | 
			
		||||
            do_not_default=set(['field2']))
 | 
			
		||||
        self.assertTrue('field1' in dev_dict)
 | 
			
		||||
        self.assertFalse('field2' in dev_dict)
 | 
			
		||||
        self.assertFalse('db_field1' in dev_dict)
 | 
			
		||||
        self.assertFalse('db_field2'in dev_dict)
 | 
			
		||||
 | 
			
		||||
        # Assert basic validation works
 | 
			
		||||
        # NOTE (ndipanov):  Move to separate test once we have
 | 
			
		||||
        #                   more complex validations in place
 | 
			
		||||
        self.assertRaises(exception.InvalidBDMFormat,
 | 
			
		||||
                          block_device.BlockDeviceDict,
 | 
			
		||||
                          {'field1': 'foo', 'bogus_field': 'lame_val'})
 | 
			
		||||
 | 
			
		||||
    def test_from_legacy(self):
 | 
			
		||||
        for legacy, new in zip(self.legacy_mapping, self.new_mapping):
 | 
			
		||||
            self.assertThat(
 | 
			
		||||
                block_device.BlockDeviceDict.from_legacy(legacy),
 | 
			
		||||
                matchers.IsSubDictOf(new))
 | 
			
		||||
 | 
			
		||||
    def test_legacy(self):
 | 
			
		||||
        for legacy, new in zip(self.legacy_mapping, self.new_mapping):
 | 
			
		||||
            self.assertThat(
 | 
			
		||||
                legacy,
 | 
			
		||||
                matchers.IsSubDictOf(new.legacy()))
 | 
			
		||||
 | 
			
		||||
    def test_legacy_mapping(self):
 | 
			
		||||
        got_legacy = block_device.legacy_mapping(self.new_mapping)
 | 
			
		||||
 | 
			
		||||
        for legacy, expected in zip(got_legacy, self.legacy_mapping):
 | 
			
		||||
            self.assertThat(expected, matchers.IsSubDictOf(legacy))
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user