<filename>test/functional/test-framework/test_tools/device_mapper.py<gh_stars>1-10
#
# Copyright(c) 2019-2020 Intel Corporation
# SPDX-License-Identifier: BSD-3-Clause-Clear
#

from enum import Enum

from test_utils.linux_command import LinuxCommand
from test_utils.size import Size, Unit
from storage_devices.device import Device
from core.test_run import TestRun
from test_utils.output import Output


class DmTarget(Enum):
    # Fill argument types for other targets if you need them
    LINEAR = (str, int)
    STRIPED = (int, int, list)
    ERROR = ()
    ZERO = ()
    CRYPT = ()
    DELAY = (str, int, int, str, int, int)
    FLAKEY = (str, int, int, int)
    MIRROR = ()
    MULTIPATH = ()
    RAID = ()
    SNAPSHOT = ()

    def __str__(self):
        return self.name.lower()


class DmTable:
    class TableEntry:
        pass


class DmTable:
    class TableEntry:
        def __init__(self, offset: int, length: int, target: DmTarget, *params):
            self.offset = int(offset)
            self.length = int(length)
            self.target = DmTarget(target)
            self.params = list(params)
            self.validate()

        def validate(self):
            if self.target.value:
                for i in range(len(self.params)):
                    try:
                        self.params[i] = self.target.value[i](self.params[i])
                    except IndexError:
                        raise ValueError("invalid dm target parameter")

        def __str__(self):
            ret = f"{self.offset} {self.length} {self.target}"
            for param in self.params:
                ret += f" {param}"

            return ret

    def __init__(self):
        self.table = []

    @classmethod
    def uniform_error_table(
        cls, start_lba: int, stop_lba: int, num_error_zones: int, error_zone_size: Size
    ):
        table = cls()
        increment = (stop_lba - start_lba) // num_error_zones

        for zone_start in range(start_lba, stop_lba, increment):
            table.add_entry(
                DmTable.TableEntry(
                    zone_start,
                    error_zone_size.get_value(Unit.Blocks512),
                    DmTarget.ERROR,
                )
            )

        return table

    @classmethod
    def passthrough_table(cls, device: Device):
        table = cls()

        table.add_entry(
            DmTable.TableEntry(
                0,
                device.size.get_value(Unit.Blocks512),
                DmTarget.LINEAR,
                device.system_path,
                0,
            )
        )

        return table

    @classmethod
    def error_table(cls, offset: int, size: Size):
        table = cls()

        table.add_entry(
            DmTable.TableEntry(offset, size.get_value(Unit.Blocks512), DmTarget.ERROR)
        )

        return table

    def fill_gaps(self, device: Device, fill_end=True):
        gaps = self.get_gaps()

        for gap in gaps[:-1]:
            self.add_entry(
                DmTable.TableEntry(
                    gap[0], gap[1], DmTarget.LINEAR, device.system_path, int(gap[0])
                )
            )

        table_end = gaps[-1][0]

        if fill_end and (Size(table_end, Unit.Blocks512) < device.size):
            self.add_entry(
                DmTable.TableEntry(
                    table_end,
                    device.size.get_value(Unit.Blocks512) - table_end,
                    DmTarget.LINEAR,
                    device.system_path,
                    table_end,
                )
            )

        return self

    def add_entry(self, entry: DmTable.TableEntry):
        self.table.append(entry)
        return self

    def get_gaps(self):
        if not self.table:
            return [(0, -1)]

        gaps = []

        self.table.sort(key=lambda entry: entry.offset)

        if self.table[0].offset != 0:
            gaps.append((0, self.table[0].offset))

        for e1, e2 in zip(self.table, self.table[1:]):
            if e1.offset + e1.length != e2.offset:
                gaps.append(
                    (e1.offset + e1.length, e2.offset - (e1.offset + e1.length))
                )

        if len(self.table) > 1:
            gaps.append((e2.offset + e2.length, -1))
        else:
            gaps.append((self.table[0].offset + self.table[0].length, -1))

        return gaps

    def validate(self):
        self.table.sort(key=lambda entry: entry.offset)

        if self.table[0].offset != 0:
            raise ValueError(f"dm table should start at LBA 0: {self.table[0]}")

        for e1, e2 in zip(self.table, self.table[1:]):
            if e1.offset + e1.length != e2.offset:
                raise ValueError(
                    f"dm table should not have any holes or overlaps: {e1} -> {e2}"
                )

    def get_size(self):
        self.table.sort(key=lambda entry: entry.offset)

        return Size(self.table[-1].offset + self.table[-1].length, Unit.Blocks512)

    def __str__(self):
        output = ""

        for entry in self.table:
            output += f"{entry}\n"

        return output


class DeviceMapper(LinuxCommand):
    @classmethod
    def remove_all(cls, force=True):
        TestRun.LOGGER.info("Removing all device mapper devices")

        cmd = "dmsetup remove_all"
        if force:
            cmd += " --force"

        return TestRun.executor.run_expect_success(cmd)

    def __init__(self, name: str):
        LinuxCommand.__init__(self, TestRun.executor, "dmsetup")
        self.name = name

    @staticmethod
    def wrap_table(table: DmTable):
        return f"<< ENDHERE\n{str(table)}ENDHERE\n"

    def get_path(self):
        return f"/dev/mapper/{self.name}"

    def clear(self):
        return TestRun.executor.run_expect_success(f"{self.command_name} clear {self.name}")

    def create(self, table: DmTable):
        try:
            table.validate()
        except ValueError:
            for entry in table.table:
                TestRun.LOGGER.error(f"{entry}")
            raise

        TestRun.LOGGER.info(f"Creating device mapper device '{self.name}'")

        for entry in table.table:
            TestRun.LOGGER.debug(f"{entry}")

        return TestRun.executor.run_expect_success(
            f"{self.command_name} create {self.name} {self.wrap_table(table)}"
        )

    def remove(self):
        TestRun.LOGGER.info(f"Removing device mapper device '{self.name}'")

        return TestRun.executor.run_expect_success(f"{self.command_name} remove {self.name}")

    def suspend(self):
        TestRun.LOGGER.info(f"Suspending device mapper device '{self.name}'")
        return TestRun.executor.run_expect_success(f"{self.command_name} suspend {self.name}")

    def resume(self):
        TestRun.LOGGER.info(f"Resuming device mapper device '{self.name}'")
        return TestRun.executor.run_expect_success(f"{self.command_name} resume {self.name}")

    def reload(self, table: DmTable):
        table.validate()
        TestRun.LOGGER.info(f"Reloading table for device mapper device '{self.name}'")

        for entry in table.table:
            TestRun.LOGGER.debug(f"{entry}")

        return TestRun.executor.run_expect_success(
            f"{self.command_name} reload {self.name} {self.wrap_table(table)}"
        )


class ErrorDevice(Device):
    def __init__(self, name: str, base_device: Device, table: DmTable = None):
        self.device = base_device
        self.mapper = DeviceMapper(name)
        self.name = name
        self.table = DmTable.passthrough_table(base_device) if not table else table
        self.active = False
        self.start()

    @property
    def system_path(self):
        if self.active:
            output = TestRun.executor.run_expect_success(f"realpath {self.mapper.get_path()}")

            return output.stdout

        return None

    @property
    def size(self):
        if self.active:
            return self.table.get_size()

        return None

    def start(self):
        self.mapper.create(self.table)
        self.active = True

    def stop(self):
        self.mapper.remove()
        self.active = False

    def change_table(self, table: DmTable, permanent=True):
        if self.active:
            self.mapper.suspend()

        self.mapper.reload(table)

        self.mapper.resume()

        if permanent:
            self.table = table

    def suspend_errors(self):
        empty_table = DmTable.passthrough_table(self.device)
        TestRun.LOGGER.info(f"Suspending issuing errors for error device '{self.name}'")

        self.change_table(empty_table, False)

    def resume_errors(self):
        TestRun.LOGGER.info(f"Resuming issuing errors for error device '{self.name}'")

        self.change_table(self.table, False)

    def suspend(self):
        if not self.active:
            TestRun.LOGGER.warning(
                f"cannot suspend error device '{self.name}'! It's already running"
            )

        self.mapper.suspend()

        self.active = False

    def resume(self):
        if self.active:
            TestRun.LOGGER.warning(
                f"cannot resume error device '{self.name}'! It's already running"
            )

        self.mapper.resume()

        self.active = True

<filename>test/functional/test-framework/test_tools/device_mapper.py<gh_stars>1-10
#
# Copyright(c) 2019-2020 Intel Corporation
# SPDX-License-Identifier: BSD-3-Clause-Clear
#

from enum import Enum

from test_utils.linux_command import LinuxCommand
from test_utils.size import Size, Unit
from storage_devices.device import Device
from core.test_run import TestRun
from test_utils.output import Output


class DmTarget(Enum):
    # Fill argument types for other targets if you need them
    LINEAR = (str, int)
    STRIPED = (int, int, list)
    ERROR = ()
    ZERO = ()
    CRYPT = ()
    DELAY = (str, int, int, str, int, int)
    FLAKEY = (str, int, int, int)
    MIRROR = ()
    MULTIPATH = ()
    RAID = ()
    SNAPSHOT = ()

    def __str__(self):
        return self.name.lower()


class DmTable:
    class TableEntry:
        pass


class DmTable:
    class TableEntry:
        def __init__(self, offset: int, length: int, target: DmTarget, *params):
            self.offset = int(offset)
            self.length = int(length)
            self.target = DmTarget(target)
            self.params = list(params)
            self.validate()

        def validate(self):
            if self.target.value:
                for i in range(len(self.params)):
                    try:
                        self.params[i] = self.target.value[i](self.params[i])
                    except IndexError:
                        raise ValueError("invalid dm target parameter")

        def __str__(self):
            ret = f"{self.offset} {self.length} {self.target}"
            for param in self.params:
                ret += f" {param}"

            return ret

    def __init__(self):
        self.table = []

    @classmethod
    def uniform_error_table(
        cls, start_lba: int, stop_lba: int, num_error_zones: int, error_zone_size: Size
    ):
        table = cls()
        increment = (stop_lba - start_lba) // num_error_zones

        for zone_start in range(start_lba, stop_lba, increment):
            table.add_entry(
                DmTable.TableEntry(
                    zone_start,
                    error_zone_size.get_value(Unit.Blocks512),
                    DmTarget.ERROR,
                )
            )

        return table

    @classmethod
    def passthrough_table(cls, device: Device):
        table = cls()

        table.add_entry(
            DmTable.TableEntry(
                0,
                device.size.get_value(Unit.Blocks512),
                DmTarget.LINEAR,
                device.system_path,
                0,
            )
        )

        return table

    @classmethod
    def error_table(cls, offset: int, size: Size):
        table = cls()

        table.add_entry(
            DmTable.TableEntry(offset, size.get_value(Unit.Blocks512), DmTarget.ERROR)
        )

        return table

    def fill_gaps(self, device: Device, fill_end=True):
        gaps = self.get_gaps()

        for gap in gaps[:-1]:
            self.add_entry(
                DmTable.TableEntry(
                    gap[0], gap[1], DmTarget.LINEAR, device.system_path, int(gap[0])
                )
            )

        table_end = gaps[-1][0]

        if fill_end and (Size(table_end, Unit.Blocks512) < device.size):
            self.add_entry(
                DmTable.TableEntry(
                    table_end,
                    device.size.get_value(Unit.Blocks512) - table_end,
                    DmTarget.LINEAR,
                    device.system_path,
                    table_end,
                )
            )

        return self

    def add_entry(self, entry: DmTable.TableEntry):
        self.table.append(entry)
        return self

    def get_gaps(self):
        if not self.table:
            return [(0, -1)]

        gaps = []

        self.table.sort(key=lambda entry: entry.offset)

        if self.table[0].offset != 0:
            gaps.append((0, self.table[0].offset))

        for e1, e2 in zip(self.table, self.table[1:]):
            if e1.offset + e1.length != e2.offset:
                gaps.append(
                    (e1.offset + e1.length, e2.offset - (e1.offset + e1.length))
                )

        if len(self.table) > 1:
            gaps.append((e2.offset + e2.length, -1))
        else:
            gaps.append((self.table[0].offset + self.table[0].length, -1))

        return gaps

    def validate(self):
        self.table.sort(key=lambda entry: entry.offset)

        if self.table[0].offset != 0:
            raise ValueError(f"dm table should start at LBA 0: {self.table[0]}")

        for e1, e2 in zip(self.table, self.table[1:]):
            if e1.offset + e1.length != e2.offset:
                raise ValueError(
                    f"dm table should not have any holes or overlaps: {e1} -> {e2}"
                )

    def get_size(self):
        self.table.sort(key=lambda entry: entry.offset)

        return Size(self.table[-1].offset + self.table[-1].length, Unit.Blocks512)

    def __str__(self):
        output = ""

        for entry in self.table:
            output += f"{entry}\n"

        return output


class DeviceMapper(LinuxCommand):
    @classmethod
    def remove_all(cls, force=True):
        TestRun.LOGGER.info("Removing all device mapper devices")

        cmd = "dmsetup remove_all"
        if force:
            cmd += " --force"

        return TestRun.executor.run_expect_success(cmd)

    def __init__(self, name: str):
        LinuxCommand.__init__(self, TestRun.executor, "dmsetup")
        self.name = name

    @staticmethod
    def wrap_table(table: DmTable):
        return f"<< ENDHERE\n{str(table)}ENDHERE\n"

    def get_path(self):
        return f"/dev/mapper/{self.name}"

    def clear(self):
        return TestRun.executor.run_expect_success(f"{self.command_name} clear {self.name}")

    def create(self, table: DmTable):
        try:
            table.validate()
        except ValueError:
            for entry in table.table:
                TestRun.LOGGER.error(f"{entry}")
            raise

        TestRun.LOGGER.info(f"Creating device mapper device '{self.name}'")

        for entry in table.table:
            TestRun.LOGGER.debug(f"{entry}")

        return TestRun.executor.run_expect_success(
            f"{self.command_name} create {self.name} {self.wrap_table(table)}"
        )

    def remove(self):
        TestRun.LOGGER.info(f"Removing device mapper device '{self.name}'")

        return TestRun.executor.run_expect_success(f"{self.command_name} remove {self.name}")

    def suspend(self):
        TestRun.LOGGER.info(f"Suspending device mapper device '{self.name}'")
        return TestRun.executor.run_expect_success(f"{self.command_name} suspend {self.name}")

    def resume(self):
        TestRun.LOGGER.info(f"Resuming device mapper device '{self.name}'")
        return TestRun.executor.run_expect_success(f"{self.command_name} resume {self.name}")

    def reload(self, table: DmTable):
        table.validate()
        TestRun.LOGGER.info(f"Reloading table for device mapper device '{self.name}'")

        for entry in table.table:
            TestRun.LOGGER.debug(f"{entry}")

        return TestRun.executor.run_expect_success(
            f"{self.command_name} reload {self.name} {self.wrap_table(table)}"
        )


class ErrorDevice(Device):
    def __init__(self, name: str, base_device: Device, table: DmTable = None):
        self.device = base_device
        self.mapper = DeviceMapper(name)
        self.name = name
        self.table = DmTable.passthrough_table(base_device) if not table else table
        self.active = False
        self.start()

    @property
    def system_path(self):
        if self.active:
            output = TestRun.executor.run_expect_success(f"realpath {self.mapper.get_path()}")

            return output.stdout

        return None

    @property
    def size(self):
        if self.active:
            return self.table.get_size()

        return None

    def start(self):
        self.mapper.create(self.table)
        self.active = True

    def stop(self):
        self.mapper.remove()
        self.active = False

    def change_table(self, table: DmTable, permanent=True):
        if self.active:
            self.mapper.suspend()

        self.mapper.reload(table)

        self.mapper.resume()

        if permanent:
            self.table = table

    def suspend_errors(self):
        empty_table = DmTable.passthrough_table(self.device)
        TestRun.LOGGER.info(f"Suspending issuing errors for error device '{self.name}'")

        self.change_table(empty_table, False)

    def resume_errors(self):
        TestRun.LOGGER.info(f"Resuming issuing errors for error device '{self.name}'")

        self.change_table(self.table, False)

    def suspend(self):
        if not self.active:
            TestRun.LOGGER.warning(
                f"cannot suspend error device '{self.name}'! It's already running"
            )

        self.mapper.suspend()

        self.active = False

    def resume(self):
        if self.active:
            TestRun.LOGGER.warning(
                f"cannot resume error device '{self.name}'! It's already running"
            )

        self.mapper.resume()

        self.active = True

# # Copyright(c) 2019-2020 Intel Corporation # SPDX-License-Identifier: BSD-3-Clause-Clear # # Fill argument types for other targets if you need them