summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJenkins <jenkins@review.openstack.org>2013-06-10 19:49:11 +0000
committerGerrit Code Review <review@openstack.org>2013-06-10 19:49:11 +0000
commit71364c57f4a7d0283efebbd20565bcefbf23b0a7 (patch)
tree47b2719fb53fa14a82b8dbd92da9109caa14b20a
parent08cfd77a3658380df71f6eabbfa765b5ffcae053 (diff)
parentd581ed04a584fdd83df78e43366a3617b154a626 (diff)
Merge "BDM class and transformation functions"
-rw-r--r--nova/block_device.py163
-rw-r--r--nova/exception.py11
-rw-r--r--nova/tests/test_block_device.py123
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))