Introduce a function to parse image device mappings

Images have two kinds of device mappings attribute: virtual (mappings)
and block (block_device_mapping). The last one may be stored in two
different formats (legacy and bdm v2). Device descriptions are merged
from this two attributes to a single list used for instance boot
operation.

We should have a function which parses and merges image device mappings
and returns the result device list of a certain format to use it in
various parts of ec2api.

This patch introduce the function which implements the requirements
above and returns device mappings in bdm v2 format.

Change-Id: I7ad8673dcb6ddba11c6e00e22aa68a35ea1ec0fd
This commit is contained in:
Feodor Tersin 2015-07-16 11:53:33 +03:00
parent 516d4a1ce5
commit 9be22b8ac8
2 changed files with 356 additions and 0 deletions

View File

@ -12,7 +12,9 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import copy
import datetime
import json
import re
# TODO(termie): replace minidom with etree
from xml.dom import minidom
@ -42,6 +44,9 @@ ec2_opts = [
CONF = cfg.CONF
CONF.register_opts(ec2_opts)
LEGACY_BDM_FIELDS = set(['device_name', 'delete_on_termination', 'snapshot_id',
'volume_id', 'volume_size', 'no_device'])
_c2u = re.compile('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))')
@ -385,6 +390,89 @@ def get_os_image(context, ec2_image_id):
raise exception.InvalidAMIIDNotFound(id=ec2_image_id)
def deserialize_os_image_properties(os_image):
def prepare_property(property_name):
if property_name in properties:
properties[property_name] = json.loads(properties[property_name])
properties = copy.copy(os_image.properties)
prepare_property('mappings')
prepare_property('block_device_mapping')
return properties
def create_virtual_bdm(device_name, virtual_name):
bdm = {'device_name': device_name,
'source_type': 'blank',
'destination_type': 'local',
'device_type': 'disk',
'delete_on_termination': True,
'boot_index': -1,
'virtual_name': virtual_name}
if virtual_name == 'swap':
bdm['guest_format'] = 'swap'
return bdm
def get_os_image_mappings(os_image_properties):
mappings = []
names = set()
# TODO(ft): validate device names for both virtual and block device
# mappings
def is_virtual(virtual_name):
return virtual_name == 'swap' or (virtual_name and
_ephemeral.match(virtual_name))
# NOTE(ft): substitute mapping if only device name is specified
def add_mapping(mapping):
device_name = block_device_strip_dev(mapping.get('device_name'))
if device_name in names:
for i, m in enumerate(mappings):
if (device_name ==
block_device_strip_dev(m.get('device_name'))):
mappings[i] = mapping
break
else:
if device_name:
names.add(device_name)
mappings.append(mapping)
# TODO(ft): From Juno virtual device mapping has precedence of block one
# in boot logic. This function should do the same, despite Nova EC2
# behavior.
# TODO(ft): Nova EC2 prepended device names for virtual device mappings.
# But AWS doesn't do it.
for vdm in os_image_properties.get('mappings', []):
if is_virtual(vdm.get('virtual')):
add_mapping(create_virtual_bdm(
block_device_prepend_dev(vdm.get('device')), vdm['virtual']))
legacy_mapping = not os_image_properties.get('bdm_v2', False)
for bdm in os_image_properties.get('block_device_mapping', []):
if legacy_mapping:
virtual_name = bdm.get('virtual_name')
if is_virtual(virtual_name):
new_bdm = create_virtual_bdm(bdm.get('device_name'),
virtual_name)
else:
new_bdm = {key: val for key, val in bdm.iteritems()
if key in LEGACY_BDM_FIELDS}
if bdm.get('snapshot_id'):
new_bdm.update({'source_type': 'snapshot',
'destination_type': 'volume'})
elif bdm.get('volume_id'):
new_bdm.update({'source_type': 'volume',
'destination_type': 'volume'})
bdm = new_bdm
bdm.setdefault('delete_on_termination', False)
add_mapping(bdm)
return mappings
def get_os_public_network(context):
neutron = clients.neutron(context)
search_opts = {'router:external': True, 'name': CONF.external_network}
@ -415,6 +503,8 @@ def get_attached_gateway(context, vpc_id, gateway_kind):
# NOTE(ft): following functions are copied from various parts of Nova
_ephemeral = re.compile('^ephemeral(\d|[1-9]\d+)$')
_dev = re.compile('^/dev/')

View File

@ -315,6 +315,272 @@ class EC2UtilsTestCase(testtools.TestCase):
self.assertNotEqual(0, len(log.output))
self.assertNotIn('None', log.output)
def test_get_os_image_mappings(self):
# check virtual device mapping transformation with substitution
properties = {
'mappings': [
{'device': '/dev/vda', 'virtual': 'root'},
{'device': 'vda', 'virtual': 'ami'},
{'device': 'vdb', 'virtual': 'ephemeral0'},
{'device': '/dev/vdb', 'virtual': 'swap'},
{'device': '/dev/vdc', 'virtual': 'swap'},
{'device': 'vdc', 'virtual': 'ephemeral0'},
{'device': 'vdd'},
{'device': '/dev/vdd', 'virtual': None},
{'device': 'vdd', 'virtual': ''},
{'device': '/dev/vdd', 'virtual': 'swamp'},
{'virtual': 'ephemeral2'},
{'device': None, 'virtual': 'ephemeral3'},
{'device': '', 'virtual': 'ephemeral4'},
],
}
expected = [
{'device_name': '/dev/vdb',
'source_type': 'blank',
'destination_type': 'local',
'device_type': 'disk',
'delete_on_termination': True,
'boot_index': -1,
'guest_format': 'swap',
'virtual_name': 'swap'},
{'device_name': '/dev/vdc',
'source_type': 'blank',
'destination_type': 'local',
'device_type': 'disk',
'delete_on_termination': True,
'boot_index': -1,
'virtual_name': 'ephemeral0'},
{'device_name': None,
'source_type': 'blank',
'destination_type': 'local',
'device_type': 'disk',
'delete_on_termination': True,
'boot_index': -1,
'virtual_name': 'ephemeral2'},
{'device_name': None,
'source_type': 'blank',
'destination_type': 'local',
'device_type': 'disk',
'delete_on_termination': True,
'boot_index': -1,
'virtual_name': 'ephemeral3'},
{'device_name': '',
'source_type': 'blank',
'destination_type': 'local',
'device_type': 'disk',
'delete_on_termination': True,
'boot_index': -1,
'virtual_name': 'ephemeral4'},
]
result = ec2utils.get_os_image_mappings(properties)
self.assertThat(expected, matchers.ListMatches(result), verbose=True)
# check legacy block device mapping transformation with substitution
properties = {
'block_device_mapping': [
{'device_name': '/dev/vdb',
'virtual_name': 'ephemeral0'},
{'device_name': 'vdc',
'virtual_name': 'swap',
'snapshot_id': 'fake_snapshot_id_0'},
{'device_name': '/dev/vda',
'snapshot_id': 'fake_snapshot_id_1',
'delete_on_termination': True,
'volume_size': 100},
{'snapshot_id': 'fake_snapshot_id_2'},
{'device_name': '/dev/vdd',
'virtual_name': 'ephemeral2'},
{'device_name': 'vdd',
'volume_id': 'fake_volume_id_3',
'delete_on_termination': False},
{'device_name': 'vde',
'volume_id': 'fake_volume_id_4'},
{'device_name': '/dev/vde',
'snapshot_id': 'fake_snapshot_id_4',
'no_device': True},
{'snapshot_id': 'fake_snapshot_id_5',
'volume_id': 'fake_volume_id_5',
'volume_size': 50},
],
}
expected = [
{'device_name': '/dev/vdb',
'source_type': 'blank',
'destination_type': 'local',
'device_type': 'disk',
'delete_on_termination': True,
'boot_index': -1,
'virtual_name': 'ephemeral0'},
{'device_name': 'vdc',
'source_type': 'blank',
'destination_type': 'local',
'device_type': 'disk',
'delete_on_termination': True,
'boot_index': -1,
'guest_format': 'swap',
'virtual_name': 'swap'},
{'device_name': '/dev/vda',
'snapshot_id': 'fake_snapshot_id_1',
'source_type': 'snapshot',
'destination_type': 'volume',
'delete_on_termination': True,
'volume_size': 100},
{'snapshot_id': 'fake_snapshot_id_2',
'source_type': 'snapshot',
'destination_type': 'volume',
'delete_on_termination': False},
{'device_name': 'vdd',
'volume_id': 'fake_volume_id_3',
'source_type': 'volume',
'destination_type': 'volume',
'delete_on_termination': False},
{'device_name': '/dev/vde',
'snapshot_id': 'fake_snapshot_id_4',
'source_type': 'snapshot',
'destination_type': 'volume',
'no_device': True,
'delete_on_termination': False},
{'snapshot_id': 'fake_snapshot_id_5',
'volume_id': 'fake_volume_id_5',
'source_type': 'snapshot',
'destination_type': 'volume',
'volume_size': 50,
'delete_on_termination': False},
]
result = ec2utils.get_os_image_mappings(properties)
self.assertThat(expected, matchers.ListMatches(result), verbose=True)
# check bdm v2 with substitution
properties = {
'bdm_v2': True,
'block_device_mapping': [
{'device_name': '/dev/vdb',
'snapshot_id': 'fake_snapshot_id_1',
'source_type': 'snapshot',
'destination_type': 'volume',
'volume_size': 20,
'delete_on_termination': True},
{'device_name': '/dev/vdb',
'source_type': 'blank',
'destination_type': 'volume',
'volume_size': 10,
'delete_on_termination': True},
{'device_name': '/dev/vdc',
'snapshot_id': 'fake_snapshot_id_2',
'source_type': 'snapshot',
'destination_type': 'volume'},
{'device_name': 'vdc',
'volume_id': 'fake_volume_id_2',
'source_type': 'volume',
'destination_type': 'volume'},
{'device_name': 'vdd',
'snapshot_id': 'fake_snapshot_id_3',
'source_type': 'snapshot',
'destination_type': 'volume'},
{'device_name': '/dev/vdd',
'image_id': 'fake_image_id_1',
'source_type': 'image',
'destination_type': 'volume',
'volume_size': 30},
],
}
expected = [
{'device_name': '/dev/vdb',
'source_type': 'blank',
'destination_type': 'volume',
'volume_size': 10,
'delete_on_termination': True},
{'device_name': 'vdc',
'volume_id': 'fake_volume_id_2',
'source_type': 'volume',
'destination_type': 'volume',
'delete_on_termination': False},
{'device_name': '/dev/vdd',
'image_id': 'fake_image_id_1',
'source_type': 'image',
'destination_type': 'volume',
'volume_size': 30,
'delete_on_termination': False},
]
result = ec2utils.get_os_image_mappings(properties)
self.assertThat(expected, matchers.ListMatches(result), verbose=True)
# check bdm v2 vs vdm susbtitution
properties = {
'mappings': [
{'device': 'vdb', 'virtual': 'ephemeral0'},
{'device': 'vdc', 'virtual': 'ephemeral1'},
{'device': 'vdh', 'virtual': 'ephemeral2'},
],
'bdm_v2': True,
'block_device_mapping': [
{'device_name': '/dev/vda',
'snapshot_id': 'fake_snapshot_id_1',
'source_type': 'snapshot',
'destination_type': 'volume'},
{'device_name': '/dev/vdc',
'snapshot_id': 'fake_snapshot_id_2',
'source_type': 'snapshot',
'destination_type': 'volume'},
{'device_name': '/dev/vdd',
'snapshot_id': 'fake_snapshot_id_3',
'source_type': 'snapshot',
'destination_type': 'volume'}
],
}
expected = [
{'device_name': '/dev/vdb',
'source_type': 'blank',
'destination_type': 'local',
'device_type': 'disk',
'delete_on_termination': True,
'boot_index': -1,
'virtual_name': 'ephemeral0'},
{'device_name': '/dev/vdc',
'snapshot_id': 'fake_snapshot_id_2',
'source_type': 'snapshot',
'destination_type': 'volume',
'delete_on_termination': False},
{'device_name': '/dev/vdh',
'source_type': 'blank',
'destination_type': 'local',
'device_type': 'disk',
'delete_on_termination': True,
'boot_index': -1,
'virtual_name': 'ephemeral2'},
{'device_name': '/dev/vda',
'snapshot_id': 'fake_snapshot_id_1',
'source_type': 'snapshot',
'destination_type': 'volume',
'delete_on_termination': False},
{'device_name': '/dev/vdd',
'snapshot_id': 'fake_snapshot_id_3',
'source_type': 'snapshot',
'destination_type': 'volume',
'delete_on_termination': False},
]
result = ec2utils.get_os_image_mappings(properties)
self.assertThat(expected, matchers.ListMatches(result), verbose=True)
# check legacy bdm vs vdm susbtitution
properties = {
'mappings': [
{'device': 'vdb', 'virtual': 'ephemeral0'},
{'device': 'vdc', 'virtual': 'ephemeral1'},
{'device': 'vdh', 'virtual': 'ephemeral2'},
],
'block_device_mapping': [
{'device_name': '/dev/vda',
'snapshot_id': 'fake_snapshot_id_1'},
{'device_name': '/dev/vdc',
'snapshot_id': 'fake_snapshot_id_2'},
{'device_name': '/dev/vdd',
'snapshot_id': 'fake_snapshot_id_3'}
],
}
result = ec2utils.get_os_image_mappings(properties)
self.assertThat(expected, matchers.ListMatches(result), verbose=True)
def test_block_device_strip_dev(self):
self.assertEqual(ec2utils.block_device_strip_dev('/dev/sda'), 'sda')
self.assertEqual(ec2utils.block_device_strip_dev('sda'), 'sda')