diff options
| -rw-r--r-- | curtin/block/bcache.py | 229 | ||||
| -rw-r--r-- | curtin/block/clear_holders.py | 151 | ||||
| -rw-r--r-- | examples/tests/bcache-ceph-nvme.yaml | 227 | ||||
| -rw-r--r-- | examples/tests/dirty_disks_config.yaml | 19 | ||||
| -rw-r--r-- | tests/data/bcache-super-show-backing | 14 | ||||
| -rw-r--r-- | tests/data/bcache-super-show-caching | 18 | ||||
| -rw-r--r-- | tests/unittests/test_block_bcache.py | 448 | ||||
| -rw-r--r-- | tests/unittests/test_clear_holders.py | 351 | ||||
| -rw-r--r-- | tests/vmtests/test_bcache_ceph.py | 93 |
9 files changed, 1112 insertions, 438 deletions
diff --git a/curtin/block/bcache.py b/curtin/block/bcache.py index 852cef23..c31852ee 100644 --- a/curtin/block/bcache.py +++ b/curtin/block/bcache.py @@ -1,11 +1,15 @@ # This file is part of curtin. See LICENSE file for copyright and license info. +import errno import os from curtin import util from curtin.log import LOG from . import sys_block_path +# Wait up to 20 minutes (150 + 300 + 750 = 1200 seconds) +BCACHE_RETRIES = [sleep for nap in [1, 2, 5] for sleep in [nap] * 150] + def superblock_asdict(device=None, data=None): """ Convert output from bcache-super-show into a dictionary""" @@ -14,7 +18,12 @@ def superblock_asdict(device=None, data=None): raise ValueError('Supply a device name, or data to parse') if not data: - data, _err = util.subp(['bcache-super-show', device], capture=True) + try: + data, _err = util.subp(['bcache-super-show', device], capture=True) + except util.ProcessExecutionError as e: + LOG.debug('Failed to parse bcache superblock on %s:%s', + device, e) + return None bcache_super = {} for line in data.splitlines(): if not line: @@ -25,8 +34,22 @@ def superblock_asdict(device=None, data=None): return bcache_super -def parse_sb_version(sb_version): - """ Convert sb_version string to integer if possible""" +def parse_sb_version(device=None, sbdict=None): + """ Parse bcache 'sb_version' field to integer if possible. + + """ + if not device and not sbdict: + raise ValueError('Supply a device name or bcache superblock dict') + + if not sbdict: + sbdict = superblock_asdict(device=device) + if not sbdict: + LOG.info('Cannot parse sb.version without bcache superblock') + return None + if not isinstance(sbdict, dict): + raise ValueError('Invalid sbdict type, must be dict') + + sb_version = sbdict.get('sb.version') try: # 'sb.version': '1 [backing device]' # 'sb.version': '3 [caching device]' @@ -34,11 +57,25 @@ def parse_sb_version(sb_version): except (AttributeError, ValueError): LOG.warning("Failed to parse bcache 'sb.version' field" " as integer: %s", sb_version) - return None + raise return version +def _check_bcache_type(device, sysfs_attr, sb_version, superblock=False): + """ helper for checking bcache type via sysfs or bcache superblock. """ + if not superblock: + if not device.endswith('bcache'): + sys_block = os.path.join(sys_block_path(device), 'bcache') + else: + sys_block = device + bcache_sys_attr = os.path.join(sys_block, sysfs_attr) + LOG.debug('path exists %s', bcache_sys_attr) + return os.path.exists(bcache_sys_attr) + else: + return parse_sb_version(device=device) == sb_version + + def is_backing(device, superblock=False): """ Test if device is a bcache backing device @@ -47,15 +84,7 @@ def is_backing(device, superblock=False): However if a device is not active then read the superblock of the device and check that sb.version == 1""" - - if not superblock: - sys_block = sys_block_path(device) - bcache_sys_attr = os.path.join(sys_block, 'bcache', 'label') - return os.path.exists(bcache_sys_attr) - else: - bcache_super = superblock_asdict(device=device) - sb_version = parse_sb_version(bcache_super['sb.version']) - return bcache_super and sb_version == 1 + return _check_bcache_type(device, 'label', 1, superblock=superblock) def is_caching(device, superblock=False): @@ -67,21 +96,171 @@ def is_caching(device, superblock=False): However if a device is not active then read the superblock of the device and check that sb.version == 3""" - if not superblock: - sys_block = sys_block_path(device) - bcache_sysattr = os.path.join(sys_block, 'bcache', - 'cache_replacement_policy') - return os.path.exists(bcache_sysattr) - else: - bcache_super = superblock_asdict(device=device) - sb_version = parse_sb_version(bcache_super['sb.version']) - return bcache_super and sb_version == 3 + LOG.debug('Checking if %s is bcache caching device', device) + return _check_bcache_type(device, 'cache_replacement_policy', 3, + superblock=superblock) + + +def sysfs_path(device, strict=True): + """ Return /sys/class/block/<device>/bcache path for device. """ + path = os.path.join(sys_block_path(device, strict=strict), 'bcache') + if strict and not os.path.exists(path): + err = OSError( + "device '{}' did not have existing syspath '{}'".format( + device, path)) + err.errno = errno.ENOENT + raise err + + return path def write_label(label, device): """ write label to bcache device """ - sys_block = sys_block_path(device) - bcache_sys_attr = os.path.join(sys_block, 'bcache', 'label') - util.write_file(bcache_sys_attr, content=label) + bcache_sys_attr = os.path.join(sysfs_path(device), 'label') + util.write_file(bcache_sys_attr, content=label, mode=None) + + +def get_attached_cacheset(device): + """ return the sysfs path to an attached cacheset. """ + bcache_cache = os.path.join(sysfs_path(device), 'cache') + if os.path.exists(bcache_cache): + return os.path.basename(os.path.realpath(bcache_cache)) + + return None + + +def get_cacheset_members(cset_uuid): + """ return a list of sysfs paths to backing devices + attached to the specified cache set. + + Example: + % get_cacheset_members('08307315-48e7-4e46-8742-2ec37d615829') + ['/sys/devices/pci0000:00/0000:00:08.0/virtio5/block/vdc/bcache', + '/sys/devices/pci0000:00/0000:00:07.0/virtio4/block/vdb/bcache', + '/sys/devices/pci0000:00/0000:00:06.0/virtio3/block/vda/vda1/bcache'] + """ + cset_path = '/sys/fs/bcache/%s' % cset_uuid + members = [] + if os.path.exists(cset_path): + # extract bdev* links + bdevs = [link for link in os.listdir(cset_path) + if link.startswith('bdev')] + # resolve symlink to target + members = [os.path.realpath("%s/%s" % (cset_path, bdev)) + for bdev in bdevs] + + return members + + +def get_cacheset_cachedev(cset_uuid): + """ Return a sysfs path to a cacheset cache device's bcache dir.""" + + # XXX: bcache cachesets only have a single cache0 entry + cachedev = '/sys/fs/bcache/%s/cache0' % cset_uuid + if os.path.exists(cachedev): + return os.path.realpath(cachedev) + + return None + + +def get_backing_device(bcache_kname): + """ For a given bcacheN kname, return the backing device + bcache sysfs dir. + + bcache0 -> /sys/.../devices/.../device/bcache + """ + bcache_deps = '/sys/class/block/%s/slaves' % bcache_kname + + try: + # if the bcache device is deleted, this may fail + deps = os.listdir(bcache_deps) + except util.FileMissingError as e: + LOG.debug('Transient race, bcache slave path not found: %s', e) + return None + + # a running bcache device has two entries in slaves, the cacheset + # device, and the backing device. There may only be the backing + # device (if a bcache device is found but not currently attached + # to a cacheset. + if len(deps) == 0: + raise RuntimeError( + '%s unexpected empty dir: %s' % (bcache_kname, bcache_deps)) + + for dev in (sysfs_path(dep) for dep in deps): + if is_backing(dev): + return dev + + return None + + +def stop_cacheset(cset_uuid): + """stop specified bcache cacheset.""" + # we may be called with a full path or just the uuid + if cset_uuid.startswith('/sys/fs/bcache/'): + cset_device = cset_uuid + else: + cset_device = "/sys/fs/bcache/%s" % cset_uuid + LOG.info('Stopping bcache set device: %s', cset_device) + _stop_device(cset_device) + + +def stop_device(device): + """Stop the specified bcache device.""" + if not device.startswith('/sys'): + raise ValueError('Invalid device %s, must be sysfs path' % device) + + if not any(f(device) for f in (is_backing, is_caching)): + raise ValueError('Cannot stop non-bcache device: %s' % device) + + LOG.debug('Stopping bcache layer on %s', device) + _stop_device(device) + + +def _stop_device(device): + """ write to sysfs 'stop' and wait for path to be removed + + The caller needs to ensure that supplied path to the device + is a 'bcache' sysfs path on a device. This may be one of the + following scenarios: + + Cacheset: + /sys/fs/bcache/<uuid>/ + + Bcache device: + /sys/class/block/bcache0/bcache + + Backing device + /sys/class/block/vdb/bcache + + Cached device + /sys/class/block/nvme0n1p1/bcache/set + + To support all of these, we append 'stop' to the path + and write '1' and then wait for the 'stop' path to + be removed. + """ + bcache_stop = os.path.join(device, 'stop') + if not os.path.exists(bcache_stop): + LOG.debug('bcache._stop_device: already removed %s', bcache_stop) + return + + LOG.debug('bcache._stop_device: device=%s stop_path=%s', + device, bcache_stop) + try: + util.write_file(bcache_stop, '1', mode=None) + except (IOError, OSError) as e: + # Note: if we get any exceptions in the above exception classes + # it is a result of attempting to write "1" into the sysfs path + # The range of errors changes depending on when we race with + # the kernel asynchronously removing the sysfs path. Therefore + # we log the exception errno we got, but do not re-raise as + # the calling process is watching whether the same sysfs path + # is being removed; if it fails to go away then we'll have + # a log of the exceptions to debug. + LOG.debug('Error writing to bcache stop file %s, device removed: %s', + bcache_stop, e) + finally: + util.wait_for_removal(bcache_stop, retries=BCACHE_RETRIES) + # vi: ts=4 expandtab syntax=python diff --git a/curtin/block/clear_holders.py b/curtin/block/clear_holders.py index 67caee39..fb7fba4a 100644 --- a/curtin/block/clear_holders.py +++ b/curtin/block/clear_holders.py @@ -6,13 +6,13 @@ top of a block device, making it possible to reuse the block device without having to reboot the system """ -import errno import glob import os import time from curtin import (block, udev, util) from curtin.swap import is_swap_device +from curtin.block import bcache from curtin.block import lvm from curtin.block import mdadm from curtin.block import zfs @@ -47,80 +47,29 @@ def get_dmsetup_uuid(device): return out.strip() -def get_bcache_using_dev(device, strict=True): - """ - Get the /sys/fs/bcache/ path of the bcache cache device bound to - specified device - """ - # FIXME: when block.bcache is written this should be moved there - sysfs_path = block.sys_block_path(device) - path = os.path.realpath(os.path.join(sysfs_path, 'bcache', 'cache')) - if strict and not os.path.exists(path): - err = OSError( - "device '{}' did not have existing syspath '{}'".format( - device, path)) - err.errno = errno.ENOENT - raise err - - return path - - -def get_bcache_sys_path(device, strict=True): - """ - Get the /sys/class/block/<device>/bcache path - """ - sysfs_path = block.sys_block_path(device, strict=strict) - path = os.path.join(sysfs_path, 'bcache') - if strict and not os.path.exists(path): - err = OSError( - "device '{}' did not have existing syspath '{}'".format( - device, path)) - err.errno = errno.ENOENT - raise err - - return path - - -def maybe_stop_bcache_device(device): - """Attempt to stop the provided device_path or raise unexpected errors.""" - bcache_stop = os.path.join(device, 'stop') - try: - util.write_file(bcache_stop, '1', mode=None) - except (IOError, OSError) as e: - # Note: if we get any exceptions in the above exception classes - # it is a result of attempting to write "1" into the sysfs path - # The range of errors changes depending on when we race with - # the kernel asynchronously removing the sysfs path. Therefore - # we log the exception errno we got, but do not re-raise as - # the calling process is watching whether the same sysfs path - # is being removed; if it fails to go away then we'll have - # a log of the exceptions to debug. - LOG.debug('Error writing to bcache stop file %s, device removed: %s', - bcache_stop, e) - - def shutdown_bcache(device): """ Shut down bcache for specified bcache device - 1. Stop the cacheset that `device` is connected to - 2. Stop the 'device' + 1. wipe the bcache device contents + 2. extract the cacheset uuid (if cached) + 3. extract the backing device + 4. stop cacheset (if present) + 5. stop the bcacheN device + 6. wait for removal of sysfs path to bcacheN, bcacheN/bcache and + backing/bcache to go away """ if not device.startswith('/sys/class/block'): raise ValueError('Invalid Device (%s): ' 'Device path must start with /sys/class/block/', device) - LOG.info('Wiping superblock on bcache device: %s', device) - _wipe_superblock(block.sysfs_to_devpath(device), exclusive=False) - # bcache device removal should be fast but in an extreme # case, might require the cache device to flush large # amounts of data to a backing device. The strategy here # is to wait for approximately 30 seconds but to check # frequently since curtin cannot proceed until devices # cleared. - removal_retries = [0.2] * 150 # 30 seconds total bcache_shutdown_message = ('shutdown_bcache running on {} has determined ' 'that the device has already been shut down ' 'during handling of another bcache dev. ' @@ -130,60 +79,39 @@ def shutdown_bcache(device): LOG.info(bcache_shutdown_message) return - # get slaves [vdb1, vdc], allow for slaves to not have bcache dir - try: - slave_paths = [get_bcache_sys_path(k, strict=False) for k in - os.listdir(os.path.join(device, 'slaves'))] - except util.FileMissingError as e: - LOG.debug('Transient race, bcache slave path not found: %s', e) - slave_paths = [] - - # stop cacheset if it exists - bcache_cache_sysfs = get_bcache_using_dev(device, strict=False) - if not os.path.exists(bcache_cache_sysfs): - LOG.info('bcache cacheset already removed: %s', - os.path.basename(bcache_cache_sysfs)) - else: - LOG.info('stopping bcache cacheset at: %s', bcache_cache_sysfs) - maybe_stop_bcache_device(bcache_cache_sysfs) - try: - util.wait_for_removal(bcache_cache_sysfs, retries=removal_retries) - except OSError: - LOG.info('Failed to stop bcache cacheset %s', bcache_cache_sysfs) - raise + LOG.info('Wiping superblock on bcache device: %s', device) + _wipe_superblock(block.sysfs_to_devpath(device), exclusive=False) + + # collect required information before stopping bcache device + # UUID from /sys/fs/cache/UUID + cset_uuid = bcache.get_attached_cacheset(device) + # /sys/class/block/vdX which is a backing dev of device (bcacheN) + backing_sysfs = bcache.get_backing_device(block.path_to_kname(device)) + # /sys/class/block/bcacheN/bache + bcache_sysfs = bcache.sysfs_path(device, strict=False) + + # stop cacheset if one is presennt + if cset_uuid: + LOG.info('%s was attached to cacheset %s, stopping cacheset', + device, cset_uuid) + bcache.stop_cacheset(cset_uuid) # let kernel settle before the next remove udev.udevadm_settle() + LOG.info('bcache cacheset stopped: %s', cset_uuid) - # after stopping cache set, we may need to stop the device - # both the dev and sysfs entry should be gone. - - # we know the bcacheN device is really gone when we've removed: - # /sys/class/block/{bcacheN} - # /sys/class/block/slaveN1/bcache - # /sys/class/block/slaveN2/bcache - bcache_block_sysfs = get_bcache_sys_path(device, strict=False) - to_check = [device] + slave_paths + # test and log whether the device paths are still present + to_check = [bcache_sysfs, backing_sysfs] found_devs = [os.path.exists(p) for p in to_check] LOG.debug('os.path.exists on blockdevs:\n%s', list(zip(to_check, found_devs))) if not any(found_devs): LOG.info('bcache backing device already removed: %s (%s)', - bcache_block_sysfs, device) - LOG.debug('bcache slave paths checked: %s', slave_paths) - return + bcache_sysfs, device) + LOG.debug('bcache backing device checked: %s', backing_sysfs) else: - LOG.info('stopping bcache backing device at: %s', bcache_block_sysfs) - maybe_stop_bcache_device(bcache_block_sysfs) - try: - # wait for them all to go away - for dev in [device, bcache_block_sysfs] + slave_paths: - util.wait_for_removal(dev, retries=removal_retries) - except OSError: - LOG.info('Failed to stop bcache backing device %s', - bcache_block_sysfs) - raise - + LOG.info('stopping bcache backing device at: %s', bcache_sysfs) + bcache.stop_device(bcache_sysfs) return @@ -335,10 +263,13 @@ def wipe_superblock(device): for bcache_path in ['bcache', 'bcache/set']: stop_path = os.path.join(device, bcache_path) if os.path.exists(stop_path): - LOG.debug('Attempting to release bcache layer from device: %s', - device) - maybe_stop_bcache_device(stop_path) - continue + LOG.debug('Attempting to release bcache layer from device: %s:%s', + device, stop_path) + if stop_path.endswith('set'): + rp = os.path.realpath(stop_path) + bcache.stop_cacheset(rp) + else: + bcache._stop_device(stop_path) _wipe_superblock(blockdev) @@ -537,8 +468,10 @@ def plan_shutdown_holder_trees(holders_trees): for holders_tree in holders_trees: flatten_holders_tree(holders_tree) - # return list of entry dicts with highest level first - return [reg[k] for k in sorted(reg, key=lambda x: reg[x]['level'] * -1)] + # return list of entry dicts with highest level first, then dev_type + return [reg[k] + for k in sorted(reg, key=lambda x: (reg[x]['level'] * -1, + reg[x]['dev_type']))] def format_holders_tree(holders_tree): diff --git a/examples/tests/bcache-ceph-nvme.yaml b/examples/tests/bcache-ceph-nvme.yaml new file mode 100644 index 00000000..507bc0ec --- /dev/null +++ b/examples/tests/bcache-ceph-nvme.yaml @@ -0,0 +1,227 @@ +install: + unmount: disabled +showtrace: true +storage: + config: + - grub_device: true + id: sda + model: MG04SCA60EA + name: sda + ptable: gpt + serial: '500003986840e04d' + type: disk + wipe: superblock + - id: sdb + model: MG04SCA60EA + name: sdb + serial: '500003986833378d' + type: disk + wipe: superblock + - id: sdc + model: MG04SCA60EA + name: sdc + serial: '5000039868108f0d' + type: disk + wipe: superblock + - id: sdd + model: MG04SCA60EA + name: sdd + serial: '5000039868107619' + type: disk + wipe: superblock + - id: sde + model: MG04SCA60EA + name: sde + serial: '5000039868418549' + type: disk + wipe: superblock + - id: sdf + model: SAMSUNG MZ7LM240 + name: sdf + ptable: gpt + serial: 'S3LKNX0K202278' + type: disk + wipe: superblock + - id: sdg + model: MG04SCA60EA + name: sdg + serial: '5000039868333799' + type: disk + wipe: superblock + - id: sdh + model: SAMSUNG MZ7LM240 + name: sdh + ptable: gpt + serial: 'S3LKNX0K200071' + type: disk + wipe: superblock + - id: nvme0n1 + model: UCSC-NVME-H32003 + name: nvme0n1 + ptable: gpt + serial: nvme-SDM000014FB6 + type: disk + wipe: superblock + - id: nvme1n1 + model: UCSC-NVME-H32003 + name: nvme1n1 + ptable: gpt + serial: nvme-SDM000014F3C + type: disk + wipe: superblock + - device: sda + id: sda-part1 + name: sda-part1 + number: 1 + offset: 4194304B + size: 5G + type: partition + uuid: 11d66990-9b49-4fe5-b933-d8f1527023d3 + wipe: superblock + - device: sdf + id: sdf-part1 + name: sdf-part1 + number: 1 + offset: 4194304B + size: 5G + type: partition + uuid: e86f3316-aacc-4958-a6db-34875a5fde7c + wipe: superblock + - device: sdf + id: sdf-part2 + name: sdf-part2 + number: 2 + size: 5G + type: partition + uuid: aa5d9117-de31-4311-9bf1-28ae45e9748f + wipe: superblock + - device: sdf + id: sdf-part3 + name: sdf-part3 + number: 3 + size: 5G + type: partition + uuid: a312bb83-e34a-4d05-b45e-006d2f4291ee + wipe: superblock + - device: sdh + id: sdh-part1 + name: sdh-part1 + number: 1 + offset: 4194304B + size: 5G + type: partition + uuid: a15f79c9-4277-4c58-8a68-65a6f59864f3 + wipe: superblock + - devices: + - sdf-part3 + - sdh-part1 + id: md0 + name: md0 + raidlevel: 1 + spare_devices: [] + type: raid + - device: nvme0n1 + id: nvme0n1-part1 + name: nvme0n1-part1 + number: 1 + offset: 4194304B + size: 4G + type: partition + uuid: 5a406a80-dd85-4f5a-83a5-9dd0bf27cb6e + wipe: superblock + - device: nvme0n1 + id: nvme0n1-part2 + name: nvme0n1-part2 + number: 2 + size: 4G + type: partition + uuid: a1ab6ecb-e4b1-44eb-b895-949808741ab3 + wipe: superblock + - backing_device: sda-part1 + cache_device: nvme0n1-part2 + cache_mode: writeback + id: osddata0 + name: osddata0 + type: bcache + - backing_device: sdc + cache_device: nvme0n1-part2 + cache_mode: writeback + id: osddata2 + name: osddata2 + type: bcache + - backing_device: sdb + cache_device: nvme0n1-part2 + cache_mode: writeback + id: osddata1 + name: osddata1 + type: bcache + - device: nvme1n1 + id: nvme1n1-part1 + name: nvme1n1-part1 + number: 1 + offset: 4194304B + size: 4G + type: partition + uuid: fa904f69-2de7-43c6-a9b6-14b4e7139ce7 + wipe: superblock + - device: nvme1n1 + id: nvme1n1-part2 + name: nvme1n1-part2 + number: 2 + size: 4G + type: partition + uuid: 2f5e22d5-6737-4ad2-94ff-e0cf7ef8c97c + wipe: superblock + - backing_device: sdg + cache_device: nvme1n1-part2 + cache_mode: writeback + id: osddata5 + name: osddata5 + type: bcache + - backing_device: sde + cache_device: nvme1n1-part2 + cache_mode: writeback + id: osddata4 + name: osddata4 + type: bcache + - backing_device: sdd + cache_device: nvme1n1-part2 + cache_mode: writeback + id: osddata3 + name: osddata3 + type: bcache + - fstype: fat32 + id: sdf-part1_format + label: '' + type: format + uuid: 7ddf7d92-5e9f-4347-93e2-b34455339342 + volume: sdf-part1 + - fstype: ext4 + id: sdf-part2_format + label: '' + type: format + uuid: 771ea4e9-873c-48ab-9ac6-e49ede275019 + volume: sdf-part2 + - fstype: ext4 + id: md0_format + label: os + type: format + uuid: b29b461c-34f0-4d22-9454-0034e34b1b5c + volume: md0 + - device: md0_format + id: md0_mount + options: '' + path: / + type: mount + - device: sdf-part2_format + id: sdf-part2_mount + options: '' + path: /boot + type: mount + - device: sdf-part1_format + id: sdf-part1_mount + options: '' + path: /boot/efi + type: mount + version: 1 +verbosity: 3 diff --git a/examples/tests/dirty_disks_config.yaml b/examples/tests/dirty_disks_config.yaml index fb9a0d68..bcf3fbc9 100644 --- a/examples/tests/dirty_disks_config.yaml +++ b/examples/tests/dirty_disks_config.yaml @@ -52,6 +52,22 @@ bucket: done # remove any existing metadata written from early disk config rm -f /etc/mdadm/mdadm.conf + - &naptime | + #!/bin/sh + # This function attempts to settle and flush IO to devices + # in some scenarios vmtest devices have had lots of IO + # sent to them and they've yet to consume it all. Give it a + # chance to flush themselves + echo "VMTEST: io flush nap time, 12 second cleanse" + sleep 3 + sync; sync; sync; + sleep 3 + echo 3 > /proc/sys/vm/drop_caches + sleep 3 + sync; sync; sync; + sleep 3 + echo "VMTEST: io flush nap time complete;" + early_commands: # running block-meta custom from the install environment @@ -61,9 +77,10 @@ early_commands: # that could unintentionally mess things up. 01-blockmeta: [env, -u, OUTPUT_FSTAB, TARGET_MOUNT_POINT=/tmp/my.bdir/target, - WORKING_DIR=/tmp/my.bdir/work.d, + WORKING_DIR=/tmp/my.bdir/work.d, curtin, --showtrace, -v, block-meta, --umount, custom] 02-enable_swaps: [sh, -c, *swapon] 03-disable_rpool: [sh, -c, *zpool_export] 04-lvm_stop: [sh, -c, *lvm_stop] 05-mdadm_stop: [sh, -c, *mdadm_stop] + 06-naptime: [sh, -c, *naptime] diff --git a/tests/data/bcache-super-show-backing b/tests/data/bcache-super-show-backing new file mode 100644 index 00000000..03debdf1 --- /dev/null +++ b/tests/data/bcache-super-show-backing @@ -0,0 +1,14 @@ +sb.magic ok +sb.first_sector 8 [match] +sb.csum B92908820E241EDD [match] +sb.version 1 [backing device] + +dev.label (empty) +dev.uuid f36394c0-3cc0-4423-8d6f-ffac130f171a +dev.sectors_per_block 1 +dev.sectors_per_bucket 1024 +dev.data.first_sector 16 +dev.data.cache_mode 1 [writeback] +dev.data.cache_state 2 [dirty] + +cset.uuid 01da3829-ea92-4600-bd40-7f95974f3087 diff --git a/tests/data/bcache-super-show-caching b/tests/data/bcache-super-show-caching new file mode 100644 index 00000000..9125700f --- /dev/null +++ b/tests/data/bcache-super-show-caching @@ -0,0 +1,18 @@ +sb.magic ok +sb.first_sector 8 [match] +sb.csum 2F8BB7E8DC53E0B6 [match] +sb.version 3 [cache device] + +dev.label (empty) +dev.uuid ff51a56d-eddc-41b3-867d-8744277c5281 +dev.sectors_per_block 1 +dev.sectors_per_bucket 1024 +dev.cache.first_sector 1024 +dev.cache.cache_sectors 234372096 +dev.cache.total_sectors 234373120 +dev.cache.ordered yes +dev.cache.discard no +dev.cache.pos 0 +dev.cache.replacement 0 [lru] + +cset.uuid 01da3829-ea92-4600-bd40-7f95974f3087 diff --git a/tests/unittests/test_block_bcache.py b/tests/unittests/test_block_bcache.py new file mode 100644 index 00000000..79365223 --- /dev/null +++ b/tests/unittests/test_block_bcache.py @@ -0,0 +1,448 @@ +import mock +import os + +from curtin.block import bcache +from curtin.util import (FileMissingError, load_file, ProcessExecutionError) +from .helpers import CiTestCase + + +class TestBlockBcache(CiTestCase): + + def setUp(self): + super(TestBlockBcache, self).setUp() + self.add_patch('curtin.block.bcache.util.subp', 'mock_subp') + + def _datafile(self, name): + path = 'tests/data' + return os.path.join(path, name) + + expected = { + 'backing': { + "cset.uuid": "01da3829-ea92-4600-bd40-7f95974f3087", + "dev.data.cache_mode": "1 [writeback]", + "dev.data.cache_state": "2 [dirty]", + "dev.data.first_sector": "16", + "dev.label": "(empty)", + "dev.sectors_per_block": "1", + "dev.sectors_per_bucket": "1024", + "dev.uuid": "f36394c0-3cc0-4423-8d6f-ffac130f171a", + "sb.csum": "B92908820E241EDD [match]", + "sb.first_sector": "8 [match]", + "sb.magic": "ok", + "sb.version": "1 [backing device]" + }, + 'caching': { + "cset.uuid": "01da3829-ea92-4600-bd40-7f95974f3087", + "dev.cache.cache_sectors": "234372096", + "dev.cache.discard": "no", + "dev.cache.first_sector": "1024", + "dev.cache.ordered": "yes", + "dev.cache.pos": "0", + "dev.cache.replacement": "0 [lru]", + "dev.cache.total_sectors": "234373120", + "dev.label": "(empty)", + "dev.sectors_per_block": "1", + "dev.sectors_per_bucket": "1024", + "dev.uuid": "ff51a56d-eddc-41b3-867d-8744277c5281", + "sb.csum": "2F8BB7E8DC53E0B6 [match]", + "sb.first_sector": "8 [match]", + "sb.magic": "ok", + "sb.version": "3 [cache device]" + }, + } + + @mock.patch('curtin.block.bcache.util.subp') + def test_superblock_asdict(self, m_subp): + """ verify parsing bcache-super-show matches expected results.""" + device = self.random_string() + results = {} + prefix = 'bcache-super-show-' + scenarios = ['backing', 'caching'] + + # XXX: Parameterize me + for superblock in scenarios: + datafile = prefix + superblock + contents = load_file(self._datafile(datafile)) + m_subp.return_value = (contents, '') + + results[superblock] = bcache.superblock_asdict(device=device) + + for superblock in scenarios: + comment = 'mismatch in %s' % superblock + self.assertDictEqual( + self.expected[superblock], results[superblock], comment) + + def test_superblock_asdict_no_dev_no_data(self): + """ superblock_asdict raises ValueError without device or data.""" + with self.assertRaises(ValueError): + bcache.superblock_asdict() + + @mock.patch('curtin.block.bcache.util.subp') + def test_superblock_asdict_calls_bcache_super_show(self, m_subp): + """ superblock_asdict calls bcache-super-show on device.""" + device = self.random_string() + m_subp.return_value = ('', '') + bcache.superblock_asdict(device=device) + m_subp.assert_called_with(['bcache-super-show', device], capture=True) + + @mock.patch('curtin.block.bcache.util.subp') + def test_superblock_asdict_does_not_call_subp_with_data(self, m_subp): + """ superblock_asdict does not bcache-super-show with data provided.""" + key = self.random_string() + value = self.random_string() + mydata = "\t".join([key, value]) + result = bcache.superblock_asdict(data=mydata) + self.assertEqual({key: value}, result) + m_subp.assert_not_called() + + @mock.patch('curtin.block.bcache.util.subp') + def test_superblock_asdict_returns_none_invalid_superblock(self, m_subp): + device = self.random_string() + m_subp.side_effect = ProcessExecutionError(stdout=self.random_string(), + stderr=self.random_string(), + exit_code=2) + self.assertEqual(None, bcache.superblock_asdict(device=device)) + + def test_parse_sb_version(self): + """ parse_sb_version converts sb.version field into integer value. """ + sbdict = {'sb.version': '1 [backing device]'} + self.assertEqual(1, bcache.parse_sb_version(sbdict=sbdict)) + + # XXX: Parameterize me + def test_parse_sb_version_raises_exceptions_on_garbage_dict(self): + """ parse_sb_version raises Exceptions on garbage dicts.""" + with self.assertRaises(AttributeError): + bcache.parse_sb_version(sbdict={self.random_string(): + self.random_string()}) + + # XXX: Parameterize me + def test_parse_sb_version_raises_exceptions_on_non_dict(self): + """ parse_sb_version raises Exceptions on non-dict input.""" + with self.assertRaises(ValueError): + bcache.parse_sb_version(sbdict=self.random_string()) + + @mock.patch('curtin.block.bcache.superblock_asdict') + def test_is_backing_superblock(self, m_sbdict): + """ is_backing returns True when given backing superblock dict. """ + bdict = {'sb.version': '1 [backing device]'} + m_sbdict.return_value = bdict + self.assertEqual(True, bcache.is_backing(self.random_string(), + superblock=True)) + + @mock.patch('curtin.block.bcache.superblock_asdict') + def test_is_backing_superblock_invalid(self, m_sbdict): + """ is_backing returns False when parsing invalid superblock. """ + m_sbdict.return_value = None + self.assertEqual(False, bcache.is_backing(self.random_string(), + superblock=True)) + + @mock.patch('curtin.block.bcache.os.path.exists') + @mock.patch('curtin.block.bcache.sys_block_path') + def test_is_backing_sysfs(self, m_sysb_path, m_path_exists): + """ is_backing returns True if sysfs path has bcache/label. """ + kname = self.random_string() + m_sysb_path.return_value = '/sys/class/block/%s' % kname + m_path_exists.return_value = True + self.assertEqual(True, bcache.is_backing(kname)) + + @mock.patch('curtin.block.bcache.os.path.exists') + @mock.patch('curtin.block.bcache.sys_block_path') + def test_is_backing_sysfs_false(self, m_sysb_path, m_path_exists): + """ is_backing returns False if path does not have bcache/label. """ + kname = self.random_string() + m_sysb_path.return_value = '/sys/class/block/%s' % kname + m_path_exists.return_value = False + self.assertEqual(False, bcache.is_backing(kname)) + + @mock.patch('curtin.block.bcache.superblock_asdict') + def test_is_cacheing_superblock(self, m_sbdict): + """ is_caching returns True when given caching superblock dict. """ + bdict = {'sb.version': '3 [caching device]'} + m_sbdict.return_value = bdict + self.assertEqual(True, bcache.is_caching(self.random_string(), + superblock=True)) + + @mock.patch('curtin.block.bcache.superblock_asdict') + def test_is_caching_superblock_invalid(self, m_sbdict): + """ is_caching returns False when parsing invalid superblock. """ + m_sbdict.return_value = None + self.assertEqual(False, bcache.is_caching(self.random_string(), + superblock=True)) + + @mock.patch('curtin.block.bcache.os.path.exists') + @mock.patch('curtin.block.bcache.sys_block_path') + def test_is_caching_sysfs(self, m_sysb_path, m_path_exists): + """ is_caching returns True if sysfs path has bcache/label. """ + kname = self.random_string() + m_sysb_path.return_value = '/sys/class/block/%s' % kname + m_path_exists.return_value = True + self.assertEqual(True, bcache.is_caching(kname)) + + @mock.patch('curtin.block.bcache.os.path.exists') + @mock.patch('curtin.block.bcache.sys_block_path') + def test_is_caching_sysfs_false(self, m_sysb_path, m_path_exists): + """ is_caching returns False if path does not have bcache/label. """ + kname = self.random_string() + m_sysb_path.return_value = '/sys/class/block/%s' % kname + m_path_exists.return_value = False + self.assertEqual(False, bcache.is_caching(kname)) + + @mock.patch('curtin.block.bcache.os.path.exists') + @mock.patch('curtin.block.bcache.sys_block_path') + def test_sysfs_path(self, m_sysb_path, m_path_exists): + """ sysfs_path returns /sys/class/block/<device>/bcache for device.""" + kname = self.random_string() + m_sysb_path.return_value = '/sys/class/block/%s' % kname + m_path_exists.return_value = True + self.assertEqual('/sys/class/block/%s/bcache' % kname, + bcache.sysfs_path(kname)) + + @mock.patch('curtin.block.bcache.os.path.exists') + @mock.patch('curtin.block.bcache.sys_block_path') + def test_sysfs_path_raise_strict_nopath(self, m_sysb_path, m_path_exists): + """ sysfs_path raises OSError on strict=True and missing path. """ + kname = self.random_string() + m_sysb_path.return_value = '/sys/class/block/%s' % kname + m_path_exists.return_value = False + with self.assertRaises(OSError): + bcache.sysfs_path(kname) + + @mock.patch('curtin.block.bcache.os.path.exists') + @mock.patch('curtin.block.bcache.sys_block_path') + def test_sysfs_path_non_strict(self, m_sysb_path, m_path_exists): + """ sysfs_path returns path if missing and strict=False.""" + kname = self.random_string() + m_sysb_path.return_value = '/sys/class/block/%s' % kname + m_path_exists.return_value = False + self.assertEqual('/sys/class/block/%s/bcache' % kname, + bcache.sysfs_path(kname, strict=False)) + + @mock.patch('curtin.block.bcache.sysfs_path') + @mock.patch('curtin.block.bcache.util.write_file') + def test_write_label(self, m_write_file, m_sysfs_path): + """ write_label writes label to device/bcache/label attribute.""" + label = self.random_string() + kname = self.random_string() + bdir = '/sys/class/block/%s/bcache' % kname + label_path = bdir + '/label' + m_sysfs_path.return_value = bdir + bcache.write_label(label, kname) + m_write_file.assert_called_with(label_path, content=label, mode=None) + + @mock.patch('curtin.block.bcache.os.path.realpath') + @mock.patch('curtin.block.bcache.os.path.basename') + @mock.patch('curtin.block.bcache.os.path.exists') + @mock.patch('curtin.block.bcache.sysfs_path') + def test_get_attached_cacheset(self, m_spath, m_exists, m_base, m_real): + """ get_attached_cacheset resolves 'cache' symlink under bcache dir.""" + kname = self.random_string() + cset_uuid = self.random_string() + bdir = '/sys/class/block/%s/bcache' % kname + m_spath.return_value = bdir + m_exists.return_value = True + cacheset = '/sys/fs/bcache/%s' % cset_uuid + m_base.return_value = cacheset + + self.assertEqual(cacheset, bcache.get_attached_cacheset(kname)) + m_exists.assert_called_with(bdir + '/cache') + + @mock.patch('curtin.block.bcache.os.path.exists') + @mock.patch('curtin.block.bcache.os.path.realpath') + @mock.patch('curtin.block.bcache.os.listdir') + def test_get_cacheset_members(self, m_listdir, m_real, m_exists): + """ get_cacheset_members finds backing devices using cacheset.""" + cset_uuid = self.random_string() + bdev_target = self.random_string() + cset_dir_keys = [ + 'average_key_size', 'bdev0', 'block_size', 'btree_cache_size', + 'bucket_size', 'cache0', 'cache_available_percent', 'clear_stats', + 'congested', 'congested_read_threshold_us', + 'congested_write_threshold_us', 'errors', 'flash_vol_create', + 'internal', 'io_error_halflife', 'io_error_limit', + 'journal_delay_ms', 'root_usage_percent', 'stats_day', + 'stats_five_minute', 'stats_hour', 'stats_total', 'stop', + 'synchronous', 'tree_depth', 'unregister', + ] + cset_path = '/sys/fs/bcache/%s' % cset_uuid + m_listdir.return_value = cset_dir_keys + m_real.side_effect = iter([bdev_target]) + m_exists.return_value = True + results = bcache.get_cacheset_members(cset_uuid) + self.assertEqual([bdev_target], results) + m_listdir.assert_called_with(cset_path) + + @mock.patch('curtin.block.bcache.os.path.exists') + @mock.patch('curtin.block.bcache.os.path.realpath') + def test_get_cacheset_cachedev(self, m_real, m_exists): + """ get_cacheset_cachedev finds cacheset device path.""" + cset_uuid = self.random_string() + cachedev_target = self.random_string() + cset_path = '/sys/fs/bcache/%s/cache0' % cset_uuid + m_exists.return_value = True + m_real.side_effect = iter([cachedev_target]) + results = bcache.get_cacheset_cachedev(cset_uuid) + self.assertEqual(cachedev_target, results) + m_real.assert_called_with(cset_path) + + @mock.patch('curtin.block.bcache.is_backing') + @mock.patch('curtin.block.bcache.sysfs_path') + @mock.patch('curtin.block.bcache.os.listdir') + def test_get_backing_device(self, m_list, m_sysp, m_back): + """ extract sysfs path to backing device from bcache kname.""" + bcache_kname = self.random_string() + backing_kname = self.random_string() + caching_kname = self.random_string() + m_list.return_value = [backing_kname, caching_kname] + m_sysp.side_effect = lambda x: '/sys/class/block/%s/bcache' % x + m_back.side_effect = iter([True, False]) + + self.assertEqual('/sys/class/block/%s/bcache' % backing_kname, + bcache.get_backing_device(bcache_kname)) + + @mock.patch('curtin.block.bcache.is_backing') + @mock.patch('curtin.block.bcache.sysfs_path') + @mock.patch('curtin.block.bcache.os.listdir') + def test_get_backing_device_none_empty_dir(self, m_list, m_sysp, m_back): + """ get_backing_device returns None on missing deps dir. """ + bcache_kname = self.random_string() + m_list.side_effect = FileMissingError('does not exist') + self.assertEqual(None, bcache.get_backing_device(bcache_kname)) + + @mock.patch('curtin.block.bcache.is_backing') + @mock.patch('curtin.block.bcache.sysfs_path') + @mock.patch('curtin.block.bcache.os.listdir') + def test_get_backing_device_raise_empty_dir(self, m_list, m_sysp, m_back): + """ get_backing_device raises RuntimeError on empty deps dir.""" + bcache_kname = self.random_string() + m_list.return_value = [] + with self.assertRaises(RuntimeError): + bcache.get_backing_device(bcache_kname) + + @mock.patch('curtin.block.bcache._stop_device') + def test_stop_cacheset(self, m_stop): + """ stop_cacheset calls _stop_device with correct sysfs path. """ + cset_uuid = self.random_string() + bcache.stop_cacheset(cset_uuid) + m_stop.assert_called_with('/sys/fs/bcache/%s' % cset_uuid) + + @mock.patch('curtin.block.bcache._stop_device') + def test_stop_cacheset_full_path(self, m_stop): + """ stop_cacheset accepts full path to cacheset. """ + cset_path = '/sys/fs/bcache/%s' % self.random_string() + bcache.stop_cacheset(cset_path) + m_stop.assert_called_with(cset_path) + + @mock.patch('curtin.block.bcache._stop_device') + @mock.patch('curtin.block.bcache.is_caching') + @mock.patch('curtin.block.bcache.is_backing') + def test_stop_device_backing(self, m_back, m_cache, m_stop): + """ stop_device allows backing device to be stopped. """ + device = '/sys/class/block/%s' % self.random_string() + m_back.return_value = True + bcache.stop_device(device) + m_stop.assert_called_with(device) + m_back.assert_called_with(device) + self.assertEqual(0, m_cache.call_count) + + @mock.patch('curtin.block.bcache._stop_device') + @mock.patch('curtin.block.bcache.is_caching') + @mock.patch('curtin.block.bcache.is_backing') + def test_stop_device_caching(self, m_back, m_cache, m_stop): + """ stop_device allows caching device to be stopped. """ + device = '/sys/class/block/%s' % self.random_string() + m_back.return_value = False + m_cache.return_value = True + bcache.stop_device(device) + m_stop.assert_called_with(device) + m_back.assert_called_with(device) + m_cache.assert_called_with(device) + + @mock.patch('curtin.block.bcache._stop_device') + @mock.patch('curtin.block.bcache.is_caching') + @mock.patch('curtin.block.bcache.is_backing') + def test_stop_device_raise_non_syspath(self, m_back, m_cache, m_stop): + """ stop_device raises ValueError if device is not sysfs path.""" + device = self.random_string() + with self.assertRaises(ValueError): + bcache.stop_device(device) + self.assertEqual(0, m_stop.call_count) + self.assertEqual(0, m_back.call_count) + self.assertEqual(0, m_cache.call_count) + + @mock.patch('curtin.block.bcache._stop_device') + @mock.patch('curtin.block.bcache.is_caching') + @mock.patch('curtin.block.bcache.is_backing') + def test_stop_device_raise_non_bcache_dev(self, m_back, m_cache, m_stop): + """ stop_device raises ValueError if device is not bcache device.""" + device = '/sys/class/block/%s' % self.random_string() + m_back.return_value = False + m_cache.return_value = False + with self.assertRaises(ValueError): + bcache.stop_device(device) + self.assertEqual(0, m_stop.call_count) + self.assertEqual(1, m_back.call_count) + self.assertEqual(1, m_cache.call_count) + + @mock.patch('curtin.block.bcache.util.wait_for_removal') + @mock.patch('curtin.block.bcache.util.write_file') + @mock.patch('curtin.block.bcache.os.path.exists') + def test__stop_device_stops_bcache_devs(self, m_exists, m_write, m_wait): + """ _stop_device accepts path and issue stop.""" + device = self.random_string() + stop_path = os.path.join(device, 'stop') + m_exists.return_value = True + bcache._stop_device(device) + m_exists.assert_called_with(stop_path) + m_write.assert_called_with(stop_path, '1', mode=None) + m_wait.assert_called_with(stop_path, retries=bcache.BCACHE_RETRIES) + + @mock.patch('curtin.block.bcache.util.wait_for_removal') + @mock.patch('curtin.block.bcache.util.write_file') + @mock.patch('curtin.block.bcache.os.path.exists') + def test__stop_device_already_removed(self, m_exists, m_write, m_wait): + """ _stop_device skips if device path is missing. """ + device = self.random_string() + stop_path = os.path.join(device, 'stop') + m_exists.return_value = False + + bcache._stop_device(device) + m_exists.assert_called_with(stop_path) + self.assertEqual(0, m_write.call_count) + self.assertEqual(0, m_wait.call_count) + + @mock.patch('curtin.block.bcache.util.wait_for_removal') + @mock.patch('curtin.block.bcache.util.write_file') + @mock.patch('curtin.block.bcache.os.path.exists') + def test__stop_device_eats_err_calls_wait(self, m_exists, m_write, m_wait): + """ _stop_device eats IOError or OSErrors wait still called""" + device = self.random_string() + stop_path = os.path.join(device, 'stop') + m_exists.return_value = True + m_write.side_effect = IOError('permission denied') + + bcache._stop_device(device) + + m_exists.assert_called_with(stop_path) + m_write.assert_called_with(stop_path, '1', mode=None) + m_wait.assert_called_with(stop_path, retries=bcache.BCACHE_RETRIES) + + @mock.patch('curtin.block.bcache.util.wait_for_removal') + @mock.patch('curtin.block.bcache.util.write_file') + @mock.patch('curtin.block.bcache.os.path.exists') + def test__stop_device_raises_if_wait_expires(self, m_exists, m_write, + m_wait): + """ _stop_device raises OSError if wait time expires """ + device = self.random_string() + stop_path = os.path.join(device, 'stop') + m_exists.return_value = True + m_wait.side_effect = ( + OSError('Timeout exeeded for removal of %s' % stop_path)) + with self.assertRaises(OSError): + bcache._stop_device(device) + + m_exists.assert_called_with(stop_path) + m_write.assert_called_with(stop_path, '1', mode=None) + m_wait.assert_called_with(stop_path, retries=bcache.BCACHE_RETRIES) + + +# vi: ts=4 expandtab syntax=python diff --git a/tests/unittests/test_clear_holders.py b/tests/unittests/test_clear_holders.py index 10ea1abc..e4d8f8d6 100644 --- a/tests/unittests/test_clear_holders.py +++ b/tests/unittests/test_clear_holders.py @@ -1,9 +1,9 @@ # This file is part of curtin. See LICENSE file for copyright and license info. -import errno import mock import os import textwrap +import uuid from curtin.block import clear_holders from curtin.util import ProcessExecutionError @@ -89,30 +89,6 @@ class TestClearHolders(CiTestCase): self.assertEqual(res, uuid) mock_block.sysfs_to_devpath.assert_called_with(self.test_syspath) - @mock.patch('curtin.block.clear_holders.block') - @mock.patch('curtin.block.clear_holders.os') - def test_get_bcache_using_dev(self, mock_os, mock_block): - """Ensure that get_bcache_using_dev works""" - fake_bcache = '/sys/fs/bcache/fake' - mock_os.path.join.side_effect = os.path.join - mock_block.sys_block_path.return_value = self.test_syspath - mock_os.path.realpath.return_value = fake_bcache - - bcache_dir = clear_holders.get_bcache_using_dev(self.test_blockdev) - mock_os.path.realpath.assert_called_with(self.test_syspath + - '/bcache/cache') - self.assertEqual(bcache_dir, fake_bcache) - - @mock.patch('curtin.block.clear_holders.os') - @mock.patch('curtin.block.clear_holders.block') - def test_get_bcache_sys_path(self, mock_block, mock_os): - fake_backing = '/sys/class/block/fake' - mock_block.sys_block_path.return_value = fake_backing - mock_os.path.join.side_effect = os.path.join - mock_os.path.exists.return_value = True - bcache_dir = clear_holders.get_bcache_sys_path("/dev/fake") - self.assertEqual(bcache_dir, fake_backing + "/bcache") - @mock.patch('curtin.block.clear_holders.get_dmsetup_uuid') @mock.patch('curtin.block.clear_holders.block') def test_differentiate_lvm_and_crypt( @@ -133,264 +109,68 @@ class TestClearHolders(CiTestCase): mock_block.path_to_kname.assert_called_with(self.test_syspath) mock_get_dmsetup_uuid.assert_called_with(self.test_syspath) - @mock.patch('curtin.block.clear_holders.block') - @mock.patch('curtin.block.clear_holders.udev.udevadm_settle') - @mock.patch('curtin.block.clear_holders.get_bcache_sys_path') - @mock.patch('curtin.block.clear_holders.util') @mock.patch('curtin.block.clear_holders.os') - @mock.patch('curtin.block.clear_holders.LOG') - @mock.patch('curtin.block.clear_holders.get_bcache_using_dev') - def test_shutdown_bcache(self, mock_get_bcache, mock_log, mock_os, - mock_util, mock_get_bcache_block, - mock_udevadm_settle, mock_block): + @mock.patch('curtin.block.clear_holders.util') + @mock.patch('curtin.block.clear_holders.udev') + @mock.patch('curtin.block.clear_holders.block') + @mock.patch('curtin.block.clear_holders.bcache') + def test_shutdown_bcache(self, m_bcache, m_block, m_udev, m_util, m_os): """test clear_holders.shutdown_bcache""" - # - # pass in a sysfs path to a bcache block device, - # determine the bcache cset it is part of (or not) - # 1) stop the cset device (if it's enabled) - # 2) wait on cset to be removed if it was present - # 3) stop the block device (if it's still present after stopping cset) - # 4) wait on bcache block device to be removed - # - device = self.test_syspath - mock_block.sys_block_path.return_value = self.test_blockdev - bcache_cset_uuid = 'c08ae789-a964-46fb-a66e-650f0ae78f94' - - mock_os.path.exists.return_value = True - mock_os.path.join.side_effect = os.path.join - # os.path.realpath on symlink of /sys/class/block/null/bcache/cache -> - # to /sys/fs/bcache/cset_UUID - mock_get_bcache.return_value = '/sys/fs/bcache/' + bcache_cset_uuid - mock_get_bcache_block.return_value = device + '/bcache' + backing_dev = 'backing1' + backing_sys = '/sys/class/block/%s' % backing_dev + m_block.sysfs_to_devpath.return_value = self.test_blockdev + cset_uuid = str(uuid.uuid4()) + + def my_sysblock(p, strict=False): + return '/sys/class/block/%s' % os.path.basename(p) + + def my_bsys(p, strict=False): + return my_sysblock(p, strict=strict) + '/bcache' + + m_bcache.sysfs_path.side_effect = my_bsys + m_os.path.join.side_effect = os.path.join + m_os.listdir.return_value = [backing_dev] + m_os.path.exists.return_value = True + m_bcache.get_attached_cacheset.return_value = cset_uuid + m_block.path_to_kname.return_value = self.test_blockdev + m_bcache.get_backing_device.return_value = backing_sys + '/bcache' + m_bcache.get_cacheset_members.return_value = [] clear_holders.shutdown_bcache(device) - mock_get_bcache.assert_called_with(device, strict=False) - mock_get_bcache_block.assert_called_with(device, strict=False) - - self.assertTrue(mock_log.info.called) - self.assertFalse(mock_log.warn.called) - mock_util.wait_for_removal.assert_has_calls([ - mock.call('/sys/fs/bcache/' + bcache_cset_uuid, - retries=self.remove_retries), - mock.call(device, retries=self.remove_retries)]) - - mock_util.write_file.assert_has_calls([ - mock.call('/sys/fs/bcache/%s/stop' % bcache_cset_uuid, - '1', mode=None), - mock.call(device + '/bcache/stop', - '1', mode=None)]) - - @mock.patch('curtin.block.clear_holders.get_bcache_sys_path') - @mock.patch('curtin.block.clear_holders.util') - @mock.patch('curtin.block.clear_holders.os') - @mock.patch('curtin.block.clear_holders.LOG') - @mock.patch('curtin.block.clear_holders.get_bcache_using_dev') - def test_shutdown_bcache_non_sysfs_device(self, mock_get_bcache, mock_log, - mock_os, mock_util, - mock_get_bcache_block): - with self.assertRaises(ValueError): - clear_holders.shutdown_bcache(self.test_blockdev) - - self.assertEqual(0, len(mock_get_bcache.call_args_list)) - self.assertEqual(0, len(mock_log.call_args_list)) - self.assertEqual(0, len(mock_os.call_args_list)) - self.assertEqual(0, len(mock_util.call_args_list)) - self.assertEqual(0, len(mock_get_bcache_block.call_args_list)) - - @mock.patch('curtin.block.clear_holders.block') - @mock.patch('curtin.block.clear_holders.get_bcache_sys_path') - @mock.patch('curtin.block.clear_holders.util') - @mock.patch('curtin.block.clear_holders.os') - @mock.patch('curtin.block.clear_holders.LOG') - @mock.patch('curtin.block.clear_holders.get_bcache_using_dev') - def test_shutdown_bcache_no_device(self, mock_get_bcache, mock_log, - mock_os, mock_util, - mock_get_bcache_block, mock_block): - mock_block.sysfs_to_devpath.return_value = self.test_blockdev - mock_os.path.exists.return_value = False - - clear_holders.shutdown_bcache(self.test_syspath) - - self.assertEqual(3, len(mock_log.info.call_args_list)) - self.assertEqual(1, len(mock_os.path.exists.call_args_list)) - self.assertEqual(0, len(mock_get_bcache.call_args_list)) - self.assertEqual(0, len(mock_util.call_args_list)) - self.assertEqual(0, len(mock_get_bcache_block.call_args_list)) - - @mock.patch('curtin.block.clear_holders.block') - @mock.patch('curtin.block.clear_holders.get_bcache_sys_path') - @mock.patch('curtin.block.clear_holders.util') - @mock.patch('curtin.block.clear_holders.os') - @mock.patch('curtin.block.clear_holders.LOG') - @mock.patch('curtin.block.clear_holders.get_bcache_using_dev') - def test_shutdown_bcache_no_cset(self, mock_get_bcache, mock_log, - mock_os, mock_util, - mock_get_bcache_block, mock_block): - mock_block.sysfs_to_devpath.return_value = self.test_blockdev - mock_os.path.exists.side_effect = iter([ - True, # backing device exists - False, # cset device not present (already removed) - True, # backing device (still) exists + # 1. wipe the bcache device contents + m_block.wipe_volume.assert_called_with(self.test_blockdev, + mode='superblock', + exclusive=False, + strict=True) + # 2. extract the backing device + m_bcache.get_backing_device.assert_called_with(self.test_blockdev) + m_bcache.sysfs_path.assert_has_calls([ + mock.call(self.test_syspath, strict=False), ]) - mock_get_bcache.return_value = '/sys/fs/bcache/fake' - mock_get_bcache_block.return_value = self.test_syspath + '/bcache' - mock_os.path.join.side_effect = os.path.join - - clear_holders.shutdown_bcache(self.test_syspath) - self.assertEqual(4, len(mock_log.info.call_args_list)) - self.assertEqual(3, len(mock_os.path.exists.call_args_list)) - self.assertEqual(1, len(mock_get_bcache.call_args_list)) - self.assertEqual(1, len(mock_get_bcache_block.call_args_list)) - self.assertEqual(1, len(mock_util.write_file.call_args_list)) - self.assertEqual(2, len(mock_util.wait_for_removal.call_args_list)) - - mock_get_bcache.assert_called_with(self.test_syspath, strict=False) - mock_get_bcache_block.assert_called_with(self.test_syspath, - strict=False) - mock_util.write_file.assert_called_with( - self.test_syspath + '/bcache/stop', '1', mode=None) - retries = self.remove_retries - mock_util.wait_for_removal.assert_has_calls([ - mock.call(self.test_syspath, retries=retries), - mock.call(self.test_syspath + '/bcache', retries=retries)]) + # 3. extract the cacheset uuid + m_bcache.get_attached_cacheset.assert_called_with(device) - @mock.patch('curtin.block.clear_holders.block') - @mock.patch('curtin.block.clear_holders.udev.udevadm_settle') - @mock.patch('curtin.block.clear_holders.get_bcache_sys_path') - @mock.patch('curtin.block.clear_holders.util') - @mock.patch('curtin.block.clear_holders.os') - @mock.patch('curtin.block.clear_holders.LOG') - @mock.patch('curtin.block.clear_holders.get_bcache_using_dev') - def test_shutdown_bcache_delete_cset_and_backing(self, mock_get_bcache, - mock_log, mock_os, - mock_util, - mock_get_bcache_block, - mock_udevadm_settle, - mock_block): - mock_block.sysfs_to_devpath.return_value = self.test_blockdev - mock_os.path.exists.side_effect = iter([ - True, # backing device exists - True, # cset device not present (already removed) - True, # backing device (still) exists - ]) - cset = '/sys/fs/bcache/fake' - mock_get_bcache.return_value = cset - mock_get_bcache_block.return_value = self.test_syspath + '/bcache' - mock_os.path.join.side_effect = os.path.join + # 4. stop the cacheset + m_bcache.stop_cacheset.assert_called_with(cset_uuid) - clear_holders.shutdown_bcache(self.test_syspath) + # 5. stop the bcacheN device + m_bcache.stop_device.assert_any_call(my_bsys(device)) - self.assertEqual(4, len(mock_log.info.call_args_list)) - self.assertEqual(3, len(mock_os.path.exists.call_args_list)) - self.assertEqual(1, len(mock_get_bcache.call_args_list)) - self.assertEqual(1, len(mock_get_bcache_block.call_args_list)) - self.assertEqual(2, len(mock_util.write_file.call_args_list)) - self.assertEqual(3, len(mock_util.wait_for_removal.call_args_list)) - - mock_get_bcache.assert_called_with(self.test_syspath, strict=False) - mock_get_bcache_block.assert_called_with(self.test_syspath, - strict=False) - mock_util.write_file.assert_has_calls([ - mock.call(cset + '/stop', '1', mode=None), - mock.call(self.test_syspath + '/bcache/stop', '1', mode=None)]) - mock_util.wait_for_removal.assert_has_calls([ - mock.call(cset, retries=self.remove_retries), - mock.call(self.test_syspath, retries=self.remove_retries) - ]) + def test_shutdown_bcache_non_sysfs_device(self): + """ raises ValueError if called on non-bcache device.""" + with self.assertRaises(ValueError): + clear_holders.shutdown_bcache(self.test_blockdev) - @mock.patch('curtin.block.clear_holders.block') - @mock.patch('curtin.block.clear_holders.udev.udevadm_settle') - @mock.patch('curtin.block.clear_holders.get_bcache_sys_path') - @mock.patch('curtin.block.clear_holders.util') @mock.patch('curtin.block.clear_holders.os') - @mock.patch('curtin.block.clear_holders.LOG') - @mock.patch('curtin.block.clear_holders.get_bcache_using_dev') - def test_shutdown_bcache_delete_cset_no_backing(self, mock_get_bcache, - mock_log, mock_os, - mock_util, - mock_get_bcache_block, - mock_udevadm_settle, - mock_block): - mock_block.sysfs_to_devpath.return_value = self.test_blockdev - mock_os.path.exists.side_effect = iter([ - True, # backing device exists - True, # cset device not present (already removed) - False, # backing device is removed with cset - ]) - cset = '/sys/fs/bcache/fake' - mock_get_bcache.return_value = cset - mock_get_bcache_block.return_value = self.test_syspath + '/bcache' - mock_os.path.join.side_effect = os.path.join - - clear_holders.shutdown_bcache(self.test_syspath) - - self.assertEqual(4, len(mock_log.info.call_args_list)) - self.assertEqual(3, len(mock_os.path.exists.call_args_list)) - self.assertEqual(1, len(mock_get_bcache.call_args_list)) - self.assertEqual(1, len(mock_get_bcache_block.call_args_list)) - self.assertEqual(1, len(mock_util.write_file.call_args_list)) - self.assertEqual(1, len(mock_util.wait_for_removal.call_args_list)) - - mock_get_bcache.assert_called_with(self.test_syspath, strict=False) - mock_util.write_file.assert_has_calls([ - mock.call(cset + '/stop', '1', mode=None), - ]) - mock_util.wait_for_removal.assert_has_calls([ - mock.call(cset, retries=self.remove_retries) - ]) - - # test bcache shutdown with 'stop' sysfs write failure @mock.patch('curtin.block.clear_holders.block') - @mock.patch('curtin.block.wipe_volume') - @mock.patch('curtin.block.clear_holders.udev.udevadm_settle') - @mock.patch('curtin.block.clear_holders.get_bcache_sys_path') - @mock.patch('curtin.block.clear_holders.util') - @mock.patch('curtin.block.clear_holders.os') - @mock.patch('curtin.block.clear_holders.LOG') - @mock.patch('curtin.block.clear_holders.get_bcache_using_dev') - def test_shutdown_bcache_stop_sysfs_write_fails(self, mock_get_bcache, - mock_log, mock_os, - mock_util, - mock_get_bcache_block, - mock_udevadm_settle, - mock_wipe, - mock_block): - """Test writes sysfs write failures pass if file not present""" - mock_block.sysfs_to_devpath.return_value = self.test_blockdev - mock_os.path.exists.side_effect = iter([ - True, # backing device exists - True, # cset device not present (already removed) - False, # backing device is removed with cset - False, # bcache/stop sysfs is missing (already removed) - ]) - cset = '/sys/fs/bcache/fake' - mock_get_bcache.return_value = cset - mock_get_bcache_block.return_value = self.test_syspath + '/bcache' - mock_os.path.join.side_effect = os.path.join - - # make writes to sysfs fail - mock_util.write_file.side_effect = IOError(errno.ENOENT, - "File not found") - + def test_shutdown_bcache_no_device(self, m_block, m_os): + """ shutdown_bcache does nothing if target device is not present.""" + m_os.path.exists.return_value = False clear_holders.shutdown_bcache(self.test_syspath) - - self.assertEqual(4, len(mock_log.info.call_args_list)) - self.assertEqual(3, len(mock_os.path.exists.call_args_list)) - self.assertEqual(1, len(mock_get_bcache.call_args_list)) - self.assertEqual(1, len(mock_get_bcache_block.call_args_list)) - self.assertEqual(1, len(mock_util.write_file.call_args_list)) - self.assertEqual(1, len(mock_util.wait_for_removal.call_args_list)) - - mock_get_bcache.assert_called_with(self.test_syspath, strict=False) - mock_util.write_file.assert_has_calls([ - mock.call(cset + '/stop', '1', mode=None), - ]) - mock_util.wait_for_removal.assert_has_calls([ - mock.call(cset, retries=self.remove_retries) - ]) + m_block.wipe_volume.assert_not_called() @mock.patch('curtin.block.quick_zero') @mock.patch('curtin.block.clear_holders.LOG') @@ -757,41 +537,6 @@ class TestClearHolders(CiTestCase): for tree, result in test_trees_and_results: self.assertEqual(clear_holders.format_holders_tree(tree), result) - @mock.patch('curtin.block.clear_holders.util.write_file') - def test_maybe_stop_bcache_device_raises_errors(self, m_write_file): - """Non-IO/OS exceptions are raised by maybe_stop_bcache_device.""" - m_write_file.side_effect = ValueError('Crazy Value Error') - with self.assertRaises(ValueError) as cm: - clear_holders.maybe_stop_bcache_device('does/not/matter') - self.assertEqual('Crazy Value Error', str(cm.exception)) - self.assertEqual( - mock.call('does/not/matter/stop', '1', mode=None), - m_write_file.call_args) - - @mock.patch('curtin.block.clear_holders.LOG') - @mock.patch('curtin.block.clear_holders.util.write_file') - def test_maybe_stop_bcache_device_handles_oserror(self, m_write_file, - m_log): - """When OSError.NOENT is raised, log the condition and move on.""" - m_write_file.side_effect = OSError(errno.ENOENT, 'Expected oserror') - clear_holders.maybe_stop_bcache_device('does/not/matter') - self.assertEqual( - 'Error writing to bcache stop file %s, device removed: %s', - m_log.debug.call_args[0][0]) - self.assertEqual('does/not/matter/stop', m_log.debug.call_args[0][1]) - - @mock.patch('curtin.block.clear_holders.LOG') - @mock.patch('curtin.block.clear_holders.util.write_file') - def test_maybe_stop_bcache_device_handles_ioerror(self, m_write_file, - m_log): - """When IOError.NOENT is raised, log the condition and move on.""" - m_write_file.side_effect = IOError(errno.ENOENT, 'Expected ioerror') - clear_holders.maybe_stop_bcache_device('does/not/matter') - self.assertEqual( - 'Error writing to bcache stop file %s, device removed: %s', - m_log.debug.call_args[0][0]) - self.assertEqual('does/not/matter/stop', m_log.debug.call_args[0][1]) - def test_get_holder_types(self): """test clear_holders.get_holder_types""" test_trees_and_results = [ diff --git a/tests/vmtests/test_bcache_ceph.py b/tests/vmtests/test_bcache_ceph.py new file mode 100644 index 00000000..30820bd3 --- /dev/null +++ b/tests/vmtests/test_bcache_ceph.py @@ -0,0 +1,93 @@ +# This file is part of curtin. See LICENSE file for copyright and license info. + +from . import VMBaseClass, skip_if_flag +from .releases import base_vm_classes as relbase + +import glob +import textwrap + + +class TestBcacheCeph(VMBaseClass): + arch_skip = [ + "s390x", # lp:1565029 + ] + test_type = 'storage' + conf_file = "examples/tests/bcache-ceph-nvme.yaml" + nr_cpus = 2 + uefi = True + dirty_disks = True + extra_disks = ['20G', '20G', '20G', '20G', '20G', '20G', '20G', '20G'] + nvme_disks = ['20G', '20G'] + extra_collect_scripts = [textwrap.dedent(""" + cd OUTPUT_COLLECT_D + ls /sys/fs/bcache/ > bcache_ls + ls -al /dev/bcache/by-uuid/ > ls_al_dev_bcache_by_uuid + ls -al /dev/bcache/by-label/ > ls_al_dev_bcache_by_label + ls -al /sys/class/block/bcache* > ls_al_sys_block_bcache + for bcache in /sys/class/block/bcache*; do + for link in $(find ${bcache}/slaves -type l); do + kname=$(basename $(readlink $link)) + outfile="bcache-super-show.$kname" + bcache-super-show /dev/${kname} > $outfile + done + done + exit 0 + """)] + + @skip_if_flag('expected_failure') + def test_bcache_output_files_exist(self): + self.output_files_exist([ + "bcache-super-show.vda1", + "bcache-super-show.vdc", + "bcache-super-show.vdd", + "bcache-super-show.vde", + "bcache-super-show.vdf", + "bcache-super-show.vdh", + "bcache-super-show.nvme0n1p2", + "bcache-super-show.nvme1n1p2"]) + + @skip_if_flag('expected_failure') + def test_bcache_devices_cset_found(self): + sblocks = glob.glob("%s/bcache-super-show.*") + for superblock in sblocks: + bcache_cset_uuid = None + for line in self.load_collect_file(superblock).splitlines(): + if line != "" and line.split()[0] == "cset.uuid": + bcache_cset_uuid = line.split()[-1].rstrip() + self.assertIsNotNone(bcache_cset_uuid) + self.assertTrue(bcache_cset_uuid in + self.load_collect_file("bcache_ls").splitlines()) + + +class TrustyTestBcacheCeph(relbase.trusty, TestBcacheCeph): + __test__ = False # covered by test_raid5_bcache + + +class TrustyHWEXTestBcacheCeph(relbase.trusty_hwe_x, TestBcacheCeph): + __test__ = False # covered by test_raid5_bcache + + +class XenialGATestBcacheCeph(relbase.xenial_ga, TestBcacheCeph): + __test__ = True + + +class XenialHWETestBcacheCeph(relbase.xenial_hwe, TestBcacheCeph): + __test__ = True + + +class XenialEdgeTestBcacheCeph(relbase.xenial_edge, TestBcacheCeph): + __test__ = True + + +class BionicTestBcacheCeph(relbase.bionic, TestBcacheCeph): + __test__ = True + + +class CosmicTestBcacheCeph(relbase.cosmic, TestBcacheCeph): + __test__ = True + + +class DiscoTestBcacheCeph(relbase.disco, TestBcacheCeph): + __test__ = True + +# vi: ts=4 expandtab syntax=python |
