import pytest
import stat
+from typing import Any, Callable, ClassVar, Optional
+
from ceph_volume.util import disk
from unittest.mock import patch, Mock, MagicMock, mock_open
from pyfakefs.fake_filesystem_unittest import TestCase
fake_filesystem.create_file('/sys/block/dm-0/queue/hw_sector_size', contents='512')
with patch("ceph_volume.util.disk.UdevData") as MockUdevData:
mock_instance = MagicMock()
- mock_instance.slashed_path = lv_path
+ mock_instance.preferred_block_path = lv_path
mock_instance.environment = {}
MockUdevData.return_value = mock_instance
result = disk.get_devices()
assert b.active_mappers()['dm-1']['uuid'] == 'abcdef'
+class _StatWithStRdev:
+ __slots__ = ('_st', 'st_rdev')
+
+ def __init__(self, st: Any, rdev: int = 0) -> None:
+ object.__setattr__(self, '_st', st)
+ object.__setattr__(self, 'st_rdev', rdev)
+
+ def __getattr__(self, name: str) -> Any:
+ return getattr(self._st, name)
+
+
+def _udev_data_patched_os_stat(path: str, *args: Any, **kwargs: Any) -> Any:
+ if TestUdevData._pyfakefs_os_stat is None:
+ raise RuntimeError('TestUdevData.setUp must assign _pyfakefs_os_stat')
+ st = TestUdevData._pyfakefs_os_stat(path, *args, **kwargs)
+ if not hasattr(st, 'st_rdev'):
+ return _StatWithStRdev(st, 0)
+ return st
+
+
class TestUdevData(TestCase):
+ _pyfakefs_os_stat: ClassVar[Optional[Callable[..., Any]]] = None
+
def setUp(self) -> None:
udev_data_lv_device: str = """
S:disk/by-id/dm-uuid-LVM-1f1RaxWlzQ61Sbc7oCIHRMdh0M8zRTSnU03ekuStqWuiA6eEDmwoGg3cWfFtE2li
self.fs.create_file('/run/udev/data/b999:0', create_missing_dirs=True, contents=udev_data_bare_device)
self.fs.create_file('/run/udev/data/b998:1', create_missing_dirs=True, contents=udev_data_lv_device)
self.fs.create_file('/run/udev/data/b997:2', create_missing_dirs=True, contents="")
+ TestUdevData._pyfakefs_os_stat = disk.os.stat
+
+ def tearDown(self) -> None:
+ try:
+ super().tearDown()
+ finally:
+ TestUdevData._pyfakefs_os_stat = None
def test_device_not_found(self) -> None:
self.fs.remove(self.fake_device)
with pytest.raises(RuntimeError):
disk.UdevData(self.fake_device)
- @patch('ceph_volume.util.disk.os.stat', MagicMock())
+ @patch('ceph_volume.util.disk.os.stat', _udev_data_patched_os_stat)
@patch('ceph_volume.util.disk.os.minor', Mock(return_value=0))
@patch('ceph_volume.util.disk.os.major', Mock(return_value=999))
def test_no_data(self) -> None:
with pytest.raises(RuntimeError):
disk.UdevData(self.fake_device)
- @patch('ceph_volume.util.disk.os.stat', MagicMock())
+ @patch('ceph_volume.util.disk.os.stat', _udev_data_patched_os_stat)
@patch('ceph_volume.util.disk.os.minor', Mock(return_value=2))
@patch('ceph_volume.util.disk.os.major', Mock(return_value=997))
def test_empty_data(self) -> None:
# no exception should be raised when a /run/udev/data/* file is empty
_ = disk.UdevData(self.fake_device)
- @patch('ceph_volume.util.disk.os.stat', MagicMock())
+ @patch('ceph_volume.util.disk.os.stat', _udev_data_patched_os_stat)
@patch('ceph_volume.util.disk.os.minor', Mock(return_value=0))
@patch('ceph_volume.util.disk.os.major', Mock(return_value=999))
def test_is_dm_false(self) -> None:
assert not disk.UdevData(self.fake_device).is_dm
- @patch('ceph_volume.util.disk.os.stat', MagicMock())
+ @patch('ceph_volume.util.disk.os.stat', _udev_data_patched_os_stat)
@patch('ceph_volume.util.disk.os.minor', Mock(return_value=1))
@patch('ceph_volume.util.disk.os.major', Mock(return_value=998))
def test_is_dm_true(self) -> None:
assert disk.UdevData(self.fake_device).is_dm
- @patch('ceph_volume.util.disk.os.stat', MagicMock())
+ @patch('ceph_volume.util.disk.os.stat', _udev_data_patched_os_stat)
@patch('ceph_volume.util.disk.os.minor', Mock(return_value=1))
@patch('ceph_volume.util.disk.os.major', Mock(return_value=998))
def test_is_lvm_true(self) -> None:
- assert disk.UdevData(self.fake_device).is_dm
+ assert disk.UdevData(self.fake_device).is_lvm
- @patch('ceph_volume.util.disk.os.stat', MagicMock())
+ @patch('ceph_volume.util.disk.os.stat', _udev_data_patched_os_stat)
@patch('ceph_volume.util.disk.os.minor', Mock(return_value=0))
@patch('ceph_volume.util.disk.os.major', Mock(return_value=999))
def test_is_lvm_false(self) -> None:
- assert not disk.UdevData(self.fake_device).is_dm
+ assert not disk.UdevData(self.fake_device).is_lvm
- @patch('ceph_volume.util.disk.os.stat', MagicMock())
+ @patch('ceph_volume.util.disk.os.stat', _udev_data_patched_os_stat)
@patch('ceph_volume.util.disk.os.minor', Mock(return_value=1))
@patch('ceph_volume.util.disk.os.major', Mock(return_value=998))
def test_slashed_path_with_lvm(self) -> None:
assert disk.UdevData(self.fake_device).slashed_path == '/dev/fake_vg1/fake-lv1'
- @patch('ceph_volume.util.disk.os.stat', MagicMock())
+ @patch('ceph_volume.util.disk.os.stat', _udev_data_patched_os_stat)
@patch('ceph_volume.util.disk.os.minor', Mock(return_value=1))
@patch('ceph_volume.util.disk.os.major', Mock(return_value=998))
def test_dashed_path_with_lvm(self) -> None:
assert disk.UdevData(self.fake_device).dashed_path == '/dev/mapper/fake_vg1-fake-lv1'
- @patch('ceph_volume.util.disk.os.stat', MagicMock())
+ @patch('ceph_volume.util.disk.os.stat', _udev_data_patched_os_stat)
+ @patch('ceph_volume.util.disk.os.minor', Mock(return_value=1))
+ @patch('ceph_volume.util.disk.os.major', Mock(return_value=998))
+ def test_preferred_block_path_lvm_falls_back_to_mapper(self) -> None:
+ """When /dev/vg/lv is missing, use /dev/mapper/vg-lv (container-style LVM)."""
+ mapper: str = '/dev/mapper/fake_vg1-fake-lv1'
+ self.fs.create_file(mapper, st_mode=(stat.S_IFBLK | 0o600))
+ assert disk.UdevData(self.fake_device).preferred_block_path == mapper
+
+ @patch('ceph_volume.util.disk.os.stat', _udev_data_patched_os_stat)
+ @patch('ceph_volume.util.disk.os.minor', Mock(return_value=1))
+ @patch('ceph_volume.util.disk.os.major', Mock(return_value=998))
+ def test_preferred_block_path_lvm_skips_slashed_when_not_block_device(self) -> None:
+ """If /dev/vg/lv exists but is not a block device (for example, a directory), try mapper."""
+ slashed: str = '/dev/fake_vg1/fake-lv1'
+ mapper: str = '/dev/mapper/fake_vg1-fake-lv1'
+ self.fs.create_dir(slashed)
+ self.fs.create_file(mapper, st_mode=(stat.S_IFBLK | 0o600))
+ assert disk.UdevData(self.fake_device).preferred_block_path == mapper
+
+ @patch('ceph_volume.util.disk.os.stat', _udev_data_patched_os_stat)
+ @patch('ceph_volume.util.disk.os.minor', Mock(return_value=1))
+ @patch('ceph_volume.util.disk.os.major', Mock(return_value=998))
+ def test_preferred_block_path_lvm_falls_back_when_candidates_are_not_block_devices(
+ self,
+ ) -> None:
+ """Neither candidate may be used if they are only directories (for example, empty udev fields)."""
+ slashed: str = '/dev/fake_vg1/fake-lv1'
+ mapper: str = '/dev/mapper/fake_vg1-fake-lv1'
+ self.fs.create_dir(slashed)
+ self.fs.create_dir(mapper)
+ assert disk.UdevData(self.fake_device).preferred_block_path == self.fake_device
+
+ @patch('ceph_volume.util.disk.os.stat', _udev_data_patched_os_stat)
+ @patch('ceph_volume.util.disk.os.minor', Mock(return_value=1))
+ @patch('ceph_volume.util.disk.os.major', Mock(return_value=998))
+ def test_preferred_block_path_lvm_prefers_slashed_when_present(self) -> None:
+ slashed: str = '/dev/fake_vg1/fake-lv1'
+ self.fs.create_file(slashed, st_mode=(stat.S_IFBLK | 0o600))
+ assert disk.UdevData(self.fake_device).preferred_block_path == slashed
+
+ @patch('ceph_volume.util.disk.os.stat', _udev_data_patched_os_stat)
+ @patch('ceph_volume.util.disk.os.minor', Mock(return_value=1))
+ @patch('ceph_volume.util.disk.os.major', Mock(return_value=998))
+ def test_preferred_block_path_lvm_falls_back_to_opening_path_when_no_symlinks(self) -> None:
+ """Neither /dev/vg/lv nor /dev/mapper/name exist; keep the original device node."""
+ assert disk.UdevData(self.fake_device).preferred_block_path == self.fake_device
+
+ @patch('ceph_volume.util.disk.os.stat', _udev_data_patched_os_stat)
+ @patch('ceph_volume.util.disk.os.minor', Mock(return_value=0))
+ @patch('ceph_volume.util.disk.os.major', Mock(return_value=999))
+ def test_preferred_block_path_bare_device(self) -> None:
+ assert disk.UdevData(self.fake_device).preferred_block_path == '/dev/cephtest'
+
+ @patch('ceph_volume.util.disk.os.stat', _udev_data_patched_os_stat)
@patch('ceph_volume.util.disk.os.minor', Mock(return_value=0))
@patch('ceph_volume.util.disk.os.major', Mock(return_value=999))
def test_slashed_path_with_bare_device(self) -> None:
assert disk.UdevData(self.fake_device).slashed_path == '/dev/cephtest'
- @patch('ceph_volume.util.disk.os.stat', MagicMock())
+ @patch('ceph_volume.util.disk.os.stat', _udev_data_patched_os_stat)
@patch('ceph_volume.util.disk.os.minor', Mock(return_value=0))
@patch('ceph_volume.util.disk.os.major', Mock(return_value=999))
def test_dashed_path_with_bare_device(self) -> None: