diff options
| author | Jenkins <jenkins@review.openstack.org> | 2013-06-10 19:49:11 +0000 |
|---|---|---|
| committer | Gerrit Code Review <review@openstack.org> | 2013-06-10 19:49:11 +0000 |
| commit | 71364c57f4a7d0283efebbd20565bcefbf23b0a7 (patch) | |
| tree | 47b2719fb53fa14a82b8dbd92da9109caa14b20a | |
| parent | 08cfd77a3658380df71f6eabbfa765b5ffcae053 (diff) | |
| parent | d581ed04a584fdd83df78e43366a3617b154a626 (diff) | |
Merge "BDM class and transformation functions"
| -rw-r--r-- | nova/block_device.py | 163 | ||||
| -rw-r--r-- | nova/exception.py | 11 | ||||
| -rw-r--r-- | nova/tests/test_block_device.py | 123 |
3 files changed, 296 insertions, 1 deletions
diff --git a/nova/block_device.py b/nova/block_device.py index b7a9881b1..1094ef3ae 100644 --- a/nova/block_device.py +++ b/nova/block_device.py @@ -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): diff --git a/nova/exception.py b/nova/exception.py index 88c038201..36a18b1b9 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -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") diff --git a/nova/tests/test_block_device.py b/nova/tests/test_block_device.py index 8189057cb..716e7636d 100644 --- a/nova/tests/test_block_device.py +++ b/nova/tests/test_block_device.py @@ -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)) |
