From 5705e10e809cdc9f70018263c54a63ac4a02809c Mon Sep 17 00:00:00 2001 From: Zack Cerza Date: Tue, 17 May 2022 11:29:02 -0600 Subject: [PATCH] ceph-volume: Optionally consume loop devices A similar proposal was rejected in #24765; I understand the logic behind the rejection, but this will allow us to run Ceph clusters on machines that lack disk resources for testing purposes. We just need to make it impossible to accidentally enable, and make it clear it is unsupported. Signed-off-by: Zack Cerza (cherry picked from commit c7f017b21ade3762ba5b7b9688bed72c6b60dc0e) --- .../ceph_volume/tests/util/test_device.py | 17 ++++++ src/ceph-volume/ceph_volume/util/device.py | 14 ++++- src/ceph-volume/ceph_volume/util/disk.py | 59 ++++++++++++++++--- 3 files changed, 78 insertions(+), 12 deletions(-) diff --git a/src/ceph-volume/ceph_volume/tests/util/test_device.py b/src/ceph-volume/ceph_volume/tests/util/test_device.py index 49a34d7eef100..e3e3b30c4be2a 100644 --- a/src/ceph-volume/ceph_volume/tests/util/test_device.py +++ b/src/ceph-volume/ceph_volume/tests/util/test_device.py @@ -1,3 +1,4 @@ +import os import pytest from copy import deepcopy from ceph_volume.util import device @@ -80,6 +81,22 @@ class TestDevice(object): disk = device.Device("/dev/sda") assert disk.is_device is True + def test_loop_device_is_not_device(self, fake_call, device_info): + data = {"/dev/loop0": {"foo": "bar"}} + lsblk = {"TYPE": "loop"} + device_info(devices=data, lsblk=lsblk) + disk = device.Device("/dev/loop0") + assert disk.is_device is False + + def test_loop_device_is_device(self, fake_call, device_info): + data = {"/dev/loop0": {"foo": "bar"}} + lsblk = {"TYPE": "loop"} + os.environ["CEPH_VOLUME_USE_LOOP_DEVICES"] = "1" + device_info(devices=data, lsblk=lsblk) + disk = device.Device("/dev/loop0") + assert disk.is_device is True + del os.environ["CEPH_VOLUME_USE_LOOP_DEVICES"] + def test_device_is_rotational(self, fake_call, device_info): data = {"/dev/sda": {"rotational": "1"}} lsblk = {"TYPE": "device", "NAME": "sda"} diff --git a/src/ceph-volume/ceph_volume/util/device.py b/src/ceph-volume/ceph_volume/util/device.py index 276cce9ff6174..c556e56768ddd 100644 --- a/src/ceph-volume/ceph_volume/util/device.py +++ b/src/ceph-volume/ceph_volume/util/device.py @@ -213,7 +213,10 @@ class Device(object): self.disk_api = dev device_type = dev.get('TYPE', '') # always check is this is an lvm member - if device_type in ['part', 'disk']: + valid_types = ['part', 'disk'] + if os.environ.get("CEPH_VOLUME_USE_LOOP_DEVICES", False): + valid_types.append('loop') + if device_type in valid_types: self._set_lvm_membership() out, err, rc = process.call([ 'ceph-bluestore-tool', 'show-label', @@ -428,7 +431,9 @@ class Device(object): @property def device_type(self): self.load_blkid_api() - if self.disk_api: + if 'type' in self.sys_api: + return self.sys_api['type'] + elif self.disk_api: return self.disk_api['TYPE'] elif self.blkid_api: return self.blkid_api['TYPE'] @@ -459,7 +464,10 @@ class Device(object): elif self.blkid_api: api = self.blkid_api if api: - return self.device_type in ['disk', 'device', 'mpath'] + valid_types = ['disk', 'device', 'mpath'] + if os.environ.get("CEPH_VOLUME_USE_LOOP_DEVICES", False): + valid_types.append('loop') + return self.device_type in valid_types return False @property diff --git a/src/ceph-volume/ceph_volume/util/disk.py b/src/ceph-volume/ceph_volume/util/disk.py index 31d9edd690d0f..923c12f5246cd 100644 --- a/src/ceph-volume/ceph_volume/util/disk.py +++ b/src/ceph-volume/ceph_volume/util/disk.py @@ -340,16 +340,18 @@ def is_device(dev): """ if not os.path.exists(dev): return False - # use lsblk first, fall back to using stat - TYPE = lsblk(dev).get('TYPE') - if TYPE: - return TYPE in ['disk', 'mpath'] + if not dev.startswith('/dev/'): + return False + if dev[len('/dev/'):].startswith('loop'): + if os.environ.get("CEPH_VOLUME_USE_LOOP_DEVICES", False) is False: + return False + logger.info( + "Allowing the use of loop devices since " + "CEPH_VOLUME_USE_LOOP_DEVICES is set." + ) # fallback to stat return _stat_is_device(os.lstat(dev).st_mode) - if stat.S_ISBLK(os.lstat(dev)): - return True - return False def is_partition(dev): @@ -756,6 +758,40 @@ def get_block_devs_lsblk(device=''): raise OSError('lsblk returned failure, stderr: {}'.format(stderr)) return [re.split(r'\s+', line) for line in stdout] + +def get_block_devs_sysfs(_sys_block_path='/sys/block', _sys_dev_block_path='/sys/dev/block'): + # First, get devices that are _not_ partitions + result = list() + dev_names = os.listdir(_sys_block_path) + for dev in dev_names: + name = kname = os.path.join("/dev", dev) + type_ = 'disk' + if get_file_contents(os.path.join(_sys_block_path, dev, 'removable')) == "1": + continue + dm_dir_path = os.path.join(_sys_block_path, dev, 'dm') + if os.path.isdir(dm_dir_path): + type_ = 'lvm' + basename = get_file_contents(os.path.join(dm_dir_path, 'name')) + name = os.path.join("/dev/mapper", basename) + if dev.startswith('loop'): + if os.environ.get("CEPH_VOLUME_USE_LOOP_DEVICES", False) is False: + continue + # Skip loop devices that are not attached + if not os.path.exists(os.path.join(_sys_block_path, dev, 'loop')): + continue + type_ = 'loop' + result.append([kname, name, type_]) + # Next, look for devices that _are_ partitions + for item in os.listdir(_sys_dev_block_path): + is_part = get_file_contents(os.path.join(_sys_dev_block_path, item, 'partition')) == "1" + dev = os.path.basename(os.readlink(os.path.join(_sys_dev_block_path, item))) + if not is_part: + continue + name = kname = os.path.join("/dev", dev) + result.append([name, kname, "part"]) + return sorted(result, key=lambda x: x[0]) + + def get_devices(_sys_block_path='/sys/block', device=''): """ Captures all available block devices as reported by lsblk. @@ -769,12 +805,16 @@ def get_devices(_sys_block_path='/sys/block', device=''): device_facts = {} - block_devs = get_block_devs_lsblk(device=device) + block_devs = get_block_devs_sysfs(_sys_block_path) + + block_types = ['disk', 'mpath'] + if os.environ.get("CEPH_VOLUME_USE_LOOP_DEVICES", False) is not False: + block_types.append('loop') for block in block_devs: devname = os.path.basename(block[0]) diskname = block[1] - if block[2] not in ['disk', 'mpath']: + if block[2] not in block_types: continue sysdir = os.path.join(_sys_block_path, devname) metadata = {} @@ -820,6 +860,7 @@ def get_devices(_sys_block_path='/sys/block', device=''): metadata['human_readable_size'] = human_readable_size(metadata['size']) metadata['path'] = diskname metadata['locked'] = is_locked_raw_device(metadata['path']) + metadata['type'] = block[2] device_facts[diskname] = metadata return device_facts -- 2.39.5