curtin-dev team mailing list archive
-
curtin-dev team
-
Mailing list archive
-
Message #02096
[Merge] ~mwhudson/curtin:v2-disk-identification into curtin:master
Michael Hudson-Doyle has proposed merging ~mwhudson/curtin:v2-disk-identification into curtin:master.
Commit message:
block_meta: all fields on a disk action must match with v2 config
This is for https://bugs.launchpad.net/subiquity/+bug/1929213 and
similar reports.
Requested reviews:
curtin developers (curtin-dev)
For more details, see:
https://code.launchpad.net/~mwhudson/curtin/+git/curtin/+merge/415700
--
Your team curtin developers is requested to review the proposed merge of ~mwhudson/curtin:v2-disk-identification into curtin:master.
diff --git a/curtin/commands/block_meta.py b/curtin/commands/block_meta.py
index f3f19dc..42325c1 100644
--- a/curtin/commands/block_meta.py
+++ b/curtin/commands/block_meta.py
@@ -13,8 +13,13 @@ from curtin.storage_config import (extract_storage_ordered_dict,
from . import populate_one_subcmd
-from curtin.udev import (compose_udev_equality, udevadm_settle,
- udevadm_trigger, udevadm_info)
+from curtin.udev import (
+ compose_udev_equality,
+ udev_all_block_device_properties,
+ udevadm_info,
+ udevadm_settle,
+ udevadm_trigger,
+ )
import glob
import os
@@ -24,6 +29,7 @@ import sys
import tempfile
import time
+
FstabData = namedtuple(
"FstabData", ('spec', 'path', 'fstype', 'options', 'freq', 'passno',
'device'))
@@ -428,6 +434,112 @@ def get_poolname(info, storage_config):
return poolname
+def v1_get_path_to_disk(vol):
+ volume_path = None
+ for disk_key in ['wwn', 'serial', 'device_id', 'path']:
+ vol_value = vol.get(disk_key)
+ try:
+ if not vol_value:
+ continue
+ if disk_key in ['wwn', 'serial']:
+ volume_path = block.lookup_disk(vol_value)
+ elif disk_key == 'path':
+ if vol_value.startswith('iscsi:'):
+ i = iscsi.ensure_disk_connected(vol_value)
+ volume_path = os.path.realpath(i.devdisk_path)
+ else:
+ # resolve any symlinks to the dev_kname so
+ # sys/class/block access is valid. ie, there are no
+ # udev generated values in sysfs
+ volume_path = os.path.realpath(vol_value)
+ # convert /dev/sdX to /dev/mapper/mpathX value
+ if multipath.is_mpath_member(volume_path):
+ volume_path = '/dev/mapper/' + (
+ multipath.get_mpath_id_from_device(volume_path))
+ elif disk_key == 'device_id':
+ dasd_device = dasd.DasdDevice(vol_value)
+ volume_path = dasd_device.devname
+ except ValueError:
+ continue
+ # verify path exists otherwise try the next key
+ if os.path.exists(volume_path):
+ break
+ else:
+ volume_path = None
+
+ if volume_path is None:
+ raise ValueError("Failed to find storage volume id='%s' config: %s"
+ % (vol['id'], vol))
+
+ return volume_path
+
+
+def v2_get_path_to_disk(vol):
+ all_disks = [
+ dev for dev in udev_all_block_device_properties()
+ if 'DM_PART' not in dev and 'PARTN' not in dev
+ ]
+
+ def disks_matching(key, value):
+ return [
+ dev['DEVNAME']
+ for dev in all_disks
+ if key in dev and dev[key] == value
+ ]
+
+ def disk_by_path(path):
+ for dev in all_disks:
+ if dev['DEVNAME'] == path or path in dev['DEVLINKS'].split():
+ return dev['DEVNAME']
+
+ cands = []
+ if 'wwn' in vol:
+ for key in 'DM_WWN', 'ID_WWN':
+ devs = disks_matching(key, vol['wwn'])
+ if devs:
+ break
+ cands.append(set(devs))
+ if 'serial' in vol:
+ for key in 'DM_SERIAL', 'ID_SERIAL':
+ devs = disks_matching(key, vol['serial'])
+ if devs:
+ break
+ cands.append(set(devs))
+ if 'device_id' in vol:
+ dasd_device = dasd.DasdDevice(vol['device_id'])
+ cands.append(set([dasd_device.devname]))
+ volume_path = dasd_device.devname
+ # verify path exists otherwise try the next key
+ if os.path.exists(volume_path):
+ cands.append(set([volume_path]))
+ if 'path' in vol:
+ path = vol['path']
+ if path.startswith('iscsi:'):
+ i = iscsi.ensure_disk_connected(path)
+ path = i.devdisk_path
+ if multipath.is_mpath_member(path):
+ # if path points to a multipath member, convert that to point
+ # at the multipath device
+ path = '/dev/mapper/' + multipath.get_mpath_id_from_device(path)
+ path = disk_by_path(path)
+ if path is not None:
+ cands.append(set([path]))
+ else:
+ cands.append(set())
+
+ cands = set.intersection(*cands)
+
+ if len(cands) == 0:
+ raise ValueError("Failed to find storage volume id='%s' config: %s"
+ % (vol['id'], vol))
+ if len(cands) > 1:
+ raise ValueError(
+ "Storage volume id='%s' config: %s identified multiple devices"
+ % (vol['id'], vol))
+
+ return next(iter(cands))
+
+
def get_path_to_storage_volume(volume, storage_config):
# Get path to block device for volume. Volume param should refer to id of
# volume in storage config
@@ -454,41 +566,10 @@ def get_path_to_storage_volume(volume, storage_config):
elif vol.get('type') == "disk":
# Get path to block device for disk. Device_id param should refer
# to id of device in storage config
- volume_path = None
- for disk_key in ['wwn', 'serial', 'device_id', 'path']:
- vol_value = vol.get(disk_key)
- try:
- if not vol_value:
- continue
- if disk_key in ['wwn', 'serial']:
- volume_path = block.lookup_disk(vol_value)
- elif disk_key == 'path':
- if vol_value.startswith('iscsi:'):
- i = iscsi.ensure_disk_connected(vol_value)
- volume_path = os.path.realpath(i.devdisk_path)
- else:
- # resolve any symlinks to the dev_kname so
- # sys/class/block access is valid. ie, there are no
- # udev generated values in sysfs
- volume_path = os.path.realpath(vol_value)
- # convert /dev/sdX to /dev/mapper/mpathX value
- if multipath.is_mpath_member(volume_path):
- volume_path = '/dev/mapper/' + (
- multipath.get_mpath_id_from_device(volume_path))
- elif disk_key == 'device_id':
- dasd_device = dasd.DasdDevice(vol_value)
- volume_path = dasd_device.devname
- except ValueError:
- continue
- # verify path exists otherwise try the next key
- if os.path.exists(volume_path):
- break
- else:
- volume_path = None
-
- if volume_path is None:
- raise ValueError("Failed to find storage volume id='%s' config: %s"
- % (vol['id'], vol))
+ if getattr(storage_config, 'version', 1) > 1:
+ volume_path = v2_get_path_to_disk(vol)
+ else:
+ volume_path = v1_get_path_to_disk(vol)
elif vol.get('type') == "lvm_partition":
# For lvm partitions, a directory in /dev/ should be present with the
@@ -2017,8 +2098,8 @@ def meta_custom(args):
storage_config_dict = extract_storage_ordered_dict(cfg)
- version = cfg['storage']['version']
- if version > 1:
+ storage_config_dict.version = cfg['storage']['version']
+ if storage_config_dict.version > 1:
from curtin.commands.block_meta_v2 import (
disk_handler_v2,
partition_handler_v2,
diff --git a/curtin/udev.py b/curtin/udev.py
index 2b7a46e..11db908 100644
--- a/curtin/udev.py
+++ b/curtin/udev.py
@@ -129,4 +129,13 @@ def udevadm_info(path=None):
return info
+def udev_all_block_device_properties():
+ import pyudev
+ props = []
+ c = pyudev.context()
+ for device in c.list_devices(subsystem='block'):
+ props.append(dict(device.properties))
+ return props
+
+
# vi: ts=4 expandtab syntax=python
diff --git a/tests/unittests/test_commands_block_meta.py b/tests/unittests/test_commands_block_meta.py
index 3e22792..48653e6 100644
--- a/tests/unittests/test_commands_block_meta.py
+++ b/tests/unittests/test_commands_block_meta.py
@@ -23,6 +23,10 @@ class TestGetPathToStorageVolume(CiTestCase):
self.add_patch(basepath + 'devsync', 'm_devsync')
self.add_patch(basepath + 'util.subp', 'm_subp')
self.add_patch(basepath + 'multipath.is_mpath_member', 'm_mp')
+ self.add_patch(
+ basepath + 'multipath.get_mpath_id_from_device', 'm_get_mpath_id')
+ self.add_patch(
+ basepath + 'udev_all_block_device_properties', 'm_udev_all')
def test_block_lookup_called_with_disk_wwn(self):
volume = 'mydisk'
@@ -57,6 +61,7 @@ class TestGetPathToStorageVolume(CiTestCase):
self.assertEqual(expected_calls, self.m_lookup.call_args_list)
def test_fallback_to_path_when_lookup_wwn_serial_fail(self):
+ self.m_mp.return_value = False
volume = 'mydisk'
wwn = self.random_string()
serial = self.random_string()
@@ -75,6 +80,7 @@ class TestGetPathToStorageVolume(CiTestCase):
self.assertEqual(path, result)
def test_block_lookup_not_called_with_wwn_or_serial_keys(self):
+ self.m_mp.return_value = False
volume = 'mydisk'
path = "/%s/%s" % (self.random_string(), self.random_string())
cfg = {'id': volume, 'type': 'disk', 'path': path}
@@ -106,6 +112,202 @@ class TestGetPathToStorageVolume(CiTestCase):
self.assertEqual(expected_calls, self.m_lookup.call_args_list)
self.m_exists.assert_has_calls([call(path)])
+ def test_v2_match_serial(self):
+ serial = self.random_string()
+ devname = self.random_string()
+ self.m_udev_all.return_value = [
+ {'DEVNAME': devname, 'ID_SERIAL': serial},
+ ]
+ disk_id = self.random_string()
+ cfg = {'id': disk_id, 'type': 'disk', 'serial': serial}
+ s_cfg = OrderedDict({disk_id: cfg})
+ s_cfg.version = 2
+ self.assertEqual(
+ devname,
+ block_meta.get_path_to_storage_volume(disk_id, s_cfg))
+
+ def test_v2_match_serial_skips_partition(self):
+ serial = self.random_string()
+ devname = self.random_string()
+ self.m_udev_all.return_value = [
+ {'DEVNAME': devname, 'ID_SERIAL': serial},
+ {'DEVNAME': devname+'p1', 'ID_SERIAL': serial, 'PARTN': "1"},
+ ]
+ disk_id = self.random_string()
+ cfg = {'id': disk_id, 'type': 'disk', 'serial': serial}
+ s_cfg = OrderedDict({disk_id: cfg})
+ s_cfg.version = 2
+ self.assertEqual(
+ devname,
+ block_meta.get_path_to_storage_volume(disk_id, s_cfg))
+
+ def test_v2_match_serial_prefer_mpath(self):
+ serial = self.random_string()
+ devname1 = self.random_string()
+ devname2 = self.random_string()
+ devname_dm = self.random_string()
+ self.m_udev_all.return_value = [
+ {'DEVNAME': devname1, 'ID_SERIAL': serial},
+ {'DEVNAME': devname2, 'ID_SERIAL': serial},
+ {'DEVNAME': devname_dm, 'DM_SERIAL': serial},
+ ]
+ disk_id = self.random_string()
+ cfg = {'id': disk_id, 'type': 'disk', 'serial': serial}
+ s_cfg = OrderedDict({disk_id: cfg})
+ s_cfg.version = 2
+ self.assertEqual(
+ devname_dm,
+ block_meta.get_path_to_storage_volume(disk_id, s_cfg))
+
+ def test_v2_match_serial_mpath_skip_partition(self):
+ serial = self.random_string()
+ devname1 = self.random_string()
+ devname2 = self.random_string()
+ devname_dm = self.random_string()
+ self.m_udev_all.return_value = [
+ {'DEVNAME': devname1, 'ID_SERIAL': serial},
+ {'DEVNAME': devname2, 'ID_SERIAL': serial},
+ {'DEVNAME': devname_dm, 'DM_SERIAL': serial},
+ {'DEVNAME': devname_dm+'p1', 'DM_SERIAL': serial, 'DM_PART': '1'},
+ ]
+ disk_id = self.random_string()
+ cfg = {'id': disk_id, 'type': 'disk', 'serial': serial}
+ s_cfg = OrderedDict({disk_id: cfg})
+ s_cfg.version = 2
+ self.assertEqual(
+ devname_dm,
+ block_meta.get_path_to_storage_volume(disk_id, s_cfg))
+
+ def test_v2_match_wwn(self):
+ wwn = self.random_string()
+ devname = self.random_string()
+ self.m_udev_all.return_value = [
+ {'DEVNAME': devname, 'ID_WWN': wwn},
+ ]
+ disk_id = self.random_string()
+ cfg = {'id': disk_id, 'type': 'disk', 'wwn': wwn}
+ s_cfg = OrderedDict({disk_id: cfg})
+ s_cfg.version = 2
+ self.assertEqual(
+ devname,
+ block_meta.get_path_to_storage_volume(disk_id, s_cfg))
+
+ def test_v2_match_wwn_prefer_mpath(self):
+ wwn = self.random_string()
+ devname1 = self.random_string()
+ devname2 = self.random_string()
+ devname_dm = self.random_string()
+ self.m_udev_all.return_value = [
+ {'DEVNAME': devname1, 'ID_WWN': wwn},
+ {'DEVNAME': devname2, 'ID_WWN': wwn},
+ {'DEVNAME': devname_dm, 'DM_WWN': wwn},
+ ]
+ disk_id = self.random_string()
+ cfg = {'id': disk_id, 'type': 'disk', 'wwn': wwn}
+ s_cfg = OrderedDict({disk_id: cfg})
+ s_cfg.version = 2
+ self.assertEqual(
+ devname_dm,
+ block_meta.get_path_to_storage_volume(disk_id, s_cfg))
+
+ def test_v2_match_serial_and_wwn(self):
+ serial = self.random_string()
+ wwn = self.random_string()
+ devname = self.random_string()
+ self.m_udev_all.return_value = [
+ {'DEVNAME': devname, 'ID_SERIAL': serial, 'ID_WWN': wwn},
+ ]
+ disk_id = self.random_string()
+ cfg = {'id': disk_id, 'type': 'disk', 'serial': serial, 'wwn': wwn}
+ s_cfg = OrderedDict({disk_id: cfg})
+ s_cfg.version = 2
+ self.assertEqual(
+ devname,
+ block_meta.get_path_to_storage_volume(disk_id, s_cfg))
+
+ def test_v2_different_serial_same_wwn(self):
+ serial1 = self.random_string()
+ serial2 = self.random_string()
+ wwn = self.random_string()
+ devname1 = self.random_string()
+ devname2 = self.random_string()
+ self.m_udev_all.return_value = [
+ {'DEVNAME': devname1, 'ID_SERIAL': serial1, 'ID_WWN': wwn},
+ {'DEVNAME': devname2, 'ID_SERIAL': serial2, 'ID_WWN': wwn},
+ ]
+ disk_id = self.random_string()
+ cfg = {'id': disk_id, 'type': 'disk', 'serial': serial2, 'wwn': wwn}
+ s_cfg = OrderedDict({disk_id: cfg})
+ s_cfg.version = 2
+ self.assertEqual(
+ devname2,
+ block_meta.get_path_to_storage_volume(disk_id, s_cfg))
+
+ def test_v2_fails_duplicate_wwn(self):
+ serial1 = self.random_string()
+ serial2 = self.random_string()
+ wwn = self.random_string()
+ devname1 = self.random_string()
+ devname2 = self.random_string()
+ self.m_udev_all.return_value = [
+ {'DEVNAME': devname1, 'ID_SERIAL': serial1, 'ID_WWN': wwn},
+ {'DEVNAME': devname2, 'ID_SERIAL': serial2, 'ID_WWN': wwn},
+ ]
+ disk_id = self.random_string()
+ cfg = {'id': disk_id, 'type': 'disk', 'wwn': wwn}
+ s_cfg = OrderedDict({disk_id: cfg})
+ s_cfg.version = 2
+ with self.assertRaises(ValueError):
+ block_meta.get_path_to_storage_volume(disk_id, s_cfg)
+
+ def test_v2_match_path(self):
+ self.m_mp.return_value = False
+ devname = self.random_string()
+ self.m_udev_all.return_value = [
+ {'DEVNAME': devname},
+ ]
+ disk_id = self.random_string()
+ cfg = {'id': disk_id, 'type': 'disk', 'path': devname}
+ s_cfg = OrderedDict({disk_id: cfg})
+ s_cfg.version = 2
+ self.assertEqual(
+ devname,
+ block_meta.get_path_to_storage_volume(disk_id, s_cfg))
+
+ def test_v2_match_path_link(self):
+ self.m_mp.return_value = False
+ devname = self.random_string()
+ link1 = self.random_string()
+ link2 = self.random_string()
+ links = link1 + ' ' + link2
+ self.m_udev_all.return_value = [
+ {'DEVNAME': devname, 'DEVLINKS': links},
+ ]
+ disk_id = self.random_string()
+ cfg = {'id': disk_id, 'type': 'disk', 'path': link1}
+ s_cfg = OrderedDict({disk_id: cfg})
+ s_cfg.version = 2
+ self.assertEqual(
+ devname,
+ block_meta.get_path_to_storage_volume(disk_id, s_cfg))
+
+ def test_v2_match_path_prefers_multipath(self):
+ self.m_mp.return_value = True
+ self.m_get_mpath_id.return_value = 'mpatha'
+ devname1 = self.random_string()
+ devname2 = self.random_string()
+ self.m_udev_all.return_value = [
+ {'DEVNAME': devname1, 'DEVLINKS': ''},
+ {'DEVNAME': devname2, 'DEVLINKS': '/dev/mapper/mpatha'},
+ ]
+ disk_id = self.random_string()
+ cfg = {'id': disk_id, 'type': 'disk', 'path': devname1}
+ s_cfg = OrderedDict({disk_id: cfg})
+ s_cfg.version = 2
+ self.assertEqual(
+ devname2,
+ block_meta.get_path_to_storage_volume(disk_id, s_cfg))
+
class TestBlockMetaSimple(CiTestCase):
def setUp(self):
Follow ups
-
[Merge] ~mwhudson/curtin:v2-disk-identification into curtin:master
From: Server Team CI bot, 2023-01-25
-
[Merge] ~mwhudson/curtin:v2-disk-identification into curtin:master
From: Michael Hudson-Doyle, 2023-01-25
-
Re: [Merge] ~mwhudson/curtin:v2-disk-identification into curtin:master
From: Dan Bungert, 2023-01-25
-
Re: [Merge] ~mwhudson/curtin:v2-disk-identification into curtin:master
From: Server Team CI bot, 2023-01-25
-
Re: [Merge] ~mwhudson/curtin:v2-disk-identification into curtin:master
From: Michael Hudson-Doyle, 2023-01-25
-
Re: [Merge] ~mwhudson/curtin:v2-disk-identification into curtin:master
From: Michael Hudson-Doyle, 2022-05-22
-
Re: [Merge] ~mwhudson/curtin:v2-disk-identification into curtin:master
From: Dan Bungert, 2022-05-20
-
Re: [Merge] ~mwhudson/curtin:v2-disk-identification into curtin:master
From: Server Team CI bot, 2022-03-14
-
Re: [Merge] ~mwhudson/curtin:v2-disk-identification into curtin:master
From: Server Team CI bot, 2022-02-24
-
Re: [Merge] ~mwhudson/curtin:v2-disk-identification into curtin:master
From: Michael Hudson-Doyle, 2022-02-24
-
Re: [Merge] ~mwhudson/curtin:v2-disk-identification into curtin:master
From: Dan Bungert, 2022-02-24
-
Re: [Merge] ~mwhudson/curtin:v2-disk-identification into curtin:master
From: Server Team CI bot, 2022-02-17
-
Re: [Merge] ~mwhudson/curtin:v2-disk-identification into curtin:master
From: Server Team CI bot, 2022-02-17