From f1aab1777b1e40eef9d51e5821575d10b0ab6fe3 Mon Sep 17 00:00:00 2001 From: Rafael Fonseca Date: Mon, 16 Oct 2017 15:08:55 +0200 Subject: [PATCH] WIP: #5: rewrite zipl_helper in python. Included some unit tests to make sure the helper works as expected. --- zipl/src/zipl_helper.device-mapper.py | 563 ++++++++++++++++++++++ zipl/tests/test_zipl_helper.py | 663 ++++++++++++++++++++++++++ 2 files changed, 1226 insertions(+) create mode 100644 zipl/src/zipl_helper.device-mapper.py create mode 100644 zipl/tests/test_zipl_helper.py diff --git a/zipl/src/zipl_helper.device-mapper.py b/zipl/src/zipl_helper.device-mapper.py new file mode 100644 index 000000000..9ae326f48 --- /dev/null +++ b/zipl/src/zipl_helper.device-mapper.py @@ -0,0 +1,563 @@ +#!/usr/bin/env python3 +# +# zipl_helper.device-mapper: print zipl parameters for a device-mapper device +# +# Copyright IBM Corp. 2009, 2017 +# +# s390-tools is free software; you can redistribute it and/or modify +# it under the terms of the MIT license. See LICENSE for details. +# +# Depending on the name by which the script is called, it serves one of two +# purposes: +# +# 1. Usage: zipl_helper.device-mapper or +# +# +# This tool attempts to obtain zipl parameters for a target directory or +# partition located on a device-mapper device. It assumes that the +# device-mapper table for this device conforms to the following rules: +# - directory is located on a device consisting of a single device-mapper +# target +# - only linear, mirror and multipath targets are supported +# - supported physical device types are DASD and SCSI devices +# - all of the device which contains the directory must be located on a single +# physical device (which may be mirrored or accessed through a multipath +# target) +# - any mirror in the device-mapper setup must include block 0 of the +# physical device +# +# 2. Usage: chreipl_helper.device-mapper +# +# This tool identifies the physical device which contains the specified +# device-mapper target devices. If the physical device was found, its +# major:minor parameters are printed. Otherwise, the script exits with an +# error message and a non-zero return code. +# + +import re +import os +import sys +import stat +import locale +import logging +import subprocess +from itertools import islice +from collections import namedtuple + +logger = logging.getLogger(__name__) + +SECTOR_SIZE = 512 +DASD_PARTN_MASK = 0x03 +SCSI_PARTN_MASK = 0x0f + +TOOLS = ['dmsetup', 'dasdview', 'blockdev'] + +TARGET_TYPES = ["linear", "mirror", "multipath"] + +Target = namedtuple('Target', ['start', 'length', 'type', 'data']) +LinearData = namedtuple('LinearData', ['major', 'minor', 'start']) +MirrorData = namedtuple('MirrorData', ['major', 'minor', 'start']) +MultipathData = namedtuple('MultipathData', ['major', 'minor']) + +UNRECOG_TABLE_MSG = "Unrecognized device-mapper table format for device '%s'" +UNSUPPORTED_BLOCK_MSG = "Unsupported setup: Block 0 is not mirrored in device '%s'" + +def die(msg, *args, **kwargs): + logger.critical(msg, *args, **kwargs) + sys.exit(1) + + +def run_proc(args): + subprocess.run(args, check=True, stdout=subprocess.PIPE, + stderr=subprocess.PIPE, universal_newlines=True) + + +def get_device_name(major, minor): + partitions_match = re.compile(r'^\s*(\d+)\s+(\d+)\s+\d+\s+(\S+)\s*$') + with open('/proc/partitions', 'r') as f: + for line in f: + res = re.match(partitions_match, line) + if not res: + continue + if int(res.group(1)) == major and int(res.group(2)) == minor: + return res.group(3) + return "{}:{}".format(major, minor) + + +# args: : +# Returns [major, minor, start_sector] +def get_linear_data(dev_name, args): + res = re.match(r'^(\d+):(\d+)\s+(\d+)$', args) + if not res: + die(UNRECOG_TABLE_MSG, dev_name) + return LinearData(*map(int, res.groups())) + + +# args: <#log_args> ... \ +# <#devs> ... \ +# <#features> ... +# Returns [MirrorData1 ... MirrorDataN] +def get_mirror_data(dev_name, args): + try: + it = iter(args.split()) + next(it) # + nlogs = int(next(it)) # <#log_args> + it = islice(it, nlogs, None) # log_args* + ndevs = int(next(it)) # <#devs> + + data = [] + offset = None + major_minor_match = re.compile(r'^(\d+):(\d+)$') + for _ in range(ndevs): + name, doffset = next(it), int(next(it)) # + res = re.match(major_minor_match, name) + if not res: + die(UNRECOG_TABLE_MSG, dev_name) + data.append(MirrorData(*map(int, res.groups()), doffset)) + offset = doffset if offset is None else offset + if doffset != offset: + die("Unsupported setup: Mirror target on device '%s' " + "contains entries with varying sector offsets", dev_name) + + nfeatures = int(next(it)) # <#features> + if sum(1 for _ in it) != nfeatures: + die(UNRECOG_TABLE_MSG, dev_name) + + return data + except StopIteration: + die(UNRECOG_TABLE_MSG, dev_name) + + +def get_multipath_status(device): + dev = '/dev/{}'.format(device) + try: + res = run_proc(['dmsetup', 'status', dev]) + except subprocess.CalledProcessError as e: + die("No paths found for '%s': 'dmsetup status' failed", device) + + failed = 0 + states = {} + for line in res.stdout.splitlines(): + # sample output (single line): + # 0 67108864 multipath \ + # 2 0 0 \ + # 0 \ + # 2 2 \ + # E 0 \ + # 2 2 \ + # 8:16 F 1 \ + # 0 1 \ + # 8:0 F 1 \ + # 0 1 \ + # A 0 \ + # 2 2 \ + # 8:32 A 0 \ + # 0 1 \ + # 8:48 A 0 \ + # 0 1 + it = iter(line.split()) + try: + start = int(next(it)) + length = int(next(it)) + dtype = next(it) + + if dtype != "multipath": + continue + + cnt = int(next(it)) # <#multipath_feature_args> + it = islice(it, cnt, None) # multipath_feature_args* + cnt = int(next(it)) # <#handler_status_args> + it = islice(it, cnt, None) # handler_status_args* + + ngr = int(next(it)) + ign = int(next(it)) + for i in range(ngr): + # group_state: D(isabled), A(ctive), or E(nabled) + state = next(it) + cnt = int(next(it)) # <#ps_status_args> + it = islice(it, cnt, None) # ps_status_args* + npaths = int(next(it)) # <#paths> + nsa = int(next(it)) # <#selector_args> + for p in range(npaths): + node = next(it) + active = next(it) + fail_cnt = int(next(it)) + states[node] = active + failed += 1 if active != 'A' else 0 + it = islice(it, nsa, None) # selector_args* + except StopIteration: + continue + + if len(states) == 0: + die("No paths found for '%s'", device) + + if failed == len(states): + die("All paths for '%s' failed", device) + elif failed > 0: + logger.warning("There are one or more failed paths for device '%s'", + device) + + return states + + +# args: <#features> ... +# <#handlers> ... +# <#path_groups> +# <#selector_args> ... +# <#paths> <#path_args> +# ... +# ... +# ... +# Returns: [MultipathData1, ..., MultipathDataN] +def get_multipath_data(dev_name, args): + status = get_multipath_status(dev_name) + major_minor_match = re.compile(r'(\d+):(\d+)') + + data = [] + it = iter(args.split()) + try: + nfeatures = int(next(it)) # <#features> + it = islice(it, nfeatures, None) # features* + nhandlers = int(next(it)) # <#handlers> + it = islice(it, nhandlers, None) # handlers* + pgroups = int(next(it)) # <#pathgroups> + next(it) # pathgroup + for _ in range(pgroups): + next(it) # pathselector + cnt = int(next(it)) # <#selectorargs> + it = islice(it, cnt, None) # selectorargs* + npaths = int(next(it)) + np_args = int(next(it)) + for _ in range(npaths): + p = next(it) + res = re.match(major_minor_match, p) + if not res: + die(UNRECOG_TABLE_MSG, dev_name) + major, minor = map(int, res.groups()) + # FIXME: is it possible for 'p' to not be in 'status'? + if status[p] == 'A': + data.append(MultipathData(major, minor)) + # Remove deviceargs + it = islice(it, np_args, None) + except StopIteration: + die(UNRECOG_TABLE_MSG, dev_name) + + if len(data) == 0: + die(UNRECOG_TABLE_MSG, dev_name) + + return data + + +# Returns: [target1, ..., targetN] +# target: [start, length, type, data] +# data: linear_data|mirror_data|multipath_data +def get_table(major, minor): + dev_name = get_device_name(major, minor) + + try: + res = run_proc(["dmsetup", "table", "-j", str(major), "-m", str(minor)]) + except subprocess.CalledProcessError as e: + return [] + + table = [] + handle_data = dict(zip(TARGET_TYPES, [get_linear_data, get_mirror_data, + get_multipath_data])) + target_match = re.compile(r'^(\d+)\s+(\d+)\s+(\S+)\s+(\S.*)$') + for line in res.stdout.splitlines(): + r = re.match(target_match, line) + if not r: + die(UNRECOG_TABLE_MSG, dev_name) + start, length, target_type, args = r.groups() + + if target_type not in TARGET_TYPES: + die("Unrecognized setup: Unsupported device-mapper target " + "type '%s' for device '%s'", target_type, dev_name) + + data = handle_data[target_type](dev_name, args) + table.append(Target(int(start), int(length), target_type, data)) + + return table + + +def get_target_major_minor(target): + data = target.data if target.type == "linear" else target.data[0] + return data.major, data.minor + + +def get_target_start(target): + if target.type == "linear": + data = target.data + elif target.type == "mirror": + data = target.data[0] + else: + return 0 + return data.start + + +# Returns (phy_major, phy_minor, phy_offset, target_list) +# target_list: [TargetData1, ..., TargetDataN] +def get_physical_device(major, minor, directory=None): + table = get_table(major, minor) + if len(table) == 0: + die("Could not retrieve device-mapper information for " + "device '%s'", get_device_name(major, minor)) + + # Filesystem must be on a single dm target + if len(table) != 1: + die("Unsupported setup: Directory '%s' is located on a " + "multi-target device-mapper device", directory) + + target = table[0] + target_list = [(major, minor, target)] + start = target.start + length = target.length + while True: + # Convert fs_start to offset on parent dm device + start += get_target_start(target) + mmaj, mmin = get_target_major_minor(target) + table = get_table(mmaj, mmin) + if len(table) == 0: # found non-dm device + return (mmaj, mmin, start, target_list) + + def in_range(target, start, length): + return ((target.start + target.length - 1) >= start) and \ + (target.start <= (start + length - 1)) + + # Get target in parent table which contains filesystem + # We are interested only in targets between start and start+length-1 + table = list(filter(lambda t: in_range(t, start, length), table)) + if len(table) != 1: + die("Unsupported setup: Could not map directory '%s' to a " + "single physical device", directory) + target = table[0] + target_list.append((mmaj, mmin, target)) + # Convert fs_start to offset on parent target + start -= target.start + + +def get_major_minor(filename): + try: + dev = os.stat(filename).st_dev + return os.major(dev), os.minor(dev) + except: + die("Could not stat '%s'", filename) + + +def get_physical_device_dir(directory): + major, minor = get_major_minor(directory) + return get_physical_device(major, minor, directory=directory) + + +# Returns (type, cylinders, heads, sectors) +def get_dasd_info(device): + cylinder_match = re.compile(r'^number of cylinders.*\s(\d+)\s*$') + heads_match = re.compile(r'^tracks per cylinder.*\s(\d+)\s*$') + sectors_match = re.compile(r'^blocks per track.*\s(\d+)\s*$') + type_match = re.compile(r'^type\s+:\s+(\S+)\s*$') + format_match = re.compile(r'^format.*\s+dec\s(\d+)\s') + + cyl, heads, sectors, disk_type, dformat = None, None, None, None, None + try: + res = run_proc(['dasdview', '-x', device]) + for line in res.stdout.splitlines(): + r = re.match(cylinder_match, line) + if r: + cyl = int(r.group(1)) + continue + r = re.match(heads_match, line) + if r: + heads = int(r.group(1)) + continue + r = re.match(sectors_match, line) + if r: + sectors = int(r.group(1)) + continue + r = re.match(type_match, line) + if r: + disk_type = r.group(1) + continue + r = re.match(format_match, line) + if r: + dformat = int(r.group(1)) + continue + except subprocess.CalledProcessError as e: + return None + + if None in [cyl, heads, sectors, disk_type, dformat]: + return None + + if disk_type == "ECKD": + disk_type = "LDL" if dformat == 1 else "CDL" + + return (disk_type, cyl, heads, sectors) + + +def get_partition_start(major, minor): + ddir = '/sys/dev/block/{}:{}'.format(major, minor) + if not os.path.isdir(ddir): + die("Could not determine partition start for '%s'", + get_device_name(major, minor)) + + try: + with open('{}/start'.format(ddir), 'r') as f: + return int(f.read()) + except FileNotFoundError: + return 0 + + +def is_dasd(ttype): + return ttype in ["CDL", "LDL", "FBA"] + + +# Return (major, minor) of the base device on which the partition is located +def get_partition_base(dtype, major, minor): + return (major, minor & (~DASD_PARTN_MASK if is_dasd(dtype) else ~SCSI_PARTN_MASK)) + + +def get_blocksize(device): + try: + # FIXME: replace this by proper system call + r = run_proc(['blockdev', '--getss', device]) + return int(r.stdout) + except subprocess.CalledProcessError as e: + return None + + +def create_temp_device_node(dtype, major, minor): + for num in range(100): + name = '/dev/zipl-dm-temp-{:02d}'.format(num) + if os.path.exists(name): + continue + os.mknod(name, dtype|0o600, os.makedev(major, minor)) + return name + die("Could not create temporary device node in '/dev'") + + +# Return (type, blocksize, geometry, bootsectors, partstart) for device +def get_device_characteristics(major, minor): + name = get_device_name(major, minor) + dev = create_temp_device_node(stat.S_IFBLK, major, minor) + try: + blocksize = get_blocksize(dev) + if blocksize is None: + die("Could not get block size for %s", name) + info = get_dasd_info(dev) + geometry = None + if info is not None: + dtype, cyl, heads, sectors = info + geometry = "{},{},{}".format(cyl,heads,sectors) + if dtype == "CDL": + bootsectors = blocksize * sectors / SECTOR_SIZE + elif dtype == "LDL": + bootsectors = blocksize * 2 / SECTOR_SIZE + elif dtype == "FBA": + # first block contains IPL records + bootsectors = blocksize / SECTOR_SIZE + else: # assume SCSI if get_dasd_info failed + dtype = "SCSI" + # first block contains IPL records + bootsectors = blocksize / SECTOR_SIZE + except: + raise + finally: + os.remove(dev) + + partstart = get_partition_start(major, minor) + # Convert partition start in sectors to blocks + partstart = partstart / (blocksize / SECTOR_SIZE) + return (dtype, blocksize, geometry, bootsectors, partstart) + + +def check_tools(): + try: + return sum(map(attrgetter('returncode'), + map(subprocess.run, ([c, '--version'] for c in tools)))) == 0 + except: + return False + + +# Return (major, minor) for the topmost target in the target list that maps the +# region on (bottom_major, bottom_minor) defined by start and length at offset 0 +def get_target_base(bot_major, bot_minor, start, length, target_list): + top_major, top_minor = bot_major, bot_minor + for idx, tgt in zip(range(len(target_list)-1, -1, -1), reversed(target_list)): + major, minor, target = tgt + if (target.start != 0) or (get_target_start(target) != 0) or \ + (target.length < length): + # Check for mirrorring between base device and fs device + mirror = [t for t in target_list[:idx] if t[2].type == "mirror"] + if len(mirror) > 0: + major, minor, _ = mirror[0] + die(UNSUPPORTED_BLOCK_MSG, get_device_name(major, minor)) + return top_major, top_minor + top_major, top_minor = major, minor + return top_major, top_minor + + +if __name__ == '__main__': + # Setup and use a standard locale to avoid localized scripting output + locale.setlocale(locale.LC_ALL, 'C') + + toolname=sys.argv[0] + + if toolname == "chreipl_helper.device-mapper": + if len(sys.argv) <= 1: + die("Usage: %s ", toolname) + r = re.match(r'^\s*(\d+)\s*:\s*(\d+)\s*$', sys.argv[1]) + if not r: + die("Usage: %s ", toolname) + major, minor = int(r.group(1)), int(r.group(2)) + phy_maj, phy_min, _, _ = get_physical_device(major, minor) + print("{}:{}".format(phy_maj, phy_min)) + sys.exit(0) + + if len(sys.argv) <= 1: + die("Usage: %s or ", toolname) + + check_tools() + + r = re.match(r'^\s*(\d+)\s*:\s*(\d+)\s*$', sys.argv[1]) + if r: + major, minor = int(r.group(1)), int(r.group(2)) + phy_maj, phy_min, phy_offset, target_list = \ + get_physical_device(major, minor) + else: + phy_maj, phy_min, phy_offset, target_list = \ + get_physical_device_dir(sys.argv[1]) + + phy_type, phy_blksize, phy_geometry, phy_bootsectors, phy_partstart = \ + get_device_characteristics(phy_maj, phy_min) + + if phy_partstart > 0: + # Only the partition of the physical device is mapped so only the + # physical device can provide access to the boot record + base_major, base_minor = \ + get_partition_base(phy_type, phy_maj, phy_min) + # Check for mirror + mirror = [t for t in target_list if t[2].type == "mirror"] + if len(mirror) > 0: + major, minor, _ = mirror[0] + die(UNSUPPORTED_BLOCK_MSG, get_device_name(major, minor)) + # Adjust filesystem offset + phy_offset += phy_partstart * (phy_blksize / SECTOR_SIZE) + phy_partstart = 0 + # Update device geometry + _, _, phy_geometry, _, _ = \ + get_device_characteristics(base_major, base_minor) + else: + # All of the device is mapped, so the base device is the top most dm + # device which provides access to boot sectors + base_major, base_minor = \ + get_target_base(phy_maj, phy_min, 0, phy_bootsectors, target_list) + + # Check for valid offset of filesystem + if ((phy_offset % (phy_blksize / SECTOR_SIZE)) != 0): + die("Filesystem not aligned on physical block size") + + # Print resulting information + print("targetbase={}:{}".format(base_major, base_minor)) + print("targettype={}".format(phy_type)) + if phy_geometry: + print("targetgeometry={}".format(phy_geometry)) + print("targetblocksize={}".format(phy_blksize)) + print("targetoffset={}".format(int(phy_offset / (phy_blksize / SECTOR_SIZE)))) diff --git a/zipl/tests/test_zipl_helper.py b/zipl/tests/test_zipl_helper.py new file mode 100644 index 000000000..fc5a43f66 --- /dev/null +++ b/zipl/tests/test_zipl_helper.py @@ -0,0 +1,663 @@ +import io +import sys +import unittest +import unittest.mock as mock +import src.zipl_helper as dm +from random import randint +from collections import defaultdict +from subprocess import CompletedProcess, CalledProcessError + + +@mock.patch('builtins.open', new_callable=mock.mock_open()) +class TestGetDeviceName(unittest.TestCase): + def setUp(self): + self.partitions = io.StringIO( +""" +major minor #blocks name + + 8 0 250059096 sda + 8 1 1048576 sda1 + 8 2 249009152 sda2 + 253 0 249007104 dm-0 + 253 1 52428800 dm-1 + 253 2 6066176 dm-2 + 253 3 190509056 dm-3 +""") + + def test_get_device_name(self, m): + m.return_value.__enter__.return_value = self.partitions + l = zip([(8, 0), (8, 1), (8, 2), (253, 0), (253, 1), (253, 2), (253, 3)], + ['sda', 'sda1', 'sda2', 'dm-0', 'dm-1', 'dm-2', 'dm-3']) + + for (major, minor), name in l: + with self.subTest(major=major, minor=minor, name=name): + self.assertEqual(name, dm.get_device_name(major, minor)) + m.assert_any_call('/proc/partitions', 'r') + + def test_get_device_name_not_in_file(self, m): + m.return_value.__enter__.return_value = self.partitions + self.assertEqual('9:0', dm.get_device_name(9, 0)) + + def test_get_device_name_empty_file(self, m): + m.return_value.__enter__.return_value = '' + self.assertEqual('9:1', dm.get_device_name(9, 1)) + + +class TestLinearData(unittest.TestCase): + def setUp(self): + self.devname = '/boot-new' + self.major = randint(0, 255) + self.minor = randint(0, 255) + self.start = randint(0, sys.maxsize) + + def test_get_linear_data(self): + args = '{}:{} {}'.format(self.major, self.minor, self.start) + expected = dm.LinearData(self.major, self.minor, self.start) + res = dm.get_linear_data(self.devname, args) + self.assertEqual(expected, res) + + def test_get_linear_data_extra_sep_spaces(self): + expected = dm.LinearData(self.major, self.minor, self.start) + for i in range(2, 10): + args = '{}:{}{}{}'.format(self.major, self.minor, ' ' * i, self.start) + with self.subTest(i=i): + res = dm.get_linear_data(self.devname, args) + self.assertEqual(expected, res) + + def test_get_linear_data_fails(self): + args = '{}:{} {}'.format(self.major + 1, self.minor + 1, self.start + 1) + expected = dm.LinearData(self.major, self.minor, self.start) + res = dm.get_linear_data(self.devname, args) + self.assertNotEqual(expected, res) + + def test_get_linear_data_extra_invalid_spaces(self): + args = ' {}: {} {}' + with self.assertRaises(SystemExit) as se: + dm.get_linear_data(self.devname, args) + self.assertEqual(se.exception.code, 1) + + def test_get_linear_data_invalid_input(self): + invalids = ['', ' ', ':', ': 1', '10: 1', ':2 1', '10:2', '1:', ':1', + 'invalid', 'invalid:invalid', 'invalid:invalid invalid', + '10:2 A', 'A:2 3', '10:B 3', 'A:B 3', 'A:B C', + '1.0:3 4', '10:1.2 3', '10:2 1.2', '1.0:1.1 1', '1.0:1.1 1.2'] + for invalid in invalids: + with self.subTest(invalid=invalid): + with self.assertRaises(SystemExit) as se: + dm.get_linear_data(self.devname, invalid) + self.assertEqual(se.exception.code, 1) + + +class TestMirrorData(unittest.TestCase): + def setUp(self): + self.devname = '/boot-new' + self.critical_msg = dm.UNRECOG_TABLE_MSG % self.devname + + def test_get_mirror_data(self): + args = 'log_type 1 log_arg 1 9:1 0 1 feature1' + res = dm.get_mirror_data(self.devname, args) + self.assertEqual(res, [dm.MirrorData(9, 1, 0)]) + + def test_get_mirror_data_invalid_name(self): + args = 'log_type 1 log_arg 1 dev 0 1 feature1' + with self.assertLogs(level='CRITICAL') as cm: + with self.assertRaises(SystemExit) as se: + dm.get_mirror_data(self.devname, args) + self.assertEqual(se.exception.code, 1) + self.assertEqual(cm.output, + ["CRITICAL:src.zipl_helper:%s" % self.critical_msg]) + + def test_get_mirror_data_truncated_features(self): + args = 'log_type 1 log_arg 1 9:1 0 2 feature1' + with self.assertLogs(level='CRITICAL') as cm: + with self.assertRaises(SystemExit) as se: + dm.get_mirror_data(self.devname, args) + self.assertEqual(se.exception.code, 1) + self.assertEqual(cm.output, + ["CRITICAL:src.zipl_helper:%s" % self.critical_msg]) + + +@mock.patch('src.zipl_helper.run_proc') +class TestMultipathStatus(unittest.TestCase): + def setUp(self): + self.devname = '/boot-new' + + def test_get_multipath_status(self, run_mock): + expected = {'8:16': 'A', '8:0': 'A', '8:32': 'A', '8:48': 'A'} + run_mock.return_value = CompletedProcess('', 0, + stdout='0 67108864 multipath 2 0 0 0 2 2 E 0 2 2 8:16 A 1 0 1 8:0 A 1 0 1 A 0 2 2 8:32 A 0 0 1 8:48 A 0 0 1') + self.assertEqual(dm.get_multipath_status(self.devname), expected) + + def test_get_multipath_status_2(self, run_mock): + expected = {'8:64': 'A', '8:32': 'A'} + run_mock.return_value = CompletedProcess('', 0, + stdout='0 20981760 multipath 2 0 0 0 2 1 A 0 1 2 8:64 A 0 0 1 E 0 1 2 8:32 A 0 0 1') + self.assertEqual(dm.get_multipath_status(self.devname), expected) + + def test_get_multipath_status_some_failed_warning(self, run_mock): + expected = {'8:16': 'F', '8:0': 'F', '8:32': 'A', '8:48': 'A'} + run_mock.return_value = CompletedProcess('', 0, + stdout='0 67108864 multipath 2 0 0 0 2 2 E 0 2 2 8:16 F 1 0 1 8:0 F 1 0 1 A 0 2 2 8:32 A 0 0 1 8:48 A 0 0 1') + + with self.assertLogs(level='WARNING') as cm: + dm.get_multipath_status(self.devname) + self.assertEqual(cm.output, + ["WARNING:src.zipl_helper:There are one or more " + "failed paths for device '%s'" % self.devname]) + + def test_get_multipath_status_all_failed_critical(self, run_mock): + #expected = {'8:16': 'F', '8:0': 'F', '8:32': 'F', '8:48': 'F'} + run_mock.return_value = CompletedProcess('', 0, + stdout='0 67108864 multipath 2 0 0 0 2 2 E 0 2 2 8:16 F 1 0 1 8:0 F 1 0 1 F 0 2 2 8:32 F 0 0 1 8:48 F 0 0 1') + with self.assertLogs(level='CRITICAL') as cm: + with self.assertRaises(SystemExit) as se: + dm.get_multipath_status(self.devname) + self.assertEqual(se.exception.code, 1) + self.assertEqual(cm.output, + ["CRITICAL:src.zipl_helper:All paths for '%s' failed" % + self.devname]) + + def test_get_multipath_status_empty_critical(self, run_mock): + run_mock.return_value = CompletedProcess('', 0, stdout='') + with self.assertLogs(level='CRITICAL') as cm: + with self.assertRaises(SystemExit) as se: + dm.get_multipath_status(self.devname) + self.assertEqual(se.exception.code, 1) + self.assertEqual(cm.output, + ["CRITICAL:src.zipl_helper:No paths found for '%s'" % + self.devname]) + + def test_get_multipath_status_no_paths_critical(self, run_mock): + run_mock.return_value = CompletedProcess('', 0, + stdout='0 67108864 multipath 2 0 0 0 2 2 E 0 0 0') + with self.assertLogs(level='CRITICAL') as cm: + with self.assertRaises(SystemExit) as se: + dm.get_multipath_status(self.devname) + self.assertEqual(se.exception.code, 1) + self.assertEqual(cm.output, + ["CRITICAL:src.zipl_helper:No paths found for '%s'" % + self.devname]) + + def test_get_multipath_status_truncated_input_critical(self, run_mock): + run_mock.return_value = CompletedProcess('', 0, + stdout='0 67108864 multipath 2 0 0 0 2 2 E 0 2 2') + with self.assertLogs(level='CRITICAL') as cm: + with self.assertRaises(SystemExit) as se: + dm.get_multipath_status(self.devname) + self.assertEqual(se.exception.code, 1) + self.assertEqual(cm.output, + ["CRITICAL:src.zipl_helper:No paths found for '%s'" % + self.devname]) + + def test_get_multipath_status_dmsetup_failed_critical(self, run_mock): + run_mock.side_effect = CalledProcessError(1, 'dmsetup') + with self.assertLogs(level='CRITICAL') as cm: + with self.assertRaises(SystemExit) as se: + dm.get_multipath_status(self.devname) + self.assertEqual(se.exception.code, 1) + self.assertEqual(cm.output, + ["CRITICAL:src.zipl_helper:No paths found for '%s': " + "'dmsetup status' failed" % self.devname]) + + +def constant_factory(value): + return lambda: value + + +@mock.patch('src.zipl_helper.get_multipath_status') +class TestMultipathData(unittest.TestCase): + + def setUp(self): + self.devname = '/boot-new' + self.critical_msg = dm.UNRECOG_TABLE_MSG % self.devname + + def test_get_multipath_data_no_features(self, status_mock): + status_mock.return_value = defaultdict(constant_factory('A')) + args = "0 0 1 1 pref-path 0 1 1 8:16 100" + expected = [dm.MultipathData(8, 16), dm.MultipathData(8, 80), + dm.MultipathData(8, 144), dm.MultipathData(8, 208)] + + def test_get_multipath_data_no_handlers(self, status_mock): + status_mock.return_value = defaultdict(constant_factory('A')) + args = "1 queue_if_no_path 0 1 1 round-robin 0 2 1 8:0 100 8:16 100" + expected = [dm.MultipathData(8, 0), dm.MultipathData(8, 16)] + self.assertEqual(dm.get_multipath_data(self.devname, args), expected) + + # FIXME: dunno if this is a valid configuration + #def test_get_multipath_data_no_groups(self, status_mock): + # status_mock.return_value = defaultdict(constant_factory('A')) + # args = "1 queue_if_no_path 1 handler 0 1 round-robin 0 1 1 8:0 100" + # expected = [dm.MultipathData(8, 0)] + # self.assertEqual(dm.get_multipath_data(self.devname, args), expected) + + def test_get_multipath_data_no_sel_args(self, status_mock): + status_mock.return_value = defaultdict(constant_factory('A')) + args = "1 queue_if_no_path 0 1 1 round-robin 0 1 1 8:0 100" + expected = [dm.MultipathData(8, 0)] + self.assertEqual(dm.get_multipath_data(self.devname, args), expected) + + def test_get_multipath_data_no_path_args(self, status_mock): + status_mock.return_value = defaultdict(constant_factory('A')) + args = "1 queue_if_no_path 0 1 1 round-robin 0 2 0 8:0 8:16" + expected = [dm.MultipathData(8, 0), dm.MultipathData(8, 16)] + self.assertEqual(dm.get_multipath_data(self.devname, args), expected) + + def test_get_multipath_data(self, status_mock): + status_mock.return_value = defaultdict(constant_factory('A')) + args = "1 queue_if_no_path 0 1 1 round-robin 0 4 1 8:16 100 8:80 100 8:144 100 8:208 100" + expected = [dm.MultipathData(8, 16), dm.MultipathData(8, 80), + dm.MultipathData(8, 144), dm.MultipathData(8, 208)] + self.assertEqual(dm.get_multipath_data(self.devname, args), expected) + + def test_get_multipath_data_multiple_groups(self, status_mock): + status_mock.return_value = defaultdict(constant_factory('A')) + args = "0 0 2 1 service-time 0 1 2 8:48 1 1 service-time 0 1 2 8:16 1 1" + expected = [dm.MultipathData(8, 48), dm.MultipathData(8, 16)] + self.assertEqual(dm.get_multipath_data(self.devname, args), expected) + + def test_get_multipath_data_empty_input(self, status_mock): + with self.assertLogs(level='CRITICAL') as cm: + with self.assertRaises(SystemExit) as se: + dm.get_multipath_data(self.devname, '') + self.assertEqual(se.exception.code, 1) + self.assertEqual(cm.output, + ["CRITICAL:src.zipl_helper:%s" % self.critical_msg]) + + def test_get_multipath_data_truncated_input(self, status_mock): + with self.assertLogs(level='CRITICAL') as cm: + with self.assertRaises(SystemExit) as se: + dm.get_multipath_data(self.devname, '0 0 2 1') + self.assertEqual(se.exception.code, 1) + self.assertEqual(cm.output, + ["CRITICAL:src.zipl_helper:%s" % self.critical_msg]) + + def test_get_multipath_data_critical(self, status_mock): + status_mock.return_value = {} + with self.assertLogs(level='CRITICAL') as cm: + with self.assertRaises(SystemExit) as se: + dm.get_multipath_data(self.devname, '') + self.assertEqual(se.exception.code, 1) + self.assertEqual(cm.output, + ["CRITICAL:src.zipl_helper:%s" % self.critical_msg]) + + +@mock.patch('src.zipl_helper.run_proc') +@mock.patch('src.zipl_helper.get_device_name') +class TestTable(unittest.TestCase): + + def setUp(self): + self.devname = '253:1' + self.critical_msg = dm.UNRECOG_TABLE_MSG % self.devname + + def test_get_table_linear(self, name_mock, run_mock): + run_mock.return_value = CompletedProcess('', 0, + stdout='0 104857600 linear %s 393152512' % self.devname) + data = dm.LinearData(253, 1, 393152512) + expected = [dm.Target(0, 104857600, 'linear', data)] + self.assertEqual(dm.get_table(253, 1), expected) + + def test_get_table_linear_invalid_data(self, name_mock, run_mock): + run_mock.return_value = CompletedProcess('', 0, stdout='0 0 linear 0 0') + name_mock.return_value = self.devname + + with self.assertLogs(level='CRITICAL') as cm: + with self.assertRaises(SystemExit) as se: + dm.get_table(253, 1) + self.assertEqual(se.exception.code, 1) + self.assertEqual(cm.output, + ["CRITICAL:src.zipl_helper:%s" % self.critical_msg]) + + def test_get_table_linear_dmsetup_exception(self, name_mock, run_mock): + run_mock.side_effect = CalledProcessError(1, 'dmsetup') + self.assertEqual(dm.get_table(253, 1), []) + + def test_get_table_linear_invalid_input(self, name_mock, run_mock): + run_mock.return_value = CompletedProcess('', 0, + stdout='A B linear %s 0' % self.devname) + name_mock.return_value = self.devname + + with self.assertLogs(level='CRITICAL') as cm: + with self.assertRaises(SystemExit) as se: + dm.get_table(253, 1) + self.assertEqual(se.exception.code, 1) + self.assertEqual(cm.output, + ["CRITICAL:src.zipl_helper:%s" % self.critical_msg]) + + def test_get_table_invalid_type(self, name_mock, run_mock): + run_mock.return_value = CompletedProcess('', 0, + stdout='0 1 invalid %s 1' % self.devname) + name_mock.return_value = self.devname + + with self.assertLogs(level='CRITICAL') as cm: + with self.assertRaises(SystemExit) as se: + dm.get_table(253, 1) + self.assertEqual(se.exception.code, 1) + self.assertEqual(cm.output, + ["CRITICAL:src.zipl_helper:Unrecognized setup: " + "Unsupported device-mapper target type '%s' for " + "device '%s'" % ('invalid', self.devname)]) + + def test_get_table_multipath(self, name_mock, run_mock): + def run_proc_mock(args): + if args[1] == 'status': + return CompletedProcess('', 0, + stdout='0 104878080 multipath 2 0 0 0 2 1 A 0 1 2 8:48 A 0 0 1 E 0 1 2 8:16 A 0 0 1') + elif args[1] == 'table': + return CompletedProcess('', 0, + stdout='0 104878080 multipath 0 0 2 1 service-time 0 1 2 8:48 1 1 service-time 0 1 2 8:16 1 1') + else: + return CalledProcessError(1, 'dmsetup') + name_mock.return_value = self.devname + run_mock.side_effect = run_proc_mock + expected = [dm.Target(0, 104878080, 'multipath', + [dm.MultipathData(8, 48), dm.MultipathData(8, 16)])] + self.assertEqual(dm.get_table(253, 1), expected) + + +@mock.patch('src.zipl_helper.get_device_name') +@mock.patch('src.zipl_helper.get_table') +class TestPhysicalDevice(unittest.TestCase): + + def setUp(self): + self.devname = '253:1' + + def test_get_physical_device_empty_table(self, table_mock, name_mock): + table_mock.return_value = [] + name_mock.return_value = self.devname + with self.assertLogs(level='CRITICAL') as cm: + with self.assertRaises(SystemExit) as se: + dm.get_physical_device(253, 1) + self.assertEqual(se.exception.code, 1) + self.assertEqual(cm.output, + ["CRITICAL:src.zipl_helper:Could not retrieve " + "device-mapper information for device '%s'" % + self.devname]) + + def test_get_physical_device_multitarget_table(self, table_mock, name_mock): + table_mock.return_value = [dm.Target(0, 0, 'linear', None), + dm.Target(1, 1, 'linear', None)] + with self.assertLogs(level='CRITICAL') as cm: + with self.assertRaises(SystemExit) as se: + dm.get_physical_device(253, 1) + self.assertEqual(se.exception.code, 1) + self.assertEqual(cm.output, + ["CRITICAL:src.zipl_helper:Unsupported setup: " + "Directory '%s' is located on a multi-target " + "device-mapper device" % None]) + + def test_get_physical_device_linear_table(self, table_mock, name_mock): + data = dm.LinearData(253, 0, 393152512) + target = dm.Target(253, 0, 'linear', data) + def cond_get_table(major, minor): + if (major, minor) == (253, 1): + return [target] + else: + return [] + + table_mock.side_effect = cond_get_table + self.assertEqual(dm.get_physical_device(253, 1), + (253, 0, 393152765, [(253, 1, target)])) + + def test_get_physical_device_mirror_table(self, table_mock, name_mock): + data = [dm.MirrorData(253, 0, 393152512)] + target = dm.Target(253, 0, 'mirror', data) + def cond_get_table(major, minor): + if (major, minor) == (253, 1): + return [target] + else: + return [] + table_mock.side_effect = cond_get_table + self.assertEqual(dm.get_physical_device(253, 1), + (253, 0, 393152765, [(253, 1, target)])) + + def test_get_physical_device_multipath_table(self, table_mock, name_mock): + data = [dm.MultipathData(8, 48)] + #target = dm.Target( + + +@mock.patch('src.zipl_helper.run_proc') +class TestDasdInfo(unittest.TestCase): + def setUp(self): + self.devname = '/dev/dasde' + + def test_get_dasd_info(self, run_mock): + run_mock.return_value = CompletedProcess('', 0, stdout=""" + +--- general DASD information -------------------------------------------------- +device node : /dev/dasde +busid : 0.0.0204 +type : ECKD +device type : hex 3390 dec 13200 + +--- DASD geometry ------------------------------------------------------------- +number of cylinders : hex d0b dec 3339 +tracks per cylinder : hex f dec 15 +blocks per track : hex c dec 12 +blocksize : hex 1000 dec 4096 + +--- extended DASD information ------------------------------------------------- +real device number : hex 0 dec 0 +subchannel identifier : hex 9ff dec 2559 +CU type (SenseID) : hex 3990 dec 14736 +CU model (SenseID) : hex e9 dec 233 +device type (SenseID) : hex 3390 dec 13200 +device model (SenseID) : hex a dec 10 +open count : hex 3 dec 3 +req_queue_len : hex 0 dec 0 +chanq_len : hex 0 dec 0 +status : hex 5 dec 5 +label_block : hex 2 dec 2 +FBA_layout : hex 0 dec 0 +characteristics_size : hex 40 dec 64 +confdata_size : hex 100 dec 256 +format : hex 2 dec 2 CDL formatted +features : hex 0 dec 0 default + +characteristics : 3990e933 900a5e8c 3ff72024 0d0b000f + e000e5a2 05940222 13090674 00000000 + 00000000 00000000 24241502 dfee0001 + 0677080f 007f4800 1f3c0000 00000d0b + +configuration_data : dc010100 f0f0f2f1 f0f7f9f0 f0c9c2d4 + f7f5f0f0 f0f0f0f0 f0d3f2f5 f9f11413 + 40000004 00000000 00000000 00000d0a + 00000000 00000000 00000000 00000000 + d4020000 f0f0f2f1 f0f7f9f3 f1c9c2d4 + f7f5f0f0 f0f0f0f0 f0d3f2f5 f9f11400 + d0000000 f0f0f2f1 f0f7f9f3 f1c9c2d4 + f7f5f0f0 f0f0f0f0 f0d3f2f5 f9f01400 + f0000001 f0f0f2f1 f0f7f9f0 f0c9c2d4 + f7f5f0f0 f0f0f0f0 f0d3f2f5 f9f11400 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 80000002 2d001e00 0015003b 00000215 + 0008c013 5a82b5a6 00020000 0000a000 +""") + expected = ('CDL', 3339, 15, 12) + self.assertEqual(dm.get_dasd_info(self.devname), expected) + + def test_get_dasd_info_missing_sectors(self, run_mock): + run_mock.return_value = CompletedProcess('', 0, stdout=""" + +--- general DASD information -------------------------------------------------- +device node : /dev/dasde +type : ECKD +device type : hex 3390 dec 13200 + +--- DASD geometry ------------------------------------------------------------- +number of cylinders : hex d0b dec 3339 +tracks per cylinder : hex f dec 15 +blocksize : hex 1000 dec 4096 + +--- extended DASD information ------------------------------------------------- +real device number : hex 0 dec 0 +status : hex 5 dec 5 +confdata_size : hex 100 dec 256 +format : hex 2 dec 2 CDL formatted +features : hex 0 dec 0 default +""") + self.assertEqual(dm.get_dasd_info(self.devname), None) + + def test_get_dasd_info_missing_type(self, run_mock): + run_mock.return_value = CompletedProcess('', 0, stdout=""" + +--- general DASD information -------------------------------------------------- +device node : /dev/dasde +device type : hex 3390 dec 13200 + +--- DASD geometry ------------------------------------------------------------- +number of cylinders : hex d0b dec 3339 +tracks per cylinder : hex f dec 15 +blocks per track : hex c dec 12 +blocksize : hex 1000 dec 4096 + +--- extended DASD information ------------------------------------------------- +real device number : hex 0 dec 0 +status : hex 5 dec 5 +confdata_size : hex 100 dec 256 +format : hex 2 dec 2 CDL formatted +features : hex 0 dec 0 default +""") + self.assertEqual(dm.get_dasd_info(self.devname), None) + + def test_get_dasd_info_missing_cylinders(self, run_mock): + run_mock.return_value = CompletedProcess('', 0, stdout=""" + +--- general DASD information -------------------------------------------------- +device node : /dev/dasde +type : ECKD +device type : hex 3390 dec 13200 + +--- DASD geometry ------------------------------------------------------------- +tracks per cylinder : hex f dec 15 +blocks per track : hex c dec 12 +blocksize : hex 1000 dec 4096 + +--- extended DASD information ------------------------------------------------- +real device number : hex 0 dec 0 +status : hex 5 dec 5 +confdata_size : hex 100 dec 256 +format : hex 2 dec 2 CDL formatted +features : hex 0 dec 0 default +""") + self.assertEqual(dm.get_dasd_info(self.devname), None) + + def test_get_dasd_info_missing_heads(self, run_mock): + run_mock.return_value = CompletedProcess('', 0, stdout=""" + +--- general DASD information -------------------------------------------------- +device node : /dev/dasde +type : ECKD +device type : hex 3390 dec 13200 + +--- DASD geometry ------------------------------------------------------------- +number of cylinders : hex d0b dec 3339 +blocks per track : hex c dec 12 +blocksize : hex 1000 dec 4096 + +--- extended DASD information ------------------------------------------------- +real device number : hex 0 dec 0 +status : hex 5 dec 5 +confdata_size : hex 100 dec 256 +format : hex 2 dec 2 CDL formatted +features : hex 0 dec 0 default +""") + self.assertEqual(dm.get_dasd_info(self.devname), None) + + def test_get_dasd_info_missing_format(self, run_mock): + run_mock.return_value = CompletedProcess('', 0, stdout=""" + +--- general DASD information -------------------------------------------------- +device node : /dev/dasde +type : ECKD +device type : hex 3390 dec 13200 + +--- DASD geometry ------------------------------------------------------------- +number of cylinders : hex d0b dec 3339 +tracks per cylinder : hex f dec 15 +blocks per track : hex c dec 12 +blocksize : hex 1000 dec 4096 + +--- extended DASD information ------------------------------------------------- +real device number : hex 0 dec 0 +status : hex 5 dec 5 +confdata_size : hex 100 dec 256 +features : hex 0 dec 0 default +""") + self.assertEqual(dm.get_dasd_info(self.devname), None) + + def test_get_dasd_info_dasdview_fails(self, run_mock): + run_mock.side_effect = CalledProcessError(1, 'dasdview') + self.assertEqual(dm.get_dasd_info(self.devname), None) + + def test_get_dasd_info_type_cdl(self, run_mock): + run_mock.return_value = CompletedProcess('', 0, stdout=""" + +--- general DASD information -------------------------------------------------- +device node : /dev/dasde +type : ECKD +device type : hex 3390 dec 13200 + +--- DASD geometry ------------------------------------------------------------- +number of cylinders : hex d0b dec 3339 +tracks per cylinder : hex f dec 15 +blocks per track : hex c dec 12 +blocksize : hex 1000 dec 4096 + +--- extended DASD information ------------------------------------------------- +real device number : hex 0 dec 0 +format : hex 2 dec 2 CDL formatted + +""") + dtype, _, _, _ = dm.get_dasd_info(self.devname) + self.assertEqual(dtype, 'CDL') + + def test_get_dasd_info_type_ldl(self, run_mock): + run_mock.return_value = CompletedProcess('', 0, stdout=""" + +--- general DASD information -------------------------------------------------- +device node : /dev/dasde +type : ECKD +device type : hex 3390 dec 13200 + +--- DASD geometry ------------------------------------------------------------- +number of cylinders : hex d0b dec 3339 +tracks per cylinder : hex f dec 15 +blocks per track : hex c dec 12 +blocksize : hex 1000 dec 4096 + +--- extended DASD information ------------------------------------------------- +real device number : hex 0 dec 0 +format : hex 1 dec 1 LDL formatted + +""") + dtype, _, _, _ = dm.get_dasd_info(self.devname) + self.assertEqual(dtype, 'LDL') + + def test_get_dasd_info_type_fba(self, run_mock): + run_mock.return_value = CompletedProcess('', 0, stdout=""" + +--- general DASD information -------------------------------------------------- +device node : /dev/dasde +type : FBA +device type : hex 3390 dec 13200 + +--- DASD geometry ------------------------------------------------------------- +number of cylinders : hex d0b dec 3339 +tracks per cylinder : hex f dec 15 +blocks per track : hex c dec 12 +blocksize : hex 1000 dec 4096 + +--- extended DASD information ------------------------------------------------- +real device number : hex 0 dec 0 +format : hex 3 dec 3 FBA formatted + +""") + dtype, _, _, _ = dm.get_dasd_info(self.devname) + self.assertEqual(dtype, 'FBA') + + +if __name__ == '__main__': + unittest.main()