From 849f59855c219d4b65ab572d8e5cb4edd222ce96 Mon Sep 17 00:00:00 2001 From: Robert Baldyga Date: Fri, 23 Dec 2022 12:50:17 +0100 Subject: [PATCH] tests: Embed test framework within OCL repository Signed-off-by: Robert Baldyga --- README.md | 2 +- test/functional/requirements.txt | 16 +- test/functional/test-framework/__init__.py | 4 + .../test-framework/connection/__init__.py | 4 + .../connection/base_executor.py | 85 ++ .../connection/dummy_executor.py | 15 + .../connection/local_executor.py | 48 + .../test-framework/connection/ssh_executor.py | 142 +++ .../test-framework/core/__init__.py | 0 .../test-framework/core/pair_testing.py | 107 +++ .../functional/test-framework/core/plugins.py | 124 +++ .../test-framework/core/test_run.py | 65 ++ .../test-framework/core/test_run_utils.py | 272 ++++++ .../internal_plugins/__init__.py | 4 + .../example_plugin/__init__.py | 22 + .../power_control_libvirt/__init__.py | 48 + .../internal_plugins/scsi_debug/__init__.py | 39 + .../internal_plugins/vdbench/__init__.py | 97 ++ .../functional/test-framework/log/__init__.py | 0 .../functional/test-framework/log/base_log.py | 78 ++ .../test-framework/log/group/__init__.py | 0 .../log/group/html_chapter_group_log.py | 43 + .../log/group/html_group_log.py | 139 +++ .../log/group/html_iteration_group_log.py | 20 + .../test-framework/log/html_file_item_log.py | 102 ++ .../test-framework/log/html_file_log.py | 29 + .../test-framework/log/html_iteration_log.py | 13 + .../test-framework/log/html_log_config.py | 204 ++++ .../test-framework/log/html_log_manager.py | 126 +++ .../test-framework/log/html_main_log.py | 53 ++ .../log/html_presentation_policy.py | 45 + .../test-framework/log/html_setup_log.py | 34 + test/functional/test-framework/log/logger.py | 220 +++++ .../test-framework/log/presentation_policy.py | 21 + .../log/template/iterations/iteration.html | 35 + .../log/template/iterations/setup.html | 37 + .../test-framework/log/template/main.css | 383 ++++++++ .../test-framework/log/template/main.html | 44 + .../test-framework/log/template/main.js | 223 +++++ .../test-framework/storage_devices/device.py | 117 +++ .../test-framework/storage_devices/disk.py | 237 +++++ .../test-framework/storage_devices/drbd.py | 66 ++ .../test-framework/storage_devices/lvm.py | 531 +++++++++++ .../storage_devices/partition.py | 22 + .../test-framework/storage_devices/raid.py | 182 ++++ .../test-framework/storage_devices/ramdisk.py | 80 ++ .../test-framework/test_tools/__init__.py | 0 .../test-framework/test_tools/blktrace.py | 225 +++++ .../test-framework/test_tools/checksec.sh | 882 ++++++++++++++++++ .../test-framework/test_tools/dd.py | 40 + .../test-framework/test_tools/ddrescue.py | 47 + .../test_tools/device_mapper.py | 329 +++++++ .../test-framework/test_tools/disk_utils.py | 397 ++++++++ .../test-framework/test_tools/drbdadm.py | 67 ++ .../test-framework/test_tools/fio/__init__.py | 0 .../test-framework/test_tools/fio/fio.py | 105 +++ .../test_tools/fio/fio_param.py | 388 ++++++++ .../test_tools/fio/fio_patterns.py | 19 + .../test_tools/fio/fio_result.py | 164 ++++ .../test-framework/test_tools/fs_utils.py | 378 ++++++++ .../test-framework/test_tools/iostat.py | 179 ++++ .../test-framework/test_tools/kedr.py | 129 +++ .../test-framework/test_tools/mdadm.py | 140 +++ .../test-framework/test_tools/nvme_cli.py | 60 ++ .../test-framework/test_tools/packaging.py | 121 +++ .../test_tools/peach_fuzzer/__init__.py | 0 .../peach_fuzzer/config_template.xml | 34 + .../test_tools/peach_fuzzer/peach_fuzzer.py | 208 +++++ .../test-framework/test_utils/__init__.py | 4 + .../test-framework/test_utils/asynchronous.py | 18 + .../test-framework/test_utils/disk_finder.py | 190 ++++ .../test-framework/test_utils/drbd.py | 61 ++ .../test-framework/test_utils/dut.py | 43 + .../test_utils/emergency_escape.py | 113 +++ .../test_utils/filesystem/__init__.py | 0 .../test_utils/filesystem/directory.py | 31 + .../test_utils/filesystem/file.py | 83 ++ .../test_utils/filesystem/fs_item.py | 102 ++ .../test_utils/filesystem/symlink.py | 91 ++ .../test-framework/test_utils/fstab.py | 20 + .../test-framework/test_utils/generator.py | 11 + .../test-framework/test_utils/io_stats.py | 112 +++ .../test_utils/linux_command.py | 79 ++ .../test-framework/test_utils/os_utils.py | 462 +++++++++ .../test-framework/test_utils/output.py | 22 + .../test-framework/test_utils/retry.py | 57 ++ .../test-framework/test_utils/scsi_debug.py | 77 ++ .../test-framework/test_utils/singleton.py | 16 + .../test-framework/test_utils/size.py | 211 +++++ .../test-framework/test_utils/systemd.py | 25 + .../test-framework/test_utils/time.py | 14 + 91 files changed, 9930 insertions(+), 2 deletions(-) create mode 100644 test/functional/test-framework/__init__.py create mode 100644 test/functional/test-framework/connection/__init__.py create mode 100644 test/functional/test-framework/connection/base_executor.py create mode 100644 test/functional/test-framework/connection/dummy_executor.py create mode 100644 test/functional/test-framework/connection/local_executor.py create mode 100644 test/functional/test-framework/connection/ssh_executor.py create mode 100644 test/functional/test-framework/core/__init__.py create mode 100644 test/functional/test-framework/core/pair_testing.py create mode 100644 test/functional/test-framework/core/plugins.py create mode 100644 test/functional/test-framework/core/test_run.py create mode 100644 test/functional/test-framework/core/test_run_utils.py create mode 100644 test/functional/test-framework/internal_plugins/__init__.py create mode 100644 test/functional/test-framework/internal_plugins/example_plugin/__init__.py create mode 100644 test/functional/test-framework/internal_plugins/power_control_libvirt/__init__.py create mode 100644 test/functional/test-framework/internal_plugins/scsi_debug/__init__.py create mode 100644 test/functional/test-framework/internal_plugins/vdbench/__init__.py create mode 100644 test/functional/test-framework/log/__init__.py create mode 100644 test/functional/test-framework/log/base_log.py create mode 100644 test/functional/test-framework/log/group/__init__.py create mode 100644 test/functional/test-framework/log/group/html_chapter_group_log.py create mode 100644 test/functional/test-framework/log/group/html_group_log.py create mode 100644 test/functional/test-framework/log/group/html_iteration_group_log.py create mode 100644 test/functional/test-framework/log/html_file_item_log.py create mode 100644 test/functional/test-framework/log/html_file_log.py create mode 100644 test/functional/test-framework/log/html_iteration_log.py create mode 100644 test/functional/test-framework/log/html_log_config.py create mode 100644 test/functional/test-framework/log/html_log_manager.py create mode 100644 test/functional/test-framework/log/html_main_log.py create mode 100644 test/functional/test-framework/log/html_presentation_policy.py create mode 100644 test/functional/test-framework/log/html_setup_log.py create mode 100644 test/functional/test-framework/log/logger.py create mode 100644 test/functional/test-framework/log/presentation_policy.py create mode 100644 test/functional/test-framework/log/template/iterations/iteration.html create mode 100644 test/functional/test-framework/log/template/iterations/setup.html create mode 100644 test/functional/test-framework/log/template/main.css create mode 100644 test/functional/test-framework/log/template/main.html create mode 100644 test/functional/test-framework/log/template/main.js create mode 100644 test/functional/test-framework/storage_devices/device.py create mode 100644 test/functional/test-framework/storage_devices/disk.py create mode 100644 test/functional/test-framework/storage_devices/drbd.py create mode 100644 test/functional/test-framework/storage_devices/lvm.py create mode 100644 test/functional/test-framework/storage_devices/partition.py create mode 100644 test/functional/test-framework/storage_devices/raid.py create mode 100644 test/functional/test-framework/storage_devices/ramdisk.py create mode 100644 test/functional/test-framework/test_tools/__init__.py create mode 100644 test/functional/test-framework/test_tools/blktrace.py create mode 100644 test/functional/test-framework/test_tools/checksec.sh create mode 100644 test/functional/test-framework/test_tools/dd.py create mode 100644 test/functional/test-framework/test_tools/ddrescue.py create mode 100644 test/functional/test-framework/test_tools/device_mapper.py create mode 100644 test/functional/test-framework/test_tools/disk_utils.py create mode 100644 test/functional/test-framework/test_tools/drbdadm.py create mode 100644 test/functional/test-framework/test_tools/fio/__init__.py create mode 100644 test/functional/test-framework/test_tools/fio/fio.py create mode 100644 test/functional/test-framework/test_tools/fio/fio_param.py create mode 100644 test/functional/test-framework/test_tools/fio/fio_patterns.py create mode 100644 test/functional/test-framework/test_tools/fio/fio_result.py create mode 100644 test/functional/test-framework/test_tools/fs_utils.py create mode 100644 test/functional/test-framework/test_tools/iostat.py create mode 100644 test/functional/test-framework/test_tools/kedr.py create mode 100644 test/functional/test-framework/test_tools/mdadm.py create mode 100644 test/functional/test-framework/test_tools/nvme_cli.py create mode 100644 test/functional/test-framework/test_tools/packaging.py create mode 100644 test/functional/test-framework/test_tools/peach_fuzzer/__init__.py create mode 100644 test/functional/test-framework/test_tools/peach_fuzzer/config_template.xml create mode 100644 test/functional/test-framework/test_tools/peach_fuzzer/peach_fuzzer.py create mode 100644 test/functional/test-framework/test_utils/__init__.py create mode 100644 test/functional/test-framework/test_utils/asynchronous.py create mode 100644 test/functional/test-framework/test_utils/disk_finder.py create mode 100644 test/functional/test-framework/test_utils/drbd.py create mode 100644 test/functional/test-framework/test_utils/dut.py create mode 100644 test/functional/test-framework/test_utils/emergency_escape.py create mode 100644 test/functional/test-framework/test_utils/filesystem/__init__.py create mode 100644 test/functional/test-framework/test_utils/filesystem/directory.py create mode 100644 test/functional/test-framework/test_utils/filesystem/file.py create mode 100644 test/functional/test-framework/test_utils/filesystem/fs_item.py create mode 100644 test/functional/test-framework/test_utils/filesystem/symlink.py create mode 100644 test/functional/test-framework/test_utils/fstab.py create mode 100644 test/functional/test-framework/test_utils/generator.py create mode 100644 test/functional/test-framework/test_utils/io_stats.py create mode 100644 test/functional/test-framework/test_utils/linux_command.py create mode 100644 test/functional/test-framework/test_utils/os_utils.py create mode 100644 test/functional/test-framework/test_utils/output.py create mode 100644 test/functional/test-framework/test_utils/retry.py create mode 100644 test/functional/test-framework/test_utils/scsi_debug.py create mode 100644 test/functional/test-framework/test_utils/singleton.py create mode 100644 test/functional/test-framework/test_utils/size.py create mode 100644 test/functional/test-framework/test_utils/systemd.py create mode 100644 test/functional/test-framework/test_utils/time.py diff --git a/README.md b/README.md index dfbcb46..44793b2 100644 --- a/README.md +++ b/README.md @@ -104,7 +104,7 @@ The complete documentation for Open CAS Linux is available in the Before running tests make sure you have a platform with at least 2 disks (one for cache and one for core). Be careful as these devices will be most likely overwritten with random data during tests. Tests can be either executed locally or on a remote platform (via ssh) specified in the dut_config. 1. Go to test directory `cd test/functional`. -1. Install dependencies with command `pip3 install -r test-framework/requirements.txt`. +1. Install dependencies with command `pip3 install -r requirements.txt`. 1. Create DUT config. See example [here](test/functional/config/example_dut_config.yml). a) Set disks params. You need at least two disks, of which at least one is an SSD drive. b) For remote execution uncomment and set the `ip`, `user` and `password` fields. diff --git a/test/functional/requirements.txt b/test/functional/requirements.txt index dbc13ee..7ede892 100644 --- a/test/functional/requirements.txt +++ b/test/functional/requirements.txt @@ -1,5 +1,19 @@ -attotime>=0.2.0 +pytest>=4.4.0,<=6.2.5 +multimethod>=1.1 +paramiko>=2.7.2 +IPy>=1.00 +aenum>=2.2.1 packaging>=20.3 +typing>=3.7.4.1 +pyyaml>=5.4 +lxml>=4.6.3 +wget>=3.2 +attotime>=0.2.0 +gitpython>=3.1.7 +cryptography>=3.4.6 +psutil>=5.8.0 +py==1.10.0 +portalocker>=2.3.1 pytest-asyncio>=0.14.0 recordclass>=0.8.4 schema==0.7.2 diff --git a/test/functional/test-framework/__init__.py b/test/functional/test-framework/__init__.py new file mode 100644 index 0000000..dce958c --- /dev/null +++ b/test/functional/test-framework/__init__.py @@ -0,0 +1,4 @@ +# +# Copyright(c) 2019-2021 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# diff --git a/test/functional/test-framework/connection/__init__.py b/test/functional/test-framework/connection/__init__.py new file mode 100644 index 0000000..dce958c --- /dev/null +++ b/test/functional/test-framework/connection/__init__.py @@ -0,0 +1,4 @@ +# +# Copyright(c) 2019-2021 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# diff --git a/test/functional/test-framework/connection/base_executor.py b/test/functional/test-framework/connection/base_executor.py new file mode 100644 index 0000000..f47867e --- /dev/null +++ b/test/functional/test-framework/connection/base_executor.py @@ -0,0 +1,85 @@ +# +# Copyright(c) 2019-2021 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# + +import time +from datetime import timedelta + +from core.test_run import TestRun +from test_utils.output import CmdException + + +class BaseExecutor: + def _execute(self, command, timeout): + raise NotImplementedError() + + def _rsync(self, src, dst, delete, symlinks, checksum, exclude_list, timeout, + dut_to_controller): + raise NotImplementedError() + + def rsync_to(self, src, dst, delete=False, symlinks=False, checksum=False, exclude_list=[], + timeout: timedelta = timedelta(seconds=90)): + return self._rsync(src, dst, delete, symlinks, checksum, exclude_list, timeout, False) + + def rsync_from(self, src, dst, delete=False, symlinks=False, checksum=False, exclude_list=[], + timeout: timedelta = timedelta(seconds=90)): + return self._rsync(src, dst, delete, symlinks, checksum, exclude_list, timeout, True) + + def is_remote(self): + return False + + def is_active(self): + return True + + def wait_for_connection(self, timeout: timedelta = None): + pass + + def run(self, command, timeout: timedelta = timedelta(minutes=30)): + if TestRun.dut and TestRun.dut.env: + command = f"{TestRun.dut.env} && {command}" + command_id = TestRun.LOGGER.get_new_command_id() + ip_info = TestRun.dut.ip if len(TestRun.duts) > 1 else "" + TestRun.LOGGER.write_command_to_command_log(command, command_id, info=ip_info) + output = self._execute(command, timeout) + TestRun.LOGGER.write_output_to_command_log(output, command_id) + return output + + def run_in_background(self, + command, + stdout_redirect_path="/dev/null", + stderr_redirect_path="/dev/null"): + command += f" > {stdout_redirect_path} 2> {stderr_redirect_path} &echo $!" + output = self.run(command) + + if output is not None: + return int(output.stdout) + + def wait_cmd_finish(self, pid: int, timeout: timedelta = timedelta(minutes=30)): + self.run(f"tail --pid={pid} -f /dev/null", timeout) + + def check_if_process_exists(self, pid: int): + output = self.run(f"ps aux | awk '{{print $2 }}' | grep ^{pid}$", timedelta(seconds=10)) + return True if output.exit_code == 0 else False + + def kill_process(self, pid: int): + # TERM signal should be used in preference to the KILL signal, since a + # process may install a handler for the TERM signal in order to perform + # clean-up steps before terminating in an orderly fashion. + self.run(f"kill -s SIGTERM {pid} &> /dev/null") + time.sleep(3) + self.run(f"kill -s SIGKILL {pid} &> /dev/null") + + def run_expect_success(self, command, timeout: timedelta = timedelta(minutes=30)): + output = self.run(command, timeout) + if output.exit_code != 0: + raise CmdException(f"Exception occurred while trying to execute '{command}' command.", + output) + return output + + def run_expect_fail(self, command, timeout: timedelta = timedelta(minutes=30)): + output = self.run(command, timeout) + if output.exit_code == 0: + raise CmdException(f"Command '{command}' executed properly but error was expected.", + output) + return output diff --git a/test/functional/test-framework/connection/dummy_executor.py b/test/functional/test-framework/connection/dummy_executor.py new file mode 100644 index 0000000..0001272 --- /dev/null +++ b/test/functional/test-framework/connection/dummy_executor.py @@ -0,0 +1,15 @@ +# +# Copyright(c) 2019-2021 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# + +from connection.base_executor import BaseExecutor + + +class DummyExecutor(BaseExecutor): + def _execute(self, command, timeout=None): + print(command) + + def _rsync(self, src, dst, delete, symlinks, checksum, exclude_list, timeout, + dut_to_controller): + print(f'COPY FROM "{src}" TO "{dst}"') diff --git a/test/functional/test-framework/connection/local_executor.py b/test/functional/test-framework/connection/local_executor.py new file mode 100644 index 0000000..fae9e28 --- /dev/null +++ b/test/functional/test-framework/connection/local_executor.py @@ -0,0 +1,48 @@ +# +# Copyright(c) 2019-2021 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# + +import subprocess +from datetime import timedelta + +from connection.base_executor import BaseExecutor +from test_utils.output import Output + + +class LocalExecutor(BaseExecutor): + def _execute(self, command, timeout): + completed_process = subprocess.run( + command, + shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + timeout=timeout.total_seconds()) + + return Output(completed_process.stdout, + completed_process.stderr, + completed_process.returncode) + + def _rsync(self, src, dst, delete=False, symlinks=False, checksum=False, exclude_list=[], + timeout: timedelta = timedelta(seconds=90), dut_to_controller=False): + options = [] + + if delete: + options.append("--delete") + if symlinks: + options.append("--links") + if checksum: + options.append("--checksum") + + for exclude in exclude_list: + options.append(f"--exclude {exclude}") + + completed_process = subprocess.run( + f'rsync -r {src} {dst} {" ".join(options)}', + shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + timeout=timeout.total_seconds()) + + if completed_process.returncode: + raise Exception(f"rsync failed:\n{completed_process}") diff --git a/test/functional/test-framework/connection/ssh_executor.py b/test/functional/test-framework/connection/ssh_executor.py new file mode 100644 index 0000000..237420f --- /dev/null +++ b/test/functional/test-framework/connection/ssh_executor.py @@ -0,0 +1,142 @@ +# +# Copyright(c) 2019-2021 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# + +import socket +import subprocess +import paramiko + +from datetime import timedelta, datetime +from connection.base_executor import BaseExecutor +from core.test_run import TestRun +from test_utils.output import Output + + +class SshExecutor(BaseExecutor): + def __init__(self, ip, username, port=22): + self.ip = ip + self.user = username + self.port = port + self.ssh = paramiko.SSHClient() + self._check_config_for_reboot_timeout() + + def __del__(self): + self.ssh.close() + + def connect(self, user=None, port=None, + timeout: timedelta = timedelta(seconds=30)): + user = user or self.user + port = port or self.port + self.ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + try: + self.ssh.connect(self.ip, username=user, + port=port, timeout=timeout.total_seconds(), + banner_timeout=timeout.total_seconds()) + except paramiko.AuthenticationException as e: + raise paramiko.AuthenticationException( + f"Authentication exception occurred while trying to connect to DUT. " + f"Please check your SSH key-based authentication.\n{e}") + except (paramiko.SSHException, socket.timeout) as e: + raise ConnectionError(f"An exception of type '{type(e)}' occurred while trying to " + f"connect to {self.ip}.\n {e}") + + def disconnect(self): + try: + self.ssh.close() + except Exception: + raise Exception(f"An exception occurred while trying to disconnect from {self.ip}") + + def _execute(self, command, timeout): + try: + (stdin, stdout, stderr) = self.ssh.exec_command(command, + timeout=timeout.total_seconds()) + except paramiko.SSHException as e: + raise ConnectionError(f"An exception occurred while executing command '{command}' on" + f" {self.ip}\n{e}") + + return Output(stdout.read(), stderr.read(), stdout.channel.recv_exit_status()) + + def _rsync(self, src, dst, delete=False, symlinks=False, checksum=False, exclude_list=[], + timeout: timedelta = timedelta(seconds=90), dut_to_controller=False): + options = [] + + if delete: + options.append("--delete") + if symlinks: + options.append("--links") + if checksum: + options.append("--checksum") + + for exclude in exclude_list: + options.append(f"--exclude {exclude}") + + src_to_dst = f"{self.user}@{self.ip}:{src} {dst} " if dut_to_controller else\ + f"{src} {self.user}@{self.ip}:{dst} " + + try: + completed_process = subprocess.run( + f'rsync -r -e "ssh -p {self.port} ' + f'-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no" ' + + src_to_dst + f'{" ".join(options)}', + shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + timeout=timeout.total_seconds()) + except Exception as e: + TestRun.LOGGER.exception(f"Exception occurred during rsync process. " + f"Please check your SSH key-based authentication.\n{e}") + + if completed_process.returncode: + raise Exception(f"rsync failed:\n{completed_process}") + + def is_remote(self): + return True + + def _check_config_for_reboot_timeout(self): + if "reboot_timeout" in TestRun.config.keys(): + self._parse_timeout_to_int() + else: + self.reboot_timeout = None + + def _parse_timeout_to_int(self): + self.reboot_timeout = int(TestRun.config["reboot_timeout"]) + if self.reboot_timeout < 0: + raise ValueError("Reboot timeout cannot be negative.") + + def reboot(self): + self.run("reboot") + self.wait_for_connection_loss() + self.wait_for_connection(timedelta(seconds=self.reboot_timeout)) \ + if self.reboot_timeout is not None else self.wait_for_connection() + + def is_active(self): + try: + self.ssh.exec_command('', timeout=5) + return True + except Exception: + return False + + def wait_for_connection(self, timeout: timedelta = timedelta(minutes=10)): + start_time = datetime.now() + with TestRun.group("Waiting for DUT ssh connection"): + while start_time + timeout > datetime.now(): + try: + self.connect() + return + except paramiko.AuthenticationException: + raise + except Exception: + continue + raise ConnectionError("Timeout occurred while trying to establish ssh connection") + + def wait_for_connection_loss(self, timeout: timedelta = timedelta(minutes=1)): + with TestRun.group("Waiting for DUT ssh connection loss"): + end_time = datetime.now() + timeout + while end_time > datetime.now(): + self.disconnect() + try: + self.connect(timeout=timedelta(seconds=5)) + except Exception: + return + raise ConnectionError("Timeout occurred before ssh connection loss") diff --git a/test/functional/test-framework/core/__init__.py b/test/functional/test-framework/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/functional/test-framework/core/pair_testing.py b/test/functional/test-framework/core/pair_testing.py new file mode 100644 index 0000000..b7eea73 --- /dev/null +++ b/test/functional/test-framework/core/pair_testing.py @@ -0,0 +1,107 @@ +# +# Copyright(c) 2020-2021 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# + +# The MIT License (MIT) +# +# Copyright (c) 2004-2020 Holger Krekel and others +# +# Permission is hereby granted, free of charge, to any person obtaining a copy of +# this software and associated documentation files (the "Software"), to deal in +# the Software without restriction, including without limitation the rights to +# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +# of the Software, and to permit persons to whom the Software is furnished to do +# so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + + +from itertools import product, combinations +import random + +from core.test_run import TestRun + +def testcase_id(param_set): + if len(param_set.values) == 1: + return param_set.values[0] + + return "-".join([str(value) for value in param_set.values]) + + +def generate_pair_testing_testcases(*argvals): + """ + Generate test_cases from provided argument values lists in such way that each possible + (argX, argY) pair will be used. + """ + # if only one argument is used, yield from it + if len(argvals) == 1: + for val in argvals[0]: + yield (val,) + + # append argument index to argument values list to avoid confusion when there are two arguments + # with the same type + for i, arg in enumerate(argvals): + for j, val in enumerate(arg): + arg[j] = (i, val) + + # generate all possible test cases + all_test_cases = list(product(*argvals)) + random.seed(TestRun.random_seed) + random.shuffle(all_test_cases) + + used_pairs = set() + for tc in all_test_cases: + current_pairs = set(combinations(tc, 2)) + # if cardinality of (current_pairs & used_pairs) is lesser than cardinality of current_pairs + # it means not all argument pairs in this tc have been used. return current tc + # and update used_pairs set + if len(current_pairs & used_pairs) != len(current_pairs): + used_pairs.update(current_pairs) + # unpack testcase by deleting argument index + yield list(list(zip(*tc))[1]) + + +def register_testcases(metafunc, argnames, argvals): + """ + Add custom parametrization test cases. Based on metafunc's parametrize method. + """ + from _pytest.python import CallSpec2, _find_parametrized_scope + from _pytest.mark import ParameterSet + from _pytest.fixtures import scope2index + + parameter_sets = [ParameterSet(values=val, marks=[], id=None) for val in argvals] + metafunc._validate_if_using_arg_names(argnames, False) + + arg_value_types = metafunc._resolve_arg_value_types(argnames, False) + + ids = [testcase_id(param_set) for param_set in parameter_sets] + + scope = _find_parametrized_scope(argnames, metafunc._arg2fixturedefs, False) + scopenum = scope2index(scope, descr=f"parametrizex() call in {metafunc.function.__name__}") + + calls = [] + for callspec in metafunc._calls or [CallSpec2(metafunc)]: + for param_index, (param_id, param_set) in enumerate(zip(ids, parameter_sets)): + newcallspec = callspec.copy() + newcallspec.setmulti2( + arg_value_types, + argnames, + param_set.values, + param_id, + param_set.marks, + scopenum, + param_index, + ) + calls.append(newcallspec) + + metafunc._calls = calls diff --git a/test/functional/test-framework/core/plugins.py b/test/functional/test-framework/core/plugins.py new file mode 100644 index 0000000..7d36224 --- /dev/null +++ b/test/functional/test-framework/core/plugins.py @@ -0,0 +1,124 @@ +# +# Copyright(c) 2020-2021 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# + +import pytest +import sys +import importlib +import signal +from core.test_run import TestRun + + +class PluginManager: + def __init__(self, item, config): + if 'plugins_dir' in config: + sys.path.append(config['plugins_dir']) + self.plugins = {} + + self.plugins_config = config.get('plugins', {}) + + self.req_plugins = config.get('req_plugins', {}) + self.opt_plugins = config.get('opt_plugins', {}) + + self.req_plugins.update(dict(map(lambda mark: (mark.args[0], mark.kwargs), + item.iter_markers(name="require_plugin")))) + + req_plugin_mod = {} + opt_plugin_mod = {} + + for name in self.req_plugins: + try: + req_plugin_mod[name] = self.__import_plugin(name) + except ModuleNotFoundError: + pytest.skip("Unable to find requested plugin!") + + for name in self.opt_plugins: + try: + opt_plugin_mod[name] = self.__import_plugin(name) + except ModuleNotFoundError as e: + TestRun.LOGGER.debug( + f"Failed to import '{name}' - optional plugin. " f"Reason: {e}" + ) + continue + + for name, mod in req_plugin_mod.items(): + try: + self.plugins[name] = mod.plugin_class( + self.req_plugins[name], + self.plugins_config.get(name, {}).get("config", {})) + except Exception: + pytest.skip(f"Unable to initialize plugin '{name}'") + + for name, mod in opt_plugin_mod.items(): + try: + self.plugins[name] = mod.plugin_class( + self.opt_plugins[name], + self.plugins_config.get(name, {}).get("config", {})) + except Exception as e: + TestRun.LOGGER.debug( + f"Failed to initialize '{name}' - optional plugin. " f"Reason: {e}" + ) + continue + + def __import_plugin(self, name): + provided_by = self.plugins_config.get(name, {}).get("provided_by") + if provided_by: + return importlib.import_module(provided_by) + + try: + return importlib.import_module(f"internal_plugins.{name}") + except ModuleNotFoundError: + pass + + return importlib.import_module(f"external_plugins.{name}") + + def hook_pre_setup(self): + for plugin in self.plugins.values(): + plugin.pre_setup() + + def hook_post_setup(self): + for plugin in self.plugins.values(): + plugin.post_setup() + + def hook_teardown(self): + for plugin in self.plugins.values(): + plugin.teardown() + + def get_plugin(self, name): + if name not in self.plugins: + raise KeyError("Requested plugin does not exist") + return self.plugins[name] + + def teardown_on_signal(self, sig_id, plugin_name): + try: + plugin = self.get_plugin(plugin_name) + except Exception as e: + TestRun.LOGGER.warning( + f"Failed to setup teardown on signal for {plugin_name}. Reason: {e}") + return + + old_sig_handler = None + + def signal_handler(sig, frame): + plugin.teardown() + + if old_sig_handler is not None: + if old_sig_handler == signal.SIG_DFL: + # In case of SIG_DFL the function pointer points to address 0, + # which is not a valid address. + # We have to reset the handler and raise the signal again + signal.signal(sig, signal.SIG_DFL) + signal.raise_signal(sig) + signal.signal(sig, signal_handler) + elif old_sig_handler == signal.SIG_IGN: + # SIG_IGN has value 1 (also an invalid address). + # Here we can just return (do nothing) + return + else: + # When we received neither SIG_IGN nor SIG_DFL, the received value is + # a valid function pointer and we can call the handler directly + old_sig_handler() + signal.signal(sig, old_sig_handler) + + old_sig_handler = signal.signal(sig_id, signal_handler) diff --git a/test/functional/test-framework/core/test_run.py b/test/functional/test-framework/core/test_run.py new file mode 100644 index 0000000..ae0762e --- /dev/null +++ b/test/functional/test-framework/core/test_run.py @@ -0,0 +1,65 @@ +# +# Copyright(c) 2019-2021 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# + + +from contextlib import contextmanager + +import pytest + +from log.logger import Log + + +class Blocked(Exception): + pass + + +class TestRun: + dut = None + executor = None + LOGGER: Log = None + plugin_manager = None + duts = None + disks = None + + @classmethod + @contextmanager + def use_dut(cls, dut): + cls.dut = dut + cls.config = cls.dut.config + cls.executor = cls.dut.executor + cls.plugin_manager = cls.dut.plugin_manager + cls.disks = cls.dut.req_disks + yield cls.executor + cls.disks = None + cls.plugin_manager = None + cls.executor = None + # setting cls.config to None omitted (causes problems in the teardown stage of execution) + cls.dut = None + + @classmethod + def step(cls, message): + return cls.LOGGER.step(message) + + @classmethod + def group(cls, message): + return cls.LOGGER.group(message) + + @classmethod + def iteration(cls, iterable, group_name=None): + TestRun.LOGGER.start_group(f"{group_name}" if group_name is not None else "Iteration list") + items = list(iterable) + for i, item in enumerate(items, start=1): + cls.LOGGER.start_iteration(f"Iteration {i}/{len(items)}") + yield item + TestRun.LOGGER.end_iteration() + TestRun.LOGGER.end_group() + + @classmethod + def fail(cls, message): + pytest.fail(message) + + @classmethod + def block(cls, message): + raise Blocked(message) diff --git a/test/functional/test-framework/core/test_run_utils.py b/test/functional/test-framework/core/test_run_utils.py new file mode 100644 index 0000000..a27d018 --- /dev/null +++ b/test/functional/test-framework/core/test_run_utils.py @@ -0,0 +1,272 @@ +# +# Copyright(c) 2019-2021 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# + +import posixpath +import random +import sys +import traceback + +import pytest +from IPy import IP + +import core.test_run +from connection.local_executor import LocalExecutor +from connection.ssh_executor import SshExecutor +from core.pair_testing import generate_pair_testing_testcases, register_testcases +from core.plugins import PluginManager +from log.base_log import BaseLogResult +from storage_devices.disk import Disk +from test_utils import disk_finder +from test_utils.dut import Dut + +TestRun = core.test_run.TestRun + + +@classmethod +def __configure(cls, config): + config.addinivalue_line( + "markers", + "require_disk(name, type): require disk of specific type, otherwise skip" + ) + config.addinivalue_line( + "markers", + "require_plugin(name, *kwargs): require specific plugins, otherwise skip" + ) + config.addinivalue_line( + "markers", + "remote_only: run test only in case of remote execution, otherwise skip" + ) + config.addinivalue_line( + "markers", + "os_dependent: run test only if its OS dependent, otherwise skip" + ) + config.addinivalue_line( + "markers", + "multidut(number): test requires a number of different platforms to be executed" + ) + config.addinivalue_line( + "markers", + "parametrizex(argname, argvalues): sparse parametrized testing" + ) + config.addinivalue_line( + "markers", + "CI: marks test for continuous integration pipeline" + ) + + cls.random_seed = config.getoption("--random-seed") or random.randrange(sys.maxsize) + random.seed(cls.random_seed) + + +TestRun.configure = __configure + + +@classmethod +def __prepare(cls, item, config): + if not config: + raise Exception("You need to specify DUT config!") + + cls.item = item + cls.config = config + + req_disks = list(map(lambda mark: mark.args, cls.item.iter_markers(name="require_disk"))) + cls.req_disks = dict(req_disks) + if len(req_disks) != len(cls.req_disks): + raise Exception("Disk name specified more than once!") + + +TestRun.prepare = __prepare + + +@classmethod +def __attach_log(cls, log_path, target_name=None): + if target_name is None: + target_name = posixpath.basename(log_path) + if cls.config.get('extra_logs'): + cls.config["extra_logs"][target_name] = log_path + else: + cls.config["extra_logs"] = {target_name: log_path} + + +TestRun.attach_log = __attach_log + + +@classmethod +def __setup_disk(cls, disk_name, disk_type): + cls.disks[disk_name] = next(filter( + lambda disk: disk.disk_type in disk_type.types() and disk not in cls.disks.values(), + cls.dut.disks + ), None) + if not cls.disks[disk_name]: + pytest.skip("Unable to find requested disk!") + + +TestRun.__setup_disk = __setup_disk + + +@classmethod +def __setup_disks(cls): + cls.disks = {} + items = list(cls.req_disks.items()) + while items: + resolved, unresolved = [], [] + for disk_name, disk_type in items: + (resolved, unresolved)[not disk_type.resolved()].append((disk_name, disk_type)) + resolved.sort( + key=lambda disk: (lambda disk_name, disk_type: disk_type)(*disk) + ) + for disk_name, disk_type in resolved: + cls.__setup_disk(disk_name, disk_type) + items = unresolved + cls.dut.req_disks = cls.disks + + +TestRun.__setup_disks = __setup_disks + + +@classmethod +def __presetup(cls): + cls.plugin_manager = PluginManager(cls.item, cls.config) + cls.plugin_manager.hook_pre_setup() + + if cls.config['type'] == 'ssh': + try: + IP(cls.config['ip']) + except ValueError: + TestRun.block("IP address from config is in invalid format.") + + port = cls.config.get('port', 22) + + if 'user' in cls.config: + cls.executor = SshExecutor( + cls.config['ip'], + cls.config['user'], + port + ) + else: + TestRun.block("There is no user given in config.") + elif cls.config['type'] == 'local': + cls.executor = LocalExecutor() + else: + TestRun.block("Execution type (local/ssh) is missing in DUT config!") + + +TestRun.presetup = __presetup + + +@classmethod +def __setup(cls): + if list(cls.item.iter_markers(name="remote_only")): + if not cls.executor.is_remote(): + pytest.skip() + + Disk.plug_all_disks() + if cls.config.get('allow_disk_autoselect', False): + cls.config["disks"] = disk_finder.find_disks() + + try: + cls.dut = Dut(cls.config) + except Exception as ex: + raise Exception(f"Failed to setup DUT instance:\n" + f"{str(ex)}\n{traceback.format_exc()}") + cls.__setup_disks() + + TestRun.LOGGER.info(f"Re-seeding random number generator with seed: {cls.random_seed}") + random.seed(cls.random_seed) + + cls.plugin_manager.hook_post_setup() + + +TestRun.setup = __setup + + +@classmethod +def __makereport(cls, item, call, res): + cls.outcome = res.outcome + step_info = { + 'result': res.outcome, + 'exception': str(call.excinfo.value) if call.excinfo else None + } + setattr(item, "rep_" + res.when, step_info) + + from _pytest.outcomes import Failed + from core.test_run import Blocked + if res.when == "call" and res.failed: + msg = f"{call.excinfo.type.__name__}: {call.excinfo.value}" + if call.excinfo.type is Failed: + cls.LOGGER.error(msg) + elif call.excinfo.type is Blocked: + cls.LOGGER.blocked(msg) + else: + cls.LOGGER.exception(msg) + elif res.when == "setup" and res.failed: + msg = f"{call.excinfo.type.__name__}: {call.excinfo.value}" + cls.LOGGER.exception(msg) + res.outcome = "failed" + + if res.outcome == "skipped": + cls.LOGGER.skip("Test skipped.") + + if res.when == "call" and cls.LOGGER.get_result() == BaseLogResult.FAILED: + res.outcome = "failed" + # To print additional message in final test report, assign it to res.longrepr + + cls.LOGGER.generate_summary(item, cls.config.get('meta')) + + +TestRun.makereport = __makereport + + +@classmethod +def __generate_tests(cls, metafunc): + marks = getattr(metafunc.function, "pytestmark", []) + + parametrizex_marks = [ + mark for mark in marks if mark.name == "parametrizex" + ] + + if not parametrizex_marks: + random.seed(TestRun.random_seed) + return + + argnames = [] + argvals = [] + for mark in parametrizex_marks: + argnames.append(mark.args[0]) + argvals.append(list(mark.args[1])) + + if metafunc.config.getoption("--parametrization-type") == "full": + for name, values in zip(argnames, argvals): + metafunc.parametrize(name, values) + elif metafunc.config.getoption("--parametrization-type") == "pair": + test_cases = generate_pair_testing_testcases(*argvals) + + register_testcases(metafunc, argnames, test_cases) + else: + raise Exception("Not supported parametrization type") + + random.seed(TestRun.random_seed) + + +TestRun.generate_tests = __generate_tests + + +@classmethod +def __addoption(cls, parser): + parser.addoption("--parametrization-type", choices=["pair", "full"], default="pair") + parser.addoption("--random-seed", type=int, default=None) + + +TestRun.addoption = __addoption + + +@classmethod +def __teardown(cls): + for dut in cls.duts: + with cls.use_dut(dut): + if cls.plugin_manager: + cls.plugin_manager.hook_teardown() + + +TestRun.teardown = __teardown diff --git a/test/functional/test-framework/internal_plugins/__init__.py b/test/functional/test-framework/internal_plugins/__init__.py new file mode 100644 index 0000000..dce958c --- /dev/null +++ b/test/functional/test-framework/internal_plugins/__init__.py @@ -0,0 +1,4 @@ +# +# Copyright(c) 2019-2021 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# diff --git a/test/functional/test-framework/internal_plugins/example_plugin/__init__.py b/test/functional/test-framework/internal_plugins/example_plugin/__init__.py new file mode 100644 index 0000000..4bdec67 --- /dev/null +++ b/test/functional/test-framework/internal_plugins/example_plugin/__init__.py @@ -0,0 +1,22 @@ +# +# Copyright(c) 2020-2021 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# + + +class ExamplePlugin: + def __init__(self, params, config): + self.params = params + print(f"Example plugin initialized with params {self.params}") + + def pre_setup(self): + print("Example plugin pre setup") + + def post_setup(self): + print("Example plugin post setup") + + def teardown(self): + print("Example plugin teardown") + + +plugin_class = ExamplePlugin diff --git a/test/functional/test-framework/internal_plugins/power_control_libvirt/__init__.py b/test/functional/test-framework/internal_plugins/power_control_libvirt/__init__.py new file mode 100644 index 0000000..ca2993e --- /dev/null +++ b/test/functional/test-framework/internal_plugins/power_control_libvirt/__init__.py @@ -0,0 +1,48 @@ +# +# Copyright(c) 2020-2021 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# +from datetime import timedelta + +from connection.local_executor import LocalExecutor +from connection.ssh_executor import SshExecutor +from core.test_run import TestRun + + +class PowerControlPlugin: + def __init__(self, params, config): + print("Power Control LibVirt Plugin initialization") + try: + self.ip = config['ip'] + self.user = config['user'] + except Exception: + raise Exception("Missing fields in config! ('ip' and 'user' required)") + + def pre_setup(self): + print("Power Control LibVirt Plugin pre setup") + if self.config['connection_type'] == 'ssh': + self.executor = SshExecutor( + self.ip, + self.user, + self.config.get('port', 22) + ) + else: + self.executor = LocalExecutor() + + def post_setup(self): + pass + + def teardown(self): + pass + + def power_cycle(self): + self.executor.run(f"virsh reset {self.config['domain']}") + TestRun.executor.wait_for_connection_loss() + timeout = TestRun.config.get('reboot_timeout') + if timeout: + TestRun.executor.wait_for_connection(timedelta(seconds=int(timeout))) + else: + TestRun.executor.wait_for_connection() + + +plugin_class = PowerControlPlugin diff --git a/test/functional/test-framework/internal_plugins/scsi_debug/__init__.py b/test/functional/test-framework/internal_plugins/scsi_debug/__init__.py new file mode 100644 index 0000000..95f6563 --- /dev/null +++ b/test/functional/test-framework/internal_plugins/scsi_debug/__init__.py @@ -0,0 +1,39 @@ +# +# Copyright(c) 2020-2021 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# +from time import sleep + +from core.test_run_utils import TestRun +from storage_devices.device import Device +from test_utils import os_utils +from test_utils.output import CmdException + + +class ScsiDebug: + def __init__(self, params, config): + self.params = params + self.module_name = "scsi_debug" + + def pre_setup(self): + pass + + def post_setup(self): + self.reload() + + def reload(self): + self.teardown() + sleep(1) + load_output = os_utils.load_kernel_module(self.module_name, self.params) + if load_output.exit_code != 0: + raise CmdException(f"Failed to load {self.module_name} module", load_output) + TestRun.LOGGER.info(f"{self.module_name} loaded successfully.") + sleep(10) + TestRun.scsi_debug_devices = Device.get_scsi_debug_devices() + + def teardown(self): + if os_utils.is_kernel_module_loaded(self.module_name): + os_utils.unload_kernel_module(self.module_name) + + +plugin_class = ScsiDebug diff --git a/test/functional/test-framework/internal_plugins/vdbench/__init__.py b/test/functional/test-framework/internal_plugins/vdbench/__init__.py new file mode 100644 index 0000000..d0f7f71 --- /dev/null +++ b/test/functional/test-framework/internal_plugins/vdbench/__init__.py @@ -0,0 +1,97 @@ +# +# Copyright(c) 2020-2021 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# + +import time +import posixpath + +from datetime import timedelta +from core.test_run import TestRun +from test_tools import fs_utils + + +class Vdbench: + def __init__(self, params, config): + print("VDBench plugin initialization") + self.run_time = timedelta(seconds=60) + + try: + self.working_dir = config["working_dir"] + self.reinstall = config["reinstall"] + self.source_dir = config["source_dir"] + except Exception: + raise Exception("Missing fields in config! ('working_dir', 'source_dir' and " + "'reinstall' required)") + + self.result_dir = posixpath.join(self.working_dir, 'result.tod') + + def pre_setup(self): + pass + + def post_setup(self): + print("VDBench plugin post setup") + if not self.reinstall and fs_utils.check_if_directory_exists(self.working_dir): + return + + if fs_utils.check_if_directory_exists(self.working_dir): + fs_utils.remove(self.working_dir, True, True) + + fs_utils.create_directory(self.working_dir) + TestRun.LOGGER.info("Copying vdbench to working dir.") + fs_utils.copy(posixpath.join(self.source_dir, "*"), self.working_dir, + True, True) + pass + + def teardown(self): + pass + + def create_config(self, config, run_time: timedelta): + self.run_time = run_time + if config[-1] != ",": + config += "," + config += f"elapsed={int(run_time.total_seconds())}" + TestRun.LOGGER.info(f"Vdbench config:\n{config}") + fs_utils.write_file(posixpath.join(self.working_dir, "param.ini"), config) + + def run(self): + cmd = f"{posixpath.join(self.working_dir, 'vdbench')} " \ + f"-f {posixpath.join(self.working_dir, 'param.ini')} " \ + f"-vr -o {self.result_dir}" + full_cmd = f"screen -dmS vdbench {cmd}" + TestRun.executor.run(full_cmd) + start_time = time.time() + + timeout = self.run_time * 1.5 + + while True: + if not TestRun.executor.run(f"ps aux | grep '{cmd}' | grep -v grep").exit_code == 0: + return self.analyze_log() + + if time.time() - start_time > timeout.total_seconds(): + TestRun.LOGGER.error("Vdbench timeout.") + return False + time.sleep(1) + + def analyze_log(self): + output = TestRun.executor.run( + f"ls -1td {self.result_dir[0:len(self.result_dir) - 3]}* | head -1") + log_path = posixpath.join(output.stdout if output.exit_code == 0 else self.result_dir, + "logfile.html") + + log_file = fs_utils.read_file(log_path) + + if "Vdbench execution completed successfully" in log_file: + TestRun.LOGGER.info("Vdbench execution completed successfully.") + return True + + if "Data Validation error" in log_file or "data_errors=1" in log_file: + TestRun.LOGGER.error("Data corruption occurred!") + elif "Heartbeat monitor:" in log_file: + TestRun.LOGGER.error("Vdbench: heartbeat.") + else: + TestRun.LOGGER.error("Vdbench unknown result.") + return False + + +plugin_class = Vdbench diff --git a/test/functional/test-framework/log/__init__.py b/test/functional/test-framework/log/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/functional/test-framework/log/base_log.py b/test/functional/test-framework/log/base_log.py new file mode 100644 index 0000000..0f717a4 --- /dev/null +++ b/test/functional/test-framework/log/base_log.py @@ -0,0 +1,78 @@ +# +# Copyright(c) 2019-2021 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# + +from enum import Enum +from re import sub + + +class BaseLogResult(Enum): + DEBUG = 10 + PASSED = 11 + WORKAROUND = 12 + WARNING = 13 + SKIPPED = 14 + FAILED = 15 + EXCEPTION = 16 + BLOCKED = 17 + CRITICAL = 18 + + +def escape(msg): + return sub(u'[^\u0020-\uD7FF\u0009\u000A\u000D\uE000-\uFFFD\U00010000-\U0010FFFF]+', '', msg) + + +class BaseLog(): + def __init__(self, begin_message=None): + self.__begin_msg = begin_message + self.__result = BaseLogResult.PASSED + + def __enter__(self): + if self.__begin_msg is not None: + self.begin(self.__begin_msg) + else: + self.begin("Start BaseLog ...") + + def __exit__(self, *args): + self.end() + + def __try_to_set_new_result(self, new_result): + if new_result.value > self.__result.value: + self.__result = new_result + + def begin(self, message): + pass + + def debug(self, message): + pass + + def info(self, message): + pass + + def workaround(self, message): + self.__try_to_set_new_result(BaseLogResult.WORKAROUND) + + def warning(self, message): + self.__try_to_set_new_result(BaseLogResult.WARNING) + + def skip(self, message): + self.__try_to_set_new_result(BaseLogResult.SKIPPED) + + def error(self, message): + self.__try_to_set_new_result(BaseLogResult.FAILED) + + def blocked(self, message): + self.__try_to_set_new_result(BaseLogResult.BLOCKED) + + def exception(self, message): + self.__try_to_set_new_result(BaseLogResult.EXCEPTION) + + def critical(self, message): + self.__try_to_set_new_result(BaseLogResult.CRITICAL) + + def end(self): + return self.__result + + def get_result(self): + return self.__result diff --git a/test/functional/test-framework/log/group/__init__.py b/test/functional/test-framework/log/group/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/functional/test-framework/log/group/html_chapter_group_log.py b/test/functional/test-framework/log/group/html_chapter_group_log.py new file mode 100644 index 0000000..56fc7ca --- /dev/null +++ b/test/functional/test-framework/log/group/html_chapter_group_log.py @@ -0,0 +1,43 @@ +# +# Copyright(c) 2019-2021 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# + +from log.base_log import BaseLogResult, BaseLog +from log.group.html_group_log import HtmlGroupLog +from datetime import datetime + + +class HtmlChapterGroupLog(HtmlGroupLog): + SET_RESULT = { + BaseLogResult.PASSED: BaseLog.info, + BaseLogResult.WORKAROUND: BaseLog.workaround, + BaseLogResult.WARNING: BaseLog.warning, + BaseLogResult.SKIPPED: BaseLog.skip, + BaseLogResult.FAILED: BaseLog.error, + BaseLogResult.BLOCKED: BaseLog.blocked, + BaseLogResult.EXCEPTION: BaseLog.exception, + BaseLogResult.CRITICAL: BaseLog.critical} + + def __init__(self, html_base, cfg, begin_msg=None, id='ch0'): + super().__init__(HtmlChapterGroupLog._factory, html_base, cfg, begin_msg, id) + + @staticmethod + def _factory(html_base, cfg, begin_msg, id): + return HtmlChapterGroupLog(html_base, cfg, begin_msg, id) + + def end_dir_group(self, ref_group): + group = super().end_group() + ref_container_id = ref_group._container.get('id') + group._header.set('ondblclick', f"chapterClick('{ref_container_id}')") + + def set_result(self, result): + if self._successor is not None: + self._successor.set_result(result) + HtmlChapterGroupLog.SET_RESULT[result](self, "set result") + + def end(self): + result = super().end() + exe_time = (datetime.now() - self._start_time).seconds + self._cfg.group_chapter_end(exe_time, self._header, self._container, result) + return result diff --git a/test/functional/test-framework/log/group/html_group_log.py b/test/functional/test-framework/log/group/html_group_log.py new file mode 100644 index 0000000..cba5075 --- /dev/null +++ b/test/functional/test-framework/log/group/html_group_log.py @@ -0,0 +1,139 @@ +# +# Copyright(c) 2019-2021 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# + +from datetime import datetime +from log.base_log import BaseLog, BaseLogResult + + +class HtmlGroupLog(BaseLog): + def __init__(self, constructor, html_base_element, cfg, begin_message, id_): + super().__init__(begin_message) + self._successor = None + self.__factory = constructor + self.__log_main_store = html_base_element + self._id = id_ + self._container = None + self._header = None + self.__msg_idx = 0 + self._start_time = datetime.now() + self._cfg = cfg + self._header_msg_type = type(begin_message) + + def begin(self, message): + policy = self._cfg.get_policy(type(message)) + self._header, self._container = policy.group_begin(self._id, message, self.__log_main_store) + super().begin(message) + + def get_step_id(self): + if self._successor is not None: + return self._successor.get_step_id() + else: + return f'step.{self._id}.{self.__msg_idx}' + + def __add_test_step(self, message, result=BaseLogResult.PASSED): + policy = self._cfg.get_policy(type(message)) + policy.standard(self.get_step_id(), message, result, self._container) + self.__msg_idx += 1 + + def get_main_log_store(self): + return self.__log_main_store + + def start_group(self, message): + self._header_msg_type = type(message) + if self._successor is not None: + result = self._successor.start_group(message) + else: + new_id = f"{self._id}.{self.__msg_idx}" + self.__msg_idx += 1 + self._successor = self.__factory(self._container, self._cfg, message, new_id) + self._successor.begin(message) + result = self._successor + return result + + def end_group(self): + if self._successor is not None: + if self._successor._successor is None: + self._successor.end() + result = self._successor + self._successor = None + else: + result = self._successor.end_group() + else: + self.end() + result = self + return result + + def debug(self, message): + if self._successor is not None: + self._successor.debug(message) + else: + self.__add_test_step(message, BaseLogResult.DEBUG) + return super().debug(message) + + def info(self, message): + if self._successor is not None: + self._successor.info(message) + else: + self.__add_test_step(message) + super().info(message) + + def workaround(self, message): + if self._successor is not None: + self._successor.workaround(message) + else: + self.__add_test_step(message, BaseLogResult.WORKAROUND) + super().workaround(message) + + def warning(self, message): + if self._successor is not None: + self._successor.warning(message) + else: + self.__add_test_step(message, BaseLogResult.WARNING) + super().warning(message) + + def skip(self, message): + if self._successor is not None: + self._successor.skip(message) + else: + self.__add_test_step(message, BaseLogResult.SKIPPED) + super().skip(message) + + def error(self, message): + if self._successor is not None: + self._successor.error(message) + else: + self.__add_test_step(message, BaseLogResult.FAILED) + super().error(message) + + def blocked(self, message): + if self._successor is not None: + self._successor.blocked(message) + else: + self.__add_test_step(message, BaseLogResult.BLOCKED) + super().blocked(message) + + def critical(self, message): + if self._successor is not None: + self._successor.critical(message) + else: + self.__add_test_step(message, BaseLogResult.CRITICAL) + super().critical(message) + + def exception(self, message): + if self._successor is not None: + self._successor.exception(message) + else: + self.__add_test_step(message, BaseLogResult.EXCEPTION) + super().exception(message) + + def end(self): + return super().end() + + def get_current_group(self): + if self._successor is not None: + result = self._successor.get_current_group() + else: + result = self + return result diff --git a/test/functional/test-framework/log/group/html_iteration_group_log.py b/test/functional/test-framework/log/group/html_iteration_group_log.py new file mode 100644 index 0000000..d594018 --- /dev/null +++ b/test/functional/test-framework/log/group/html_iteration_group_log.py @@ -0,0 +1,20 @@ +# +# Copyright(c) 2019-2021 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# + +from log.group.html_group_log import HtmlGroupLog + + +class HtmlIterationGroupLog(HtmlGroupLog): + def __init__(self, html_base, cfg, begin_msg, id='itg0'): + super().__init__(HtmlIterationGroupLog._factory, html_base, cfg, begin_msg, id) + + @staticmethod + def _factory(html_base, cfg, begin_msg, id): + return HtmlIterationGroupLog(html_base, cfg, begin_msg, id) + + def end(self): + result = super().end() + self._cfg.group_end(self._id, self._header, self._container, result) + return result diff --git a/test/functional/test-framework/log/html_file_item_log.py b/test/functional/test-framework/log/html_file_item_log.py new file mode 100644 index 0000000..b529762 --- /dev/null +++ b/test/functional/test-framework/log/html_file_item_log.py @@ -0,0 +1,102 @@ +# +# Copyright(c) 2019-2021 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# + +from log.html_file_log import HtmlFileLog +from log.group.html_chapter_group_log import HtmlChapterGroupLog +from log.group.html_iteration_group_log import HtmlIterationGroupLog +from datetime import datetime +from lxml.etree import Element + + +class HtmlFileItemLog(HtmlFileLog): + def __init__(self, html_file_path, test_title, cfg, iteration_title="Test summary"): + super().__init__(html_file_path, test_title) + root = self.get_root() + self._log_items_store = root.xpath('/html/body')[0] + self._idx = 0 + self._log_chapters_store = root.xpath('/html/body/section[@id="iteration-chapters"]')[0] + self._chapter_group = HtmlChapterGroupLog(self._log_chapters_store, cfg, test_title) + self._main_group = HtmlIterationGroupLog(self._log_items_store, cfg, test_title) + self._start_time = datetime.now() + iteration_title_node = root.xpath('/html/body/a/h1')[0] + iteration_title_node.text = iteration_title + self._config = cfg + self._fail_container = root.xpath('/html/body/div/select[@id="error-list-selector"]')[0] + + def __add_error(self, msg_idx, msg, error_class): + fail_element = Element('option', value=msg_idx) + fail_element.set('class', error_class) + fail_element.text = msg + self._fail_container.append(fail_element) + + def start_iteration(self, message): + super().begin(message) + + def get_result(self): + return self._main_group.get_result() + + def begin(self, message): + self._chapter_group.begin(message) + self._main_group.begin(message) + + def debug(self, message): + self._main_group.debug(message) + + def info(self, message): + self._main_group.info(message) + + def workaround(self, message): + self._main_group.workaround(message) + + def warning(self, message): + self._main_group.warning(message) + + def skip(self, message): + self._main_group.skip(message) + + def error(self, message): + msg_idx = self._main_group.get_step_id() + self.__add_error(msg_idx, message, "fail") + self._main_group.error(message) + + def blocked(self, message): + msg_idx = self._main_group.get_step_id() + self.__add_error(msg_idx, message, "blocked") + self._main_group.blocked(message) + + def exception(self, message): + msg_idx = self._main_group.get_step_id() + self.__add_error(msg_idx, message, "exception") + self._main_group.exception(message) + + def critical(self, message): + msg_idx = self._main_group.get_step_id() + self.__add_error(msg_idx, message, "critical") + self._main_group.critical(message) + + def start_group(self, message): + self._chapter_group.start_group(message) + self._main_group.start_group(message) + + def end_group(self): + ref_group = self._main_group.get_current_group() + self._chapter_group.set_result(ref_group.get_result()) + self._main_group.end_group() + self._chapter_group.end_dir_group(ref_group) + + def end_all_groups(self): + while self._main_group._successor is not None: + self.end_group() + + def end(self): + while self._main_group._successor is not None: + self.end_group() + self.end_group() + time_result = datetime.now() - self._start_time + time_node = self.get_root().xpath('/html/body/div[@class="iteration-execution-time"]')[0] + status_node = self.get_root().xpath('/html/body/div[@class="iteration-status"]')[0] + self._config.end_iteration_func( + time_node, status_node, time_result.total_seconds(), self.get_result()) + super().end() diff --git a/test/functional/test-framework/log/html_file_log.py b/test/functional/test-framework/log/html_file_log.py new file mode 100644 index 0000000..55680da --- /dev/null +++ b/test/functional/test-framework/log/html_file_log.py @@ -0,0 +1,29 @@ +# +# Copyright(c) 2019-2021 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# + +from log.base_log import BaseLog +from lxml.html import fromstring +from lxml.html import tostring + + +class HtmlFileLog(BaseLog): + def __init__(self, file_path, title): + super().__init__(title) + self.__path = file_path + with open(file_path) as file_stream: + self.__root = fromstring(file_stream.read()) + node_list = self.__root.xpath('/html/head/title') + node_list[0].text = title + + def get_path(self): + return self.__path + + def get_root(self): + return self.__root + + def end(self): + with open(self.__path, "wb") as file: + x = tostring(self.__root) + file.write(x) diff --git a/test/functional/test-framework/log/html_iteration_log.py b/test/functional/test-framework/log/html_iteration_log.py new file mode 100644 index 0000000..b4f6c4c --- /dev/null +++ b/test/functional/test-framework/log/html_iteration_log.py @@ -0,0 +1,13 @@ +# +# Copyright(c) 2019-2021 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# + +from log.html_file_item_log import HtmlFileItemLog + + +class HtmlIterationLog(HtmlFileItemLog): + def __init__(self, test_title, iteration_title, config): + self.iteration_closed: bool = False + html_file = config.create_iteration_file() + super().__init__(html_file, test_title, config, iteration_title) diff --git a/test/functional/test-framework/log/html_log_config.py b/test/functional/test-framework/log/html_log_config.py new file mode 100644 index 0000000..d06e7f0 --- /dev/null +++ b/test/functional/test-framework/log/html_log_config.py @@ -0,0 +1,204 @@ +# +# Copyright(c) 2019-2021 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# + +import os +from os import path, environ, makedirs +from datetime import datetime +from shutil import copyfile +from lxml.etree import Element +from log.base_log import BaseLogResult +from log.presentation_policy import null_policy + + +def convert_seconds_to_str(time_in_sec): + h = str(int(time_in_sec / 3600) % 24).zfill(2) + m = str(int(time_in_sec / 60) % 60).zfill(2) + s = str(int(time_in_sec % 60)).zfill(2) + time_msg = f"{h}:{m}:{s} [s]" + if time_in_sec > 86400: + time_msg = f"{int(time_in_sec // (3600 * 24))}d {time_msg}" + return time_msg + + +class HtmlLogConfig: + STYLE = { + BaseLogResult.DEBUG: 'debug', + BaseLogResult.PASSED: '', + BaseLogResult.WORKAROUND: 'workaround', + BaseLogResult.WARNING: 'warning', + BaseLogResult.SKIPPED: 'skip', + BaseLogResult.FAILED: 'fail', + BaseLogResult.BLOCKED: 'blocked', + BaseLogResult.CRITICAL: 'critical', + BaseLogResult.EXCEPTION: 'exception'} + + __MAIN = 'main' + __SETUP = 'setup' + __T_ITERATION = 'iteration' + __FRAMEWORK_T_FOLDER = 'template' + + MAIN = __MAIN + '.html' + CSS = __MAIN + '.css' + JS = __MAIN + '.js' + + ITERATION_FOLDER = 'iterations' + SETUP = __SETUP + ".html" + + def iteration(self): + return f'{HtmlLogConfig.__T_ITERATION}_{str(self._iteration_id).zfill(3)}.html' + + def __init__(self, base_dir=None, presentation_policy=null_policy): + self._log_base_dir = base_dir + if base_dir is None: + if os.name == 'nt': + self._log_base_dir = 'c:\\History' + else: + if environ["USER"] == 'root': + self._log_base_dir = '/root/history' + else: + self._log_base_dir = f'/home/{environ["USER"]}' + self._log_dir = None + self._presentation_policy = {} + self.register_presentation_policy(str, presentation_policy) + self._iteration_id = 0 + + def get_iteration_id(self): + return self._iteration_id + + def get_policy(self, type): + return self._presentation_policy[type] + + def get_policy_collection(self): + for type, policy in self._presentation_policy.items(): + yield policy + + def register_presentation_policy(self, type, presentation_policy): + self._presentation_policy[type] = presentation_policy + + def __find_template_file(self, name, relative_path=None): + base_dir = path.dirname(path.abspath(__file__)) + file_path = path.join(base_dir, HtmlLogConfig.__FRAMEWORK_T_FOLDER) + if relative_path is not None: + file_path = path.join(file_path, relative_path) + file_path = path.join(file_path, name) + if path.isfile(file_path): + return file_path + else: + raise Exception( + f"Unable to find file: {name} in location: {os.path.dirname(file_path)}") + + def __get_main_template_file_path(self): + return self.__find_template_file(HtmlLogConfig.MAIN) + + def _get_setup_template_file_path(self): + return self.__find_template_file(HtmlLogConfig.SETUP, HtmlLogConfig.ITERATION_FOLDER) + + def __get_iteration_template_path(self): + return self.__find_template_file(HtmlLogConfig.__T_ITERATION + '.html', + HtmlLogConfig.ITERATION_FOLDER) + + def create_html_test_log(self, test_title): + now = datetime.now() + time_stamp = f"{now.year}_{str(now.month).zfill(2)}_{str(now.day).zfill(2)}_" \ + f"{str(now.hour).zfill(2)}_{str(now.minute).zfill(2)}_{str(now.second).zfill(2)}" + self._log_dir = path.join(self._log_base_dir, test_title, time_stamp) + makedirs(self._log_dir) + additional_location = path.join(self._log_dir, HtmlLogConfig.ITERATION_FOLDER) + makedirs(additional_location) + dut_info_folder = path.join(self._log_dir, 'dut_info') + makedirs(dut_info_folder) + main_html = self.__get_main_template_file_path() + main_css = main_html.replace('html', 'css') + main_js = main_html.replace('html', 'js') + copyfile(main_html, path.join(self._log_dir, HtmlLogConfig.MAIN)) + copyfile(main_css, path.join(self._log_dir, HtmlLogConfig.CSS)) + copyfile(main_js, path.join(self._log_dir, HtmlLogConfig.JS)) + copyfile(self._get_setup_template_file_path(), path.join(additional_location, + HtmlLogConfig.SETUP)) + return self._log_dir + + def get_main_file_path(self): + return path.join(self._log_dir, HtmlLogConfig.MAIN) + + def get_setup_file_path(self): + return path.join(self._log_dir, HtmlLogConfig.ITERATION_FOLDER, HtmlLogConfig.SETUP) + + def create_iteration_file(self): + self._iteration_id += 1 + template_file = self.__get_iteration_template_path() + new_file_name = self.iteration() + result = path.join(self._log_dir, HtmlLogConfig.ITERATION_FOLDER, new_file_name) + copyfile(template_file, result) + return result + + def end_iteration(self, + iteration_selector_div, + iteration_selector_select, + iteration_id, + iteration_result): + style = "iteration-selector" + if iteration_result != BaseLogResult.PASSED: + style = f'{style} {HtmlLogConfig.STYLE[iteration_result]}' + if iteration_id and iteration_id % 8 == 0: + new_element = Element("br") + iteration_selector_div[0].append(new_element) + new_element = Element("a") + new_element.set('class', style) + new_element.set('onclick', f"selectIteration('{iteration_id}')") + new_element.text = str(iteration_id) + iteration_selector_div[0].append(new_element) + new_element = Element('option', value=f"{iteration_id}") + new_element.text = 'iteration_' + str(iteration_id).zfill(3) + if iteration_result != BaseLogResult.PASSED: + new_element.set('class', HtmlLogConfig.STYLE[iteration_result]) + iteration_selector_select.append(new_element) + + def end_setup_iteration(self, iteration_selector_div, iteration_selector_select, log_result): + if log_result != BaseLogResult.PASSED: + a_element = iteration_selector_div[0] + select_element = iteration_selector_select[0] + a_element.set('class', f'iteration-selector {HtmlLogConfig.STYLE[log_result]}') + select_element.set('class', HtmlLogConfig.STYLE[log_result]) + + def end_iteration_func(self, time_node, status_node, time_in_sec, log_result): + time_node.text = f"Execution time: {convert_seconds_to_str(time_in_sec)}" + status_node.text = f"Iteration status: {log_result.name}" + if log_result != BaseLogResult.PASSED: + status_node.set('class', f'iteration-status {HtmlLogConfig.STYLE[log_result]}') + + def end_main_log(self, test_status_div, log_result): + if log_result != BaseLogResult.PASSED: + test_status_div[0].set('class', + f"sidebar-test-status {HtmlLogConfig.STYLE[log_result]}") + test_status_div[0].text = f"Test status: {log_result.name}" + + def group_end(self, msg_id, html_header, html_container, log_result): + html_header.set('onclick', f"showHide('ul_{msg_id}')") + sub_element = Element('a', href="#top") + sub_element.text = "[TOP]" + sub_element.set('class', "top-time-marker") + html_header.append(sub_element) + div_style = 'test-group-step' + ul_style = 'iteration-content' + if log_result == BaseLogResult.PASSED: + html_container.set('style', "display: none;") + else: + div_style = f"{div_style} {HtmlLogConfig.STYLE[log_result]}" + ul_style = f"{ul_style} {HtmlLogConfig.STYLE[log_result]}" + html_header.set('class', div_style) + html_container.set('class', ul_style) + + def group_chapter_end(self, time_in_sec, html_header, html_container, log_result): + sub_element = Element('a') + sub_element.text = convert_seconds_to_str(time_in_sec) + sub_element.set('class', 'top-marker') + html_header.append(sub_element) + div_style = 'test-group-step' + ul_style = 'iteration-content' + if log_result != BaseLogResult.PASSED: + div_style = f"{div_style} {HtmlLogConfig.STYLE[log_result]}" + ul_style = f"{ul_style} {HtmlLogConfig.STYLE[log_result]}" + html_header.set('class', div_style) + html_container.set('class', ul_style) diff --git a/test/functional/test-framework/log/html_log_manager.py b/test/functional/test-framework/log/html_log_manager.py new file mode 100644 index 0000000..30ba196 --- /dev/null +++ b/test/functional/test-framework/log/html_log_manager.py @@ -0,0 +1,126 @@ +# +# Copyright(c) 2019-2021 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# + + +from log.base_log import BaseLog, escape +from log.html_iteration_log import HtmlIterationLog +from log.html_log_config import HtmlLogConfig +from log.html_main_log import HtmlMainLog +from log.html_setup_log import HtmlSetupLog + + +class HtmlLogManager(BaseLog): + def __init__(self, begin_message=None, log_config=None): + super().__init__(begin_message) + self._config = HtmlLogConfig() if log_config is None else log_config + self._main = None + self._log_setup = None + self._log_iterations = [] + self._current_log = None + self._files_path = None + + def __add(self, msg): + pass + + def begin(self, message): + self._files_path = self._config.create_html_test_log(message) + self._main = HtmlMainLog(message, self._config) + self._log_setup = HtmlSetupLog(message, config=self._config) + self._current_log = self._log_setup + self._main.begin(message) + self._current_log.begin(message) + self.__add("begin: " + message) + + @property + def base_dir(self): + return self._files_path + + def get_result(self): + log_result = self._log_setup.get_result() + for iteration in self._log_iterations: + if log_result.value < iteration.get_result().value: + log_result = iteration.get_result() + return log_result + + def end(self): + self._log_setup.end() + self._main.end_setup_iteration(self._log_setup.get_result()) + log_result = self.get_result() + self._main.end(log_result) + self.__add("end") + + def add_build_info(self, message): + self._main.add_build_info(escape(message)) + + def start_iteration(self, message): + message = escape(message) + self._log_iterations.append(HtmlIterationLog(message, message, self._config)) + self._main.start_iteration(self._config.get_iteration_id()) + self._current_log = self._log_iterations[-1] + self._current_log.begin(message) + self._log_setup.start_iteration(message) + self.__add("start_iteration: " + message) + + def end_iteration(self): + self._current_log.end() + self._main.end_iteration(self._current_log.get_result()) + self._log_setup.end_iteration(self._current_log.get_result()) + self._current_log.iteration_closed = True + self._current_log = self._log_setup + self.__add("end_iteration: ") + return self._current_log + + def debug(self, message): + self._current_log.debug(escape(message)) + self.__add("debug: " + message) + + def info(self, message): + self._current_log.info(escape(message)) + self.__add("info: " + message) + + def workaround(self, message): + self._current_log.workaround(escape(message)) + self.__add(": " + message) + + def warning(self, message): + self._current_log.warning(escape(message)) + self.__add(": " + message) + + def skip(self, message): + self._current_log.skip(escape(message)) + self.__add("warning: " + message) + + def error(self, message): + self._current_log.error(escape(message)) + self.__add("error: " + message) + + def blocked(self, message): + self._current_log.blocked(escape(message)) + self.__add(f'blocked: {message}') + self.end_all_groups() + + def exception(self, message): + self._current_log.exception(escape(message)) + self.__add("exception: " + message) + self.end_all_groups() + + def critical(self, message): + self._current_log.critical(escape(message)) + self.__add("critical: " + message) + self.end_all_groups() + + def start_group(self, message): + self._current_log.start_group(escape(message)) + self.__add("start_group: " + message) + + def end_group(self): + self._current_log.end_group() + self.__add("end_group") + + def end_all_groups(self): + for iteration in reversed(self._log_iterations): + if not iteration.iteration_closed: + self.end_iteration() + self._current_log.end_all_groups() diff --git a/test/functional/test-framework/log/html_main_log.py b/test/functional/test-framework/log/html_main_log.py new file mode 100644 index 0000000..f19f070 --- /dev/null +++ b/test/functional/test-framework/log/html_main_log.py @@ -0,0 +1,53 @@ +# +# Copyright(c) 2019-2021 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# + +from log.html_file_log import HtmlFileLog +from lxml.etree import Element + + +class HtmlMainLog(HtmlFileLog): + def __init__(self, title, config): + super().__init__(config.get_main_file_path(), title) + self._config = config + self.__current_iteration_id = None + root = self.get_root() + test_title_div = root.xpath('/html/body/div/div/div/div[@class="sidebar-test-title"]')[0] + test_title_div.text = title + self.__build_information_set = root.xpath( + '/html/body/div/div/div/div[@id="sidebar-tested-build"]')[0] + + def add_build_info(self, message): + build_info = Element("div") + build_info.text = message + self.__build_information_set.append(build_info) + + def start_iteration(self, iteration_id): + self.__current_iteration_id = iteration_id + + def end_iteration(self): + pass + + def end_iteration(self, iteration_result): + root = self.get_root() + iteration_selector_div = root.xpath('/html/body/div/div/div[@id="iteration-selector"]') + iteration_selector_select = root.xpath( + '/html/body/div/div/select[@id="sidebar-iteration-list"]')[0] + self._config.end_iteration(iteration_selector_div, + iteration_selector_select, + self.__current_iteration_id, + iteration_result) + + def end_setup_iteration(self, result): + root = self.get_root() + iteration_selector_div = root.xpath('/html/body/div/div/div[@id="iteration-selector"]')[0] + iteration_selector_select = root.xpath( + '/html/body/div/div/select[@id="sidebar-iteration-list"]')[0] + self._config.end_setup_iteration(iteration_selector_div, iteration_selector_select, result) + + def end(self, result): + root = self.get_root() + test_status_div = root.xpath('/html/body/div/div/div/div[@class="sidebar-test-status"]') + self._config.end_main_log(test_status_div, result) + super().end() diff --git a/test/functional/test-framework/log/html_presentation_policy.py b/test/functional/test-framework/log/html_presentation_policy.py new file mode 100644 index 0000000..62cb7be --- /dev/null +++ b/test/functional/test-framework/log/html_presentation_policy.py @@ -0,0 +1,45 @@ +# +# Copyright(c) 2019-2021 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# + +from log.base_log import BaseLogResult +from lxml.etree import Element +from datetime import datetime +from log.presentation_policy import PresentationPolicy +from log.html_log_config import HtmlLogConfig + + +def std_log_entry(msg_id, msg, log_result, html_node): + test_step = Element('li') + style = 'test-step' + if log_result != BaseLogResult.PASSED: + style = f"{style} {HtmlLogConfig.STYLE[log_result]}" + test_step.set('class', style) + test_time = Element('div') + test_time.set('class', 'ts-time') + test_time_txt = Element('a', name=msg_id) + time = datetime.now() + test_time_txt.text = f"{str(time.hour).zfill(2)}:" \ + f"{str(time.minute).zfill(2)}:{str(time.second).zfill(2)}" + test_time.append(test_time_txt) + test_step.append(test_time) + test_msg = Element('div') + test_msg.set('class', 'ts-msg') + test_msg.text = msg + test_step.append(test_msg) + html_node.append(test_step) + + +def group_log_begin(msg_id, msg, html_node): + element = Element("div") + sub_element = Element('a', name=msg_id) + sub_element.text = msg + element.append(sub_element) + html_node.append(element) + ul_set = Element('ul', id=f'ul_{msg_id}') + html_node.append(ul_set) + return element, ul_set + + +html_policy = PresentationPolicy(std_log_entry, group_log_begin) diff --git a/test/functional/test-framework/log/html_setup_log.py b/test/functional/test-framework/log/html_setup_log.py new file mode 100644 index 0000000..3b93fb3 --- /dev/null +++ b/test/functional/test-framework/log/html_setup_log.py @@ -0,0 +1,34 @@ +# +# Copyright(c) 2019-2021 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# + +from log.html_file_item_log import HtmlFileItemLog +from log.base_log import BaseLogResult + + +class HtmlSetupLog(HtmlFileItemLog): + + LOG_RESULT = { + BaseLogResult.PASSED: HtmlFileItemLog.info, + BaseLogResult.WORKAROUND: HtmlFileItemLog.workaround, + BaseLogResult.WARNING: HtmlFileItemLog.warning, + BaseLogResult.SKIPPED: HtmlFileItemLog.skip, + BaseLogResult.FAILED: HtmlFileItemLog.error, + BaseLogResult.BLOCKED: HtmlFileItemLog.blocked, + BaseLogResult.EXCEPTION: HtmlFileItemLog.exception, + BaseLogResult.CRITICAL: HtmlFileItemLog.critical} + + def __init__(self, test_title, config, iteration_title="Test summary"): + html_file_path = config.get_setup_file_path() + super().__init__(html_file_path, test_title, config, iteration_title) + self._last_iteration_title = '' + + def start_iteration(self, message): + self._last_iteration_title = message + + def end_iteration(self, iteration_result): + HtmlSetupLog.LOG_RESULT[iteration_result](self, self._last_iteration_title) + + def end(self): + super().end() diff --git a/test/functional/test-framework/log/logger.py b/test/functional/test-framework/log/logger.py new file mode 100644 index 0000000..07a40bb --- /dev/null +++ b/test/functional/test-framework/log/logger.py @@ -0,0 +1,220 @@ +# +# Copyright(c) 2019-2021 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# + +import logging +import os +import sys +from contextlib import contextmanager +from datetime import datetime +from threading import Lock + +import portalocker + +from log.html_log_config import HtmlLogConfig +from log.html_log_manager import HtmlLogManager +from log.html_presentation_policy import html_policy +from test_utils.output import Output +from test_utils.singleton import Singleton + + +def create_log(log_base_path, test_module, additional_args=None): + Log.setup() + log_cfg = HtmlLogConfig(base_dir=log_base_path, + presentation_policy=html_policy) + log = Log(log_config=log_cfg) + test_name = 'TestNameError' + error_msg = None + try: + test_name = test_module + if additional_args: + test_name += f"__{'_'.join(additional_args)}" + except Exception as ex: + error_msg = f'Detected some problems during calculating test name: {ex}' + finally: + log.begin(test_name) + print(f"\n{os.path.join(log.base_dir, 'main.html')}") + if error_msg: + log.exception(error_msg) + return log + + +class Log(HtmlLogManager, metaclass=Singleton): + logger = None + LOG_FORMAT = '%(asctime)s %(levelname)s:\t%(message)s' + DATE_FORMAT = "%Y/%m/%d %H:%M:%S" + command_id = 0 + lock = Lock() + + @classmethod + def destroy(cls): + del cls._instances[cls] + + @classmethod + def setup(cls): + + # Get handle to root logger. + logger = logging.getLogger() + logger.setLevel(logging.DEBUG) + + # Set paramiko log level to warning + logging.getLogger('paramiko').setLevel(logging.WARNING) + + # Create Handlers. + stdout_handler = logging.StreamHandler(sys.stdout) + + # Set logging level on handlers. + stdout_handler.setLevel(logging.DEBUG) + + # Set log formatting on each handler. + formatter = logging.Formatter(Log.LOG_FORMAT, Log.DATE_FORMAT) + stdout_handler.setFormatter(formatter) + + # Attach handlers to root logger. + logger.handlers = [] + logger.addHandler(stdout_handler) + cls.logger = logger + logger.info("Logger successfully initialized.") + + @contextmanager + def step(self, message): + self.step_info(message) + super(Log, self).start_group(message) + if Log.logger: + Log.logger.info(message) + yield + super(Log, self).end_group() + + @contextmanager + def group(self, message): + self.start_group(message) + yield + self.end_group() + + def add_build_info(self, msg): + super(Log, self).add_build_info(msg) + if Log.logger: + Log.logger.info(msg) + + def info(self, msg): + super(Log, self).info(msg) + if Log.logger: + Log.logger.info(msg) + + def debug(self, msg): + super(Log, self).debug(msg) + if Log.logger: + Log.logger.debug(msg) + + def error(self, msg): + super(Log, self).error(msg) + if Log.logger: + Log.logger.error(msg) + + def blocked(self, msg): + super(Log, self).blocked(msg) + if Log.logger: + Log.logger.fatal(msg) + + def exception(self, msg): + super(Log, self).exception(msg) + if Log.logger: + Log.logger.exception(msg) + + def critical(self, msg): + super(Log, self).critical(msg) + if Log.logger: + Log.logger.fatal(msg) + + def workaround(self, msg): + super(Log, self).workaround(msg) + if Log.logger: + Log.logger.warning(msg) + + def warning(self, msg): + super(Log, self).warning(msg) + if Log.logger: + Log.logger.warning(msg) + + def get_new_command_id(self): + self.lock.acquire() + command_id = self.command_id + self.command_id += 1 + self.lock.release() + return command_id + + def write_to_command_log(self, message): + super(Log, self).debug(message) + command_log_path = os.path.join(self.base_dir, "dut_info", 'commands.log') + timestamp = datetime.now().strftime('%Y-%m-%d_%H:%M:%S:%f') + with portalocker.Lock(command_log_path, "ab+") as command_log: + line_to_write = f"[{timestamp}] {message}\n" + command_log.write(line_to_write.encode()) + + def write_command_to_command_log(self, command, command_id, info=None): + added_info = "" if info is None else f"[{info}] " + self.write_to_command_log(f"{added_info}Command id: {command_id}\n{command}") + + def write_output_to_command_log(self, output: Output, command_id): + if output is not None: + line_to_write = f"Command id: {command_id}\n\texit code: {output.exit_code}\n" \ + f"\tstdout: {output.stdout}\n" \ + f"\tstderr: {output.stderr}\n\n\n" + self.write_to_command_log(line_to_write) + else: + self.write_to_command_log(f"Command id: {command_id}\n\tNone output.") + + def step_info(self, step_name): + from core.test_run import TestRun + decorator = "// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //\n\n" + message = f"\n\n\n{decorator}{step_name}\n\n{decorator}\n" + + try: + serial_monitor = TestRun.plugin_manager.get_plugin("serial_monitor") + serial_monitor.send_to_serial(message) + except (KeyError, AttributeError): + pass + self.write_to_command_log(message) + + def get_additional_logs(self): + from core.test_run import TestRun + from test_tools.fs_utils import check_if_file_exists + messages_log = "/var/log/messages" + if not check_if_file_exists(messages_log): + messages_log = "/var/log/syslog" + log_files = {"messages.log": messages_log, + "dmesg.log": "/tmp/dmesg"} + extra_logs = TestRun.config.get("extra_logs", {}) + log_files.update(extra_logs) + + TestRun.executor.run(f"dmesg > {log_files['dmesg.log']}") + + for log_name, log_source_path in log_files.items(): + try: + log_destination_path = os.path.join( + self.base_dir, f"dut_info", TestRun.dut.ip, log_name + ) + TestRun.executor.rsync_from(log_source_path, log_destination_path) + except Exception as e: + TestRun.LOGGER.warning( + f"There was a problem during gathering {log_name} log.\n{str(e)}" + ) + + def generate_summary(self, item, meta): + import json + summary_path = os.path.join(self.base_dir, 'info.json') + with open(summary_path, "w+") as summary: + data = { + 'module': os.path.relpath(item.fspath, os.getcwd()), + 'function': item.name, + 'meta': meta, + 'status': self.get_result().name, + 'path': os.path.normpath(self.base_dir), + 'stage_status': { + 'setup': getattr(item, "rep_setup", {}), + 'call': getattr(item, "rep_call", {}), + 'teardown': getattr(item, "rep_teardown", {}) + } + } + json.dump(data, summary) diff --git a/test/functional/test-framework/log/presentation_policy.py b/test/functional/test-framework/log/presentation_policy.py new file mode 100644 index 0000000..5409d08 --- /dev/null +++ b/test/functional/test-framework/log/presentation_policy.py @@ -0,0 +1,21 @@ +# +# Copyright(c) 2019-2021 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# + + +class PresentationPolicy: + def __init__(self, standard_log, group_begin_func): + self.standard = standard_log + self.group_begin = group_begin_func + + +def std_log_entry(msg_id, msg, log_result, html_node): + pass + + +def group_log_begin(msg_id, msg, html_node): + return html_node, html_node + + +null_policy = PresentationPolicy(std_log_entry, group_log_begin) diff --git a/test/functional/test-framework/log/template/iterations/iteration.html b/test/functional/test-framework/log/template/iterations/iteration.html new file mode 100644 index 0000000..272a5d9 --- /dev/null +++ b/test/functional/test-framework/log/template/iterations/iteration.html @@ -0,0 +1,35 @@ + + + + + [title] + + + + + +
+ View: + + Errors: + + + +
+
+

[title]

+
Iteration status: [status]
+
Execution time: [time] [s]
+
+

Groups:

+
+ + diff --git a/test/functional/test-framework/log/template/iterations/setup.html b/test/functional/test-framework/log/template/iterations/setup.html new file mode 100644 index 0000000..a9912e6 --- /dev/null +++ b/test/functional/test-framework/log/template/iterations/setup.html @@ -0,0 +1,37 @@ + + + + + Setup + + + + + +
+ View: + + Errors: + + + +
+
+ +

Test summary

+
+
Iteration status: [STATUS]
+
Execution time: [time] [s]
+
+

Groups:

+
+ + diff --git a/test/functional/test-framework/log/template/main.css b/test/functional/test-framework/log/template/main.css new file mode 100644 index 0000000..4f5449d --- /dev/null +++ b/test/functional/test-framework/log/template/main.css @@ -0,0 +1,383 @@ +/* + Copyright(c) 2019-2021 Intel Corporation + SPDX-License-Identifier: BSD-3-Clause +*/ + +html, body { + margin: 0; + padding: 0; + background-color: #F0F0F0; + font-family: Calibri; + color: black; +} + +div { display: block; } + +h2 { margin: 0; padding: 0; } +h4 { margin: 0; padding: 0; } + +div.meta-container { + margin-left: 502px; + min-width: 500px; + height: 100vh; +} + +div.main-layaut { + float: right; + width: 100%; + background-color: #FDFDFD; + height: 100vh; + overflow-y: scroll; + overflow-x: hidden; +} + +div.sidebar { + float: left; + width: 500px; + height: 100vh; + margin-left: -502px; + border: 4px; + background-color: #F0F0F0; + overflow-x: hidden; + overflow-y: auto; + text-align: center; + color: white; + overflow-x: hidden; + overflow-y: hidden; +} + +div.sidebar-hide { + padding: 3px; + height: 20px; + margin: 5px auto; + font-family: Consolas; + font-weight: normal; + font-size: 15px; + color: white; + text-shadow: 1px 1px 3px black; + background-color: rgb(40,80,180); + cursor: default; + border: 2px solid silver; + border-radius: 25px; +} + +div.sidebar-show { color: balck; height: 50%; } + +div.sidebar-test { overflow-x: hidden; overflow-y: hidden;} + +div.sidebar-test-title { + padding: 10px; + height: 40px; + margin: 5px auto; + background-color: rgb(40,80,180); + font-size: 100%; + border: 2px solid silver; + border-radius: 25px; +} + +div.sidebar-test-status { + padding: 3px; + height: 20px; + background-color: green; + border: 2px solid silver; + border-radius: 25px; +} + +div.sidebar-tested-build { + color: black; + border-radius: 25px; + width: 80%; + margin: 5px auto; + padding: 25px; + background-color: #F7F7F7; + border: 1px solid silver; + word-wrap: break-word; + word-break: break-all; + overflow: hidden; + text-align: left; +} + +div.sidebar-test-iteration { + padding: 3px; + height: 20px; + margin: 5px auto; + font-family: Consolas; + font-weight: normal; + font-size: 15px; + color: white; + text-shadow: 1px 1px 3px black; + background-color: rgb(40,80,180); + cursor: default; + border: 2px solid silver; + border-radius: 25px; +} + +.debug { display: none; } + +select.sidebar-iteration-list { + margin: 5px auto; + background-color: white; + color: black; + width: 90%; +} +select.warning { background-color: #ff0; color: black; } +select.workaround { background-color: #fff8dc; color: black; } +select.skip { background-color: silver; color: black; } +select.fail { background-color: red; color: white; } +select.blocked { background-color: #7030a0; color: white; } +select.exception { background-color: #e29517; color: white; } +select.critical { background-color: #002060; color: white; } + +option { + background-color: green; + color: white; + margin: 2px; +} +option.warning { background-color: #ff0; color: black; } +option.workaround { background-color: #fff8dc; color: black; } +option.skip { background-color: silver; color: black; } +option.error { background-color: red; color: white; } +option.blocked { background-color: #7030a0; color: white; } +option.exception { background-color: #e29517; color: white; } +select.critical { background-color: #002060; color: white; } + +a.iteration-selector { + border: 2px solid silver; + border-radius: 40px; + width: 36px; + height: 36px; + margin: 0; + padding: 0; + vertical-align: middle; + display: table-cell; + color: white; + background-color: green; + text-shadow: 0 0 3px black; + font-size: 20px; + font-weight: bold; + line-height: 1em; + text-align: center; + cursor: pointer; +} +a.warning { background-color: #ff0; } +a.workaround { background-color: #fff8dc; } +a.skip { background-color: silver; } +a.fail { background-color: red; } +a.exception { background-color: #e29517; } +a.blocked { background-color: #7030a0; } +a.critical { background-color: #002060; } +a.selected { border: 2px solid black; } + +select.error-list-selector { background-color: silver; } + +div.test-chapter-step { + margin: 4px auto; + border-style: solid; + border-color: #8CB9AE; + border-radius: 10px; + padding-left: 10px; + padding-right: 10px; + cursor: pointer; +} + +div.sidebar-copyright { + position: absolute; + background-color: #DDD; + text-align: center; + padding: 4px; + color: #888; + bottom: 0; + font-size: 12px; + font-family: Consolas; +} + +div.floating { + right: 0; + border: 3px solid silver; + width: 40%; + text-align: center; + vertical-align: top; + position: fixed; + background-color : #F0F0F0; + border-bottom: 1px solid #999; + z-index: 999; + color: #333; + box-shadow: 0 0px 6px gray; +} + +h1 { + display: block; + font-size: 2em; + font-weight: bold; +} + +div.iteration-selector { + margin: 5px auto; +} + +div.iteration-status { + padding: 3px; + height: 20px; + background-color: green; + border: 2px solid silver; + border-radius: 25px; + color: white; + text-align: center; +} + +h1.iteration-title { text-align: center; } + +div.iteration-execution-time { text-align: center; } + +section.iteration-chapters { + border-radius: 25px; + width: 80%; + margin: 10px auto; + padding: 25px; + background-color: #F7F7F7; + border: 1px solid silver; + word-wrap: break-word; + word-break: break-all; + overflow: hidden; +} + +ul.iteration-content { + list-style-type: none; + border-left-color: green; + border-left-style: solid; + margin: 0px; +} +ul.warning { border-left-color: #ff0; } +ul.workaround { border-left-color: #fff8dc; } +ul.skip { border-left-color: silver; } +ul.fail { border-left-color: red; } +ul.blocked { border-left-color: #7030a0; } +ul.critical { border-left-color: #002060; } +ul.exception { border-left-color: #e29517; } + +li.iteration-content { + border-color: rgba(192, 192, 192, 1); + background-color: rgba(238, 238, 238, 1); + display: block; + margin: 2px auto; + border: 1px solid #C0C0C0; + padding: 3px 6px; + font-family: Calibri; + font-size: 16px; + line-height: 1.15em; + word-wrap: break-word; + word-break: break-all; + overflow: hidden; + border-left-color: green; + border-left-style: solid; + word-break: break-all; +} + +div.test-group-step { + color: black; + background-color: #8CB9AE; + border: 1px solid #5C8880; + font-size: 18px; + letter-spacing: 2px; + cursor: pointer; + margin: 4px; + border-radius: 10px; + padding-left: 10px; + padding-right: 10px; + overflow-wrap: break-word; + word-wrap: break-word; + word-break: break-all; +} + +div.warning { background-color: #ff0; color: black; } +div.workaround { background-color: #fff8dc; color: black; } +div.skip { background-color: silver; color: black; } +div.fail { background-color: red; color: white; } +div.blocked { background-color: #7030a0; color: white; } +div.critical { background-color: #002060; color: white; } +div.exception { background-color: #e29517; color: white; } + +a.top-marker { cursor: pointer; float: right; } + +a.top-time-marker { + word-wrap: break-word; + float: right; +} + +li.test-step { + color: black; + border-color: rgba(192, 192, 192, 1); + background-color: rgba(238, 238, 238, 1); + display: block; + margin: 4px auto; + border: 1px solid #C0C0C0; + padding: 3px 6px; + font-family: Calibri; + font-size: 16px; + line-height: 1.15em; + word-wrap: break-word; + word-break: break-all; + overflow: hidden; + border-left-color: green; + border-left-style: solid; + border-radius: 10px; + padding-left: 10px; + padding-right: 10px +} +li.warning { background-color: #ff0; border-left-color: #ff0; } +li.workaround { background-color: #fff8dc; border-left-color: #fff8dc; } +li.skip { background-color: silver; border-left-color: silver; } +li.fail { + background-color: red; + border-left-color: red; + color: white; +} +li.blocked { + background-color: #7030a0; + border-left-color: #7030a0; + color: white; +} +li.exception { + background-color: #e29517; + border-left-color: #e29517; + color: white; +} + +li.critical { + background-color: #002060; + border-left-color: #002060; + color: white; +} + +div.ts-iteration { + float: left; + margin: 2px auto; + border: 1px solid silver; + padding: 3px 3px; + text-align: center; +} + +div.ts-total-time { + margin: 2px auto; + border: 1px solid silver; + padding: 3px 3px; + text-align: right; +} + +div.ts-time { + float: left; + font-size: 12px; + margin: 2px auto; + border: 1px solid #A7A7A7; + padding: 3px 3px; +} + +div.ts-msg { + font-size: 16px; + font-family: Courier; + margin: 2px auto; + border: 1px solid #A7A7A7; + padding: 3px 3px; + white-space: pre-wrap; + word-break: break-all; +} diff --git a/test/functional/test-framework/log/template/main.html b/test/functional/test-framework/log/template/main.html new file mode 100644 index 0000000..8d0da20 --- /dev/null +++ b/test/functional/test-framework/log/template/main.html @@ -0,0 +1,44 @@ + + + + + [test title] + + + + +
+ +
+ +
+
+ + + diff --git a/test/functional/test-framework/log/template/main.js b/test/functional/test-framework/log/template/main.js new file mode 100644 index 0000000..79e81d8 --- /dev/null +++ b/test/functional/test-framework/log/template/main.js @@ -0,0 +1,223 @@ +/* + Copyright(c) 2019-2021 Intel Corporation + SPDX-License-Identifier: BSD-3-Clause +*/ + +function onLoadDocument() { + hideDebug(); +} + +function selectMode() { + var selector = document.getElementById('mode-selector'); + if (selector.value.includes('info')) { + hideDebug(); + } else { + showDebug(); + } +} + +function hideDebug() { + var debugTestStepArray = document.getElementsByTagName('li'); + for (i = 0; i < debugTestStepArray.length; i ++) { + if(debugTestStepArray[i].className.includes('debug')) { + debugTestStepArray[i].style.display = 'none'; + } + } +} + +function showDebug() { + var debugTestStepArray = document.getElementsByTagName('li'); + for (i = 0; i < debugTestStepArray.length; i ++) { + if(debugTestStepArray[i].className.includes('debug')) { + debugTestStepArray[i].style.display = ''; + } + } +} + +function sidebarCtrl(ctrlHideId, ctrlShowClass) { + var metaContainer = document.getElementsByClassName("meta-container")[0]; + var sidebar = document.getElementsByClassName('sidebar')[0]; + var sidebarTest = document.getElementById('sidebar-test'); + var ctrlHide = document.getElementById(ctrlHideId); + var ctrlShowSet = document.getElementsByClassName(ctrlShowClass); + + if(sidebar.style.width.includes('15px')) { + showSidebar(metaContainer, sidebar, ctrlHide, ctrlShowSet, sidebarTest); + } else { + hideSidebar(metaContainer, sidebar, ctrlHide, ctrlShowSet, sidebarTest); + } +} + +function showSidebar(mContainer, sidebar, ctrlHide, ctrlShowSet, sidebarTest) { + sidebar.style.cursor = 'default'; + mContainer.style.marginLeft = ''; + sidebarTest.style.width = ''; + sidebarTest.style.height = ''; + sidebar.style.height = ''; + sidebar.style.marginLeft = ''; + sidebar.style.width = ''; + var i; + for (i = 0; i < sidebarTest.children.length; i++) { + sidebarTest.children[i].style.display = ''; + } + document.getElementById('iteration-selector').style.display = ''; + document.getElementById('sidebar-iteration-list').style.display = ''; + document.getElementById('sidebar-copyright').style.display = ''; + for(i = 0; i < ctrlShowSet.length; i ++) { + ctrlShowSet[i].style.display = 'none'; + } +} + +function hideSidebar(mContainer, sidebar, ctrlHide, ctrlShowSet, sidebarTest) { + document.getElementById('iteration-selector').style.display = 'none'; + document.getElementById('sidebar-iteration-list').style.display = 'none'; + document.getElementById('sidebar-copyright').style.display = 'none'; + var i; + for (i = 0; i < sidebarTest.children.length; i++) { + sidebarTest.children[i].style.display = 'none'; + } + sidebarTest.style.display = ''; + for(i = 0; i < ctrlShowSet.length; i ++) { + ctrlShowSet[i].style.display = ''; + ctrlShowSet[i].style.color = 'black'; + } + sidebar.style.width = '15px'; + sidebar.style.marginLeft = '-15px'; + sidebar.style.height = '100%'; + sidebarTest.style.height = '100%'; + sidebarTest.style.width = '100%'; + mContainer.style.marginLeft = '16px'; + sidebar.style.cursor = 'pointer'; +} + +function previousError() { + var errorSelector = document.getElementById("error-list-selector"); + if (errorSelector.length > 1) { + var id = errorSelector.selectedIndex; + if (id - 1 > 0) { + errorSelector.selectedIndex = (id - 1); + } else { + errorSelector.selectedIndex = (errorSelector.length - 1); + } + errorSelected('error-list-selector'); + } +} + +function nextError() { + var errorSelector = document.getElementById("error-list-selector"); + if (errorSelector.length > 1) { + var id = errorSelector.selectedIndex; + if (id + 1 < errorSelector.length) { + errorSelector.selectedIndex = (id + 1); + } else { + errorSelector.selectedIndex = 1; + } + errorSelected('error-list-selector'); + } +} + +function selectIterationFromSelect() { + var element = document.getElementById("sidebar-iteration-list"); + loadDocument(element.value); + updateIterationSelector(element); +} + +function clickSelectIteration() { + var element = document.getElementById("sidebar-iteration-list"); + for (i = 0; i < element.length; i ++) { + option = element[i]; + var cls = option.getAttribute('class'); + switch(cls) { + case "warning": + option.style.backgroundColor = "yellow"; + option.style.color = "black"; + break; + case "skip": + option.style.backgroundColor = "silver"; + option.style.color = "black"; + break; + case "fail": + option.style.backgroundColor = "red"; + option.style.color = "white"; + break; + case "exception": + option.style.backgroundColor = "blueviolet"; + option.style.color = "white"; + break; + default: + option.style.backgroundColor = "white"; + option.style.color = "black"; + break; + } + + }; +} + +function selectIteration(iteration) { + var selectElement = document.getElementById("sidebar-iteration-list"); + var docId = loadDocument(iteration); + selectElement.selectedIndex = docId; + updateIterationSelector(selectElement); +} + +function loadDocument(fileId) { + var result = 0; + if(fileId == 'M') { + document.getElementById("main-view").src = "iterations/setup.html"; + } else { + var id = pad(fileId, 3); + document.getElementById("main-view").src = "iterations/iteration_" + id + ".html"; + result = parseInt(fileId); + } + return result; +} + +function updateIterationSelector(element) { + var index = element.selectedIndex + var option_class = element[index].getAttribute('class') + if (option_class != null) { + element.setAttribute('class', "sidebar-iteration-list " + option_class); + } else { + element.setAttribute('class', "sidebar-iteration-list"); + } +} + +function errorSelected(selectorId) { + var newLocation = document.getElementById(selectorId).value; + window.location.hash = newLocation; +} + +function pad(strNumber, padding) { + while((strNumber.length + 1) <= padding) { + strNumber = "0" + strNumber; + } + return strNumber; +} + +function showHide(id) { + var ulElement = document.getElementById(id); + if(ulElement.style.display == 'none') { + ulElement.style.display = ''; + } else { + ulElement.style.display = 'none'; + } +} + +function chapterClick(id) { + var id_array = id.split('.'); + var node_id = ""; + var i = 0; + var destinationElement = document.getElementById(id); + if (destinationElement.style.display == 'none') { + do { + node_id += id_array[i]; + var ele = document.getElementById(node_id); + ele.style.display = ''; + node_id += '.'; + i += 1; + } while (i < id_array.length); + window.location = '#' + id; + } else { + destinationElement.style.display = 'none'; + } +} diff --git a/test/functional/test-framework/storage_devices/device.py b/test/functional/test-framework/storage_devices/device.py new file mode 100644 index 0000000..566f403 --- /dev/null +++ b/test/functional/test-framework/storage_devices/device.py @@ -0,0 +1,117 @@ +# +# Copyright(c) 2019-2022 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# +import posixpath + +from core.test_run import TestRun +from test_tools import disk_utils, fs_utils +from test_tools.disk_utils import get_device_filesystem_type, get_sysfs_path +from test_utils.io_stats import IoStats +from test_utils.size import Size, Unit + + +class Device: + def __init__(self, path): + disk_utils.validate_dev_path(path) + self.path = path + self.size = Size(disk_utils.get_size(self.get_device_id()), Unit.Byte) + self.filesystem = get_device_filesystem_type(self.get_device_id()) + self.mount_point = None + + def create_filesystem(self, fs_type: disk_utils.Filesystem, force=True, blocksize=None): + disk_utils.create_filesystem(self, fs_type, force, blocksize) + self.filesystem = fs_type + + def wipe_filesystem(self, force=True): + disk_utils.wipe_filesystem(self, force) + self.filesystem = None + + def is_mounted(self): + output = TestRun.executor.run(f"findmnt {self.path}") + if output.exit_code != 0: + return False + else: + mount_point_line = output.stdout.split('\n')[1] + device_path = fs_utils.readlink(self.path) + self.mount_point = mount_point_line[0:mount_point_line.find(device_path)].strip() + return True + + def mount(self, mount_point, options: [str] = None): + if not self.is_mounted(): + if disk_utils.mount(self, mount_point, options): + self.mount_point = mount_point + else: + raise Exception(f"Device is already mounted! Actual mount point: {self.mount_point}") + + def unmount(self): + if not self.is_mounted(): + TestRun.LOGGER.info("Device is not mounted.") + elif disk_utils.unmount(self): + self.mount_point = None + + def get_device_link(self, directory: str): + items = self.get_all_device_links(directory) + return next(i for i in items if i.full_path.startswith(directory)) + + def get_device_id(self): + return fs_utils.readlink(self.path).split('/')[-1] + + def get_all_device_links(self, directory: str): + from test_tools import fs_utils + output = fs_utils.ls(f"$(find -L {directory} -samefile {self.path})") + return fs_utils.parse_ls_output(output, self.path) + + def get_io_stats(self): + return IoStats.get_io_stats(self.get_device_id()) + + def get_sysfs_property(self, property_name): + path = posixpath.join(disk_utils.get_sysfs_path(self.get_device_id()), + "queue", property_name) + return TestRun.executor.run_expect_success(f"cat {path}").stdout + + def set_sysfs_property(self, property_name, value): + TestRun.LOGGER.info( + f"Setting {property_name} for device {self.get_device_id()} to {value}.") + path = posixpath.join(disk_utils.get_sysfs_path(self.get_device_id()), "queue", + property_name) + fs_utils.write_file(path, str(value)) + + def set_max_io_size(self, new_max_io_size: Size): + self.set_sysfs_property("max_sectors_kb", + int(new_max_io_size.get_value(Unit.KibiByte))) + + def get_max_io_size(self): + return Size(int(self.get_sysfs_property("max_sectors_kb")), Unit.KibiByte) + + def get_max_hw_io_size(self): + return Size(int(self.get_sysfs_property("max_hw_sectors_kb")), Unit.KibiByte) + + def get_discard_granularity(self): + return self.get_sysfs_property("discard_granularity") + + def get_discard_max_bytes(self): + return self.get_sysfs_property("discard_max_bytes") + + def get_discard_zeroes_data(self): + return self.get_sysfs_property("discard_zeroes_data") + + def get_numa_node(self): + return int(TestRun.executor.run_expect_success( + f"cat {get_sysfs_path(self.get_device_id())}/device/numa_node").stdout) + + def __str__(self): + return ( + f'system path: {self.path}, short link: /dev/{self.get_device_id()},' + f' filesystem: {self.filesystem}, mount point: {self.mount_point}, size: {self.size}' + ) + + def __repr__(self): + return str(self) + + @staticmethod + def get_scsi_debug_devices(): + scsi_debug_devices = TestRun.executor.run_expect_success( + "lsscsi --scsi_id | grep scsi_debug").stdout + return [Device(f'/dev/disk/by-id/scsi-{device.split()[-1]}') + for device in scsi_debug_devices.splitlines()] diff --git a/test/functional/test-framework/storage_devices/disk.py b/test/functional/test-framework/storage_devices/disk.py new file mode 100644 index 0000000..83b8c9d --- /dev/null +++ b/test/functional/test-framework/storage_devices/disk.py @@ -0,0 +1,237 @@ +# +# Copyright(c) 2019-2022 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# +import itertools +import json +import re +from datetime import timedelta +from enum import IntEnum + +from core.test_run import TestRun +from storage_devices.device import Device +from test_tools import disk_utils, fs_utils, nvme_cli +from test_utils import disk_finder +from test_utils.os_utils import wait +from test_utils.size import Unit +from test_tools.disk_utils import get_pci_address + + +class DiskType(IntEnum): + hdd = 0 + hdd4k = 1 + sata = 2 + nand = 3 + optane = 4 + + +class DiskTypeSetBase: + def resolved(self): + raise NotImplementedError() + + def types(self): + raise NotImplementedError() + + def json(self): + return json.dumps({ + "type": "set", + "values": [t.name for t in self.types()] + }) + + def __lt__(self, other): + return min(self.types()) < min(other.types()) + + def __le__(self, other): + return min(self.types()) <= min(other.types()) + + def __eq__(self, other): + return min(self.types()) == min(other.types()) + + def __ne__(self, other): + return min(self.types()) != min(other.types()) + + def __gt__(self, other): + return min(self.types()) > min(other.types()) + + def __ge__(self, other): + return min(self.types()) >= min(other.types()) + + +class DiskTypeSet(DiskTypeSetBase): + def __init__(self, *args): + self.__types = set(*args) + + def resolved(self): + return True + + def types(self): + return self.__types + + +class DiskTypeLowerThan(DiskTypeSetBase): + def __init__(self, disk_name): + self.__disk_name = disk_name + + def resolved(self): + return self.__disk_name in TestRun.disks + + def types(self): + if not self.resolved(): + raise LookupError("Disk type not resolved!") + disk_type = TestRun.disks[self.__disk_name].disk_type + return set(filter(lambda d: d < disk_type, [*DiskType])) + + def json(self): + return json.dumps({ + "type": "operator", + "name": "lt", + "args": [self.__disk_name] + }) + + +class Disk(Device): + def __init__( + self, + path, + disk_type: DiskType, + serial_number, + block_size, + ): + Device.__init__(self, path) + self.serial_number = serial_number + self.block_size = Unit(block_size) + self.disk_type = disk_type + self.partitions = [] + + def create_partitions( + self, + sizes: [], + partition_table_type=disk_utils.PartitionTable.gpt): + disk_utils.create_partitions(self, sizes, partition_table_type) + + def remove_partition(self, part): + part_number = int(part.path.split("part")[1]) + disk_utils.remove_parition(self, part_number) + self.partitions.remove(part) + + def umount_all_partitions(self): + TestRun.LOGGER.info( + f"Umounting all partitions from: {self.path}") + cmd = f'umount -l {fs_utils.readlink(self.path)}*?' + TestRun.executor.run(cmd) + + def remove_partitions(self): + for part in self.partitions: + if part.is_mounted(): + part.unmount() + if disk_utils.remove_partitions(self): + self.partitions.clear() + + def is_detected(self): + if self.serial_number: + serial_numbers = disk_finder.get_all_serial_numbers() + return self.serial_number in serial_numbers + elif self.path: + output = fs_utils.ls_item(f"{self.path}") + return fs_utils.parse_ls_output(output)[0] is not None + raise Exception("Couldn't check if device is detected by the system") + + def wait_for_plug_status(self, should_be_visible): + if not wait(lambda: should_be_visible == self.is_detected(), + timedelta(minutes=1), + timedelta(seconds=1)): + raise Exception(f"Timeout occurred while trying to " + f"{'plug' if should_be_visible else 'unplug'} disk.") + + def plug(self): + if self.is_detected(): + return + TestRun.executor.run_expect_success(self.plug_command) + self.wait_for_plug_status(True) + + def unplug(self): + if not self.is_detected(): + return + TestRun.executor.run_expect_success(self.unplug_command) + self.wait_for_plug_status(False) + + @staticmethod + def plug_all_disks(): + TestRun.executor.run_expect_success(NvmeDisk.plug_all_command) + TestRun.executor.run_expect_success(SataDisk.plug_all_command) + + def __str__(self): + disk_str = f'system path: {self.path}, type: {self.disk_type.name}, ' \ + f'serial: {self.serial_number}, size: {self.size}, ' \ + f'block size: {self.block_size}, partitions:\n' + for part in self.partitions: + disk_str += f'\t{part}' + return disk_str + + @staticmethod + def create_disk(path, + disk_type: DiskType, + serial_number, + block_size): + if disk_type is DiskType.nand or disk_type is DiskType.optane: + return NvmeDisk(path, disk_type, serial_number, block_size) + else: + return SataDisk(path, disk_type, serial_number, block_size) + + +class NvmeDisk(Disk): + plug_all_command = "echo 1 > /sys/bus/pci/rescan" + + def __init__(self, path, disk_type, serial_number, block_size): + Disk.__init__(self, path, disk_type, serial_number, block_size) + self.plug_command = NvmeDisk.plug_all_command + self.unplug_command = f"echo 1 > /sys/block/{self.get_device_id()}/device/remove || " \ + f"echo 1 > /sys/block/{self.get_device_id()}/device/device/remove" + self.pci_address = get_pci_address(self.get_device_id()) + + def __str__(self): + disk_str = super().__str__() + disk_str = f"pci address: {self.pci_address}, " + disk_str + return disk_str + + def format_disk(self, metadata_size=None, block_size=None, + force=True, format_params=None, reset=True): + nvme_cli.format_disk(self, metadata_size, block_size, force, format_params, reset) + + def get_lba_formats(self): + return nvme_cli.get_lba_formats(self) + + def get_lba_format_in_use(self): + return nvme_cli.get_lba_format_in_use(self) + + +class SataDisk(Disk): + plug_all_command = "for i in $(find -H /sys/devices/ -path '*/scsi_host/*/scan' -type f); " \ + "do echo '- - -' > $i; done;" + + def __init__(self, path, disk_type, serial_number, block_size): + Disk.__init__(self, path, disk_type, serial_number, block_size) + self.plug_command = SataDisk.plug_all_command + self.unplug_command = \ + f"echo 1 > {self.get_sysfs_properties(self.get_device_id()).full_path}/device/delete" + + def get_sysfs_properties(self, device_id): + ls_command = f"$(find -H /sys/devices/ -name {device_id} -type d)" + output = fs_utils.ls_item(f"{ls_command}") + sysfs_addr = fs_utils.parse_ls_output(output)[0] + if not sysfs_addr: + raise Exception(f"Failed to find sysfs address: ls -l {ls_command}") + dirs = sysfs_addr.full_path.split('/') + scsi_address = dirs[-3] + matches = re.search( + r"^(?P\d+)[-:](?P\d+)[-:](?P\d+)[-:](?P\d+)$", + scsi_address) + controller_id = matches["controller"] + port_id = matches["port"] + target_id = matches["target"] + lun = matches["lun"] + + host_path = "/".join(itertools.takewhile(lambda x: not x.startswith("host"), dirs)) + self.plug_command = f"echo '{port_id} {target_id} {lun}' > " \ + f"{host_path}/host{controller_id}/scsi_host/host{controller_id}/scan" + return sysfs_addr diff --git a/test/functional/test-framework/storage_devices/drbd.py b/test/functional/test-framework/storage_devices/drbd.py new file mode 100644 index 0000000..66baeda --- /dev/null +++ b/test/functional/test-framework/storage_devices/drbd.py @@ -0,0 +1,66 @@ +# +# Copyright(c) 2022 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause-Clear +# + +import os +import posixpath + +from core.test_run import TestRun +from storage_devices.device import Device +from test_tools.drbdadm import Drbdadm +from test_utils.filesystem.symlink import Symlink +from test_utils.output import CmdException + + +class Drbd(Device): + def __init__(self, config): + if Drbdadm.dump_config(config.name).exit_code != 0: + raise ValueError(f"Resource {config.name} not found") + self.config = config + + def create_metadata(self, force): + return Drbdadm.create_metadata(self.config.name, force) + + def up(self): + output = Drbdadm.up(self.config.name) + if output.exit_code != 0: + raise CmdException(f"Failed to create {self.config.name} drbd instance") + + self.path = posixpath.join("/dev/disk/by-id/", posixpath.basename(self.config.device)) + self.symlink = Symlink.get_symlink(self.path, self.config.device, True) + self.device = Device(self.path) + + return self.device + + def wait_for_sync(self): + return Drbdadm.wait_for_sync(self.config.name) + + def is_in_sync(self): + return Drbdadm.in_sync(self.config.name) + + def get_status(self): + return Drbdadm.get_status(self.config.name) + + def set_primary(self, force=False): + return Drbdadm.set_node_primary(self.config.name, force) + + def down(self): + output = Drbdadm.down(self.config.name) + if output.exit_code != 0: + raise CmdException(f"Failed to stop {self.config.name} drbd instance") + + self.device = None + self.symlink.remove(True, True) + + @staticmethod + def down_all(): + try: + Drbdadm.down_all() + except CmdException as e: + if "no resources defined" not in str(e): + raise e + + @staticmethod + def is_installed(): + return TestRun.executor.run("which drbdadm && modinfo drbd").exit_code == 0 diff --git a/test/functional/test-framework/storage_devices/lvm.py b/test/functional/test-framework/storage_devices/lvm.py new file mode 100644 index 0000000..b1cfa88 --- /dev/null +++ b/test/functional/test-framework/storage_devices/lvm.py @@ -0,0 +1,531 @@ +# +# Copyright(c) 2022 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# +import threading + +from typing import Union + +from api.cas.core import Core +from core.test_run import TestRun +from storage_devices.device import Device +from storage_devices.disk import Disk, NvmeDisk +from storage_devices.partition import Partition +from test_tools.fs_utils import readlink +from test_utils.disk_finder import resolve_to_by_id_link +from test_utils.filesystem.symlink import Symlink +from test_utils.size import Size + +lvm_config_path = "/etc/lvm/lvm.conf" +filter_prototype_regex = r"^\sfilter\s=\s\[" +types_prototype_regex = r"^\stypes\s=\s\[" +global_filter_prototype_regex = r"^\sglobal_filter\s=\s\[" +tab = "\\\\t" + + +class LvmConfiguration: + def __init__( + self, + lvm_filters: [] = None, + pv_num: int = None, + vg_num: int = None, + lv_num: int = None, + cache_num: int = None, + cas_dev_num: int = None + ): + self.lvm_filters = lvm_filters + self.pv_num = pv_num + self.vg_num = vg_num + self.lv_num = lv_num + self.cache_num = cache_num + self.cas_dev_num = cas_dev_num + + @staticmethod + def __read_definition_from_lvm_config( + prototype_regex: str + ): + cmd = f"grep '{prototype_regex}' {lvm_config_path}" + output = TestRun.executor.run(cmd).stdout + + return output + + @classmethod + def __add_block_dev_to_lvm_config( + cls, + block_device_type: str, + number_of_partitions: int = 16 + ): + types_definition = cls.read_types_definition_from_lvm_config() + + if types_definition: + if block_device_type in types_definition: + TestRun.LOGGER.info(f"Device type '{block_device_type}' already present in config") + return + + TestRun.LOGGER.info(f"Add block device type to existing list") + new_type_prefix = f"types = [\"{block_device_type}\", {number_of_partitions}, " + + config_update_cmd = f"sed -i 's/{types_prototype_regex}/\t{new_type_prefix}/g'" \ + f" {lvm_config_path}" + else: + TestRun.LOGGER.info(f"Create new types variable") + new_types = f"types = [\"{block_device_type}\", {number_of_partitions}]" + characteristic_line = f"# Configuration option devices\\/sysfs_scan." + config_update_cmd = f"sed -i /'{characteristic_line}'/i\\ '{tab}{new_types}' " \ + f"{lvm_config_path}" + + TestRun.LOGGER.info(f"Adding {block_device_type} ({number_of_partitions} partitions) " + f"to supported types in {lvm_config_path}") + TestRun.executor.run(config_update_cmd) + + @classmethod + def __add_filter_to_lvm_config( + cls, + filter: str + ): + if filter is None: + TestRun.LOGGER.error(f"Lvm filter for lvm config not provided.") + + filters_definition = cls.read_filter_definition_from_lvm_config() + + if filters_definition: + if filter in filters_definition: + TestRun.LOGGER.info(f"Filter definition '{filter}' already present in config") + return + + new_filter_formatted = filter.replace("/", "\\/") + new_filter_prefix = f"filter = [ \"{new_filter_formatted}\", " + + TestRun.LOGGER.info(f"Adding filter to existing list") + config_update_cmd = f"sed -i 's/{filter_prototype_regex}/\t{new_filter_prefix}/g'" \ + f" {lvm_config_path}" + else: + TestRun.LOGGER.info(f"Create new filter variable") + new_filter = f"filter = [\"{filter}\"]" + characteristic_line = f"# Configuration option devices\\/global_filter." + config_update_cmd = f"sed -i /'{characteristic_line}'/i\\ '{tab}{new_filter}' " \ + f"{lvm_config_path}" + + TestRun.LOGGER.info(f"Adding filter '{filter}' to {lvm_config_path}") + TestRun.executor.run(config_update_cmd) + + @classmethod + def read_types_definition_from_lvm_config(cls): + return cls.__read_definition_from_lvm_config(types_prototype_regex) + + @classmethod + def read_filter_definition_from_lvm_config(cls): + return cls.__read_definition_from_lvm_config(filter_prototype_regex) + + @classmethod + def read_global_filter_definition_from_lvm_config(cls): + return cls.__read_definition_from_lvm_config(global_filter_prototype_regex) + + @classmethod + def add_block_devices_to_lvm_config( + cls, + device_type: str + ): + if device_type is None: + TestRun.LOGGER.error(f"No device provided.") + + cls.__add_block_dev_to_lvm_config(device_type) + + @classmethod + def add_filters_to_lvm_config( + cls, + filters: [] + ): + if filters is None: + TestRun.LOGGER.error(f"Lvm filters for lvm config not provided.") + + for f in filters: + cls.__add_filter_to_lvm_config(f) + + @classmethod + def configure_dev_types_in_config( + cls, + devices: ([Device], Device) + ): + if isinstance(devices, list): + devs = [] + for device in devices: + dev = device.parent_device if isinstance(device, Partition) else device + devs.append(dev) + + if any(isinstance(dev, Core) for dev in devs): + cls.add_block_devices_to_lvm_config("cas") + if any(isinstance(dev, NvmeDisk) for dev in devs): + cls.add_block_devices_to_lvm_config("nvme") + else: + dev = devices.parent_device if isinstance(devices, Partition) else devices + if isinstance(dev, Core): + cls.add_block_devices_to_lvm_config("cas") + if isinstance(dev, NvmeDisk): + cls.add_block_devices_to_lvm_config("nvme") + + @classmethod + def configure_filters( + cls, + lvm_filters: [], + devices: ([Device], Device) + ): + if lvm_filters: + TestRun.LOGGER.info(f"Preparing configuration for LVMs - filters.") + LvmConfiguration.add_filters_to_lvm_config(lvm_filters) + + cls.configure_dev_types_in_config(devices) + + @staticmethod + def remove_global_filter_from_config(): + cmd = f"sed -i '/{global_filter_prototype_regex}/d' {lvm_config_path}" + TestRun.executor.run(cmd) + + @staticmethod + def remove_filters_from_config(): + cmd = f"sed -i '/{filter_prototype_regex}/d' {lvm_config_path}" + TestRun.executor.run(cmd) + + +class VolumeGroup: + __unique_vg_id = 0 + __lock = threading.Lock() + + def __init__(self, name: str = None): + self.name = name + + def __eq__(self, other): + try: + return self.name == other.name + except AttributeError: + return False + + @classmethod + def __get_vg_name(cls, prefix: str = "vg"): + with cls.__lock: + cls.__unique_vg_id += 1 + return f"{prefix}{cls.__unique_vg_id}" + + @staticmethod + def get_all_volume_groups(): + output_lines = TestRun.executor.run(f"pvscan").stdout.splitlines() + + volume_groups = {} + for line in output_lines: + if "PV" not in line: + continue + + line_elements = line.split() + pv = line_elements[line_elements.index("PV") + 1] + vg = "" + if "VG" in line: + vg = line_elements[line_elements.index("VG") + 1] + + if vg not in volume_groups: + volume_groups[vg] = [] + volume_groups[vg].append(pv) + + return volume_groups + + @staticmethod + def create_vg(vg_name: str, device_paths: str): + if not vg_name: + raise ValueError("Name needed for VG creation.") + if not device_paths: + raise ValueError("Device paths needed for VG creation.") + + cmd = f"vgcreate --yes {vg_name} {device_paths} " + TestRun.executor.run_expect_success(cmd) + + @classmethod + def is_vg_already_present(cls, dev_number: int, device_paths: str = None): + if not device_paths: + TestRun.LOGGER.exception("No devices provided.") + + volume_groups = cls.get_all_volume_groups() + + for vg in volume_groups: + for pv in volume_groups[vg]: + if len(volume_groups[vg]) == dev_number and pv in device_paths: + return cls(vg) + + for vg in volume_groups: + for pv in volume_groups[vg]: + if pv in device_paths: + TestRun.LOGGER.warning(f"Some devices are used in other LVM volume group") + return False + + @classmethod + def create(cls, device_paths: str = None): + vg_name = cls.__get_vg_name() + + VolumeGroup.create_vg(vg_name, device_paths) + + volume_groups = VolumeGroup.get_all_volume_groups() + + if vg_name in volume_groups: + return cls(vg_name) + else: + TestRun.LOGGER.error("Had not found newly created VG.") + + @staticmethod + def remove(vg_name: str): + if not vg_name: + raise ValueError("Name needed for VG remove operation.") + + cmd = f"vgremove {vg_name}" + return TestRun.executor.run(cmd) + + @staticmethod + def get_logical_volumes_path(vg_name: str): + cmd = f"lvdisplay | grep /dev/{vg_name}/ | awk '{{print $3}}'" + paths = TestRun.executor.run(cmd).stdout.splitlines() + + return paths + + +class Lvm(Disk): + __unique_lv_id = 0 + __lock = threading.Lock() + + def __init__( + self, + path_dm: str, # device mapper path + volume_group: VolumeGroup, + volume_name: str = None + ): + Device.__init__(self, resolve_to_by_id_link(path_dm)) + self.device_name = path_dm.split('/')[-1] + self.volume_group = volume_group + self.volume_name = volume_name + + def __eq__(self, other): + try: + return self.device_name == other.device_name and \ + self.volume_group == other.volume_group and \ + self.volume_name == other.volume_name + except AttributeError: + return False + + @classmethod + def __get_unique_lv_name(cls, prefix: str = "lv"): + with cls.__lock: + cls.__unique_lv_id += 1 + return f"{prefix}{cls.__unique_lv_id}" + + @classmethod + def __create( + cls, + name: str, + volume_size_cmd: str, + volume_group: VolumeGroup + ): + TestRun.LOGGER.info(f"Creating LV '{name}'.") + cmd = f"lvcreate {volume_size_cmd} --name {name} {volume_group.name} --yes" + TestRun.executor.run_expect_success(cmd) + + volumes = cls.discover_logical_volumes() + for volume in volumes: + if name == volume.volume_name: + return volume + + @classmethod + def configure_global_filter( + cls, + dev_first: Device, + lv_amount: int, + pv_devs: ([Device], Device) + ): + device_first = dev_first.parent_device if isinstance(dev_first, Partition) else dev_first + if lv_amount > 1 and isinstance(device_first, Core): + + global_filter_def = LvmConfiguration.read_global_filter_definition_from_lvm_config() + if not isinstance(pv_devs, list): + pv_devs = [pv_devs] + + if global_filter_def: + TestRun.LOGGER.info(f"Configure 'global filter' variable") + links = [] + for pv_dev in pv_devs: + link = pv_dev.get_device_link("/dev/disk/by-id") + links.append(str(link)) + + for link in links: + if link in global_filter_def: + TestRun.LOGGER.info(f"Global filter definition already contains '{link}'") + continue + + new_link_formatted = link.replace("/", "\\/") + new_global_filter_prefix = f"global_filter = [ \"r|{new_link_formatted}|\", " + + TestRun.LOGGER.info(f"Adding global filter '{link}' to existing list") + config_update_cmd = f"sed -i 's/{global_filter_prototype_regex}/\t" \ + f"{new_global_filter_prefix}/g' {lvm_config_path}" + TestRun.executor.run(config_update_cmd) + else: + for pv_dev in pv_devs: + link = pv_dev.get_device_link("/dev/disk/by-id") + global_filter = f"\"r|{link}|\"" + global_filter += ", " + global_filter = global_filter[:-2] + + TestRun.LOGGER.info(f"Create new 'global filter' variable") + + new_global = f"global_filter = [{global_filter}]" + characteristic_line = f"# Configuration option devices\\/types." + config_update_cmd = f"sed -i /'{characteristic_line}'/i\\ " \ + f"'{tab}{new_global}' {lvm_config_path}" + + TestRun.LOGGER.info(f"Adding global filter '{global_filter}' to {lvm_config_path}") + TestRun.executor.run(config_update_cmd) + + TestRun.LOGGER.info(f"Remove 'filter' in order to 'global_filter' to be used") + if LvmConfiguration.read_filter_definition_from_lvm_config(): + LvmConfiguration.remove_filters_from_config() + + @classmethod + def create_specific_lvm_configuration( + cls, + devices: ([Device], Device), + lvm_configuration: LvmConfiguration, + lvm_as_core: bool = False + ): + pv_per_vg = int(lvm_configuration.pv_num / lvm_configuration.vg_num) + lv_per_vg = int(lvm_configuration.lv_num / lvm_configuration.vg_num) + lv_size_percentage = int(100 / lv_per_vg) + + LvmConfiguration.configure_filters(lvm_configuration.lvm_filters, devices) + + logical_volumes = [] + + for vg_iter in range(lvm_configuration.vg_num): + if isinstance(devices, list): + pv_devs = [] + start_range = vg_iter * pv_per_vg + end_range = start_range + pv_per_vg + for i in range(start_range, end_range): + pv_devs.append(devices[i]) + device_first = devices[0] + else: + pv_devs = devices + device_first = devices + + for j in range(lv_per_vg): + lv = cls.create(lv_size_percentage, pv_devs) + logical_volumes.append(lv) + + if lvm_as_core: + cls.configure_global_filter(device_first, lv_per_vg, pv_devs) + + return logical_volumes + + @classmethod + def create( + cls, + volume_size_or_percent: Union[Size, int], + devices: ([Device], Device), + name: str = None + ): + if isinstance(volume_size_or_percent, Size): + size_cmd = f"--size {volume_size_or_percent.get_value()}B" + elif isinstance(volume_size_or_percent, int): + size_cmd = f"--extents {volume_size_or_percent}%VG" + else: + TestRun.LOGGER.error(f"Incorrect type of the first argument (volume_size_or_percent).") + + if not name: + name = cls.__get_unique_lv_name() + + devices_paths = cls.get_devices_path(devices) + dev_number = len(devices) if isinstance(devices, list) else 1 + + vg = VolumeGroup.is_vg_already_present(dev_number, devices_paths) + + if not vg: + vg = VolumeGroup.create(devices_paths) + + return cls.__create(name, size_cmd, vg) + + @staticmethod + def get_devices_path(devices: ([Device], Device)): + if isinstance(devices, list): + return " ".join([Symlink(dev.path).get_target() for dev in devices]) + else: + return Symlink(devices.path).get_target() + + @classmethod + def discover_logical_volumes(cls): + vol_groups = VolumeGroup.get_all_volume_groups() + volumes = [] + for vg in vol_groups: + lv_discovered = VolumeGroup.get_logical_volumes_path(vg) + if lv_discovered: + for lv_path in lv_discovered: + cls.make_sure_lv_is_active(lv_path) + lv_name = lv_path.split('/')[-1] + volumes.append( + cls( + readlink(lv_path), + VolumeGroup(vg), + lv_name + ) + ) + else: + TestRun.LOGGER.info(f"No LVMs present in the system.") + + return volumes + + @classmethod + def discover(cls): + TestRun.LOGGER.info("Discover LVMs in system...") + return cls.discover_logical_volumes() + + @staticmethod + def remove(lv_name: str, vg_name: str): + if not lv_name: + raise ValueError("LV name needed for LV remove operation.") + if not vg_name: + raise ValueError("VG name needed for LV remove operation.") + + cmd = f"lvremove -f {vg_name}/{lv_name}" + return TestRun.executor.run(cmd) + + @staticmethod + def remove_pv(pv_name: str): + if not pv_name: + raise ValueError("Name needed for PV remove operation.") + + cmd = f"pvremove {pv_name}" + return TestRun.executor.run(cmd) + + @classmethod + def remove_all(cls): + cmd = f"lvdisplay | grep 'LV Path' | awk '{{print $3}}'" + lvm_paths = TestRun.executor.run(cmd).stdout.splitlines() + for lvm_path in lvm_paths: + lv_name = lvm_path.split('/')[-1] + vg_name = lvm_path.split('/')[-2] + cls.remove(lv_name, vg_name) + + cmd = f"vgdisplay | grep 'VG Name' | awk '{{print $3}}'" + vg_names = TestRun.executor.run(cmd).stdout.splitlines() + for vg_name in vg_names: + TestRun.executor.run(f"vgchange -an {vg_name}") + VolumeGroup.remove(vg_name) + + cmd = f"pvdisplay | grep 'PV Name' | awk '{{print $3}}'" + pv_names = TestRun.executor.run(cmd).stdout.splitlines() + for pv_name in pv_names: + cls.remove_pv(pv_name) + + TestRun.LOGGER.info(f"Successfully removed all LVMs.") + + @staticmethod + def make_sure_lv_is_active(lv_path: str): + cmd = f"lvscan" + output_lines = TestRun.executor.run_expect_success(cmd).stdout.splitlines() + + for line in output_lines: + if "inactive " in line and lv_path in line: + cmd = f"lvchange -ay {lv_path}" + TestRun.executor.run_expect_success(cmd) diff --git a/test/functional/test-framework/storage_devices/partition.py b/test/functional/test-framework/storage_devices/partition.py new file mode 100644 index 0000000..080ab4c --- /dev/null +++ b/test/functional/test-framework/storage_devices/partition.py @@ -0,0 +1,22 @@ +# +# Copyright(c) 2019-2021 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# + +from storage_devices.device import Device +from test_tools import disk_utils +from test_utils.size import Size + + +class Partition(Device): + def __init__(self, parent_dev, type, number, begin: Size, end: Size): + Device.__init__(self, disk_utils.get_partition_path(parent_dev.path, number)) + self.number = number + self.parent_device = parent_dev + self.type = type + self.begin = begin + self.end = end + + def __str__(self): + return f"\tsystem path: {self.path}, size: {self.size}, type: {self.type}, " \ + f"parent device: {self.parent_device.path}\n" diff --git a/test/functional/test-framework/storage_devices/raid.py b/test/functional/test-framework/storage_devices/raid.py new file mode 100644 index 0000000..4e3685c --- /dev/null +++ b/test/functional/test-framework/storage_devices/raid.py @@ -0,0 +1,182 @@ +# +# Copyright(c) 2020-2021 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# +import threading +from enum import IntEnum, Enum + +from core.test_run import TestRun +from storage_devices.device import Device +from storage_devices.disk import Disk +from test_tools.fs_utils import readlink +from test_tools.mdadm import Mdadm +from test_utils.disk_finder import resolve_to_by_id_link +from test_utils.size import Size, Unit + + +def get_devices_paths_string(devices: [Device]): + return " ".join([d.path for d in devices]) + + +class Level(IntEnum): + Raid0 = 0 + Raid1 = 1 + Raid4 = 4 + Raid5 = 5 + Raid6 = 6 + Raid10 = 10 + + +class StripSize(IntEnum): + Strip4K = 4 + Strip8K = 8 + Strip16K = 16 + Strip32K = 32 + Strip64K = 64 + Strip128K = 128 + Strip256K = 256 + Strip1M = 1024 + + +class MetadataVariant(Enum): + Legacy = "legacy" + Imsm = "imsm" + + +class RaidConfiguration: + def __init__( + self, + level: Level = None, + metadata: MetadataVariant = MetadataVariant.Imsm, + number_of_devices: int = 0, + size: Size = None, + strip_size: StripSize = None, + name: str = None, + ): + self.level = level + self.metadata = metadata + self.number_of_devices = number_of_devices + self.size = size + self.strip_size = strip_size + self.name = name + + +class Raid(Disk): + __unique_id = 0 + __lock = threading.Lock() + + def __init__( + self, + path: str, + level: Level, + uuid: str, + container_uuid: str = None, + container_path: str = None, + metadata: MetadataVariant = MetadataVariant.Imsm, + array_devices: [Device] = [], + volume_devices: [Device] = [], + ): + Device.__init__(self, resolve_to_by_id_link(path.replace("/dev/", ""))) + self.device_name = path.split('/')[-1] + self.level = level + self.uuid = uuid + self.container_uuid = container_uuid + self.container_path = container_path + self.metadata = metadata + self.array_devices = array_devices if array_devices else volume_devices.copy() + self.volume_devices = volume_devices + self.partitions = [] + self.__block_size = None + + def __eq__(self, other): + try: + return self.uuid == other.uuid + except AttributeError: + return False + + @property + def block_size(self): + if not self.__block_size: + self.__block_size = Unit(int(self.get_sysfs_property("logical_block_size"))) + return self.__block_size + + def stop(self): + Mdadm.stop(self.path) + if self.container_path: + Mdadm.stop(self.container_path) + + @classmethod + def discover(cls): + TestRun.LOGGER.info("Discover RAIDs in system...") + raids = [] + for raid in Mdadm.examine_result(): + raids.append( + cls( + raid["path"], + Level[raid["level"]], + raid["uuid"], + raid["container"]["uuid"] if "container" in raid else None, + raid["container"]["path"] if "container" in raid else None, + MetadataVariant(raid["metadata"]), + [Device(d) for d in raid["array_devices"]], + [Device(d) for d in raid["devices"]] + ) + ) + + return raids + + @classmethod + def create( + cls, + raid_configuration: RaidConfiguration, + devices: [Device] + ): + import copy + raid_conf = copy.deepcopy(raid_configuration) + + if not raid_conf.number_of_devices: + raid_conf.number_of_devices = len(devices) + elif len(devices) < raid_conf.number_of_devices: + raise ValueError("RAID configuration requires at least " + f"{raid_conf.number_of_devices} devices") + + md_dir_path = "/dev/md/" + array_devices = devices + volume_devices = devices[:raid_conf.number_of_devices] + + if raid_conf.metadata != MetadataVariant.Legacy: + container_conf = RaidConfiguration( + name=cls.__get_unique_name(raid_conf.metadata.value), + metadata=raid_conf.metadata, + number_of_devices=len(array_devices) + ) + Mdadm.create(container_conf, get_devices_paths_string(array_devices)) + + if not raid_conf.name: + raid_conf.name = cls.__get_unique_name() + + Mdadm.create(raid_conf, get_devices_paths_string(volume_devices)) + + raid_link = md_dir_path + raid_conf.name + raid = [r for r in Mdadm.examine_result() if readlink(r["path"]) == readlink(raid_link)][0] + + return cls( + raid["path"], + raid_conf.level, + raid["uuid"], + raid["container"]["uuid"] if "container" in raid else None, + raid["container"]["path"] if "container" in raid else None, + raid_conf.metadata, + array_devices, + volume_devices + ) + + @staticmethod + def remove_all(): + Mdadm.stop() + + @classmethod + def __get_unique_name(cls, prefix: str = "Raid"): + with cls.__lock: + cls.__unique_id += 1 + return f"{prefix}{cls.__unique_id}" diff --git a/test/functional/test-framework/storage_devices/ramdisk.py b/test/functional/test-framework/storage_devices/ramdisk.py new file mode 100644 index 0000000..011b812 --- /dev/null +++ b/test/functional/test-framework/storage_devices/ramdisk.py @@ -0,0 +1,80 @@ +# +# Copyright(c) 2021 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# + +import posixpath + +from core.test_run import TestRun +from storage_devices.device import Device +from test_tools import disk_utils +from test_tools.fs_utils import ls, parse_ls_output +from test_utils.filesystem.symlink import Symlink +from test_utils.os_utils import reload_kernel_module, unload_kernel_module, is_kernel_module_loaded +from test_utils.size import Size, Unit + + +class RamDisk(Device): + _module = "brd" + + @classmethod + def create(cls, disk_size: Size, disk_count: int = 1): + if disk_count < 1: + raise ValueError("Wrong number of RAM disks requested") + + TestRun.LOGGER.info("Configure RAM disks...") + params = { + "rd_size": int(disk_size.get_value(Unit.KiB)), + "rd_nr": disk_count + } + reload_kernel_module(cls._module, params) + + if not cls._is_configured(disk_size, disk_count): + raise EnvironmentError(f"Wrong RAM disk configuration after loading '{cls._module}' " + "module") + + return cls.list() + + @classmethod + def remove_all(cls): + if not is_kernel_module_loaded(cls._module): + return + + for ram_disk in cls._list_devices(): + TestRun.executor.run(f"umount {ram_disk.full_path}") + link_path = posixpath.join("/dev/disk/by-id", ram_disk.name) + try: + link = Symlink.get_symlink(link_path=link_path, target=ram_disk.full_path) + link.remove(force=True) + except FileNotFoundError: + pass + TestRun.LOGGER.info("Removing RAM disks...") + unload_kernel_module(cls._module) + + @classmethod + def list(cls): + ram_disks = [] + for ram_disk in cls._list_devices(): + link_path = posixpath.join("/dev/disk/by-id", ram_disk.name) + link = Symlink.get_symlink( + link_path=link_path, target=ram_disk.full_path, create=True + ) + ram_disks.append(cls(link.full_path)) + + return ram_disks + + @classmethod + def _is_configured(cls, disk_size: Size, disk_count: int): + ram_disks = cls._list_devices() + return ( + len(ram_disks) >= disk_count + and Size(disk_utils.get_size(ram_disks[0].name), Unit.Byte).align_down(Unit.MiB.value) + == disk_size.align_down(Unit.MiB.value) + ) + + @staticmethod + def _list_devices(): + ls_ram_disks = ls("/dev/ram*") + if "No such file or directory" in ls_ram_disks: + return [] + return parse_ls_output(ls_ram_disks) diff --git a/test/functional/test-framework/test_tools/__init__.py b/test/functional/test-framework/test_tools/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/functional/test-framework/test_tools/blktrace.py b/test/functional/test-framework/test_tools/blktrace.py new file mode 100644 index 0000000..c9c5653 --- /dev/null +++ b/test/functional/test-framework/test_tools/blktrace.py @@ -0,0 +1,225 @@ +# +# Copyright(c) 2019-2022 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# +import math + +from aenum import IntFlag, Enum + +from datetime import timedelta + +from core.test_run import TestRun +from storage_devices.device import Device +from test_utils.filesystem.directory import Directory +from test_utils.os_utils import is_mounted, drop_caches, DropCachesMode +from test_utils.size import Size, Unit + +DEBUGFS_MOUNT_POINT = "/sys/kernel/debug" +PREFIX = "trace_" +HEADER_FORMAT = "%a|%C|%d|%e|%n|%N|%S|%5T.%9t\\n" + + +class BlkTraceMask(IntFlag): + read = 1 + write = 1 << 1 + flush = 1 << 2 + sync = 1 << 3 + queue = 1 << 4 + requeue = 1 << 5 + issue = 1 << 6 + complete = 1 << 7 + fs = 1 << 8 + pc = 1 << 9 + notify = 1 << 10 + ahead = 1 << 11 + meta = 1 << 12 + discard = 1 << 13 + drv_data = 1 << 14 + fua = 1 << 15 + + +class ActionKind(Enum): + IoDeviceRemap = "A" + IoBounce = "B" + IoCompletion = "C" + IoToDriver = "D" + IoFrontMerge = "F" + GetRequest = "G" + IoInsert = "I" + IoMerge = "M" + PlugRequest = "P" + IoHandled = "Q" + RequeueRequest = "R" + SleepRequest = "S" + TimeoutUnplug = "T" # old version of TimerUnplug + UnplugRequest = "U" + TimerUnplug = "UT" + Split = "X" + + +class RwbsKind(IntFlag): + Undefined = 0 + R = 1 # Read + W = 1 << 1 # Write + D = 1 << 2 # Discard + F = 1 << 3 # Flush + S = 1 << 4 # Synchronous + M = 1 << 5 # Metadata + A = 1 << 6 # Read Ahead + N = 1 << 7 # None of the above + + def __str__(self): + ret = [] + if self & RwbsKind.R: + ret.append("read") + if self & RwbsKind.W: + ret.append("write") + if self & RwbsKind.D: + ret.append("discard") + if self & RwbsKind.F: + ret.append("flush") + if self & RwbsKind.S: + ret.append("sync") + if self & RwbsKind.M: + ret.append("metadata") + if self & RwbsKind.A: + ret.append("readahead") + if self & RwbsKind.N: + ret.append("none") + + return "|".join(ret) + + +class BlkTrace: + def __init__(self, device: Device, *masks: BlkTraceMask): + self._mount_debugfs() + if device is None: + raise Exception("Device not provided") + self.device = device + self.masks = "" if not masks else f' -a {" -a ".join([m.name for m in masks])}' + self.blktrace_pid = -1 + self.__outputDirectoryPath = None + + @staticmethod + def _mount_debugfs(): + if not is_mounted(DEBUGFS_MOUNT_POINT): + TestRun.executor.run_expect_success(f"mount -t debugfs none {DEBUGFS_MOUNT_POINT}") + + def start_monitoring(self, buffer_size: Size = None, number_of_subbuffers: int = None): + if self.blktrace_pid != -1: + raise Exception(f"blktrace already running with PID: {self.blktrace_pid}") + + self.__outputDirectoryPath = Directory.create_temp_directory().full_path + + drop_caches(DropCachesMode.ALL) + + number_of_subbuffers = ("" if number_of_subbuffers is None + else f" --num-sub-buffers={number_of_subbuffers}") + buffer_size = ("" if buffer_size is None + else f" --buffer-size={buffer_size.get_value(Unit.KibiByte)}") + command = (f"blktrace{number_of_subbuffers}{buffer_size} --dev={self.device.path}" + f"{self.masks} --output={PREFIX} --output-dir={self.__outputDirectoryPath}") + echo_output = TestRun.executor.run_expect_success( + f"nohup {command} {self.__outputDirectoryPath}/out & echo $!" + ) + self.blktrace_pid = int(echo_output.stdout) + TestRun.LOGGER.info(f"blktrace monitoring for device {self.device.path} started" + f" (PID: {self.blktrace_pid}, output dir: {self.__outputDirectoryPath}") + + def stop_monitoring(self): + if self.blktrace_pid == -1: + raise Exception("PID for blktrace is not set - has monitoring been started?") + + drop_caches(DropCachesMode.ALL) + + TestRun.executor.run_expect_success(f"kill -s SIGINT {self.blktrace_pid}") + self.blktrace_pid = -1 + + # dummy command for swallowing output of killed command + TestRun.executor.run("sleep 2 && echo dummy") + TestRun.LOGGER.info(f"blktrace monitoring for device {self.device.path} stopped") + + return self.__parse_blktrace_output() + + def __parse_blktrace_output(self): + TestRun.LOGGER.info(f"Parsing blktrace headers from {self.__outputDirectoryPath}... " + "Be patient") + command = (f'blkparse --input-dir={self.__outputDirectoryPath} --input={PREFIX} ' + f'--format="{HEADER_FORMAT}"') + blkparse_output = TestRun.executor.run_expect_success( + command, timeout=timedelta(minutes=60) + ) + parsed_headers = [] + for line in blkparse_output.stdout.splitlines(): + # At the end per-cpu summary is posted - there is no need for it now + if line.startswith('CPU'): + break + + header = Header.parse(line) + if header is None: + continue + parsed_headers.append(header) + TestRun.LOGGER.info( + f"Parsed {len(parsed_headers)} blktrace headers from {self.__outputDirectoryPath}" + ) + parsed_headers.sort(key=lambda x: x.timestamp) + return parsed_headers + + +class Header: + def __init__(self): + self.action = None + self.block_count = None + self.byte_count = None + self.command = None + self.error_value = None + self.rwbs = RwbsKind.Undefined + self.sector_number = None + self.timestamp = None + + @staticmethod + def parse(header_line: str): + # messages/notifies are not formatted according to --format + # so should be ignored (or parsed using standard format): + if "m N" in header_line: + return None + + header_fields = header_line.split('|') + if len(header_fields) != 8: + return None + + timestamp_fields = header_fields[7].split('.') + timestamp_nano = int(timestamp_fields[-1]) if len(timestamp_fields) == 2 else 0 + + header = Header() + header.action = ActionKind(header_fields[0]) + header.command = header_fields[1] + if len(header_fields[2]): + header.rwbs = RwbsKind['|'.join(list(header_fields[2]))] + header.error_value = int(header_fields[3]) + header.block_count = int(header_fields[4]) + header.byte_count = int(header_fields[5]) + header.sector_number = int(header_fields[6]) + header.timestamp = int(timestamp_fields[0]) * math.pow(10, 9) + timestamp_nano + + return header + + def __str__(self): + ret = [] + if self.action: + ret.append(f"action: {self.action.name}") + if self.block_count: + ret.append(f"block_count: {self.block_count}") + if self.byte_count: + ret.append(f"byte_count: {self.byte_count}") + if self.command: + ret.append(f"command: {self.command}") + if self.error_value: + ret.append(f"error_value: {self.error_value}") + if self.rwbs: + ret.append(f"rwbs: {self.rwbs}") + if self.sector_number: + ret.append(f"sector_number: {self.sector_number}") + if self.timestamp: + ret.append(f"timestamp: {self.timestamp}") + return " ".join(ret) diff --git a/test/functional/test-framework/test_tools/checksec.sh b/test/functional/test-framework/test_tools/checksec.sh new file mode 100644 index 0000000..dd1f72e --- /dev/null +++ b/test/functional/test-framework/test_tools/checksec.sh @@ -0,0 +1,882 @@ +#!/bin/bash +# +# The BSD License (http://www.opensource.org/licenses/bsd-license.php) +# specifies the terms and conditions of use for checksec.sh: +# +# Copyright (c) 2009-2011, Tobias Klein. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in +# the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Tobias Klein nor the name of trapkit.de may be +# used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS +# OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED +# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +# THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH +# DAMAGE. +# +# Name : checksec.sh +# Version : 1.5 +# Author : Tobias Klein +# Date : November 2011 +# Download: http://www.trapkit.de/tools/checksec.html +# Changes : http://www.trapkit.de/tools/checksec_changes.txt +# +# Description: +# +# Modern Linux distributions offer some mitigation techniques to make it +# harder to exploit software vulnerabilities reliably. Mitigations such +# as RELRO, NoExecute (NX), Stack Canaries, Address Space Layout +# Randomization (ASLR) and Position Independent Executables (PIE) have +# made reliably exploiting any vulnerabilities that do exist far more +# challenging. The checksec.sh script is designed to test what *standard* +# Linux OS and PaX (http://pax.grsecurity.net/) security features are being +# used. +# +# As of version 1.3 the script also lists the status of various Linux kernel +# protection mechanisms. +# +# Credits: +# +# Thanks to Brad Spengler (grsecurity.net) for the PaX support. +# Thanks to Jon Oberheide (jon.oberheide.org) for the kernel support. +# Thanks to Ollie Whitehouse (Research In Motion) for rpath/runpath support. +# +# Others that contributed to checksec.sh (in no particular order): +# +# Simon Ruderich, Denis Scherbakov, Stefan Kuttler, Radoslaw Madej, +# Anthony G. Basile, Martin Vaeth and Brian Davis. +# + +# global vars +have_readelf=1 +verbose=false + +# FORTIFY_SOURCE vars +FS_end=_chk +FS_cnt_total=0 +FS_cnt_checked=0 +FS_cnt_unchecked=0 +FS_chk_func_libc=0 +FS_functions=0 +FS_libc=0 + +# version information +version() { + echo "checksec v1.5, Tobias Klein, www.trapkit.de, November 2011" + echo +} + +# help +help() { + echo "Usage: checksec [OPTION]" + echo + echo "Options:" + echo + echo " --file " + echo " --dir [-v]" + echo " --proc " + echo " --proc-all" + echo " --proc-libs " + echo " --kernel" + echo " --fortify-file " + echo " --fortify-proc " + echo " --version" + echo " --help" + echo + echo "For more information, see:" + echo " http://www.trapkit.de/tools/checksec.html" + echo +} + +# check if command exists +command_exists () { + type $1 > /dev/null 2>&1; +} + +# check if directory exists +dir_exists () { + if [ -d $1 ] ; then + return 0 + else + return 1 + fi +} + +# check user privileges +root_privs () { + if [ $(/usr/bin/id -u) -eq 0 ] ; then + return 0 + else + return 1 + fi +} + +# check if input is numeric +isNumeric () { + echo "$@" | grep -q -v "[^0-9]" +} + +# check if input is a string +isString () { + echo "$@" | grep -q -v "[^A-Za-z]" +} + +# check file(s) +filecheck() { + # check for RELRO support + if readelf -l $1 2>/dev/null | grep -q 'GNU_RELRO'; then + if readelf -d $1 2>/dev/null | grep -q 'BIND_NOW'; then + echo -n -e '\033[32mFull RELRO \033[m ' + else + echo -n -e '\033[33mPartial RELRO\033[m ' + fi + else + echo -n -e '\033[31mNo RELRO \033[m ' + fi + + # check for stack canary support + if readelf -s $1 2>/dev/null | grep -q '__stack_chk_fail'; then + echo -n -e '\033[32mCanary found \033[m ' + else + echo -n -e '\033[31mNo canary found\033[m ' + fi + + # check for NX support + if readelf -W -l $1 2>/dev/null | grep 'GNU_STACK' | grep -q 'RWE'; then + echo -n -e '\033[31mNX disabled\033[m ' + else + echo -n -e '\033[32mNX enabled \033[m ' + fi + + # check for PIE support + if readelf -h $1 2>/dev/null | grep -q 'Type:[[:space:]]*EXEC'; then + echo -n -e '\033[31mNo PIE \033[m ' + elif readelf -h $1 2>/dev/null | grep -q 'Type:[[:space:]]*DYN'; then + if readelf -d $1 2>/dev/null | grep -q '(DEBUG)'; then + echo -n -e '\033[32mPIE enabled \033[m ' + else + echo -n -e '\033[33mDSO \033[m ' + fi + else + echo -n -e '\033[33mNot an ELF file\033[m ' + fi + + # check for rpath / run path + if readelf -d $1 2>/dev/null | grep -q 'rpath'; then + echo -n -e '\033[31mRPATH \033[m ' + else + echo -n -e '\033[32mNo RPATH \033[m ' + fi + + if readelf -d $1 2>/dev/null | grep -q 'runpath'; then + echo -n -e '\033[31mRUNPATH \033[m ' + else + echo -n -e '\033[32mNo RUNPATH \033[m ' + fi +} + +# check process(es) +proccheck() { + # check for RELRO support + if readelf -l $1/exe 2>/dev/null | grep -q 'Program Headers'; then + if readelf -l $1/exe 2>/dev/null | grep -q 'GNU_RELRO'; then + if readelf -d $1/exe 2>/dev/null | grep -q 'BIND_NOW'; then + echo -n -e '\033[32mFull RELRO \033[m ' + else + echo -n -e '\033[33mPartial RELRO \033[m ' + fi + else + echo -n -e '\033[31mNo RELRO \033[m ' + fi + else + echo -n -e '\033[31mPermission denied (please run as root)\033[m\n' + exit 1 + fi + + # check for stack canary support + if readelf -s $1/exe 2>/dev/null | grep -q 'Symbol table'; then + if readelf -s $1/exe 2>/dev/null | grep -q '__stack_chk_fail'; then + echo -n -e '\033[32mCanary found \033[m ' + else + echo -n -e '\033[31mNo canary found \033[m ' + fi + else + if [ "$1" != "1" ] ; then + echo -n -e '\033[33mPermission denied \033[m ' + else + echo -n -e '\033[33mNo symbol table found\033[m ' + fi + fi + + # first check for PaX support + if cat $1/status 2> /dev/null | grep -q 'PaX:'; then + pageexec=( $(cat $1/status 2> /dev/null | grep 'PaX:' | cut -b6) ) + segmexec=( $(cat $1/status 2> /dev/null | grep 'PaX:' | cut -b10) ) + mprotect=( $(cat $1/status 2> /dev/null | grep 'PaX:' | cut -b8) ) + randmmap=( $(cat $1/status 2> /dev/null | grep 'PaX:' | cut -b9) ) + if [[ "$pageexec" = "P" || "$segmexec" = "S" ]] && [[ "$mprotect" = "M" && "$randmmap" = "R" ]] ; then + echo -n -e '\033[32mPaX enabled\033[m ' + elif [[ "$pageexec" = "p" && "$segmexec" = "s" && "$randmmap" = "R" ]] ; then + echo -n -e '\033[33mPaX ASLR only\033[m ' + elif [[ "$pageexec" = "P" || "$segmexec" = "S" ]] && [[ "$mprotect" = "m" && "$randmmap" = "R" ]] ; then + echo -n -e '\033[33mPaX mprot off \033[m' + elif [[ "$pageexec" = "P" || "$segmexec" = "S" ]] && [[ "$mprotect" = "M" && "$randmmap" = "r" ]] ; then + echo -n -e '\033[33mPaX ASLR off\033[m ' + elif [[ "$pageexec" = "P" || "$segmexec" = "S" ]] && [[ "$mprotect" = "m" && "$randmmap" = "r" ]] ; then + echo -n -e '\033[33mPaX NX only\033[m ' + else + echo -n -e '\033[31mPaX disabled\033[m ' + fi + # fallback check for NX support + elif readelf -W -l $1/exe 2>/dev/null | grep 'GNU_STACK' | grep -q 'RWE'; then + echo -n -e '\033[31mNX disabled\033[m ' + else + echo -n -e '\033[32mNX enabled \033[m ' + fi + + # check for PIE support + if readelf -h $1/exe 2>/dev/null | grep -q 'Type:[[:space:]]*EXEC'; then + echo -n -e '\033[31mNo PIE \033[m ' + elif readelf -h $1/exe 2>/dev/null | grep -q 'Type:[[:space:]]*DYN'; then + if readelf -d $1/exe 2>/dev/null | grep -q '(DEBUG)'; then + echo -n -e '\033[32mPIE enabled \033[m ' + else + echo -n -e '\033[33mDynamic Shared Object\033[m ' + fi + else + echo -n -e '\033[33mNot an ELF file \033[m ' + fi +} + +# check mapped libraries +libcheck() { + libs=( $(awk '{ print $6 }' /proc/$1/maps | grep '/' | sort -u | xargs file | grep ELF | awk '{ print $1 }' | sed 's/:/ /') ) + + printf "\n* Loaded libraries (file information, # of mapped files: ${#libs[@]}):\n\n" + + for element in $(seq 0 $((${#libs[@]} - 1))) + do + echo " ${libs[$element]}:" + echo -n " " + filecheck ${libs[$element]} + printf "\n\n" + done +} + +# check for system-wide ASLR support +aslrcheck() { + # PaX ASLR support + if !(cat /proc/1/status 2> /dev/null | grep -q 'Name:') ; then + echo -n -e ':\033[33m insufficient privileges for PaX ASLR checks\033[m\n' + echo -n -e ' Fallback to standard Linux ASLR check' + fi + + if cat /proc/1/status 2> /dev/null | grep -q 'PaX:'; then + printf ": " + if cat /proc/1/status 2> /dev/null | grep 'PaX:' | grep -q 'R'; then + echo -n -e '\033[32mPaX ASLR enabled\033[m\n\n' + else + echo -n -e '\033[31mPaX ASLR disabled\033[m\n\n' + fi + else + # standard Linux 'kernel.randomize_va_space' ASLR support + # (see the kernel file 'Documentation/sysctl/kernel.txt' for a detailed description) + printf " (kernel.randomize_va_space): " + if /sbin/sysctl -a 2>/dev/null | grep -q 'kernel.randomize_va_space = 1'; then + echo -n -e '\033[33mOn (Setting: 1)\033[m\n\n' + printf " Description - Make the addresses of mmap base, stack and VDSO page randomized.\n" + printf " This, among other things, implies that shared libraries will be loaded to \n" + printf " random addresses. Also for PIE-linked binaries, the location of code start\n" + printf " is randomized. Heap addresses are *not* randomized.\n\n" + elif /sbin/sysctl -a 2>/dev/null | grep -q 'kernel.randomize_va_space = 2'; then + echo -n -e '\033[32mOn (Setting: 2)\033[m\n\n' + printf " Description - Make the addresses of mmap base, heap, stack and VDSO page randomized.\n" + printf " This, among other things, implies that shared libraries will be loaded to random \n" + printf " addresses. Also for PIE-linked binaries, the location of code start is randomized.\n\n" + elif /sbin/sysctl -a 2>/dev/null | grep -q 'kernel.randomize_va_space = 0'; then + echo -n -e '\033[31mOff (Setting: 0)\033[m\n' + else + echo -n -e '\033[31mNot supported\033[m\n' + fi + printf " See the kernel file 'Documentation/sysctl/kernel.txt' for more details.\n\n" + fi +} + +# check cpu nx flag +nxcheck() { + if grep -q nx /proc/cpuinfo; then + echo -n -e '\033[32mYes\033[m\n\n' + else + echo -n -e '\033[31mNo\033[m\n\n' + fi +} + +# check for kernel protection mechanisms +kernelcheck() { + printf " Description - List the status of kernel protection mechanisms. Rather than\n" + printf " inspect kernel mechanisms that may aid in the prevention of exploitation of\n" + printf " userspace processes, this option lists the status of kernel configuration\n" + printf " options that harden the kernel itself against attack.\n\n" + printf " Kernel config: " + + if [ -f /proc/config.gz ] ; then + kconfig="zcat /proc/config.gz" + printf "\033[32m/proc/config.gz\033[m\n\n" + elif [ -f /boot/config-`uname -r` ] ; then + kconfig="cat /boot/config-`uname -r`" + printf "\033[33m/boot/config-`uname -r`\033[m\n\n" + printf " Warning: The config on disk may not represent running kernel config!\n\n"; + elif [ -f "${KBUILD_OUTPUT:-/usr/src/linux}"/.config ] ; then + kconfig="cat ${KBUILD_OUTPUT:-/usr/src/linux}/.config" + printf "\033[33m%s\033[m\n\n" "${KBUILD_OUTPUT:-/usr/src/linux}/.config" + printf " Warning: The config on disk may not represent running kernel config!\n\n"; + else + printf "\033[31mNOT FOUND\033[m\n\n" + exit 0 + fi + + printf " GCC stack protector support: " + if $kconfig | grep -qi 'CONFIG_CC_STACKPROTECTOR=y'; then + printf "\033[32mEnabled\033[m\n" + else + printf "\033[31mDisabled\033[m\n" + fi + + printf " Strict user copy checks: " + if $kconfig | grep -qi 'CONFIG_DEBUG_STRICT_USER_COPY_CHECKS=y'; then + printf "\033[32mEnabled\033[m\n" + else + printf "\033[31mDisabled\033[m\n" + fi + + printf " Enforce read-only kernel data: " + if $kconfig | grep -qi 'CONFIG_DEBUG_RODATA=y'; then + printf "\033[32mEnabled\033[m\n" + else + printf "\033[31mDisabled\033[m\n" + fi + printf " Restrict /dev/mem access: " + if $kconfig | grep -qi 'CONFIG_STRICT_DEVMEM=y'; then + printf "\033[32mEnabled\033[m\n" + else + printf "\033[31mDisabled\033[m\n" + fi + + printf " Restrict /dev/kmem access: " + if $kconfig | grep -qi 'CONFIG_DEVKMEM=y'; then + printf "\033[31mDisabled\033[m\n" + else + printf "\033[32mEnabled\033[m\n" + fi + + printf "\n" + printf "* grsecurity / PaX: " + + if $kconfig | grep -qi 'CONFIG_GRKERNSEC=y'; then + if $kconfig | grep -qi 'CONFIG_GRKERNSEC_HIGH=y'; then + printf "\033[32mHigh GRKERNSEC\033[m\n\n" + elif $kconfig | grep -qi 'CONFIG_GRKERNSEC_MEDIUM=y'; then + printf "\033[33mMedium GRKERNSEC\033[m\n\n" + elif $kconfig | grep -qi 'CONFIG_GRKERNSEC_LOW=y'; then + printf "\033[31mLow GRKERNSEC\033[m\n\n" + else + printf "\033[33mCustom GRKERNSEC\033[m\n\n" + fi + + printf " Non-executable kernel pages: " + if $kconfig | grep -qi 'CONFIG_PAX_KERNEXEC=y'; then + printf "\033[32mEnabled\033[m\n" + else + printf "\033[31mDisabled\033[m\n" + fi + + printf " Prevent userspace pointer deref: " + if $kconfig | grep -qi 'CONFIG_PAX_MEMORY_UDEREF=y'; then + printf "\033[32mEnabled\033[m\n" + else + printf "\033[31mDisabled\033[m\n" + fi + + printf " Prevent kobject refcount overflow: " + if $kconfig | grep -qi 'CONFIG_PAX_REFCOUNT=y'; then + printf "\033[32mEnabled\033[m\n" + else + printf "\033[31mDisabled\033[m\n" + fi + + printf " Bounds check heap object copies: " + if $kconfig | grep -qi 'CONFIG_PAX_USERCOPY=y'; then + printf "\033[32mEnabled\033[m\n" + else + printf "\033[31mDisabled\033[m\n" + fi + + printf " Disable writing to kmem/mem/port: " + if $kconfig | grep -qi 'CONFIG_GRKERNSEC_KMEM=y'; then + printf "\033[32mEnabled\033[m\n" + else + printf "\033[31mDisabled\033[m\n" + fi + + printf " Disable privileged I/O: " + if $kconfig | grep -qi 'CONFIG_GRKERNSEC_IO=y'; then + printf "\033[32mEnabled\033[m\n" + else + printf "\033[31mDisabled\033[m\n" + fi + + printf " Harden module auto-loading: " + if $kconfig | grep -qi 'CONFIG_GRKERNSEC_MODHARDEN=y'; then + printf "\033[32mEnabled\033[m\n" + else + printf "\033[31mDisabled\033[m\n" + fi + + printf " Hide kernel symbols: " + if $kconfig | grep -qi 'CONFIG_GRKERNSEC_HIDESYM=y'; then + printf "\033[32mEnabled\033[m\n" + else + printf "\033[31mDisabled\033[m\n" + fi + else + printf "\033[31mNo GRKERNSEC\033[m\n\n" + printf " The grsecurity / PaX patchset is available here:\n" + printf " http://grsecurity.net/\n" + fi + + printf "\n" + printf "* Kernel Heap Hardening: " + + if $kconfig | grep -qi 'CONFIG_KERNHEAP=y'; then + if $kconfig | grep -qi 'CONFIG_KERNHEAP_FULLPOISON=y'; then + printf "\033[32mFull KERNHEAP\033[m\n\n" + else + printf "\033[33mPartial KERNHEAP\033[m\n\n" + fi + else + printf "\033[31mNo KERNHEAP\033[m\n\n" + printf " The KERNHEAP hardening patchset is available here:\n" + printf " https://www.subreption.com/kernheap/\n\n" + fi +} + +# --- FORTIFY_SOURCE subfunctions (start) --- + +# is FORTIFY_SOURCE supported by libc? +FS_libc_check() { + printf "* FORTIFY_SOURCE support available (libc) : " + + if [ "${#FS_chk_func_libc[@]}" != "0" ] ; then + printf "\033[32mYes\033[m\n" + else + printf "\033[31mNo\033[m\n" + exit 1 + fi +} + +# was the binary compiled with FORTIFY_SOURCE? +FS_binary_check() { + printf "* Binary compiled with FORTIFY_SOURCE support: " + + for FS_elem_functions in $(seq 0 $((${#FS_functions[@]} - 1))) + do + if [[ ${FS_functions[$FS_elem_functions]} =~ _chk ]] ; then + printf "\033[32mYes\033[m\n" + return + fi + done + printf "\033[31mNo\033[m\n" + exit 1 +} + +FS_comparison() { + echo + printf " ------ EXECUTABLE-FILE ------- . -------- LIBC --------\n" + printf " FORTIFY-able library functions | Checked function names\n" + printf " -------------------------------------------------------\n" + + for FS_elem_libc in $(seq 0 $((${#FS_chk_func_libc[@]} - 1))) + do + for FS_elem_functions in $(seq 0 $((${#FS_functions[@]} - 1))) + do + FS_tmp_func=${FS_functions[$FS_elem_functions]} + FS_tmp_libc=${FS_chk_func_libc[$FS_elem_libc]} + + if [[ $FS_tmp_func =~ ^$FS_tmp_libc$ ]] ; then + printf " \033[31m%-30s\033[m | __%s%s\n" $FS_tmp_func $FS_tmp_libc $FS_end + let FS_cnt_total++ + let FS_cnt_unchecked++ + elif [[ $FS_tmp_func =~ ^$FS_tmp_libc(_chk) ]] ; then + printf " \033[32m%-30s\033[m | __%s%s\n" $FS_tmp_func $FS_tmp_libc $FS_end + let FS_cnt_total++ + let FS_cnt_checked++ + fi + + done + done +} + +FS_summary() { + echo + printf "SUMMARY:\n\n" + printf "* Number of checked functions in libc : ${#FS_chk_func_libc[@]}\n" + printf "* Total number of library functions in the executable: ${#FS_functions[@]}\n" + printf "* Number of FORTIFY-able functions in the executable : %s\n" $FS_cnt_total + printf "* Number of checked functions in the executable : \033[32m%s\033[m\n" $FS_cnt_checked + printf "* Number of unchecked functions in the executable : \033[31m%s\033[m\n" $FS_cnt_unchecked + echo +} + +# --- FORTIFY_SOURCE subfunctions (end) --- + +if !(command_exists readelf) ; then + printf "\033[31mWarning: 'readelf' not found! It's required for most checks.\033[m\n\n" + have_readelf=0 +fi + +# parse command-line arguments +case "$1" in + + --version) + version + exit 0 + ;; + + --help) + help + exit 0 + ;; + + --dir) + if [ "$3" = "-v" ] ; then + verbose=true + fi + if [ $have_readelf -eq 0 ] ; then + exit 1 + fi + if [ -z "$2" ] ; then + printf "\033[31mError: Please provide a valid directory.\033[m\n\n" + exit 1 + fi + # remove trailing slashes + tempdir=`echo $2 | sed -e "s/\/*$//"` + if [ ! -d $tempdir ] ; then + printf "\033[31mError: The directory '$tempdir' does not exist.\033[m\n\n" + exit 1 + fi + cd $tempdir + printf "RELRO STACK CANARY NX PIE RPATH RUNPATH FILE\n" + for N in [A-Za-z]*; do + if [ "$N" != "[A-Za-z]*" ]; then + # read permissions? + if [ ! -r $N ]; then + printf "\033[31mError: No read permissions for '$tempdir/$N' (run as root).\033[m\n" + else + # ELF executable? + out=`file $N` + if [[ ! $out =~ ELF ]] ; then + if [ "$verbose" = "true" ] ; then + printf "\033[34m*** Not an ELF file: $tempdir/" + file $N + printf "\033[m" + fi + else + filecheck $N + if [ `find $tempdir/$N \( -perm -004000 -o -perm -002000 \) -type f -print` ]; then + printf "\033[37;41m%s%s\033[m" $2 $N + else + printf "%s%s" $tempdir/ $N + fi + echo + fi + fi + fi + done + exit 0 + ;; + + --file) + if [ $have_readelf -eq 0 ] ; then + exit 1 + fi + if [ -z "$2" ] ; then + printf "\033[31mError: Please provide a valid file.\033[m\n\n" + exit 1 + fi + # does the file exist? + if [ ! -e $2 ] ; then + printf "\033[31mError: The file '$2' does not exist.\033[m\n\n" + exit 1 + fi + # read permissions? + if [ ! -r $2 ] ; then + printf "\033[31mError: No read permissions for '$2' (run as root).\033[m\n\n" + exit 1 + fi + # ELF executable? + out=`file $2` + if [[ ! $out =~ ELF ]] ; then + printf "\033[31mError: Not an ELF file: " + file $2 + printf "\033[m\n" + exit 1 + fi + printf "RELRO STACK CANARY NX PIE RPATH RUNPATH FILE\n" + filecheck $2 + if [ `find $2 \( -perm -004000 -o -perm -002000 \) -type f -print` ] ; then + printf "\033[37;41m%s%s\033[m" $2 $N + else + printf "%s" $2 + fi + echo + exit 0 + ;; + + --proc-all) + if [ $have_readelf -eq 0 ] ; then + exit 1 + fi + cd /proc + printf "* System-wide ASLR" + aslrcheck + printf "* Does the CPU support NX: " + nxcheck + printf " COMMAND PID RELRO STACK CANARY NX/PaX PIE\n" + for N in [1-9]*; do + if [ $N != $$ ] && readlink -q $N/exe > /dev/null; then + printf "%16s" `head -1 $N/status | cut -b 7-` + printf "%7d " $N + proccheck $N + echo + fi + done + if [ ! -e /usr/bin/id ] ; then + printf "\n\033[33mNote: If you are running 'checksec.sh' as an unprivileged user, you\n" + printf " will not see all processes. Please run the script as root.\033[m\n\n" + else + if !(root_privs) ; then + printf "\n\033[33mNote: You are running 'checksec.sh' as an unprivileged user.\n" + printf " Too see all processes, please run the script as root.\033[m\n\n" + fi + fi + exit 0 + ;; + + --proc) + if [ $have_readelf -eq 0 ] ; then + exit 1 + fi + if [ -z "$2" ] ; then + printf "\033[31mError: Please provide a valid process name.\033[m\n\n" + exit 1 + fi + if !(isString "$2") ; then + printf "\033[31mError: Please provide a valid process name.\033[m\n\n" + exit 1 + fi + cd /proc + printf "* System-wide ASLR" + aslrcheck + printf "* Does the CPU support NX: " + nxcheck + printf " COMMAND PID RELRO STACK CANARY NX/PaX PIE\n" + for N in `ps -Ao pid,comm | grep $2 | cut -b1-6`; do + if [ -d $N ] ; then + printf "%16s" `head -1 $N/status | cut -b 7-` + printf "%7d " $N + # read permissions? + if [ ! -r $N/exe ] ; then + if !(root_privs) ; then + printf "\033[31mNo read permissions for '/proc/$N/exe' (run as root).\033[m\n\n" + exit 1 + fi + if [ ! `readlink $N/exe` ] ; then + printf "\033[31mPermission denied. Requested process ID belongs to a kernel thread.\033[m\n\n" + exit 1 + fi + exit 1 + fi + proccheck $N + echo + fi + done + exit 0 + ;; + + --proc-libs) + if [ $have_readelf -eq 0 ] ; then + exit 1 + fi + if [ -z "$2" ] ; then + printf "\033[31mError: Please provide a valid process ID.\033[m\n\n" + exit 1 + fi + if !(isNumeric "$2") ; then + printf "\033[31mError: Please provide a valid process ID.\033[m\n\n" + exit 1 + fi + cd /proc + printf "* System-wide ASLR" + aslrcheck + printf "* Does the CPU support NX: " + nxcheck + printf "* Process information:\n\n" + printf " COMMAND PID RELRO STACK CANARY NX/PaX PIE\n" + N=$2 + if [ -d $N ] ; then + printf "%16s" `head -1 $N/status | cut -b 7-` + printf "%7d " $N + # read permissions? + if [ ! -r $N/exe ] ; then + if !(root_privs) ; then + printf "\033[31mNo read permissions for '/proc/$N/exe' (run as root).\033[m\n\n" + exit 1 + fi + if [ ! `readlink $N/exe` ] ; then + printf "\033[31mPermission denied. Requested process ID belongs to a kernel thread.\033[m\n\n" + exit 1 + fi + exit 1 + fi + proccheck $N + echo + libcheck $N + fi + exit 0 + ;; + + --kernel) + cd /proc + printf "* Kernel protection information:\n\n" + kernelcheck + exit 0 + ;; + + --fortify-file) + if [ $have_readelf -eq 0 ] ; then + exit 1 + fi + if [ -z "$2" ] ; then + printf "\033[31mError: Please provide a valid file.\033[m\n\n" + exit 1 + fi + # does the file exist? + if [ ! -e $2 ] ; then + printf "\033[31mError: The file '$2' does not exist.\033[m\n\n" + exit 1 + fi + # read permissions? + if [ ! -r $2 ] ; then + printf "\033[31mError: No read permissions for '$2' (run as root).\033[m\n\n" + exit 1 + fi + # ELF executable? + out=`file $2` + if [[ ! $out =~ ELF ]] ; then + printf "\033[31mError: Not an ELF file: " + file $2 + printf "\033[m\n" + exit 1 + fi + if [ -e /lib/libc.so.6 ] ; then + FS_libc=/lib/libc.so.6 + elif [ -e /lib64/libc.so.6 ] ; then + FS_libc=/lib64/libc.so.6 + elif [ -e /lib/i386-linux-gnu/libc.so.6 ] ; then + FS_libc=/lib/i386-linux-gnu/libc.so.6 + elif [ -e /lib/x86_64-linux-gnu/libc.so.6 ] ; then + FS_libc=/lib/x86_64-linux-gnu/libc.so.6 + else + printf "\033[31mError: libc not found.\033[m\n\n" + exit 1 + fi + + FS_chk_func_libc=( $(readelf -s $FS_libc | grep _chk@@ | awk '{ print $8 }' | cut -c 3- | sed -e 's/_chk@.*//') ) + FS_functions=( $(readelf -s $2 | awk '{ print $8 }' | sed 's/_*//' | sed -e 's/@.*//') ) + + FS_libc_check + FS_binary_check + FS_comparison + FS_summary + + exit 0 + ;; + + --fortify-proc) + if [ $have_readelf -eq 0 ] ; then + exit 1 + fi + if [ -z "$2" ] ; then + printf "\033[31mError: Please provide a valid process ID.\033[m\n\n" + exit 1 + fi + if !(isNumeric "$2") ; then + printf "\033[31mError: Please provide a valid process ID.\033[m\n\n" + exit 1 + fi + cd /proc + N=$2 + if [ -d $N ] ; then + # read permissions? + if [ ! -r $N/exe ] ; then + if !(root_privs) ; then + printf "\033[31mNo read permissions for '/proc/$N/exe' (run as root).\033[m\n\n" + exit 1 + fi + if [ ! `readlink $N/exe` ] ; then + printf "\033[31mPermission denied. Requested process ID belongs to a kernel thread.\033[m\n\n" + exit 1 + fi + exit 1 + fi + if [ -e /lib/libc.so.6 ] ; then + FS_libc=/lib/libc.so.6 + elif [ -e /lib64/libc.so.6 ] ; then + FS_libc=/lib64/libc.so.6 + elif [ -e /lib/i386-linux-gnu/libc.so.6 ] ; then + FS_libc=/lib/i386-linux-gnu/libc.so.6 + elif [ -e /lib/x86_64-linux-gnu/libc.so.6 ] ; then + FS_libc=/lib/x86_64-linux-gnu/libc.so.6 + else + printf "\033[31mError: libc not found.\033[m\n\n" + exit 1 + fi + printf "* Process name (PID) : %s (%d)\n" `head -1 $N/status | cut -b 7-` $N + FS_chk_func_libc=( $(readelf -s $FS_libc | grep _chk@@ | awk '{ print $8 }' | cut -c 3- | sed -e 's/_chk@.*//') ) + FS_functions=( $(readelf -s $2/exe | awk '{ print $8 }' | sed 's/_*//' | sed -e 's/@.*//') ) + + FS_libc_check + FS_binary_check + FS_comparison + FS_summary + fi + exit 0 + ;; + + *) + if [ "$#" != "0" ] ; then + printf "\033[31mError: Unknown option '$1'.\033[m\n\n" + fi + help + exit 1 + ;; +esac diff --git a/test/functional/test-framework/test_tools/dd.py b/test/functional/test-framework/test_tools/dd.py new file mode 100644 index 0000000..1ca51db --- /dev/null +++ b/test/functional/test-framework/test_tools/dd.py @@ -0,0 +1,40 @@ +# +# Copyright(c) 2019-2021 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# + +import test_utils.linux_command as linux_comm +import test_utils.size as size +from core.test_run import TestRun + + +class Dd(linux_comm.LinuxCommand): + def __init__(self): + linux_comm.LinuxCommand.__init__(self, TestRun.executor, 'dd') + + def block_size(self, value: size.Size): + return self.set_param('bs', int(value.get_value())) + + def count(self, value): + return self.set_param('count', value) + + def input(self, value): + return self.set_param('if', value) + + def iflag(self, *values): + return self.set_param('iflag', *values) + + def oflag(self, *values): + return self.set_param('oflag', *values) + + def conv(self, *values): + return self.set_param('conv', *values) + + def output(self, value): + return self.set_param('of', value) + + def seek(self, value): + return self.set_param('seek', value) + + def skip(self, value): + return self.set_param('skip', value) diff --git a/test/functional/test-framework/test_tools/ddrescue.py b/test/functional/test-framework/test_tools/ddrescue.py new file mode 100644 index 0000000..dfd3079 --- /dev/null +++ b/test/functional/test-framework/test_tools/ddrescue.py @@ -0,0 +1,47 @@ +# +# Copyright(c) 2019-2021 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# + +import test_utils.linux_command as linux_comm +import test_utils.size as size +from core.test_run import TestRun + + +class Ddrescue(linux_comm.LinuxCommand): + def __init__(self): + linux_comm.LinuxCommand.__init__(self, TestRun.executor, 'ddrescue') + self.source_path = None + self.destination_path = None + self.param_name_prefix = "--" + + def source(self, value): + self.source_path = value + return self + + def destination(self, value): + self.destination_path = value + return self + + def reverse(self): + return self.set_flags("reverse") + + def synchronous(self): + return self.set_flags("synchronous") + + def direct(self): + return self.set_flags("direct") + + def force(self): + return self.set_flags("force") + + def block_size(self, value: size.Size): + return self.set_param('sector-size', int(value.get_value())) + + def size(self, value: size.Size): + return self.set_param('size', int(value.get_value())) + + def __str__(self): + command = linux_comm.LinuxCommand.__str__(self) + command += f" {self.source_path} {self.destination_path}" + return command diff --git a/test/functional/test-framework/test_tools/device_mapper.py b/test/functional/test-framework/test_tools/device_mapper.py new file mode 100644 index 0000000..ede38df --- /dev/null +++ b/test/functional/test-framework/test_tools/device_mapper.py @@ -0,0 +1,329 @@ +# +# Copyright(c) 2019-2021 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# + +from enum import Enum + +from core.test_run import TestRun +from storage_devices.device import Device +from test_utils.disk_finder import resolve_to_by_id_link +from test_utils.linux_command import LinuxCommand +from test_utils.size import Size, Unit + + +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.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.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.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() + self.path = resolve_to_by_id_link(self.mapper.get_path().replace('/dev/', '')) + + @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 diff --git a/test/functional/test-framework/test_tools/disk_utils.py b/test/functional/test-framework/test_tools/disk_utils.py new file mode 100644 index 0000000..38ac3a7 --- /dev/null +++ b/test/functional/test-framework/test_tools/disk_utils.py @@ -0,0 +1,397 @@ +# +# Copyright(c) 2019-2022 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# + +import posixpath +import re +import time +from enum import Enum + +from core.test_run import TestRun +from test_tools import fs_utils +from test_tools.dd import Dd +from test_tools.fs_utils import readlink, parse_ls_output, ls +from test_utils.output import CmdException +from test_utils.size import Size, Unit + +SECTOR_SIZE = 512 + + +class Filesystem(Enum): + xfs = 0 + ext3 = 1 + ext4 = 2 + + +class PartitionTable(Enum): + msdos = 0 + gpt = 1 + + +class PartitionType(Enum): + efi = 0 + primary = 1 + extended = 2 + logical = 3 + lvm = 4 + msr = 5 + swap = 6 + standard = 7 + unknown = 8 + + +def create_filesystem(device, filesystem: Filesystem, force=True, blocksize=None): + TestRun.LOGGER.info( + f"Creating filesystem ({filesystem.name}) on device: {device.path}") + force_param = ' -f ' if filesystem == Filesystem.xfs else ' -F ' + force_param = force_param if force else '' + block_size_param = f' -b size={blocksize}' if filesystem == Filesystem.xfs \ + else f' -b {blocksize}' + block_size_param = block_size_param if blocksize else '' + cmd = f'mkfs.{filesystem.name} {force_param} {device.path} {block_size_param}' + cmd = re.sub(' +', ' ', cmd) + TestRun.executor.run_expect_success(cmd) + TestRun.LOGGER.info( + f"Successfully created filesystem on device: {device.path}") + + +def create_partition_table(device, partition_table_type: PartitionTable = PartitionTable.gpt): + TestRun.LOGGER.info( + f"Creating partition table ({partition_table_type.name}) for device: {device.path}") + cmd = f'parted --script {device.path} mklabel {partition_table_type.name}' + TestRun.executor.run_expect_success(cmd) + device.partition_table = partition_table_type + TestRun.LOGGER.info( + f"Successfully created {partition_table_type.name} " + f"partition table on device: {device.path}") + + +def get_partition_path(parent_dev, number): + # TODO: change this to be less specific hw dependent (kernel) + if "dev/cas" not in parent_dev: + id_separator = '-part' + else: + id_separator = 'p' # "cas1-1p1" + return f'{parent_dev}{id_separator}{number}' + + +def remove_parition(device, part_number): + TestRun.LOGGER.info(f"Removing part {part_number} from {device.path}") + cmd = f'parted --script {device.path} rm {part_number}' + output = TestRun.executor.run(cmd) + + if output.exit_code != 0: + TestRun.executor.run_expect_success("partprobe") + + +def create_partition( + device, + part_size, + part_number, + part_type: PartitionType = PartitionType.primary, + unit=Unit.MebiByte, + aligned: bool = True): + TestRun.LOGGER.info( + f"Creating {part_type.name} partition on device: {device.path}") + + begin = get_first_partition_offset(device, aligned) + for part in device.partitions: + begin += part.size + if part.type == PartitionType.logical: + begin += Size(1, Unit.MebiByte if not aligned else device.block_size) + + if part_type == PartitionType.logical: + begin += Size(1, Unit.MebiByte if not aligned else device.block_size) + + if part_size != Size.zero(): + end = (begin + part_size) + end_cmd = f'{end.get_value(unit)}{unit_to_string(unit)}' + else: + end_cmd = '100%' + + cmd = f'parted --script {device.path} mkpart ' \ + f'{part_type.name} ' \ + f'{begin.get_value(unit)}{unit_to_string(unit)} ' \ + f'{end_cmd}' + output = TestRun.executor.run(cmd) + + if output.exit_code != 0: + TestRun.executor.run_expect_success("partprobe") + + TestRun.executor.run_expect_success("udevadm settle") + if not check_partition_after_create( + part_size, + part_number, + device.path, + part_type, + aligned): + raise Exception("Could not create partition!") + + if part_type != PartitionType.extended: + from storage_devices.partition import Partition + new_part = Partition(device, + part_type, + part_number, + begin, + end if type(end) is Size else device.size) + dd = Dd().input("/dev/zero") \ + .output(new_part.path) \ + .count(1) \ + .block_size(Size(1, Unit.Blocks4096)) \ + .oflag("direct") + dd.run() + device.partitions.append(new_part) + + TestRun.LOGGER.info(f"Successfully created {part_type.name} partition on {device.path}") + + +def available_disk_size(device): + dev = f"/dev/{device.get_device_id()}" + # get number of device's sectors + disk_sectors = int(TestRun.executor.run(f"fdisk -l {dev} | grep {dev} | grep sectors " + f"| awk '{{print $7 }}' ").stdout) + # get last occupied sector + last_occupied_sector = int(TestRun.executor.run(f"fdisk -l {dev} | grep {dev} " + f"| awk '{{print $3 }}' | tail -1").stdout) + available_disk_sectors = disk_sectors - last_occupied_sector + return Size(available_disk_sectors, Unit(get_block_size(device))) + + +def create_partitions(device, sizes: [], partition_table_type=PartitionTable.gpt): + create_partition_table(device, partition_table_type) + partition_type = PartitionType.primary + partition_number_offset = 0 + msdos_part_max_size = Size(2, Unit.TeraByte) + + for s in sizes: + size = Size( + s.get_value(device.block_size) - device.block_size.value, device.block_size) + if partition_table_type == PartitionTable.msdos and \ + len(sizes) > 4 and len(device.partitions) == 3: + if available_disk_size(device) > msdos_part_max_size: + part_size = msdos_part_max_size + else: + part_size = Size.zero() + create_partition(device, + part_size, + 4, + PartitionType.extended) + partition_type = PartitionType.logical + partition_number_offset = 1 + + partition_number = len(device.partitions) + 1 + partition_number_offset + create_partition(device, + size, + partition_number, + partition_type, + Unit.MebiByte, + True) + + +def get_block_size(device): + try: + block_size = float(TestRun.executor.run( + f"cat {get_sysfs_path(device)}/queue/hw_sector_size").stdout) + except ValueError: + block_size = Unit.Blocks512.value + return block_size + + +def get_size(device): + output = TestRun.executor.run_expect_success(f"cat {get_sysfs_path(device)}/size") + blocks_count = int(output.stdout) + return blocks_count * SECTOR_SIZE + + +def get_sysfs_path(device): + sysfs_path = f"/sys/class/block/{device}" + if TestRun.executor.run(f"test -d {sysfs_path}").exit_code != 0: + sysfs_path = f"/sys/block/{device}" + return sysfs_path + + +def get_pci_address(device): + pci_address = TestRun.executor.run(f"cat /sys/block/{device}/device/address").stdout + return pci_address + + +def check_partition_after_create(size, part_number, parent_dev_path, part_type, aligned): + partition_path = get_partition_path(parent_dev_path, part_number) + if "dev/cas" not in partition_path: + cmd = f"find {partition_path} -type l" + else: + cmd = f"find {partition_path}" + output = TestRun.executor.run_expect_success(cmd).stdout + if partition_path not in output: + TestRun.LOGGER.info( + "Partition created, but could not find it in system, trying 'hdparm -z'") + TestRun.executor.run_expect_success(f"hdparm -z {parent_dev_path}") + output_after_hdparm = TestRun.executor.run_expect_success( + f"parted --script {parent_dev_path} print").stdout + TestRun.LOGGER.info(output_after_hdparm) + + counter = 0 + while partition_path not in output and counter < 10: + time.sleep(2) + output = TestRun.executor.run(cmd).stdout + counter += 1 + + if len(output.split('\n')) > 1 or partition_path not in output: + return False + + if aligned and part_type != PartitionType.extended \ + and size.get_value(Unit.Byte) % Unit.Blocks4096.value != 0: + TestRun.LOGGER.warning( + f"Partition {partition_path} is not 4k aligned: {size.get_value(Unit.KibiByte)}KiB") + + partition_size = get_size(readlink(partition_path).split('/')[-1]) + if part_type == PartitionType.extended or \ + partition_size == size.get_value(Unit.Byte): + return True + + TestRun.LOGGER.warning( + f"Partition size {partition_size} does not match expected {size.get_value(Unit.Byte)} size." + ) + return True + + +def get_first_partition_offset(device, aligned: bool): + if aligned: + return Size(1, Unit.MebiByte) + # 33 sectors are reserved for the backup GPT + return Size(34, Unit(device.blocksize)) \ + if device.partition_table == PartitionTable.gpt else Size(1, device.blocksize) + + +def remove_partitions(device): + from test_utils.os_utils import Udev + if device.is_mounted(): + device.unmount() + + for partition in device.partitions: + unmount(partition) + + TestRun.LOGGER.info(f"Removing partitions from device: {device.path} " + f"({device.get_device_id()}).") + device.wipe_filesystem() + Udev.trigger() + Udev.settle() + output = TestRun.executor.run(f"ls {device.path}* -1") + if len(output.stdout.split('\n')) > 1: + TestRun.LOGGER.error(f"Could not remove partitions from device {device.path}") + return False + return True + + +def mount(device, mount_point, options: [str] = None): + if not fs_utils.check_if_directory_exists(mount_point): + fs_utils.create_directory(mount_point, True) + TestRun.LOGGER.info(f"Mounting device {device.path} ({device.get_device_id()}) " + f"to {mount_point}.") + cmd = f"mount {device.path} {mount_point}" + if options: + cmd = f"{cmd} -o {','.join(options)}" + output = TestRun.executor.run(cmd) + if output.exit_code != 0: + raise Exception(f"Failed to mount {device.path} to {mount_point}") + device.mount_point = mount_point + + +def unmount(device): + TestRun.LOGGER.info(f"Unmounting device {device.path} ({device.get_device_id()}).") + if device.mount_point is not None: + output = TestRun.executor.run(f"umount {device.mount_point}") + if output.exit_code != 0: + TestRun.LOGGER.error("Could not unmount device.") + return False + return True + else: + TestRun.LOGGER.info("Device is not mounted.") + return True + + +def unit_to_string(unit): + unit_string = { + Unit.Byte: 'B', + Unit.Blocks512: 's', + Unit.Blocks4096: 's', + Unit.KibiByte: 'KiB', + Unit.MebiByte: 'MiB', + Unit.GibiByte: 'GiB', + Unit.TebiByte: 'TiB', + Unit.KiloByte: 'kB', + Unit.MegaByte: 'MB', + Unit.GigaByte: 'GB', + Unit.TeraByte: 'TB' + } + return unit_string.get(unit, "Invalid unit.") + + +def wipe_filesystem(device, force=True): + TestRun.LOGGER.info(f"Erasing the device: {device.path}") + force_param = ' -f' if force else '' + cmd = f'wipefs -a{force_param} {device.path}' + TestRun.executor.run_expect_success(cmd) + TestRun.LOGGER.info( + f"Successfully wiped device: {device.path}") + + +def check_if_device_supports_trim(device): + if device.get_device_id().startswith("nvme"): + return True + command_output = TestRun.executor.run( + f'hdparm -I {device.path} | grep "TRIM supported"') + return command_output.exit_code == 0 + + +def get_device_filesystem_type(device_id): + cmd = f'lsblk -l -o NAME,FSTYPE | sort | uniq | grep "{device_id} "' + try: + stdout = TestRun.executor.run_expect_success(cmd).stdout + except CmdException: + # unusual devices might not be listed in output (i.e. RAID containers) + if TestRun.executor.run(f"test -b /dev/{device_id}").exit_code != 0: + raise + else: + return None + split_stdout = stdout.strip().split() + if len(split_stdout) <= 1: + return None + else: + try: + return Filesystem[split_stdout[1]] + except KeyError: + TestRun.LOGGER.warning(f"Unrecognized filesystem: {split_stdout[1]}") + return None + + +def _is_by_id_path(path: str): + """check if given path already is proper by-id path""" + dev_by_id_dir = "/dev/disk/by-id" + by_id_paths = parse_ls_output(ls(dev_by_id_dir), dev_by_id_dir) + return path in [posixpath.join(dev_by_id_dir, id_path.full_path) for id_path in by_id_paths] + + +def _is_dev_path_whitelisted(path: str): + """check if given path is whitelisted""" + whitelisted_paths = [r"cas\d+-\d+", r"/dev/dm-\d+"] + + for whitelisted_path in whitelisted_paths: + if re.search(whitelisted_path, path) is not None: + return True + + return False + + +def validate_dev_path(path: str): + if not posixpath.isabs(path): + raise ValueError(f'Given path "{path}" is not absolute.') + + if _is_dev_path_whitelisted(path): + return path + + if _is_by_id_path(path): + return path + + raise ValueError(f'By-id device link {path} is broken.') diff --git a/test/functional/test-framework/test_tools/drbdadm.py b/test/functional/test-framework/test_tools/drbdadm.py new file mode 100644 index 0000000..8213e91 --- /dev/null +++ b/test/functional/test-framework/test_tools/drbdadm.py @@ -0,0 +1,67 @@ +# +# Copyright(c) 2022 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause-Clear +# + +from core.test_run import TestRun + + +class Drbdadm: + # create metadata for resource + @staticmethod + def create_metadata(resource_name: str, force: bool): + cmd = "drbdadm create-md" + (" --force" if force else "") + f" {resource_name}" + return TestRun.executor.run_expect_success(cmd) + + # enable resource + @staticmethod + def up(resource_name: str): + cmd = f"drbdadm up {resource_name}" + return TestRun.executor.run_expect_success(cmd) + + # disable resource + @staticmethod + def down_all(): + cmd = f"drbdadm down all" + return TestRun.executor.run_expect_success(cmd) + + @staticmethod + def down(resource_name): + cmd = f"drbdadm down {resource_name}" + return TestRun.executor.run_expect_success(cmd) + + # promote resource to primary + @staticmethod + def set_node_primary(resource_name: str, force=False): + cmd = f"drbdadm primary {resource_name}" + cmd += " --force" if force else "" + return TestRun.executor.run_expect_success(cmd) + + # demote resource to secondary + @staticmethod + def set_node_secondary(resource_name: str): + cmd = f"drbdadm secondary {resource_name}" + return TestRun.executor.run_expect_success(cmd) + + # check status for all or for specified resource + @staticmethod + def get_status(resource_name: str = ""): + cmd = f"drbdadm status {resource_name}" + return TestRun.executor.run_expect_success(cmd) + + @staticmethod + def in_sync(resource_name: str): + cmd = f"drbdadm status {resource_name} | grep Inconsistent" + return TestRun.executor.run(cmd).exit_code == 1 + + # wait sync + @staticmethod + def wait_for_sync(resource_name: str = ""): + # ssh connection might timeout in case on long sync + cmd = f"drbdadm wait-sync {resource_name}" + return TestRun.executor.run_expect_success(cmd) + + @staticmethod + def dump_config(resource_name: str): + cmd = f"drbdadm dump {resource_name}" + return TestRun.executor.run(cmd) diff --git a/test/functional/test-framework/test_tools/fio/__init__.py b/test/functional/test-framework/test_tools/fio/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/functional/test-framework/test_tools/fio/fio.py b/test/functional/test-framework/test_tools/fio/fio.py new file mode 100644 index 0000000..963ae0e --- /dev/null +++ b/test/functional/test-framework/test_tools/fio/fio.py @@ -0,0 +1,105 @@ +# +# Copyright(c) 2019-2022 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# + +import datetime +import uuid + +import test_tools.fio.fio_param +import test_tools.fs_utils +from core.test_run import TestRun +from test_tools import fs_utils +from test_utils import os_utils + + +class Fio: + def __init__(self, executor_obj=None): + self.fio_version = "fio-3.30" + self.default_run_time = datetime.timedelta(hours=1) + self.jobs = [] + self.executor = executor_obj if executor_obj is not None else TestRun.executor + self.base_cmd_parameters: test_tools.fio.fio_param.FioParam = None + self.global_cmd_parameters: test_tools.fio.fio_param.FioParam = None + + def create_command(self, output_type=test_tools.fio.fio_param.FioOutput.json): + self.base_cmd_parameters = test_tools.fio.fio_param.FioParamCmd(self, self.executor) + self.global_cmd_parameters = test_tools.fio.fio_param.FioParamConfig(self, self.executor) + + self.fio_file = f'fio_run_{datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S")}_{uuid.uuid4().hex}' + self.base_cmd_parameters\ + .set_param('eta', 'always')\ + .set_param('output-format', output_type.value)\ + .set_param('output', self.fio_file) + + self.global_cmd_parameters.set_flags('group_reporting') + + return self.global_cmd_parameters + + def is_installed(self): + return self.executor.run("fio --version").stdout.strip() == self.fio_version + + def install(self): + fio_url = f"http://brick.kernel.dk/snaps/{self.fio_version}.tar.bz2" + fio_package = os_utils.download_file(fio_url) + fs_utils.uncompress_archive(fio_package) + TestRun.executor.run_expect_success(f"cd {fio_package.parent_dir}/{self.fio_version}" + f" && ./configure && make -j && make install") + + def calculate_timeout(self): + if "time_based" not in self.global_cmd_parameters.command_flags: + return self.default_run_time + + total_time = self.global_cmd_parameters.get_parameter_value("runtime") + if len(total_time) != 1: + raise ValueError("Wrong fio 'runtime' parameter configuration") + total_time = int(total_time[0]) + ramp_time = self.global_cmd_parameters.get_parameter_value("ramp_time") + if ramp_time is not None: + if len(ramp_time) != 1: + raise ValueError("Wrong fio 'ramp_time' parameter configuration") + ramp_time = int(ramp_time[0]) + total_time += ramp_time + return datetime.timedelta(seconds=total_time) + + def run(self, timeout: datetime.timedelta = None): + if timeout is None: + timeout = self.calculate_timeout() + + self.prepare_run() + return self.executor.run(str(self), timeout) + + def run_in_background(self): + self.prepare_run() + return self.executor.run_in_background(str(self)) + + def prepare_run(self): + if not self.is_installed(): + self.install() + + if len(self.jobs) > 0: + self.executor.run(f"{str(self)}-showcmd -") + TestRun.LOGGER.info(self.executor.run(f"cat {self.fio_file}").stdout) + TestRun.LOGGER.info(str(self)) + + def execution_cmd_parameters(self): + if len(self.jobs) > 0: + separator = "\n\n" + return f"{str(self.global_cmd_parameters)}\n" \ + f"{separator.join(str(job) for job in self.jobs)}" + else: + return str(self.global_cmd_parameters) + + def __str__(self): + if len(self.jobs) > 0: + command = f"echo '{self.execution_cmd_parameters()}' |" \ + f" {str(self.base_cmd_parameters)} -" + else: + fio_parameters = test_tools.fio.fio_param.FioParamCmd(self, self.executor) + fio_parameters.command_env_var.update(self.base_cmd_parameters.command_env_var) + fio_parameters.command_param.update(self.base_cmd_parameters.command_param) + fio_parameters.command_param.update(self.global_cmd_parameters.command_param) + fio_parameters.command_flags.extend(self.global_cmd_parameters.command_flags) + fio_parameters.set_param('name', 'fio') + command = str(fio_parameters) + return command diff --git a/test/functional/test-framework/test_tools/fio/fio_param.py b/test/functional/test-framework/test_tools/fio/fio_param.py new file mode 100644 index 0000000..8db911b --- /dev/null +++ b/test/functional/test-framework/test_tools/fio/fio_param.py @@ -0,0 +1,388 @@ +# +# Copyright(c) 2019-2022 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# + +import datetime +import json +import secrets +from enum import Enum +from types import SimpleNamespace as Namespace + +from connection.base_executor import BaseExecutor +from core.test_run import TestRun +from storage_devices.device import Device +from test_tools.fio.fio_result import FioResult +from test_utils.linux_command import LinuxCommand +from test_utils.size import Size + + +class CpusAllowedPolicy(Enum): + shared = 0, + split = 1 + + +class ErrorFilter(Enum): + none = 0, + read = 1, + write = 2, + io = 3, + verify = 4, + all = 5 + + +class FioOutput(Enum): + normal = 'normal' + terse = 'terse' + json = 'json' + jsonplus = 'json+' + + +class IoEngine(Enum): + # Basic read or write I/O. fseek is used to position the I/O location. + sync = 0, + # Linux native asynchronous I/O. + libaio = 1, + # Basic pread or pwrite I/O. + psync = 2, + # Basic readv or writev I/O. + # Will emulate queuing by coalescing adjacent IOs into a single submission. + vsync = 3, + # Basic preadv or pwritev I/O. + pvsync = 4, + # POSIX asynchronous I/O using aio_read and aio_write. + posixaio = 5, + # File is memory mapped with mmap and data copied using memcpy. + mmap = 6, + # RADOS Block Device + rbd = 7, + # SPDK Block Device + spdk_bdev = 8 + + +class ReadWrite(Enum): + randread = 0, + randrw = 1, + randwrite = 2, + read = 3, + readwrite = 4, + write = 5, + trim = 6, + randtrim = 7, + trimwrite = 8 + + +class VerifyMethod(Enum): + # Use an md5 sum of the data area and store it in the header of each block. + md5 = 0, + # Use an experimental crc64 sum of the data area and store it in the header of each block. + crc64 = 1, + # Use optimized sha1 as the checksum function. + sha1 = 2, + # Verify a strict pattern. + # Normally fio includes a header with some basic information and a checksum, but if this + # option is set, only the specific pattern set with verify_pattern is verified. + pattern = 3, + # Write extra information about each I/O (timestamp, block number, etc.). + # The block number is verified. + meta = 4 + + +class RandomGenerator(Enum): + tausworthe = 0, + lfsr = 1, + tausworthe64 = 2 + + +class FioParam(LinuxCommand): + def __init__(self, fio, command_executor: BaseExecutor, command_name): + LinuxCommand.__init__(self, command_executor, command_name) + self.verification_pattern = '' + self.fio = fio + + def get_verification_pattern(self): + if not self.verification_pattern: + self.verification_pattern = f'0x{secrets.token_hex(32)}' + return self.verification_pattern + + def allow_mounted_write(self, value: bool = True): + return self.set_param('allow_mounted_write', int(value)) + + # example: "bs=8k,32k" => 8k for reads, 32k for writes and trims + def block_size(self, *sizes: Size): + return self.set_param('blocksize', *[int(size) for size in sizes]) + + def blocksize_range(self, ranges): + value = [] + for bs_range in ranges: + str_range = str(int(bs_range[0])) + '-' + str(int(bs_range[1])) + value.append(str_range) + return self.set_param('blocksize_range', ",".join(value)) + + def bs_split(self, value): + return self.set_param('bssplit', value) + + def buffer_pattern(self, pattern): + return self.set_param('buffer_pattern', pattern) + + def continue_on_error(self, value: ErrorFilter): + return self.set_param('continue_on_error', value.name) + + def cpus_allowed(self, value): + return self.set_param('cpus_allowed', ",".join(value)) + + def cpus_allowed_policy(self, value: CpusAllowedPolicy): + return self.set_param('cpus_allowed_policy', value.name) + + def direct(self, value: bool = True): + if 'buffered' in self.command_param: + self.remove_param('buffered') + return self.set_param('direct', int(value)) + + def directory(self, directory): + return self.set_param('directory', directory) + + def do_verify(self, value: bool = True): + return self.set_param('do_verify', int(value)) + + def exit_all_on_error(self, value: bool = True): + return self.set_flags('exitall_on_error') if value \ + else self.remove_flag('exitall_on_error') + + def group_reporting(self, value: bool = True): + return self.set_flags('group_reporting') if value else self.remove_flag('group_reporting') + + def file_name(self, path): + return self.set_param('filename', path) + + def file_size(self, size: Size): + return self.set_param('filesize', int(size)) + + def file_size_range(self, ranges): + value = [] + for bs_range in ranges: + str_range = str(int(bs_range[0])) + '-' + str(int(bs_range[1])) + value.append(str_range) + return self.set_param('filesize', ",".join(value)) + + def fsync(self, value: int): + return self.set_param('fsync', value) + + def ignore_errors(self, read_errors, write_errors, verify_errors): + separator = ':' + return self.set_param( + 'ignore_error', + separator.join(str(err) for err in read_errors), + separator.join(str(err) for err in write_errors), + separator.join(str(err) for err in verify_errors)) + + def io_depth(self, value: int): + if value != 1: + if 'ioengine' in self.command_param and \ + self.command_param['ioengine'] == 'sync': + TestRun.LOGGER.warning("Setting iodepth will have no effect with " + "'ioengine=sync' setting") + return self.set_param('iodepth', value) + + def io_engine(self, value: IoEngine): + if value == IoEngine.sync: + if 'iodepth' in self.command_param and self.command_param['iodepth'] != 1: + TestRun.LOGGER.warning("Setting 'ioengine=sync' will cause iodepth setting " + "to be ignored") + return self.set_param('ioengine', value.name) + + def io_size(self, value: Size): + return self.set_param('io_size', int(value.get_value())) + + def loops(self, value: int): + return self.set_param('loops', value) + + def no_random_map(self, value: bool = True): + if 'verify' in self.command_param: + raise ValueError("'NoRandomMap' parameter is mutually exclusive with verify") + if value: + return self.set_flags('norandommap') + else: + return self.remove_flag('norandommap') + + def nr_files(self, value: int): + return self.set_param('nrfiles', value) + + def num_ios(self, value: int): + return self.set_param('number_ios', value) + + def num_jobs(self, value: int): + return self.set_param('numjobs', value) + + def offset(self, value: Size): + return self.set_param('offset', int(value.get_value())) + + def offset_increment(self, value: Size): + return self.set_param('offset_increment', f"{value.value}{value.unit.get_short_name()}") + + def percentage_random(self, value: int): + if value <= 100: + return self.set_param('percentage_random', value) + raise ValueError("Argument out of range. Should be 0-100.") + + def pool(self, value): + return self.set_param('pool', value) + + def ramp_time(self, value: datetime.timedelta): + return self.set_param('ramp_time', int(value.total_seconds())) + + def random_distribution(self, value): + return self.set_param('random_distribution', value) + + def rand_repeat(self, value: int): + return self.set_param('randrepeat', value) + + def rand_seed(self, value: int): + return self.set_param('randseed', value) + + def read_write(self, rw: ReadWrite): + return self.set_param('readwrite', rw.name) + + def run_time(self, value: datetime.timedelta): + if value.total_seconds() == 0: + raise ValueError("Runtime parameter must not be set to 0.") + return self.set_param('runtime', int(value.total_seconds())) + + def serialize_overlap(self, value: bool = True): + return self.set_param('serialize_overlap', int(value)) + + def size(self, value: Size): + return self.set_param('size', int(value.get_value())) + + def stonewall(self, value: bool = True): + return self.set_flags('stonewall') if value else self.remove_param('stonewall') + + def sync(self, value: bool = True): + return self.set_param('sync', int(value)) + + def time_based(self, value: bool = True): + return self.set_flags('time_based') if value else self.remove_flag('time_based') + + def thread(self, value: bool = True): + return self.set_flags('thread') if value else self.remove_param('thread') + + def lat_percentiles(self, value: bool): + return self.set_param('lat_percentiles', int(value)) + + def scramble_buffers(self, value: bool): + return self.set_param('scramble_buffers', int(value)) + + def slat_percentiles(self, value: bool): + return self.set_param('slat_percentiles', int(value)) + + def spdk_core_mask(self, value: str): + return self.set_param('spdk_core_mask', value) + + def spdk_json_conf(self, path): + return self.set_param('spdk_json_conf', path) + + def clat_percentiles(self, value: bool): + return self.set_param('clat_percentiles', int(value)) + + def percentile_list(self, value: []): + val = ':'.join(value) if len(value) > 0 else '100' + return self.set_param('percentile_list', val) + + def verification_with_pattern(self, pattern=None): + if pattern is not None and pattern != '': + self.verification_pattern = pattern + return self.verify(VerifyMethod.pattern) \ + .set_param('verify_pattern', self.get_verification_pattern()) \ + .do_verify() + + def verify(self, value: VerifyMethod): + return self.set_param('verify', value.name) + + def create_only(self, value: bool = False): + return self.set_param('create_only', int(value)) + + def verify_pattern(self, pattern=None): + return self.set_param('verify_pattern', pattern or self.get_verification_pattern()) + + def verify_backlog(self, value: int): + return self.set_param('verify_backlog', value) + + def verify_dump(self, value: bool = True): + return self.set_param('verify_dump', int(value)) + + def verify_fatal(self, value: bool = True): + return self.set_param('verify_fatal', int(value)) + + def verify_only(self, value: bool = True): + return self.set_flags('verify_only') if value else self.remove_param('verify_only') + + def write_hint(self, value: str): + return self.set_param('write_hint', value) + + def write_percentage(self, value: int): + if value <= 100: + return self.set_param('rwmixwrite', value) + raise ValueError("Argument out of range. Should be 0-100.") + + def random_generator(self, value: RandomGenerator): + return self.set_param('random_generator', value.name) + + def target(self, target): + if isinstance(target, Device): + return self.file_name(target.path) + return self.file_name(target) + + def add_job(self, job_name=None): + if not job_name: + job_name = f'job{len(self.fio.jobs)}' + new_job = FioParamConfig(self.fio, self.command_executor, f'[{job_name}]') + self.fio.jobs.append(new_job) + return new_job + + def clear_jobs(self): + self.fio.jobs = [] + + return self + + def edit_global(self): + return self.fio.global_cmd_parameters + + def run(self, fio_timeout: datetime.timedelta = None): + if "per_job_logs" in self.fio.global_cmd_parameters.command_param: + self.fio.global_cmd_parameters.set_param("per_job_logs", '0') + fio_output = self.fio.run(fio_timeout) + if fio_output.exit_code != 0: + raise Exception(f"Exception occurred while trying to execute fio, exit_code:" + f"{fio_output.exit_code}.\n" + f"stdout: {fio_output.stdout}\nstderr: {fio_output.stderr}") + TestRun.executor.run(f"sed -i '/^[[:alnum:]]/d' {self.fio.fio_file}") # Remove warnings + out = self.command_executor.run_expect_success(f"cat {self.fio.fio_file}").stdout + return self.get_results(out) + + def run_in_background(self): + if "per_job_logs" in self.fio.global_cmd_parameters.command_param: + self.fio.global_cmd_parameters.set_param("per_job_logs", '0') + return self.fio.run_in_background() + + @staticmethod + def get_results(result): + data = json.loads(result, object_hook=lambda d: Namespace(**d)) + jobs_list = [] + if hasattr(data, 'jobs'): + jobs = data.jobs + for job in jobs: + job_result = FioResult(data, job) + jobs_list.append(job_result) + return jobs_list + + +class FioParamCmd(FioParam): + def __init__(self, fio, command_executor: BaseExecutor, command_name='fio'): + FioParam.__init__(self, fio, command_executor, command_name) + self.param_name_prefix = "--" + + +class FioParamConfig(FioParam): + def __init__(self, fio, command_executor: BaseExecutor, command_name='[global]'): + FioParam.__init__(self, fio, command_executor, command_name) + self.param_name_prefix = "\n" diff --git a/test/functional/test-framework/test_tools/fio/fio_patterns.py b/test/functional/test-framework/test_tools/fio/fio_patterns.py new file mode 100644 index 0000000..22ca70b --- /dev/null +++ b/test/functional/test-framework/test_tools/fio/fio_patterns.py @@ -0,0 +1,19 @@ +# +# Copyright(c) 2020-2021 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# + +import secrets +from aenum import Enum + + +class Pattern(Enum): + cyclic = "0x00336699ccffcc996633" + sequential = "0x" + "".join([f"{i:02x}" for i in range(0, 256)]) + high = "0xaa" + low = "0x84210" + zeroes = "0x00" + ones = "0xff" + bin_1 = high + bin_2 = "0x55" + random = "0x" + secrets.token_hex() diff --git a/test/functional/test-framework/test_tools/fio/fio_result.py b/test/functional/test-framework/test_tools/fio/fio_result.py new file mode 100644 index 0000000..334e977 --- /dev/null +++ b/test/functional/test-framework/test_tools/fio/fio_result.py @@ -0,0 +1,164 @@ +# +# Copyright(c) 2019-2021 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# + + +from test_utils.size import Size, Unit, UnitPerSecond +from test_utils.time import Time + + +class FioResult: + def __init__(self, result, job): + self.result = result + self.job = job + + def __str__(self): + result_dict = { + "Total read I/O": self.read_io(), + "Total read bandwidth ": self.read_bandwidth(), + "Read bandwidth average ": self.read_bandwidth_average(), + "Read bandwidth deviation ": self.read_bandwidth_deviation(), + "Read IOPS": self.read_iops(), + "Read runtime": self.read_runtime(), + "Read average completion latency": self.read_completion_latency_average(), + "Total write I/O": self.write_io(), + "Total write bandwidth ": self.write_bandwidth(), + "Write bandwidth average ": self.write_bandwidth_average(), + "Write bandwidth deviation ": self.write_bandwidth_deviation(), + "Write IOPS": self.write_iops(), + "Write runtime": self.write_runtime(), + "Write average completion latency": self.write_completion_latency_average(), + } + + disks_name = self.disks_name() + if disks_name: + result_dict.update({"Disk name": ",".join(disks_name)}) + + result_dict.update({"Total number of errors": self.total_errors()}) + + s = "" + for key in result_dict.keys(): + s += f"{key}: {result_dict[key]}\n" + return s + + def total_errors(self): + return getattr(self.job, "total_err", 0) + + def disks_name(self): + disks_name = [] + if hasattr(self.result, "disk_util"): + for disk in self.result.disk_util: + disks_name.append(disk.name) + return disks_name + + def read_io(self): + return Size(self.job.read.io_kbytes, Unit.KibiByte) + + def read_bandwidth(self): + return Size(self.job.read.bw, UnitPerSecond(Unit.KibiByte)) + + def read_bandwidth_average(self): + return Size(self.job.read.bw_mean, UnitPerSecond(Unit.KibiByte)) + + def read_bandwidth_deviation(self): + return Size(self.job.read.bw_dev, UnitPerSecond(Unit.KibiByte)) + + def read_iops(self): + return self.job.read.iops + + def read_runtime(self): + return Time(microseconds=self.job.read.runtime) + + def read_completion_latency_min(self): + return Time(nanoseconds=self.job.read.lat_ns.min) + + def read_completion_latency_max(self): + return Time(nanoseconds=self.job.read.lat_ns.max) + + def read_completion_latency_average(self): + return Time(nanoseconds=self.job.read.lat_ns.mean) + + def read_completion_latency_percentile(self): + return self.job.read.lat_ns.percentile.__dict__ + + def read_requests_number(self): + return self.result.disk_util[0].read_ios + + def write_io(self): + return Size(self.job.write.io_kbytes, Unit.KibiByte) + + def write_bandwidth(self): + return Size(self.job.write.bw, UnitPerSecond(Unit.KibiByte)) + + def write_bandwidth_average(self): + return Size(self.job.write.bw_mean, UnitPerSecond(Unit.KibiByte)) + + def write_bandwidth_deviation(self): + return Size(self.job.write.bw_dev, UnitPerSecond(Unit.KibiByte)) + + def write_iops(self): + return self.job.write.iops + + def write_runtime(self): + return Time(microseconds=self.job.write.runtime) + + def write_completion_latency_average(self): + return Time(nanoseconds=self.job.write.lat_ns.mean) + + def write_completion_latency_min(self): + return Time(nanoseconds=self.job.write.lat_ns.min) + + def write_completion_latency_max(self): + return Time(nanoseconds=self.job.write.lat_ns.max) + + def write_completion_latency_average(self): + return Time(nanoseconds=self.job.write.lat_ns.mean) + + def write_completion_latency_percentile(self): + return self.job.write.lat_ns.percentile.__dict__ + + def write_requests_number(self): + return self.result.disk_util[0].write_ios + + def trim_io(self): + return Size(self.job.trim.io_kbytes, Unit.KibiByte) + + def trim_bandwidth(self): + return Size(self.job.trim.bw, UnitPerSecond(Unit.KibiByte)) + + def trim_bandwidth_average(self): + return Size(self.job.trim.bw_mean, UnitPerSecond(Unit.KibiByte)) + + def trim_bandwidth_deviation(self): + return Size(self.job.trim.bw_dev, UnitPerSecond(Unit.KibiByte)) + + def trim_iops(self): + return self.job.trim.iops + + def trim_runtime(self): + return Time(microseconds=self.job.trim.runtime) + + def trim_completion_latency_average(self): + return Time(nanoseconds=self.job.trim.lat_ns.mean) + + def trim_completion_latency_min(self): + return Time(nanoseconds=self.job.trim.lat_ns.min) + + def trim_completion_latency_max(self): + return Time(nanoseconds=self.job.trim.lat_ns.max) + + def trim_completion_latency_average(self): + return Time(nanoseconds=self.job.trim.lat_ns.mean) + + def trim_completion_latency_percentile(self): + return self.job.trim.lat_ns.percentile.__dict__ + + @staticmethod + def result_list_to_dict(results): + result_dict = {} + + for result in results: + result_dict[result.job.jobname] = result.job + + return result_dict diff --git a/test/functional/test-framework/test_tools/fs_utils.py b/test/functional/test-framework/test_tools/fs_utils.py new file mode 100644 index 0000000..512bacc --- /dev/null +++ b/test/functional/test-framework/test_tools/fs_utils.py @@ -0,0 +1,378 @@ +# +# Copyright(c) 2019-2022 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# + + +import base64 +import math +import textwrap + +from aenum import IntFlag, Enum +from collections import namedtuple +from datetime import datetime + +from core.test_run import TestRun +from test_tools.dd import Dd +from test_utils.size import Size, Unit + + +class Permissions(IntFlag): + r = 4 + w = 2 + x = 1 + + def __str__(self): + ret_string = "" + for p in Permissions: + if p in self: + ret_string += p.name + return ret_string + + +class PermissionsUsers(IntFlag): + u = 4 + g = 2 + o = 1 + + def __str__(self): + ret_string = "" + for p in PermissionsUsers: + if p in self: + ret_string += p.name + return ret_string + + +class PermissionSign(Enum): + add = '+' + remove = '-' + set = '=' + + +class FilesPermissions(): + perms_exceptions = {} + + def __init__(self, files_list: list): + self.files_list = files_list + + def add_exceptions(self, perms: dict): + self.perms_exceptions.update(perms) + + def check(self, file_perm: int = 644, dir_perm: int = 755): + failed_perms = [] + FailedPerm = namedtuple("FailedPerm", ["file", "current_perm", "expected_perm"]) + + for file in self.files_list: + perm = get_permissions(file) + + if file in self.perms_exceptions: + if perm != self.perms_exceptions[file]: + failed_perms.append(FailedPerm(file, perm, self.perms_exceptions[file])) + continue + + if check_if_regular_file_exists(file): + if perm != file_perm: + failed_perms.append(FailedPerm(file, perm, file_perm)) + elif check_if_directory_exists(file): + if perm != dir_perm: + failed_perms.append(FailedPerm(file, perm, dir_perm)) + else: + raise Exception(f"{file}: File type not recognized.") + + return failed_perms + + +def create_directory(path, parents: bool = False): + cmd = f"mkdir {'--parents ' if parents else ''}\"{path}\"" + return TestRun.executor.run_expect_success(cmd) + + +def check_if_directory_exists(path): + return TestRun.executor.run(f"test -d \"{path}\"").exit_code == 0 + + +def check_if_file_exists(path): + return TestRun.executor.run(f"test -e \"{path}\"").exit_code == 0 + + +def check_if_regular_file_exists(path): + return TestRun.executor.run(f"test -f \"{path}\"").exit_code == 0 + + +def check_if_symlink_exists(path): + return TestRun.executor.run(f"test -L \"{path}\"").exit_code == 0 + + +def copy(source: str, + destination: str, + force: bool = False, + recursive: bool = False, + dereference: bool = False): + cmd = f"cp{' --force' if force else ''}" \ + f"{' --recursive' if recursive else ''}" \ + f"{' --dereference' if dereference else ''} " \ + f"\"{source}\" \"{destination}\"" + return TestRun.executor.run_expect_success(cmd) + + +def move(source, destination, force: bool = False): + cmd = f"mv{' --force' if force else ''} \"{source}\" \"{destination}\"" + return TestRun.executor.run_expect_success(cmd) + + +def remove(path, force: bool = False, recursive: bool = False, ignore_errors: bool = False): + cmd = f"rm{' --force' if force else ''}{' --recursive' if recursive else ''} \"{path}\"" + output = TestRun.executor.run(cmd) + if output.exit_code != 0 and not ignore_errors: + raise Exception(f"Could not remove file {path}." + f"\nstdout: {output.stdout}\nstderr: {output.stderr}") + return output + + +def get_permissions(path, dereference: bool = True): + cmd = f"stat --format='%a' {'--dereference' if dereference else ''} \"{path}\"" + return int(TestRun.executor.run_expect_success(cmd).stdout) + + +def chmod(path, permissions: Permissions, users: PermissionsUsers, + sign: PermissionSign = PermissionSign.set, recursive: bool = False): + cmd = f"chmod{' --recursive' if recursive else ''} " \ + f"{str(users)}{sign.value}{str(permissions)} \"{path}\"" + output = TestRun.executor.run(cmd) + return output + + +def chmod_numerical(path, permissions: int, recursive: bool = False): + cmd = f"chmod{' --recursive' if recursive else ''} {permissions} \"{path}\"" + return TestRun.executor.run_expect_success(cmd) + + +def chown(path, owner, group, recursive): + cmd = f"chown {'--recursive ' if recursive else ''}{owner}:{group} \"{path}\"" + return TestRun.executor.run_expect_success(cmd) + + +def create_file(path): + if not path.strip(): + raise ValueError("Path cannot be empty or whitespaces.") + cmd = f"touch \"{path}\"" + return TestRun.executor.run_expect_success(cmd) + + +def compare(file, other_file): + output = TestRun.executor.run( + f"cmp --silent \"{file}\" \"{other_file}\"") + if output.exit_code == 0: + return True + elif output.exit_code > 1: + raise Exception(f"Compare command execution failed. {output.stdout}\n{output.stderr}") + else: + return False + + +def diff(file, other_file): + output = TestRun.executor.run( + f"diff \"{file}\" \"{other_file}\"") + if output.exit_code == 0: + return None + elif output.exit_code > 1: + raise Exception(f"Diff command execution failed. {output.stdout}\n{output.stderr}") + else: + return output.stderr + + +# For some reason separators other than '/' don't work when using sed on system paths +# This requires escaping '/' in pattern and target string +def escape_sed_string(string: str, sed_replace: bool = False): + string = string.replace("'", r"\x27").replace("/", r"\/") + # '&' has special meaning in sed replace and needs to be escaped + if sed_replace: + string = string.replace("&", r"\&") + return string + + +def insert_line_before_pattern(file, pattern, new_line): + pattern = escape_sed_string(pattern) + new_line = escape_sed_string(new_line) + cmd = f"sed -i '/{pattern}/i {new_line}' \"{file}\"" + return TestRun.executor.run_expect_success(cmd) + + +def replace_first_pattern_occurrence(file, pattern, new_string): + pattern = escape_sed_string(pattern) + new_string = escape_sed_string(new_string, sed_replace=True) + cmd = f"sed -i '0,/{pattern}/s//{new_string}/' \"{file}\"" + return TestRun.executor.run_expect_success(cmd) + + +def replace_in_lines(file, pattern, new_string, regexp=False): + pattern = escape_sed_string(pattern) + new_string = escape_sed_string(new_string, sed_replace=True) + cmd = f"sed -i{' -r' if regexp else ''} 's/{pattern}/{new_string}/g' \"{file}\"" + return TestRun.executor.run_expect_success(cmd) + + +def append_line(file, string): + cmd = f"echo '{string}' >> \"{file}\"" + return TestRun.executor.run_expect_success(cmd) + + +def remove_lines(file, pattern, regexp=False): + pattern = escape_sed_string(pattern) + cmd = f"sed -i{' -r' if regexp else ''} '/{pattern}/d' \"{file}\"" + return TestRun.executor.run_expect_success(cmd) + + +def read_file(file): + if not file.strip(): + raise ValueError("File path cannot be empty or whitespace.") + output = TestRun.executor.run_expect_success(f"cat \"{file}\"") + return output.stdout + + +def write_file(file, content, overwrite: bool = True, unix_line_end: bool = True): + if not file.strip(): + raise ValueError("File path cannot be empty or whitespace.") + if not content: + raise ValueError("Content cannot be empty.") + if unix_line_end: + content.replace('\r', '') + content += '\n' + max_length = 60000 + split_content = textwrap.TextWrapper(width=max_length, replace_whitespace=False).wrap(content) + split_content[-1] += '\n' + for s in split_content: + redirection_char = '>' if overwrite else '>>' + overwrite = False + encoded_content = base64.b64encode(s.encode("utf-8")) + cmd = f"printf '{encoded_content.decode('utf-8')}' " \ + f"| base64 --decode {redirection_char} \"{file}\"" + TestRun.executor.run_expect_success(cmd) + + +def uncompress_archive(file, destination=None): + from test_utils.filesystem.file import File + + if not isinstance(file, File): + file = File(file) + if not destination: + destination = file.parent_dir + command = (f"unzip -u {file.full_path} -d {destination}" + if str(file).endswith(".zip") + else f"tar --extract --file={file.full_path} --directory={destination}") + TestRun.executor.run_expect_success(command) + + +def ls(path, options=''): + default_options = "-lA --time-style=+'%Y-%m-%d %H:%M:%S'" + output = TestRun.executor.run( + f"ls {default_options} {options} \"{path}\"") + return output.stdout + + +def ls_item(path): + output = ls(path, '-d') + return output.splitlines()[0] if output else None + + +def parse_ls_output(ls_output, dir_path=''): + split_output = ls_output.split('\n') + fs_items = [] + for line in split_output: + if not line.strip(): + continue + line_fields = line.split() + if len(line_fields) < 8: + continue + file_type = line[0] + if file_type not in ['-', 'd', 'l', 'b', 'c', 'p', 's']: + continue + permissions = line_fields[0][1:].replace('.', '') + owner = line_fields[2] + group = line_fields[3] + has_size = ',' not in line_fields[4] + if has_size: + size = Size(float(line_fields[4]), Unit.Byte) + else: + size = None + line_fields.pop(4) + split_date = line_fields[5].split('-') + split_time = line_fields[6].split(':') + modification_time = datetime(int(split_date[0]), int(split_date[1]), int(split_date[2]), + int(split_time[0]), int(split_time[1]), int(split_time[2])) + if dir_path and file_type != 'l': + full_path = '/'.join([dir_path, line_fields[7]]) + else: + full_path = line_fields[7] + + from test_utils.filesystem.file import File, FsItem + from test_utils.filesystem.directory import Directory + from test_utils.filesystem.symlink import Symlink + + if file_type == '-': + fs_item = File(full_path) + elif file_type == 'd': + fs_item = Directory(full_path) + elif file_type == 'l': + fs_item = Symlink(full_path) + else: + fs_item = FsItem(full_path) + + fs_item.permissions.user = Permissions['|'.join(list(permissions[:3].replace('-', '')))] \ + if permissions[:3] != '---' else Permissions(0) + fs_item.permissions.group = Permissions['|'.join(list(permissions[3:6].replace('-', '')))] \ + if permissions[3:6] != '---' else Permissions(0) + fs_item.permissions.other = Permissions['|'.join(list(permissions[6:].replace('-', '')))] \ + if permissions[6:] != '---' else Permissions(0) + + fs_item.owner = owner + fs_item.group = group + fs_item.size = size + fs_item.modification_time = modification_time + fs_items.append(fs_item) + return fs_items + + +def find_all_files(path: str, recursive: bool = True): + if not path.strip(): + raise ValueError("No path given.") + + output = TestRun.executor.run_expect_success(f"find \"{path}\" {'-maxdepth 1' if not recursive else ''} \( -type f -o -type l \) -print") + + return output.stdout.splitlines() + + +def find_all_dirs(path: str, recursive: bool = True): + if not path.strip(): + raise ValueError("No path given.") + + output = TestRun.executor.run_expect_success(f"find \"{path}\" {'-maxdepth 1' if not recursive else ''} -type d -print") + + return output.stdout.splitlines() + + +def find_all_items(path: str, recursive: bool = True): + return [*find_all_files(path, recursive), *find_all_dirs(path, recursive)] + + +def readlink(link: str, options="--canonicalize-existing"): + return TestRun.executor.run_expect_success( + f"readlink {options} \"{link}\"" + ).stdout + + +def create_random_test_file(target_file_path: str, + file_size: Size = Size(1, Unit.MebiByte), + random: bool = True): + from test_utils.filesystem.file import File + bs = Size(512, Unit.KibiByte) + cnt = math.ceil(file_size.value / bs.value) + file = File.create_file(target_file_path) + dd = Dd().output(target_file_path) \ + .input("/dev/urandom" if random else "/dev/zero") \ + .block_size(bs) \ + .count(cnt) \ + .oflag("direct") + dd.run() + file.refresh_item() + return file diff --git a/test/functional/test-framework/test_tools/iostat.py b/test/functional/test-framework/test_tools/iostat.py new file mode 100644 index 0000000..3e310bf --- /dev/null +++ b/test/functional/test-framework/test_tools/iostat.py @@ -0,0 +1,179 @@ +# +# Copyright(c) 2020-2022 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# + +from core.test_run import TestRun +from storage_devices.device import Device +from test_utils.size import Size, Unit, UnitPerSecond +from test_utils.time import Time +import csv + + +class IOstatExtended: + iostat_option = "x" + + def __init__(self, device_statistics: dict): + + # Notes about params: + # await param is displayed only on flag -s + # avgrq-sz doesn't appear in newer versions of iostat -x + + self.device_name = device_statistics["Device"] + # rrqm/s + self.read_requests_merged_per_sec = float(device_statistics["rrqm/s"]) + # wrqm/s + self.write_requests_merged_per_sec = float(device_statistics["wrqm/s"]) + # r/s + self.read_requests_per_sec = float(device_statistics["r/s"]) + # w/s + self.write_requests_per_sec = float(device_statistics["w/s"]) + # rkB/s + self.reads_per_sec = Size(float(device_statistics["rkB/s"]), UnitPerSecond(Unit.KiloByte)) + # wkB/s + self.writes_per_sec = Size(float(device_statistics["wkB/s"]), UnitPerSecond(Unit.KiloByte)) + # avgqu-sz - in newer versions is named aqu-sz + self.average_queue_length = float( + device_statistics["aqu-sz"] + if "aqu-sz" in device_statistics + else device_statistics.get("avgqu-sz", 0) + ) + # r_await + self.read_average_service_time = Time(milliseconds=float(device_statistics["r_await"])) + # w_await + self.write_average_service_time = Time(milliseconds=float(device_statistics["w_await"])) + # iostat's documentation says to not trust 11th field + # util + self.utilization = float(device_statistics["%util"]) + + def __str__(self): + return ( + f"\n=========={self.device_name} IO stats: ==========\n" + f"Read requests merged per second: {self.read_requests_merged_per_sec}\n" + f"Write requests merged per second: {self.write_requests_merged_per_sec}\n" + f"Read requests: {self.read_requests_per_sec}\n" + f"Write requests: {self.write_requests_per_sec}\n" + f"Reads per second: {self.reads_per_sec}\n" + f"Writes per second {self.writes_per_sec}\n" + f"Average queue length {self.average_queue_length}\n" + f"Read average service time {self.read_average_service_time}\n" + f"Write average service time: {self.write_average_service_time}\n" + f"Utilization: {self.utilization}\n" + f"=================================================\n" + ) + + def __repr__(self): + return str(self) + + def __eq__(self, other): + if not other: + return False + return ( + self.read_requests_merged_per_sec == other.read_requests_merged_per_sec + and self.write_requests_merged_per_sec == other.write_requests_merged_per_sec + and self.read_requests_per_sec == other.read_requests_per_sec + and self.write_requests_per_sec == other.write_requests_per_sec + and self.reads_per_sec == other.reads_per_sec + and self.writes_per_sec == other.writes_per_sec + and self.average_queue_length == other.average_queue_length + and self.read_average_service_time == other.read_average_service_time + and self.write_average_service_time == other.write_average_service_time + and self.utilization == other.utilization + ) + + @classmethod + def get_iostat_list( + cls, + devices_list: [Device], + since_boot: bool = True, + interval: int = 1, + ): + """ + Returns list of IOstat objects containing extended statistics displayed + in kibibytes/kibibytes per second. + """ + return _get_iostat_list(cls, devices_list, since_boot, interval) + + +class IOstatBasic: + iostat_option = "d" + + def __init__(self, device_statistics): + + self.device_name = device_statistics["Device"] + # tps + self.transfers_per_second = float(device_statistics["tps"]) + # kB_read/s + self.reads_per_second = Size( + float(device_statistics["kB_read/s"]), UnitPerSecond(Unit.KiloByte) + ) + # kB_wrtn/s + self.writes_per_second = Size( + float(device_statistics["kB_wrtn/s"]), UnitPerSecond(Unit.KiloByte) + ) + # kB_read + self.total_reads = Size(float(device_statistics["kB_read"]), Unit.KibiByte) + # kB_wrtn + self.total_writes = Size(float(device_statistics["kB_wrtn"]), Unit.KibiByte) + + def __str__(self): + return ( + f"\n=========={self.device_name} IO stats: ==========\n" + f"Transfers per second: {self.transfers_per_second}\n" + f"Kilobytes read per second: {self.reads_per_second}\n" + f"Kilobytes written per second: {self.writes_per_second}\n" + f"Kilobytes read: {self.total_reads}\n" + f"Kilobytes written: {self.total_writes}\n" + f"=================================================\n" + ) + + def __repr__(self): + return str(self) + + def __eq__(self, other): + if not isinstance(other, IOstatBasic): + return False + return vars(self) == vars(other) + + @classmethod + def get_iostat_list( + cls, + devices_list: [Device], + since_boot: bool = True, + interval: int = 1, + ): + """ + Returns list of IOstat objects containing basic statistics displayed + in kibibytes/kibibytes per second. + """ + return _get_iostat_list(cls, devices_list, since_boot, interval) + + +def _get_iostat_list( + class_type: type, + devices_list: [Device], + since_boot: bool, + interval: int, +): + if interval < 1: + raise ValueError("iostat interval must be positive!") + + iostat_cmd = f"iostat -k -{class_type.iostat_option} " + + if not since_boot: + iostat_cmd += f"-y {interval} 1 " + + iostat_cmd += " ".join([name.get_device_id() for name in devices_list]) + + sed_cmd = "sed -n '/^$/d;s/\s\+/,/g;/^Device/,$p'" + + cmd = f"{iostat_cmd} | {sed_cmd}" + + lines = TestRun.executor.run(cmd).stdout.splitlines() + table_contents = csv.DictReader(lines, delimiter=",") + + ret = [] + for device in table_contents: + ret += [class_type(device)] + + return ret diff --git a/test/functional/test-framework/test_tools/kedr.py b/test/functional/test-framework/test_tools/kedr.py new file mode 100644 index 0000000..cceb43f --- /dev/null +++ b/test/functional/test-framework/test_tools/kedr.py @@ -0,0 +1,129 @@ +# +# Copyright(c) 2019-2021 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# + +import wget +import os +from enum import Enum + +from core.test_run import TestRun +from test_tools import fs_utils +from test_utils.os_utils import DEBUGFS_MOUNT_POINT + + +KEDR_0_6_URL = "https://github.com/euspectre/kedr/archive/v0.6.tar.gz" +BUILD_DIR = "build" +LEAKS_LOGS_PATH = f"{DEBUGFS_MOUNT_POINT}/kedr_leak_check" +KMALLOC_FAULT_SIMULATION_PATH = "/sys/kernel/debug/kedr_fault_simulation" + + +class KedrProfile(Enum): + MEM_LEAK_CHECK = "leak_check.conf" + FAULT_SIM = "fsim.conf" + + +class Kedr: + @staticmethod + def is_installed(): + return "KEDR version" in TestRun.executor.run("kedr --version").stdout.strip() + + @classmethod + def install(cls): + if cls.is_installed(): + TestRun.LOGGER.info("Kedr is already installed!") + return + + # TODO check if cmake is installed before + # TODO consider using os_utils.download_file() + kedr_archive = wget.download(KEDR_0_6_URL) + + TestRun.executor.rsync_to( + f"\"{kedr_archive}\"", + f"{TestRun.config['working_dir']}") + + kedr_dir = TestRun.executor.run_expect_success( + f"cd {TestRun.config['working_dir']} && " + f"tar -ztf \"{kedr_archive}\" | sed -e 's@/.*@@' | uniq" + ).stdout + + TestRun.executor.run_expect_success( + f"cd {TestRun.config['working_dir']} && " + f"tar -xf \"{kedr_archive}\" && " + f"cd {kedr_dir} && " + f"mkdir -p {BUILD_DIR} && " + f"cd {BUILD_DIR} && " + f"cmake ../sources/ && " + f"make && " + f"make install" + ) + + os.remove(kedr_archive) + TestRun.LOGGER.info("Kedr installed succesfully") + + @classmethod + def is_loaded(cls): + if not cls.is_installed(): + raise Exception("Kedr is not installed!") + + if "KEDR status: loaded" in TestRun.executor.run_expect_success("kedr status").stdout: + return True + else: + return False + + @classmethod + def start(cls, module, profile: KedrProfile = KedrProfile.MEM_LEAK_CHECK): + if not cls.is_installed(): + raise Exception("Kedr is not installed!") + + TestRun.LOGGER.info(f"Starting kedr with {profile} profile") + start_cmd = f"kedr start {module} -f {profile.value}" + TestRun.executor.run_expect_success(start_cmd) + + # TODO extend to scenarios other than kmalloc + def setup_fault_injections(condition: str = "1"): + TestRun.executor.run_expect_success( + f'echo "kmalloc" > {KMALLOC_FAULT_SIMULATION_PATH}/points/kmalloc/current_indicator') + TestRun.executor.run_expect_success( + f'echo "{condition}" > {KMALLOC_FAULT_SIMULATION_PATH}/points/kmalloc/expression') + + @classmethod + def fsim_show_last_fault(cls): + if not cls.is_installed(): + raise Exception("Kedr is not installed!") + + if not cls.is_loaded(): + raise Exception("Kedr is not loaded!") + + return fs_utils.read_file(f"{KMALLOC_FAULT_SIMULATION_PATH}/last_fault") + + @classmethod + def stop(cls): + if not cls.is_installed(): + raise Exception("Kedr is not installed!") + + TestRun.executor.run_expect_success("kedr stop") + + @classmethod + def check_for_mem_leaks(cls, module): + if not cls.is_installed(): + raise Exception("Kedr is not installed!") + + if not cls.is_loaded(): + raise Exception("Kedr is not loaded!") + + if fs_utils.check_if_directory_exists(f"{LEAKS_LOGS_PATH}/{module}"): + logs_path = f"{LEAKS_LOGS_PATH}/{module}" + elif fs_utils.check_if_directory_exists(f"{DEBUGFS_MOUNT_POINT}"): + logs_path = f"{LEAKS_LOGS_PATH}" + else: + raise Exception("Couldn't find kedr logs dir!") + + leaks = fs_utils.read_file(f"{logs_path}/possible_leaks") + frees = fs_utils.read_file(f"{logs_path}/unallocated_frees") + summary = fs_utils.read_file(f"{logs_path}/info") + if leaks or frees: + raise Exception("Memory leaks found!\n" + f"Kedr summary: {summary}\n" + f"Possible memory leaks: {leaks}\n" + f"Unallocated frees: {frees}\n") diff --git a/test/functional/test-framework/test_tools/mdadm.py b/test/functional/test-framework/test_tools/mdadm.py new file mode 100644 index 0000000..ae962bb --- /dev/null +++ b/test/functional/test-framework/test_tools/mdadm.py @@ -0,0 +1,140 @@ +# +# Copyright(c) 2020-2021 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# +import re + +from core.test_run import TestRun +from test_utils.size import Unit + + +class Mdadm: + @staticmethod + def assemble(device_paths: str = None): + cmd = f"mdadm --assemble " + (device_paths if device_paths else "--scan") + return TestRun.executor.run(cmd) + + @staticmethod + def create(conf, device_paths: str): + if not conf.name: + raise ValueError("Name needed for RAID creation.") + if not device_paths: + raise ValueError("Device paths needed for RAID creation.") + + cmd = f"mdadm --create --run /dev/md/{conf.name} " + if conf.metadata.value != "legacy": + cmd += f"--metadata={conf.metadata.value} " + if conf.level is not None: + cmd += f"--level={conf.level.value} " + if conf.number_of_devices: + cmd += f"--raid-devices={conf.number_of_devices} " + if conf.strip_size: + cmd += f"--chunk={conf.strip_size} " + if conf.size: + cmd += f"--size={int(conf.size.get_value(Unit.KibiByte))} " + cmd += device_paths + return TestRun.executor.run_expect_success(cmd) + + @staticmethod + def detail(raid_device_paths: str): + if not raid_device_paths: + raise ValueError("Provide paths of RAID devices to show details for.") + cmd = f"mdadm --detail {raid_device_paths} --prefer=by-id" + return TestRun.executor.run_expect_success(cmd) + + @classmethod + def detail_result(cls, raid_device_paths: str): + output = cls.detail(raid_device_paths) + details = {} + for device_details in re.split("^/dev/", output.stdout, flags=re.MULTILINE): + if not device_details: + continue + lines = device_details.splitlines() + key = "/dev/" + lines[0].rstrip(':') + details[key] = {} + details[key]["path"] = key + details[key]["devices"] = cls.__parse_devices(device_details) + details[key]["level"] = cls.__parse_level(device_details) + details[key]["uuid"] = cls.__parse_uuid(device_details) + metadata = cls.__parse_metadata(device_details) + if metadata: + details[key]["metadata"] = metadata + + return details + + @staticmethod + def examine(brief: bool = True, device_paths: str = None): + cmd = f"mdadm --examine " + if brief: + cmd += "--brief " + cmd += (device_paths if device_paths else "--scan") + return TestRun.executor.run_expect_success(cmd) + + @classmethod + def examine_result(cls, device_paths: str = None): + output = cls.examine(device_paths=device_paths) + raids = [] + + uuid_path_prefix = "/dev/disk/by-id/md-uuid-" + # sometimes links for RAIDs are not properly created, force udev to create them + TestRun.executor.run("udevadm trigger && udevadm settle") + + for line in output.stdout.splitlines(): + split_line = line.split() + try: + uuid = [i for i in split_line if i.startswith("UUID=")][0].split("=")[-1] + except IndexError: + continue + raid_link = uuid_path_prefix + uuid + raid = Mdadm.detail_result(raid_link)[raid_link] + if raid["level"] == "Container": + continue + raid["metadata"], raid["array_devices"] = "legacy", [] + container = ( + [i for i in split_line if i.startswith("container=")][0] + if "container=" in line else None + ) + if container: + container_link = uuid_path_prefix + container.split("=")[-1] + raid["container"] = cls.detail_result(container_link)[container_link] + raid["metadata"] = raid["container"]["metadata"] + raid["array_devices"] = raid["container"]["devices"] + raids.append(raid) + return raids + + @staticmethod + def stop(device_paths: str = None): + cmd = f"mdadm --stop " + (device_paths if device_paths else "--scan") + return TestRun.executor.run_expect_success(cmd) + + @staticmethod + def zero_superblock(device_paths: str): + cmd = f"mdadm --zero-superblock {device_paths}" + return TestRun.executor.run_expect_success(cmd) + + @staticmethod + def __parse_devices(details: str): + devices = [] + for detail in [d.strip() for d in details.splitlines() if " /dev/" in d]: + devices.append(detail.split()[-1]) + return devices + + @staticmethod + def __parse_level(details: str): + level = [line for line in details.splitlines() if "Raid Level" in line][0].split(" : ")[-1] + return level.capitalize() + + @staticmethod + def __parse_uuid(details: str): + uuid = [line for line in details.splitlines() if "UUID" in line][0].split(" : ")[-1] + return uuid + + @staticmethod + def __parse_metadata(details: str): + try: + return [ + line.strip() for line in details.splitlines() + if line.strip().startswith("Version :") + ][0].split(" : ")[-1] + except IndexError: + return None diff --git a/test/functional/test-framework/test_tools/nvme_cli.py b/test/functional/test-framework/test_tools/nvme_cli.py new file mode 100644 index 0000000..5844587 --- /dev/null +++ b/test/functional/test-framework/test_tools/nvme_cli.py @@ -0,0 +1,60 @@ +# +# Copyright(c) 2021 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# +import json +from core.test_run import TestRun + + +def format_disk(device, metadata_size=None, block_size=None, + force=True, format_params=None, reset=True): + force_param = '-f' if force else '' + reset_param = '-r' if reset else '' + format_params = ' '.join(format_params) if format_params else '' + lbafs = get_lba_formats(device) + if metadata_size: + lbafs = [lbaf for lbaf in lbafs if lbaf['metadata_size'] == metadata_size] + if block_size: + lbafs = [lbaf for lbaf in lbafs if lbaf['block_size'] == block_size] + if len(lbafs) == 1: + TestRun.LOGGER.info( + f"Formatting device {device.path} with {metadata_size} metadata size " + f"and {lbafs[0]['block_size']} block size") + TestRun.executor.run_expect_success( + f"nvme format {device.path} -l {lbafs[0]['lba_format']} " + f"{force_param} {reset_param} {format_params}") + TestRun.LOGGER.info(f"Successfully format device: {device.path}") + else: + raise Exception(f"Wrong parameters to format device: {device.path}") + elif block_size: + lbafs = [lbaf for lbaf in lbafs if lbaf['block_size'] == block_size] + if len(lbafs) > 0: + TestRun.LOGGER.info( + f"Formatting device {device.path} with {block_size} block size") + TestRun.executor.run_expect_success( + f"nvme format {device.path} -b {block_size} " + f"{force_param} {reset_param} {format_params}") + TestRun.LOGGER.info(f"Successfully format device: {device.path}") + else: + raise Exception(f"Wrong parameters to format device: {device.path}") + else: + raise Exception("Cannot format device without specified parameters") + + +def get_lba_formats(device): + output = json.loads(TestRun.executor.run_expect_success( + f"nvme id-ns {device.path} -o json").stdout) + entries = output['lbafs'] + lbafs = [] + for entry in entries: + lbaf = {"lba_format": entries.index(entry), + "metadata_size": entry['ms'], + "block_size": 2 ** entry['ds'], + "in_use": entries.index(entry) == output['flbas']} + lbafs.append(lbaf) + return lbafs + + +def get_lba_format_in_use(device): + lbafs = get_lba_formats(device) + return next((lbaf for lbaf in lbafs if lbaf['in_use'])) diff --git a/test/functional/test-framework/test_tools/packaging.py b/test/functional/test-framework/test_tools/packaging.py new file mode 100644 index 0000000..01935d8 --- /dev/null +++ b/test/functional/test-framework/test_tools/packaging.py @@ -0,0 +1,121 @@ +# +# Copyright(c) 2022 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# + + +import os +import re + +from core.test_run import TestRun +from test_utils.output import CmdException + + +class RpmSet(): + def __init__(self, packages_paths: list): + self.packages = packages_paths + + def _get_package_names(self): + return " ".join([os.path.splitext(os.path.basename(pckg))[0] for pckg in self.packages]) + + def check_if_installed(self): + if not self.packages: + raise ValueError("No packages given.") + + output = TestRun.executor.run(f"rpm --query {self._get_package_names()}") + + return output.exit_code == 0 + + def install(self): + TestRun.LOGGER.info(f"Installing RPM packages") + + if not self.packages: + raise ValueError("No packages given.") + + output = TestRun.executor.run( + f"rpm --upgrade --verbose --replacepkgs {' '.join(self.packages)}" + ) + if ( + output.exit_code != 0 + or re.search("error", output.stdout, re.IGNORECASE) + or re.search("error", output.stderr, re.IGNORECASE) + ): + raise CmdException("Installation failed or errors found during the process.", output) + + def uninstall(self): + TestRun.LOGGER.info(f"Uninstalling RPM packages") + + if not self.check_if_installed(): + raise FileNotFoundError("Could not uninstall - packages not installed yet.") + + output = TestRun.executor.run(f"rpm --erase --verbose {self._get_package_names()}") + if ( + output.exit_code != 0 + or re.search("error", output.stdout, re.IGNORECASE) + or re.search("error", output.stderr, re.IGNORECASE) + ): + raise CmdException("Uninstallation failed or errors found during the process.", output) + + @staticmethod + def uninstall_all_matching(*packages_names: str): + for name in packages_names: + TestRun.LOGGER.info(f"Uninstalling all RPM packages matching '{name}'") + TestRun.executor.run_expect_success( + f"rpm --query --all | grep {name} | " + f"xargs --no-run-if-empty rpm --erase --verbose" + ) + + +class DebSet(): + def __init__(self, packages_paths: list): + self.packages = packages_paths + + def _get_package_names(self): + return " ".join([os.path.basename(pckg).split("_")[0] for pckg in self.packages]) + + def check_if_installed(self): + if not self.packages: + raise ValueError("No packages given.") + + output = TestRun.executor.run(f"dpkg --no-pager --list {self._get_package_names()}") + + return output.exit_code == 0 + + def install(self): + TestRun.LOGGER.info(f"Installing DEB packages") + + if not self.packages: + raise ValueError("No packages given.") + + output = TestRun.executor.run( + f"dpkg --force-confdef --force-confold --install {' '.join(self.packages)}" + ) + if ( + output.exit_code != 0 + or re.search("error", output.stdout, re.IGNORECASE) + or re.search("error", output.stderr, re.IGNORECASE) + ): + raise CmdException("Installation failed or errors found during the process.", output) + + def uninstall(self): + TestRun.LOGGER.info(f"Uninstalling DEB packages") + + if not self.check_if_installed(): + raise FileNotFoundError("Could not uninstall - packages not installed yet.") + + output = TestRun.executor.run(f"dpkg --purge {self._get_package_names()}") + if ( + output.exit_code != 0 + or re.search("error", output.stdout, re.IGNORECASE) + or re.search("error", output.stderr, re.IGNORECASE) + ): + raise CmdException("Uninstallation failed or errors found during the process.", output) + + @staticmethod + def uninstall_all_matching(*packages_names: str): + for name in packages_names: + TestRun.LOGGER.info(f"Uninstalling all DEB packages matching '{name}'") + TestRun.executor.run_expect_success( + f"dpkg-query --no-pager --showformat='${{Package}}\n' --show | grep {name} | " + f"xargs --no-run-if-empty dpkg --purge" + ) diff --git a/test/functional/test-framework/test_tools/peach_fuzzer/__init__.py b/test/functional/test-framework/test_tools/peach_fuzzer/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/functional/test-framework/test_tools/peach_fuzzer/config_template.xml b/test/functional/test-framework/test_tools/peach_fuzzer/config_template.xml new file mode 100644 index 0000000..1c6c8a8 --- /dev/null +++ b/test/functional/test-framework/test_tools/peach_fuzzer/config_template.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/functional/test-framework/test_tools/peach_fuzzer/peach_fuzzer.py b/test/functional/test-framework/test_tools/peach_fuzzer/peach_fuzzer.py new file mode 100644 index 0000000..4adb3a3 --- /dev/null +++ b/test/functional/test-framework/test_tools/peach_fuzzer/peach_fuzzer.py @@ -0,0 +1,208 @@ +# +# Copyright(c) 2021 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# + +import os +import wget +import base64 +import posixpath +import random +import tempfile +import lxml.etree as etree +from collections import namedtuple + +from core.test_run import TestRun +from test_tools import fs_utils +from test_tools.fs_utils import create_directory, check_if_file_exists, write_file + + +class PeachFuzzer: + """ + API to work with Peach Fuzzer tool in Test-Framework. + Peach Fuzzer is used only for generating fuzzed values that later are used in Test-Framework + in order to execute fuzzed CLI commands or to prepare fuzzed config files. + """ + + peach_fuzzer_3_0_url = "https://sourceforge.net/projects/peachfuzz/files/Peach/3.0/" \ + "peach-3.0.202-linux-x86_64-release.zip" + base_dir = "/root/Fuzzy" + peach_dir = "peach-3.0.202-linux-x86_64-release" + xml_config_template = posixpath.join(posixpath.dirname(__file__), "config_template.xml") + xml_config_file = posixpath.join(base_dir, "fuzzerConfig.xml") + xml_namespace = "http://peachfuzzer.com/2012/Peach" + fuzzy_output_file = posixpath.join(base_dir, "fuzzedParams.txt") + tested_param_placeholder = b"{param}" + # escape backslash first, so it doesn't interfere with escaping other characters + escape_chars = '\\\n"\'&|;()`<>$! ' + + @classmethod + def get_fuzzed_command(cls, command_template: bytes, count: int): + """ + Generate command with fuzzed parameter provided on command_template. + Command is ready to be executed with test executor + :param command_template: byte string with command to be executed. + parameter to be replaced with fuzzed string has to be tested_param_placeholder + :param count: amount of fuzzed commands to generate + :returns: named tuple with fuzzed param and CLI ready to be executed with Test-Framework + executors. Param is returned in order to implement correct values checkers in the tests + """ + TestRun.LOGGER.info(f"Try to get commands with fuzzed parameters") + FuzzedCommand = namedtuple('FuzzedCommand', ['param', 'command']) + if cls.tested_param_placeholder not in command_template: + TestRun.block("No param placeholder is found in command template!") + cmd_prefix = b"echo " + cmd_suffix = b" | base64 --decode | sh" + for fuzzed_parameter in cls.generate_peach_fuzzer_parameters(count): + yield FuzzedCommand(fuzzed_parameter, + cmd_prefix + base64.b64encode(command_template.replace( + cls.tested_param_placeholder, fuzzed_parameter)) + cmd_suffix) + + @classmethod + def generate_peach_fuzzer_parameters(cls, count: int): + """ + Generate fuzzed parameter according to Peach Fuzzer XML config + Fuzzed parameter later can be used for either generating cli command or config. + :param count: amount of fuzzed strings to generate + :returns: fuzzed value in byte string + """ + if not cls._is_installed(): + TestRun.LOGGER.info("Try to install Peach Fuzzer") + cls._install() + if not cls._is_xml_config_prepared(): + TestRun.block("No Peach Fuzzer XML config needed to generate fuzzed values was found!") + fs_utils.remove(cls.fuzzy_output_file, force=True, ignore_errors=True) + TestRun.LOGGER.info(f"Generate {count} unique fuzzed values") + cmd = f"cd {cls.base_dir}; {cls.peach_dir}/peach --range 0,{count - 1} " \ + f"--seed {random.randrange(2 ** 32)} {cls.xml_config_file} > " \ + f"{cls.base_dir}/peachOutput.log" + TestRun.executor.run_expect_success(cmd) + if not check_if_file_exists(cls.fuzzy_output_file): + TestRun.block("No expected fuzzy output file was found!") + + # process fuzzy output file locally on the controller as it can be very big + local_fuzzy_file = tempfile.NamedTemporaryFile(delete=False) + local_fuzzy_file.close() + TestRun.executor.rsync_from(cls.fuzzy_output_file, local_fuzzy_file.name) + with open(local_fuzzy_file.name, "r") as fd: + for fuzzed_param_line in fd: + fuzzed_param_bytes = base64.b64decode(fuzzed_param_line) + fuzzed_param_bytes = cls._escape_special_chars(fuzzed_param_bytes) + yield fuzzed_param_bytes + + @classmethod + def generate_config(cls, data_model_config: list): + """ + Generate Peach Fuzzer XML config based on template provided in xml_config_template + and data template passed as an argument. + :param data_model_config: dictionary with config that has to be used for generating + DataModel section in PeachFuzzer XML config. Config can be stored in test in more compact + form, e.g. in yaml, and can be converted to dict just before passing to this function. + Example of such config in yaml: + - name: String + attributes: + name: CacheId + value: '1' + size: '14' + mutable: 'true' + children: + - name: Hint + attributes: + name: NumericalString + value: 'true' + """ + + if not posixpath.exists(cls.xml_config_template): + TestRun.block("Peach fuzzer xml config template not found!") + root = etree.parse(cls.xml_config_template) + data_model = root.find(f'{{{cls.xml_namespace}}}DataModel[@name="Value"]') + cls.__create_xml_nodes(data_model, data_model_config) + create_directory(cls.base_dir, True) + write_file(cls.xml_config_file, etree.tostring(root, encoding="unicode")) + + @classmethod + def copy_config(cls, config_file: str): + """ + Instead of generating config with "generate_config" method, config can be prepared manually + and just passed as is to PeachFuzzer. + :param config_file: Peach Fuzzer XML config to be copied to the DUT + """ + if not posixpath.exists(config_file): + TestRun.block("Peach fuzzer xml config to be copied doesn't exist!") + create_directory(cls.base_dir, True) + TestRun.executor.rsync_to(config_file, cls.xml_config_file) + + @classmethod + def __create_xml_nodes(cls, xml_node, config): + """ + Create XML code for Peach Fuzzer based on python dict config + """ + for element in config: + new_node = etree.Element(element["name"]) + for attr_name, attr_value in element["attributes"].items(): + new_node.set(attr_name, attr_value) + if element.get("children"): + cls.__create_xml_nodes(new_node, element.get("children")) + xml_node.append(new_node) + + @classmethod + def _install(cls): + """ + Install Peach Fuzzer on the DUT + """ + peach_archive = wget.download(cls.peach_fuzzer_3_0_url) + create_directory(cls.base_dir, True) + TestRun.executor.rsync_to(f"\"{peach_archive}\"", f"{cls.base_dir}") + TestRun.executor.run_expect_success( + f'cd {cls.base_dir} && unzip -u "{peach_archive}"') + if cls._is_installed(): + TestRun.LOGGER.info("Peach fuzzer installed successfully") + os.remove(peach_archive) + else: + TestRun.block("Peach fuzzer installation failed!") + + @classmethod + def _is_installed(cls): + """ + Check if Peach Fuzzer is installed on the DUT + """ + if not cls._is_mono_installed(): + TestRun.block("Mono is not installed, can't continue with Peach Fuzzer!") + if fs_utils.check_if_directory_exists(posixpath.join(cls.base_dir, cls.peach_dir)): + return "Peach" in TestRun.executor.run( + f"cd {cls.base_dir} && {cls.peach_dir}/peach --version").stdout.strip() + else: + return False + + @classmethod + def _escape_special_chars(cls, fuzzed_str: bytes): + """ + Escape special chars provided in escape_chars list in the fuzzed string generated by + Peach Fuzzer + Escaping is done for example in order to make fuzzed string executable in Linux CLI + If fuzzed string will be used in other places, escape_chars list may be overwritten. + """ + for i in cls.escape_chars: + i = bytes(i, "utf-8") + if i in fuzzed_str[:]: + fuzzed_str = fuzzed_str.replace(i, b'\\' + i) + return fuzzed_str + + @classmethod + def _is_xml_config_prepared(cls): + """ + Check if Peach Fuzzer XML config is present on the DUT + """ + if fs_utils.check_if_file_exists(cls.xml_config_file): + return True + else: + return False + + @staticmethod + def _is_mono_installed(): + """ + Check if Mono (.NET compatible framework) is installed on the DUT + If it's not, it has to be installed manually. + For RHEL-based OSes it's usually mono-complete package + """ + return TestRun.executor.run("which mono").exit_code == 0 diff --git a/test/functional/test-framework/test_utils/__init__.py b/test/functional/test-framework/test_utils/__init__.py new file mode 100644 index 0000000..dce958c --- /dev/null +++ b/test/functional/test-framework/test_utils/__init__.py @@ -0,0 +1,4 @@ +# +# Copyright(c) 2019-2021 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# diff --git a/test/functional/test-framework/test_utils/asynchronous.py b/test/functional/test-framework/test_utils/asynchronous.py new file mode 100644 index 0000000..9be0159 --- /dev/null +++ b/test/functional/test-framework/test_utils/asynchronous.py @@ -0,0 +1,18 @@ +# +# Copyright(c) 2020-2021 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# + +import concurrent + + +def start_async_func(func, *args): + """ + Starts asynchronous task and returns an Future object, which in turn returns an + actual result after triggering result() method on it. + - result() method is waiting for the task to be completed. + - done() method returns True when task ended (have a result or ended with an exception) + otherwise returns False + """ + executor = concurrent.futures.ThreadPoolExecutor() + return executor.submit(func, *args) diff --git a/test/functional/test-framework/test_utils/disk_finder.py b/test/functional/test-framework/test_utils/disk_finder.py new file mode 100644 index 0000000..21eb0db --- /dev/null +++ b/test/functional/test-framework/test_utils/disk_finder.py @@ -0,0 +1,190 @@ +# +# Copyright(c) 2019-2021 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# + +import posixpath + +from core.test_run import TestRun +from test_tools import disk_utils +from test_tools.fs_utils import check_if_file_exists, readlink +from test_utils import os_utils +from test_utils.output import CmdException + + +def find_disks(): + devices_result = [] + + TestRun.LOGGER.info("Finding platform's disks.") + + # TODO: intelmas should be implemented as a separate tool in the future. + # There will be intelmas installer in case, when it is not installed + output = TestRun.executor.run('intelmas') + if output.exit_code != 0: + raise Exception(f"Error while executing command: 'intelmas'.\n" + f"stdout: {output.stdout}\nstderr: {output.stderr}") + block_devices = get_block_devices_list() + try: + discover_ssd_devices(block_devices, devices_result) + discover_hdd_devices(block_devices, devices_result) + except Exception as e: + raise Exception(f"Exception occurred while looking for disks: {str(e)}") + + return devices_result + + +def get_block_devices_list(): + devices = TestRun.executor.run_expect_success("ls /sys/block -1").stdout.splitlines() + os_disks = get_system_disks() + block_devices = [] + + for dev in devices: + if ('sd' in dev or 'nvme' in dev) and dev not in os_disks: + block_devices.append(dev) + + return block_devices + + +def discover_hdd_devices(block_devices, devices_res): + for dev in block_devices: + if TestRun.executor.run_expect_success(f"cat /sys/block/{dev}/removable").stdout == "1": + continue # skip removable drives + block_size = disk_utils.get_block_size(dev) + if int(block_size) == 4096: + disk_type = 'hdd4k' + else: + disk_type = 'hdd' + devices_res.append({ + "type": disk_type, + "path": f"{resolve_to_by_id_link(dev)}", + "serial": TestRun.executor.run_expect_success( + f"sg_inq /dev/{dev} | grep -i 'serial number'" + ).stdout.split(': ')[1].strip(), + "blocksize": block_size, + "size": disk_utils.get_size(dev)}) + block_devices.clear() + + +# This method discovers only Intel SSD devices +def discover_ssd_devices(block_devices, devices_res): + ssd_count = int(TestRun.executor.run_expect_success( + 'intelmas show -intelssd | grep DevicePath | wc -l').stdout) + for i in range(0, ssd_count): + # Workaround for intelmas bug that lists all of the devices (non intel included) + # with -intelssd flag + if TestRun.executor.run( + f"intelmas show -display index -intelssd {i} | grep -w Intel").exit_code == 0: + device_path = TestRun.executor.run_expect_success( + f"intelmas show -intelssd {i} | grep DevicePath").stdout.split()[2] + dev = device_path.replace("/dev/", "") + if "sg" in dev: + sata_dev = TestRun.executor.run_expect_success( + f"sg_map | grep {dev}").stdout.split()[1] + dev = sata_dev.replace("/dev/", "") + if dev not in block_devices: + continue + serial_number = TestRun.executor.run_expect_success( + f"intelmas show -intelssd {i} | grep SerialNumber").stdout.split()[2].strip() + if 'nvme' not in device_path: + disk_type = 'sata' + device_path = dev + elif TestRun.executor.run( + f"intelmas show -intelssd {i} | grep Optane").exit_code == 0: + disk_type = 'optane' + else: + disk_type = 'nand' + + devices_res.append({ + "type": disk_type, + "path": resolve_to_by_id_link(device_path), + "serial": serial_number, + "blocksize": disk_utils.get_block_size(dev), + "size": disk_utils.get_size(dev)}) + block_devices.remove(dev) + + +def get_disk_serial_number(dev_path): + commands = [ + f"(udevadm info --query=all --name={dev_path} | grep 'SCSI.*_SERIAL' || " + f"udevadm info --query=all --name={dev_path} | grep 'ID_SERIAL_SHORT') | " + "awk --field-separator '=' '{print $NF}'", + f"sg_inq {dev_path} 2> /dev/null | grep '[Ss]erial number:' | " + "awk '{print $NF}'", + f"udevadm info --query=all --name={dev_path} | grep 'ID_SERIAL' | " + "awk --field-separator '=' '{print $NF}'" + ] + for command in commands: + serial = TestRun.executor.run(command).stdout + if serial: + return serial.split('\n')[0] + return None + + +def get_all_serial_numbers(): + serial_numbers = {} + block_devices = get_block_devices_list() + for dev in block_devices: + serial = get_disk_serial_number(dev) + try: + path = resolve_to_by_id_link(dev) + except Exception: + continue + if serial: + serial_numbers[serial] = path + else: + TestRun.LOGGER.warning(f"Device {path} ({dev}) does not have a serial number.") + serial_numbers[path] = path + return serial_numbers + + +def get_system_disks(): + system_device = TestRun.executor.run_expect_success('mount | grep " / "').stdout.split()[0] + readlink_output = readlink(system_device) + device_name = readlink_output.split('/')[-1] + sys_block_path = os_utils.get_sys_block_path() + used_device_names = __get_slaves(device_name) + if not used_device_names: + used_device_names = [device_name] + disk_names = [] + for device_name in used_device_names: + if check_if_file_exists(f'{sys_block_path}/{device_name}/partition'): + parent_device = readlink(f'{sys_block_path}/{device_name}/..').split('/')[-1] + disk_names.append(parent_device) + else: + disk_names.append(device_name) + + return disk_names + + +def __get_slaves(device_name: str): + try: + device_names = TestRun.executor.run_expect_success( + f'ls {os_utils.get_sys_block_path()}/{device_name}/slaves').stdout.splitlines() + except CmdException as e: + if "No such file or directory" not in e.output.stderr: + raise + return None + device_list = [] + for device_name in device_names: + slaves = __get_slaves(device_name) + if slaves: + for slave in slaves: + device_list.append(slave) + else: + device_list.append(device_name) + return device_list + + +def resolve_to_by_id_link(path): + by_id_paths = TestRun.executor.run_expect_success("ls /dev/disk/by-id -1").stdout.splitlines() + dev_full_paths = [posixpath.join("/dev/disk/by-id", by_id_path) for by_id_path in by_id_paths] + + for full_path in dev_full_paths: + # handle exception for broken links + try: + if readlink(full_path) == readlink(posixpath.join("/dev", path)): + return full_path + except CmdException: + continue + + raise ValueError(f'By-id device link not found for device {path}') diff --git a/test/functional/test-framework/test_utils/drbd.py b/test/functional/test-framework/test_utils/drbd.py new file mode 100644 index 0000000..bec4497 --- /dev/null +++ b/test/functional/test-framework/test_utils/drbd.py @@ -0,0 +1,61 @@ +# +# Copyright(c) 2022 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause-Clear +# + +import os + +from test_utils.filesystem.file import File + + +class Resource: + def __init__(self, name, device, nodes, options=None): + self.name = name + self.device = device + self.nodes = nodes + self.options = options + + def __str__(self): + output = ( + f"resource {self.name} {{ \n" + f" device {self.device}; \n" + f"{''.join([str(node) for node in self.nodes])}" + ) + + if self.options: + output += f" options {{\n" + for (k, v) in self.options.items(): + output += f" {k} {v};\n" + output += f" }}\n" + + output += f"}}" + return output + + def __repr__(self): + return str(self) + + def save(self, path="/etc/drbd.d/", filename=None): + filename = filename if filename else f"{self.name}.res" + file = File(path + filename) + file.write(str(self)) + + +class Node: + def __init__(self, name, disk, meta_disk, ip, port): + self.name = name + self.disk = disk + self.meta_disk = meta_disk + self.ip = ip + self.port = port + + def __str__(self): + return ( + f" on {self.name} {{ \n" + f" disk {self.disk};\n" + f" meta-disk {self.meta_disk};\n" + f" address {self.ip}:{self.port};\n" + f" }} \n" + ) + + def __repr__(self): + return str(self) diff --git a/test/functional/test-framework/test_utils/dut.py b/test/functional/test-framework/test_utils/dut.py new file mode 100644 index 0000000..0780dcb --- /dev/null +++ b/test/functional/test-framework/test_utils/dut.py @@ -0,0 +1,43 @@ +# +# Copyright(c) 2019-2021 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# + +from storage_devices.disk import Disk, DiskType + + +class Dut: + def __init__(self, dut_info): + self.config = dut_info + self.disks = [] + for disk_info in dut_info.get('disks', []): + self.disks.append(Disk.create_disk(disk_info['path'], + DiskType[disk_info['type']], + disk_info['serial'], + disk_info['blocksize'])) + self.disks.sort(key=lambda disk: disk.disk_type, reverse=True) + + self.ipmi = dut_info['ipmi'] if 'ipmi' in dut_info else None + self.spider = dut_info['spider'] if 'spider' in dut_info else None + self.wps = dut_info['wps'] if 'wps' in dut_info else None + self.env = dut_info['env'] if 'env' in dut_info else None + self.ip = dut_info['ip'] if 'ip' in dut_info else "127.0.0.1" + + def __str__(self): + dut_str = f'ip: {self.ip}\n' + dut_str += f'ipmi: {self.ipmi["ip"]}\n' if self.ipmi is not None else '' + dut_str += f'spider: {self.spider["ip"]}\n' if self.spider is not None else '' + dut_str += f'wps: {self.wps["ip"]} port: {self.wps["port"]}\n' \ + if self.wps is not None else '' + dut_str += f'disks:\n' + for disk in self.disks: + dut_str += f"\t{disk}" + dut_str += "\n" + return dut_str + + def get_disks_of_type(self, disk_type: DiskType): + ret_list = [] + for d in self.disks: + if d.disk_type == disk_type: + ret_list.append(d) + return ret_list diff --git a/test/functional/test-framework/test_utils/emergency_escape.py b/test/functional/test-framework/test_utils/emergency_escape.py new file mode 100644 index 0000000..a997032 --- /dev/null +++ b/test/functional/test-framework/test_utils/emergency_escape.py @@ -0,0 +1,113 @@ +# +# Copyright(c) 2022 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# + +from textwrap import dedent +from string import Template +from pathlib import Path + +from .systemd import enable_service, reload_daemon, systemd_service_directory, disable_service +from test_tools.fs_utils import ( + create_file, + write_file, + remove, +) + + +class EmergencyEscape: + escape_marker = "EMERGENCY_ESCAPE" + escape_service = Path("emergency-escape.service") + escape_service_template = Template( + dedent( + f""" + [Unit] + After=emergency.target + IgnoreOnIsolate=true + DefaultDependencies=no + + [Service] + Type=oneshot + ExecStart=/bin/sh -c '/usr/bin/echo "{escape_marker}" > /dev/kmsg' + $user_method + ExecStart=/usr/bin/systemctl daemon-reload + ExecStart=/usr/bin/systemctl default --no-block + + [Install] + WantedBy=emergency.target + """ + ).strip() + ) + cleanup_service = Path("emergency-escape-cleanup.service") + cleanup_service_template = Template( + dedent( + """ + [Unit] + After=emergency-escape.service + IgnoreOnIsolate=true + DefaultDependencies=no + + [Service] + Type=oneshot + $user_method + ExecStart=/usr/bin/systemctl disable emergency-escape.service + ExecStart=/usr/bin/rm -f /usr/lib/systemd/system/emergency-escape.service + ExecStart=/usr/bin/systemctl daemon-reload + + [Install] + WantedBy=emergency-escape.service + """ + ).strip() + ) + + def __init__(self): + self.escape_method = [] + self.cleanup_method = [] + + def arm(self): + escape_path = str(systemd_service_directory / EmergencyEscape.escape_service) + cleanup_path = str(systemd_service_directory / EmergencyEscape.cleanup_service) + + create_file(escape_path) + create_file(cleanup_path) + + user_escape = "\n".join([f"ExecStart={method}" for method in self.escape_method]) + user_cleanup = "\n".join([f"ExecStart={method}" for method in self.cleanup_method]) + + escape_contents = EmergencyEscape.escape_service_template.substitute( + user_method=user_escape + ) + cleanup_contents = EmergencyEscape.cleanup_service_template.substitute( + user_method=user_cleanup + ) + + write_file(escape_path, escape_contents) + write_file(cleanup_path, cleanup_contents) + + enable_service(EmergencyEscape.escape_service) + enable_service(EmergencyEscape.cleanup_service) + + def cleanup(self): + remove(str(systemd_service_directory / EmergencyEscape.cleanup_service), ignore_errors=True) + remove(str(systemd_service_directory / EmergencyEscape.escape_service), ignore_errors=True) + reload_daemon() + + @classmethod + def verify_trigger_in_log(cls, log_list): + for l in log_list: + if cls.escape_marker in l: + return True + + return False + + def add_escape_method_command(self, method): + self.escape_method.append(method) + + def add_cleanup_method_command(self, method): + self.cleanup_method.append(method) + + def __enter__(self): + self.arm() + + def __exit__(self, exc_type, exc_value, exc_traceback): + self.cleanup() diff --git a/test/functional/test-framework/test_utils/filesystem/__init__.py b/test/functional/test-framework/test_utils/filesystem/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/functional/test-framework/test_utils/filesystem/directory.py b/test/functional/test-framework/test_utils/filesystem/directory.py new file mode 100644 index 0000000..7e46332 --- /dev/null +++ b/test/functional/test-framework/test_utils/filesystem/directory.py @@ -0,0 +1,31 @@ +# +# Copyright(c) 2019-2021 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# +from core.test_run import TestRun +from test_tools import fs_utils +from test_tools.fs_utils import check_if_directory_exists +from test_utils.filesystem.fs_item import FsItem + + +class Directory(FsItem): + def __init__(self, full_path): + FsItem.__init__(self, full_path) + + def ls(self): + output = fs_utils.ls(f"{self.full_path}") + return fs_utils.parse_ls_output(output, self.full_path) + + @staticmethod + def create_directory(path: str, parents: bool = False): + fs_utils.create_directory(path, parents) + output = fs_utils.ls_item(path) + return fs_utils.parse_ls_output(output)[0] + + @staticmethod + def create_temp_directory(parent_dir_path: str = "/tmp"): + command = f"mktemp --directory --tmpdir={parent_dir_path}" + output = TestRun.executor.run_expect_success(command) + if not check_if_directory_exists(output.stdout): + TestRun.LOGGER.exception("'mktemp' succeeded, but created directory does not exist") + return Directory(output.stdout) diff --git a/test/functional/test-framework/test_utils/filesystem/file.py b/test/functional/test-framework/test_utils/filesystem/file.py new file mode 100644 index 0000000..e6071e1 --- /dev/null +++ b/test/functional/test-framework/test_utils/filesystem/file.py @@ -0,0 +1,83 @@ +# +# Copyright(c) 2019-2021 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# + +from core.test_run import TestRun +from test_tools import fs_utils +from test_tools.dd import Dd +from test_utils.filesystem.fs_item import FsItem +from test_utils.size import Size + + +class File(FsItem): + def __init__(self, full_path): + FsItem.__init__(self, full_path) + + def compare(self, other_file): + return fs_utils.compare(str(self), str(other_file)) + + def diff(self, other_file): + return fs_utils.diff(str(self), str(other_file)) + + def md5sum(self, binary=True): + output = TestRun.executor.run( + f"md5sum {'-b' if binary else ''} {self.full_path}") + if output.exit_code != 0: + raise Exception(f"Md5sum command execution failed! {output.stdout}\n{output.stderr}") + return output.stdout.split()[0] + + def read(self): + return fs_utils.read_file(str(self)) + + def write(self, content, overwrite: bool = True): + fs_utils.write_file(str(self), content, overwrite) + self.refresh_item() + + def get_properties(self): + return FileProperties(self) + + @staticmethod + def create_file(path: str): + fs_utils.create_file(path) + output = fs_utils.ls_item(path) + return fs_utils.parse_ls_output(output)[0] + + def padding(self, size: Size): + dd = Dd().input("/dev/zero").output(self).count(1).block_size(size) + dd.run() + self.refresh_item() + + def remove(self, force: bool = False, ignore_errors: bool = False): + fs_utils.remove(str(self), force=force, ignore_errors=ignore_errors) + + def copy(self, + destination, + force: bool = False, + recursive: bool = False, + dereference: bool = False): + fs_utils.copy(str(self), destination, force, recursive, dereference) + if fs_utils.check_if_directory_exists(destination): + path = f"{destination}{'/' if destination[-1] != '/' else ''}{self.name}" + else: + path = destination + output = fs_utils.ls_item(path) + return fs_utils.parse_ls_output(output)[0] + + +class FileProperties: + def __init__(self, file): + file = fs_utils.parse_ls_output(fs_utils.ls_item(file.full_path))[0] + self.full_path = file.full_path + self.parent_dir = FsItem.get_parent_dir(self.full_path) + self.name = FsItem.get_name(self.full_path) + self.modification_time = file.modification_time + self.owner = file.owner + self.group = file.group + self.permissions = file.permissions + self.size = file.size + + def __eq__(self, other): + return (self.permissions == other.permissions and self.size == other.size + and self.owner == other.owner and self.group == other.group + and self.name == other.name) diff --git a/test/functional/test-framework/test_utils/filesystem/fs_item.py b/test/functional/test-framework/test_utils/filesystem/fs_item.py new file mode 100644 index 0000000..c060ae3 --- /dev/null +++ b/test/functional/test-framework/test_utils/filesystem/fs_item.py @@ -0,0 +1,102 @@ +# +# Copyright(c) 2019-2021 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# + +import posixpath + +from test_tools import fs_utils + + +class FsItem: + def __init__(self, full_path): + self.full_path = full_path + # all below values must be refreshed in refresh_item() + self.parent_dir = self.get_parent_dir(self.full_path) + self.name = self.get_name(self.full_path) + self.modification_time = None + self.owner = None + self.group = None + self.permissions = FsPermissions() + self.size = None + + @staticmethod + def get_name(path): + head, tail = posixpath.split(path) + return tail or posixpath.basename(head) + + @staticmethod + def get_parent_dir(path): + head, tail = posixpath.split(path) + if tail: + return head + else: + head, tail = posixpath.split(head) + return head + + def __str__(self): + return self.full_path + + def chmod_numerical(self, permissions: int, recursive: bool = False): + fs_utils.chmod_numerical(self.full_path, permissions, recursive) + self.refresh_item() + + def chmod(self, + permissions: fs_utils.Permissions, + users: fs_utils.PermissionsUsers, + sign: fs_utils.PermissionSign = fs_utils.PermissionSign.set, + recursive: bool = False): + fs_utils.chmod(self.full_path, permissions, users, sign=sign, recursive=recursive) + self.refresh_item() + + def chown(self, owner, group, recursive: bool = False): + fs_utils.chown(self.full_path, owner, group, recursive) + self.refresh_item() + + def copy(self, + destination, + force: bool = False, + recursive: bool = False, + dereference: bool = False): + target_dir_exists = fs_utils.check_if_directory_exists(destination) + fs_utils.copy(str(self), destination, force, recursive, dereference) + if target_dir_exists: + path = f"{destination}{'/' if destination[-1] != '/' else ''}{self.name}" + else: + path = destination + output = fs_utils.ls_item(f"{path}") + return fs_utils.parse_ls_output(output)[0] + + def move(self, + destination, + force: bool = False): + target_dir_exists = fs_utils.check_if_directory_exists(destination) + fs_utils.move(str(self), destination, force) + if target_dir_exists: + self.full_path = f"{destination}{'/' if destination[-1] != '/' else ''}{self.name}" + else: + self.full_path = destination + self.refresh_item() + return self + + def refresh_item(self): + updated_file = fs_utils.parse_ls_output(fs_utils.ls_item(self.full_path))[0] + # keep order the same as in __init__() + self.parent_dir = updated_file.parent_dir + self.name = updated_file.name + self.modification_time = updated_file.modification_time + self.owner = updated_file.owner + self.group = updated_file.group + self.permissions = updated_file.permissions + self.size = updated_file.size + return self + + +class FsPermissions: + def __init__(self, user=None, group=None, other=None): + self.user = user + self.group = group + self.other = other + + def __eq__(self, other): + return self.user == other.user and self.group == other.group and self.other == other.other diff --git a/test/functional/test-framework/test_utils/filesystem/symlink.py b/test/functional/test-framework/test_utils/filesystem/symlink.py new file mode 100644 index 0000000..e67906c --- /dev/null +++ b/test/functional/test-framework/test_utils/filesystem/symlink.py @@ -0,0 +1,91 @@ +# +# Copyright(c) 2019-2021 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# + +from core.test_run import TestRun +from test_tools.fs_utils import ( + readlink, + create_directory, + check_if_symlink_exists, + check_if_directory_exists, +) +from test_utils.filesystem.file import File + + +class Symlink(File): + def __init__(self, full_path): + File.__init__(self, full_path) + + def md5sum(self, binary=True): + output = TestRun.executor.run_expect_success( + f"md5sum {'-b' if binary else ''} {self.get_target()}" + ) + return output.stdout.split()[0] + + def get_target(self): + return readlink(self.full_path) + + def get_symlink_path(self): + return self.full_path + + def remove_symlink(self): + path = self.get_symlink_path() + TestRun.executor.run_expect_success(f"rm -f {path}") + + @classmethod + def create_symlink(cls, link_path: str, target: str, force: bool = False): + """ + Creates a Symlink - new or overwrites existing one if force parameter is True + :param link_path: path to the place where we want to create a symlink + :param target: the path of an object that the requested Symlink points to + :param force: determines if the existing symlink with the same name should be overridden + return: Symlink object located under link_path + """ + cmd = f"ln --symbolic {target} {link_path}" + is_dir = check_if_directory_exists(link_path) + parent_dir = cls.get_parent_dir(link_path) + if is_dir: + raise IsADirectoryError(f"'{link_path}' is an existing directory.") + if force: + if not check_if_directory_exists(parent_dir): + create_directory(parent_dir, True) + TestRun.executor.run_expect_success(f"rm -f {link_path}") + TestRun.executor.run_expect_success(cmd) + return cls(link_path) + + @classmethod + def get_symlink(cls, link_path: str, target: str = None, create: bool = False): + """ + Request a Symlink (create new or identify existing) + :param link_path: full path of the requested Symlink + :param target: path of an object that the requested Symlink points to + (required if create is True) + :param create: determines if the requested Symlink should be created if it does not exist + :return: Symlink object located under link_path + """ + if create and not target: + raise AttributeError("Target is required for symlink creation.") + + is_symlink = check_if_symlink_exists(link_path) + if is_symlink: + if not target or readlink(link_path) == readlink(target): + return cls(link_path) + else: + raise FileExistsError("Existing symlink points to a different target.") + elif not create: + raise FileNotFoundError("Requested symlink does not exist.") + + is_dir = check_if_directory_exists(link_path) + if is_dir: + raise IsADirectoryError( + f"'{link_path}' is an existing directory." "\nUse a full path for symlink creation." + ) + + parent_dir = cls.get_parent_dir(link_path) + if not check_if_directory_exists(parent_dir): + create_directory(parent_dir, True) + + cmd = f"ln --symbolic {target} {link_path}" + TestRun.executor.run_expect_success(cmd) + return cls(link_path) diff --git a/test/functional/test-framework/test_utils/fstab.py b/test/functional/test-framework/test_utils/fstab.py new file mode 100644 index 0000000..fceb375 --- /dev/null +++ b/test/functional/test-framework/test_utils/fstab.py @@ -0,0 +1,20 @@ +# +# Copyright(c) 2019-2021 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# + +from test_tools import fs_utils +from test_utils import systemd + + +def add_mountpoint(device, mount_point, fs_type, mount_now=True): + fs_utils.append_line("/etc/fstab", + f"{device.path} {mount_point} {fs_type.name} defaults 0 0") + systemd.reload_daemon() + if mount_now: + systemd.restart_service("local-fs.target") + + +def remove_mountpoint(device): + fs_utils.remove_lines("/etc/fstab", device.path) + systemd.reload_daemon() diff --git a/test/functional/test-framework/test_utils/generator.py b/test/functional/test-framework/test_utils/generator.py new file mode 100644 index 0000000..8c64ba6 --- /dev/null +++ b/test/functional/test-framework/test_utils/generator.py @@ -0,0 +1,11 @@ +# +# Copyright(c) 2019-2021 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# + +import random +import string + + +def random_string(length: int, chars=string.ascii_letters + string.digits): + return ''.join(random.choice(chars) for i in range(length)) diff --git a/test/functional/test-framework/test_utils/io_stats.py b/test/functional/test-framework/test_utils/io_stats.py new file mode 100644 index 0000000..9a3f218 --- /dev/null +++ b/test/functional/test-framework/test_utils/io_stats.py @@ -0,0 +1,112 @@ +# +# Copyright(c) 2020-2021 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# +import re + +from core.test_run import TestRun +from test_utils.output import CmdException + +SYSFS_LINE_FORMAT = r"^(\d+\s+){10,}\d+$" +PROCFS_LINE_FORMAT = r"^\d+\s+\d+\s+\w+\s+" + SYSFS_LINE_FORMAT[1:] + + +# This class represents block device I/O statistics. +# For more information see: +# https://www.kernel.org/doc/Documentation/admin-guide/iostats.rst +class IoStats: + def __init__(self): + self.reads = None # field 0 + self.reads_merged = None # field 1 + self.sectors_read = None # field 2 + self.read_time_ms = None # field 3 + self.writes = None # field 4 + self.writes_merged = None # field 5 + self.sectors_written = None # field 6 + self.write_time_ms = None # field 7 + self.ios_in_progress = None # field 8 + self.io_time_ms = None # field 9 + self.io_time_weighed_ms = None # field 10 + # only in kernels 4.18+ + self.discards = None # field 11 + self.discards_merged = None # field 12 + self.sectors_discarded = None # field 13 + self.discard_time_ms = None # field 14 + # only in kernels 5.5+ + self.flushes = None # field 15 + self.flush_time_ms = None # field 16 + + def __sub__(self, other): + if self.reads < other.reads: + raise Exception("Cannot subtract Reads") + if self.writes < other.writes: + raise Exception("Cannot subtract Writes") + + stats = IoStats() + stats.reads = self.reads - other.reads + stats.reads_merged = self.reads_merged - other.reads_merged + stats.sectors_read = self.sectors_read - other.sectors_read + stats.read_time_ms = self.read_time_ms - other.read_time_ms + stats.writes = self.writes - other.writes + stats.writes_merged = self.writes_merged - other.writes_merged + stats.sectors_written = self.sectors_written - other.sectors_written + stats.write_time_ms = self.write_time_ms - other.write_time_ms + stats.ios_in_progress = 0 + stats.io_time_ms = self.io_time_ms - other.io_time_ms + stats.io_time_weighed_ms = self.io_time_weighed_ms - other.io_time_weighed_ms + if stats.discards and other.discards: + stats.discards = self.discards - other.discards + if stats.discards_merged and other.discards_merged: + stats.discards_merged = self.discards_merged - other.discards_merged + if stats.sectors_discarded and other.sectors_discarded: + stats.sectors_discarded = self.sectors_discarded - other.sectors_discarded + if stats.discard_time_ms and other.discard_time_ms: + stats.discard_time_ms = self.discard_time_ms - other.discard_time_ms + if stats.flushes and other.flushes: + stats.flushes = self.flushes - other.flushes + if stats.flush_time_ms and other.flush_time_ms: + stats.flush_time_ms = self.flush_time_ms - other.flush_time_ms + return stats + + @staticmethod + def parse(stats_line: str): + stats_line = stats_line.strip() + + if re.match(SYSFS_LINE_FORMAT, stats_line): + fields = stats_line.split() + elif re.match(PROCFS_LINE_FORMAT, stats_line): + fields = stats_line.split()[3:] + else: + raise Exception(f"Wrong input format for diskstat parser") + + values = [int(f) for f in fields] + + stats = IoStats() + stats.reads = values[0] + stats.reads_merged = values[1] + stats.sectors_read = values[2] + stats.read_time_ms = values[3] + stats.writes = values[4] + stats.writes_merged = values[5] + stats.sectors_written = values[6] + stats.write_time_ms = values[7] + stats.ios_in_progress = values[8] + stats.io_time_ms = values[9] + stats.io_time_weighed_ms = values[10] + if len(values) > 11: + stats.discards = values[11] + stats.discards_merged = values[12] + stats.sectors_discarded = values[13] + stats.discard_time_ms = values[14] + if len(values) > 15: + stats.flushes = values[15] + stats.flush_time_ms = values[16] + return stats + + @staticmethod + def get_io_stats(device_id): + stats_output = TestRun.executor.run_expect_success( + f"cat /proc/diskstats | grep '{device_id} '") + if not stats_output.stdout.strip(): + raise CmdException("Failed to get statistics for device " + device_id, stats_output) + return IoStats.parse(stats_line=stats_output.stdout.splitlines()[0]) diff --git a/test/functional/test-framework/test_utils/linux_command.py b/test/functional/test-framework/test_utils/linux_command.py new file mode 100644 index 0000000..b6d887b --- /dev/null +++ b/test/functional/test-framework/test_utils/linux_command.py @@ -0,0 +1,79 @@ +# +# Copyright(c) 2019-2021 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# + +from collections import defaultdict + + +class LinuxCommand: + def __init__(self, command_executor, command_name): + self.command_executor = command_executor + self.command_param = defaultdict(list) + self.command_flags = [] + self.command_name = command_name + self.param_name_prefix = '' + self.param_separator = ' ' + self.param_value_prefix = '=' + self.param_value_list_separator = ',' + self.command_env_var = defaultdict(list) + self.env_var_separator = ' ' + self.env_var_value_prefix = '=' + + def run(self): + return self.command_executor.run(str(self)) + + def run_in_background(self): + return self.command_executor.run_in_background(str(self)) + + def set_flags(self, *flag): + for f in flag: + self.command_flags.append(f) + return self + + def remove_flag(self, flag): + if flag in self.command_flags: + self.command_flags.remove(flag) + return self + + def set_param(self, key, *values): + self.remove_param(key) + + for val in values: + self.command_param[key].append(str(val)) + return self + + def remove_param(self, key): + if key in self.command_param: + del self.command_param[key] + return self + + def set_env_var(self, key, *values): + self.remove_env_var(key) + + for val in values: + self.command_env_var[key].append(str(val)) + return self + + def remove_env_var(self, key): + if key in self.command_env_var: + del self.command_env_var[key] + return self + + def get_parameter_value(self, param_name): + if param_name in self.command_param.keys(): + return self.command_param[param_name] + return None + + def __str__(self): + command = '' + for key, value in self.command_env_var.items(): + command += f'{key}{self.env_var_value_prefix}{",".join(value)}' \ + f'{self.env_var_separator}' + command += self.command_name + for key, value in self.command_param.items(): + command += f'{self.param_separator}{self.param_name_prefix}' \ + f'{key}{self.param_value_prefix}{",".join(value)}' + for flag in self.command_flags: + command += f'{self.param_separator}{self.param_name_prefix}{flag}' + return command diff --git a/test/functional/test-framework/test_utils/os_utils.py b/test/functional/test-framework/test_utils/os_utils.py new file mode 100644 index 0000000..8d68662 --- /dev/null +++ b/test/functional/test-framework/test_utils/os_utils.py @@ -0,0 +1,462 @@ +# +# Copyright(c) 2019-2022 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# + +import math +import posixpath +import re +import time +from datetime import timedelta, datetime + +from aenum import IntFlag, Enum, IntEnum +from packaging import version + +from core.test_run import TestRun +from storage_devices.device import Device +from test_tools.dd import Dd +from test_tools.disk_utils import get_sysfs_path +from test_tools.fs_utils import check_if_directory_exists, create_directory, check_if_file_exists +from test_utils.filesystem.file import File +from test_utils.output import CmdException +from test_utils.retry import Retry +from test_utils.size import Size, Unit + +DEBUGFS_MOUNT_POINT = "/sys/kernel/debug" +MEMORY_MOUNT_POINT = "/mnt/memspace" + + +class DropCachesMode(IntFlag): + PAGECACHE = 1 + SLAB = 2 + ALL = PAGECACHE | SLAB + + +class OvercommitMemoryMode(Enum): + DEFAULT = 0 + ALWAYS = 1 + NEVER = 2 + + +class Runlevel(IntEnum): + """ + Halt the system. + SysV Runlevel: 0 + systemd Target: runlevel0.target, poweroff.target + """ + runlevel0 = 0 + poweroff = runlevel0 + + """ + Single user mode. + SysV Runlevel: 1, s, single + systemd Target: runlevel1.target, rescue.target + """ + runlevel1 = 1 + rescue = runlevel1 + + """ + User-defined/Site-specific runlevels. By default, identical to 3. + SysV Runlevel: 2, 4 + systemd Target: runlevel2.target, runlevel4.target, multi-user.target + """ + runlevel2 = 2 + + """ + Multi-user, non-graphical. Users can usually login via multiple consoles or via the network. + SysV Runlevel: 3 + systemd Target: runlevel3.target, multi-user.target + """ + runlevel3 = 3 + multi_user = runlevel3 + + """ + Multi-user, graphical. Usually has all the services of runlevel 3 plus a graphical login. + SysV Runlevel: 5 + systemd Target: runlevel5.target, graphical.target + """ + runlevel5 = 5 + graphical = runlevel5 + + """ + Reboot + SysV Runlevel: 6 + systemd Target: runlevel6.target, reboot.target + """ + runlevel6 = 6 + reboot = runlevel6 + + """ + Emergency shell + SysV Runlevel: emergency + systemd Target: emergency.target + """ + runlevel7 = 7 + emergency = runlevel7 + + +class SystemManagerType(Enum): + sysv = 0 + systemd = 1 + + +def get_system_manager(): + output = TestRun.executor.run_expect_success("ps -p 1").stdout + type = output.split('\n')[1].split()[3] + if type == "init": + return SystemManagerType.sysv + elif type == "systemd": + return SystemManagerType.systemd + raise Exception(f"Unknown system manager type ({type}).") + + +def change_runlevel(runlevel: Runlevel): + if runlevel == get_runlevel(): + return + if Runlevel.runlevel0 < runlevel < Runlevel.runlevel6: + system_manager = get_system_manager() + if system_manager == SystemManagerType.systemd: + TestRun.executor.run_expect_success(f"systemctl set-default {runlevel.name}.target") + else: + TestRun.executor.run_expect_success( + f"sed -i 's/^.*id:.*$/id:{runlevel.value}:initdefault: /' /etc/inittab") + TestRun.executor.run_expect_success(f"init {runlevel.value}") + + +def get_runlevel(): + system_manager = get_system_manager() + if system_manager == SystemManagerType.systemd: + result = TestRun.executor.run_expect_success("systemctl get-default") + try: + name = result.stdout.split(".")[0].replace("-", "_") + return Runlevel[name] + except Exception: + raise Exception(f"Cannot parse '{result.output}' to runlevel.") + else: + result = TestRun.executor.run_expect_success("runlevel") + try: + split_output = result.stdout.split() + runlevel = Runlevel(int(split_output[1])) + return runlevel + except Exception: + raise Exception(f"Cannot parse '{result.output}' to runlevel.") + + +class Udev(object): + @staticmethod + def enable(): + TestRun.LOGGER.info("Enabling udev") + TestRun.executor.run_expect_success("udevadm control --start-exec-queue") + + @staticmethod + def disable(): + TestRun.LOGGER.info("Disabling udev") + TestRun.executor.run_expect_success("udevadm control --stop-exec-queue") + + @staticmethod + def trigger(): + TestRun.executor.run_expect_success("udevadm trigger") + + @staticmethod + def settle(): + TestRun.executor.run_expect_success("udevadm settle") + + +def drop_caches(level: DropCachesMode = DropCachesMode.ALL): + TestRun.executor.run_expect_success( + f"echo {level.value} > /proc/sys/vm/drop_caches") + + +def disable_memory_affecting_functions(): + """Disables system functions affecting memory""" + # Don't allow sshd to be killed in case of out-of-memory: + TestRun.executor.run( + "echo '-1000' > /proc/`cat /var/run/sshd.pid`/oom_score_adj" + ) + TestRun.executor.run( + "echo -17 > /proc/`cat /var/run/sshd.pid`/oom_adj" + ) # deprecated + TestRun.executor.run_expect_success( + f"echo {OvercommitMemoryMode.NEVER.value} > /proc/sys/vm/overcommit_memory" + ) + TestRun.executor.run_expect_success("echo '100' > /proc/sys/vm/overcommit_ratio") + TestRun.executor.run_expect_success( + "echo '64 64 32' > /proc/sys/vm/lowmem_reserve_ratio" + ) + TestRun.executor.run_expect_success("swapoff --all") + drop_caches(DropCachesMode.SLAB) + + +def defaultize_memory_affecting_functions(): + """Sets default values to system functions affecting memory""" + TestRun.executor.run_expect_success( + f"echo {OvercommitMemoryMode.DEFAULT.value} > /proc/sys/vm/overcommit_memory" + ) + TestRun.executor.run_expect_success("echo 50 > /proc/sys/vm/overcommit_ratio") + TestRun.executor.run_expect_success( + "echo '256 256 32' > /proc/sys/vm/lowmem_reserve_ratio" + ) + TestRun.executor.run_expect_success("swapon --all") + + +def get_free_memory(): + """Returns free amount of memory in bytes""" + output = TestRun.executor.run_expect_success("free -b") + output = output.stdout.splitlines() + for line in output: + if 'free' in line: + index = line.split().index('free') + 1 # 1st row has 1 element less than following rows + if 'Mem' in line: + mem_line = line.split() + + return Size(int(mem_line[index])) + + +def get_mem_available(): + """Returns amount of available memory from /proc/meminfo""" + cmd = "cat /proc/meminfo | grep MemAvailable | awk '{ print $2 }'" + mem_available = TestRun.executor.run(cmd).stdout + + return Size(int(mem_available), Unit.KibiByte) + + +def get_module_mem_footprint(module_name): + """Returns allocated size of specific module's metadata from /proc/vmallocinfo""" + cmd = f"cat /proc/vmallocinfo | grep {module_name} | awk '{{ print $2 }}' " + output_lines = TestRun.executor.run(cmd).stdout.splitlines() + memory_used = 0 + for line in output_lines: + memory_used += int(line) + + return Size(memory_used) + + +def allocate_memory(size: Size): + """Allocates given amount of memory""" + mount_ramfs() + TestRun.LOGGER.info(f"Allocating {size.get_value(Unit.MiB):0.2f} MiB of memory.") + bs = Size(1, Unit.Blocks512) + dd = ( + Dd() + .block_size(bs) + .count(math.ceil(size / bs)) + .input("/dev/zero") + .output(f"{MEMORY_MOUNT_POINT}/data") + ) + output = dd.run() + if output.exit_code != 0: + raise CmdException("Allocating memory failed.", output) + + +def get_number_of_processors_from_cpuinfo(): + """Returns number of processors (count) which are listed out in /proc/cpuinfo""" + cmd = f"cat /proc/cpuinfo | grep processor | wc -l" + output = TestRun.executor.run(cmd).stdout + + return int(output) + + +def get_number_of_processes(process_name): + cmd = f"ps aux | grep {process_name} | grep -v grep | wc -l" + output = TestRun.executor.run(cmd).stdout + + return int(output) + + +def mount_ramfs(): + """Mounts ramfs to enable allocating memory space""" + if not check_if_directory_exists(MEMORY_MOUNT_POINT): + create_directory(MEMORY_MOUNT_POINT) + if not is_mounted(MEMORY_MOUNT_POINT): + TestRun.executor.run_expect_success(f"mount -t ramfs ramfs {MEMORY_MOUNT_POINT}") + + +def unmount_ramfs(): + """Unmounts ramfs and releases whole space allocated by it in memory""" + TestRun.executor.run_expect_success(f"umount {MEMORY_MOUNT_POINT}") + + +def download_file(url, destination_dir="/tmp"): + # TODO use wget module instead + command = ("wget --tries=3 --timeout=5 --continue --quiet " + f"--directory-prefix={destination_dir} {url}") + TestRun.executor.run_expect_success(command) + path = f"{destination_dir.rstrip('/')}/{File.get_name(url)}" + return File(path) + + +def get_kernel_version(): + version_string = TestRun.executor.run_expect_success("uname -r").stdout + version_string = version_string.split('-')[0] + return version.Version(version_string) + + +class ModuleRemoveMethod(Enum): + rmmod = "rmmod" + modprobe = "modprobe -r" + + +def is_kernel_module_loaded(module_name): + output = TestRun.executor.run(f"lsmod | grep ^{module_name}") + return output.exit_code == 0 + + +def get_sys_block_path(): + sys_block = "/sys/class/block" + if not check_if_directory_exists(sys_block): + sys_block = "/sys/block" + return sys_block + + +def load_kernel_module(module_name, module_args: {str, str}=None): + cmd = f"modprobe {module_name}" + if module_args is not None: + for key, value in module_args.items(): + cmd += f" {key}={value}" + return TestRun.executor.run(cmd) + + +def unload_kernel_module(module_name, unload_method: ModuleRemoveMethod = ModuleRemoveMethod.rmmod): + cmd = f"{unload_method.value} {module_name}" + return TestRun.executor.run_expect_success(cmd) + + +def get_kernel_module_parameter(module_name, parameter): + param_file_path = f"/sys/module/{module_name}/parameters/{parameter}" + if not check_if_file_exists(param_file_path): + raise FileNotFoundError(f"File {param_file_path} does not exist!") + return File(param_file_path).read() + + +def is_mounted(path: str): + if path is None or path.isspace(): + raise Exception("Checked path cannot be empty") + command = f"mount | grep --fixed-strings '{path.rstrip('/')} '" + return TestRun.executor.run(command).exit_code == 0 + + +def mount_debugfs(): + if not is_mounted(DEBUGFS_MOUNT_POINT): + TestRun.executor.run_expect_success(f"mount -t debugfs none {DEBUGFS_MOUNT_POINT}") + + +def reload_kernel_module(module_name, module_args: {str, str}=None, + unload_method: ModuleRemoveMethod = ModuleRemoveMethod.rmmod): + if is_kernel_module_loaded(module_name): + unload_kernel_module(module_name, unload_method) + + Retry.run_while_false( + lambda: load_kernel_module(module_name, module_args).exit_code == 0, + timeout=timedelta(seconds=5) + ) + + +def get_module_path(module_name): + cmd = f"modinfo {module_name}" + + # module path is in second column of first line of `modinfo` output + module_info = TestRun.executor.run_expect_success(cmd).stdout + module_path = module_info.splitlines()[0].split()[1] + + return module_path + + +def get_executable_path(exec_name): + cmd = f"which {exec_name}" + + path = TestRun.executor.run_expect_success(cmd).stdout + + return path + + +def get_udev_service_path(unit_name): + cmd = f"systemctl cat {unit_name}" + + # path is in second column of first line of output + info = TestRun.executor.run_expect_success(cmd).stdout + path = info.splitlines()[0].split()[1] + + return path + + +def kill_all_io(): + # TERM signal should be used in preference to the KILL signal, since a + # process may install a handler for the TERM signal in order to perform + # clean-up steps before terminating in an orderly fashion. + TestRun.executor.run("killall -q --signal TERM dd fio blktrace") + time.sleep(3) + TestRun.executor.run("killall -q --signal KILL dd fio blktrace") + TestRun.executor.run("kill -9 `ps aux | grep -i vdbench.* | awk '{ print $2 }'`") + + if TestRun.executor.run("pgrep -x dd").exit_code == 0: + raise Exception(f"Failed to stop dd!") + if TestRun.executor.run("pgrep -x fio").exit_code == 0: + raise Exception(f"Failed to stop fio!") + if TestRun.executor.run("pgrep -x blktrace").exit_code == 0: + raise Exception(f"Failed to stop blktrace!") + if TestRun.executor.run("pgrep vdbench").exit_code == 0: + raise Exception(f"Failed to stop vdbench!") + + +def wait(predicate, timeout: timedelta, interval: timedelta = None): + start_time = datetime.now() + result = False + while start_time + timeout > datetime.now(): + result = predicate() + if result: + break + if interval is not None: + time.sleep(interval.total_seconds()) + return result + + +def sync(): + TestRun.executor.run_expect_success("sync") + + +def get_dut_cpu_number(): + return int(TestRun.executor.run_expect_success("nproc").stdout) + + +def get_dut_cpu_physical_cores(): + """ Get list of CPU numbers that don't share physical cores """ + output = TestRun.executor.run_expect_success("lscpu --all --parse").stdout + + core_list = [] + visited_phys_cores = [] + for line in output.split("\n"): + if "#" in line: + continue + + cpu_no, phys_core_no = line.split(",")[:2] + if phys_core_no not in visited_phys_cores: + core_list.append(cpu_no) + visited_phys_cores.append(phys_core_no) + + return core_list + + +def set_wbt_lat(device: Device, value: int): + if value < 0: + raise ValueError("Write back latency can't be negative number") + + wbt_lat_config_path = posixpath.join( + get_sysfs_path(device.get_device_id()), "queue/wbt_lat_usec" + ) + + return TestRun.executor.run_expect_success(f"echo {value} > {wbt_lat_config_path}") + + +def get_wbt_lat(device: Device): + wbt_lat_config_path = posixpath.join( + get_sysfs_path(device.get_device_id()), "queue/wbt_lat_usec" + ) + + return int(TestRun.executor.run_expect_success(f"cat {wbt_lat_config_path}").stdout) + + +def get_cores_ids_range(numa_node: int): + output = TestRun.executor.run_expect_success(f"lscpu --all --parse").stdout + parse_output = re.findall(r'(\d+),(\d+),(?:\d+),(\d+),,', output, re.I) + + return [element[0] for element in parse_output if int(element[2]) == numa_node] diff --git a/test/functional/test-framework/test_utils/output.py b/test/functional/test-framework/test_utils/output.py new file mode 100644 index 0000000..73e8f94 --- /dev/null +++ b/test/functional/test-framework/test_utils/output.py @@ -0,0 +1,22 @@ +# +# Copyright(c) 2019-2021 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# + + +class Output: + def __init__(self, output_out, output_err, return_code): + self.stdout = output_out.decode('utf-8', errors="ignore").rstrip() if \ + type(output_out) == bytes else output_out + self.stderr = output_err.decode('utf-8', errors="ignore").rstrip() if \ + type(output_err) == bytes else output_err + self.exit_code = return_code + + def __str__(self): + return f"exit_code: {self.exit_code}\nstdout: {self.stdout}\nstderr: {self.stderr}" + + +class CmdException(Exception): + def __init__(self, message: str, output: Output): + super().__init__(f"{message}\n{str(output)}") + self.output = output diff --git a/test/functional/test-framework/test_utils/retry.py b/test/functional/test-framework/test_utils/retry.py new file mode 100644 index 0000000..10ca573 --- /dev/null +++ b/test/functional/test-framework/test_utils/retry.py @@ -0,0 +1,57 @@ +# +# Copyright(c) 2021 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# + +from datetime import datetime, timedelta +from functools import partial + +from core.test_run import TestRun + + +class Retry: + """ + The Retry class implements methods designed to retry execution until desired result. + The func parameter is meant to be a method. If this method needs args/kwargs, they should be + encapsulated with the method, i.e. using a partial function (an example of this is contained + within run_command_until_success()) + """ + @classmethod + def run_command_until_success( + cls, command: str, retries: int = None, timeout: timedelta = None + ): + # encapsulate method and args/kwargs as a partial function + func = partial(TestRun.executor.run_expect_success, command) + return cls.run_while_exception(func, retries=retries, timeout=timeout) + + @classmethod + def run_while_exception(cls, func, retries: int = None, timeout: timedelta = None): + result = None + + def wrapped_func(): + nonlocal result + try: + result = func() + return True + except: + return False + + cls.run_while_false(wrapped_func, retries=retries, timeout=timeout) + return result + + @classmethod + def run_while_false(cls, func, retries: int = None, timeout: timedelta = None): + if retries is None and timeout is None: + raise AttributeError("At least one stop condition is required for Retry calls!") + start = datetime.now() + retry_calls = 0 + result = func() + + while not result: + result = func() + retry_calls += 1 + if result \ + or (timeout is not None and datetime.now() - start > timeout) \ + or (retries is not None and retry_calls == retries): + break + return result diff --git a/test/functional/test-framework/test_utils/scsi_debug.py b/test/functional/test-framework/test_utils/scsi_debug.py new file mode 100644 index 0000000..d5fea09 --- /dev/null +++ b/test/functional/test-framework/test_utils/scsi_debug.py @@ -0,0 +1,77 @@ +# +# Copyright(c) 2022 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# + +import re + +from core.test_run import TestRun + +syslog_path = "/var/log/messages" + + +class Logs: + last_read_line = 1 + FLUSH = re.compile(r"scsi_debug:[\s\S]*cmd 35") + FUA = re.compile(r"scsi_debug:[\s\S]*cmd 2a 08") + + @staticmethod + def check_syslog_for_signals(): + Logs.check_syslog_for_flush() + Logs.check_syslog_for_fua() + + @staticmethod + def check_syslog_for_flush(): + """Check syslog for FLUSH logs""" + log_lines = Logs._read_syslog(Logs.last_read_line) + flush_logs_counter = Logs._count_logs(log_lines, Logs.FLUSH) + log_type = "FLUSH" + Logs._validate_logs_amount(flush_logs_counter, log_type) + + @staticmethod + def check_syslog_for_fua(): + """Check syslog for FUA logs""" + log_lines = Logs._read_syslog(Logs.last_read_line) + fua_logs_counter = Logs._count_logs(log_lines, Logs.FUA) + log_type = "FUA" + Logs._validate_logs_amount(fua_logs_counter, log_type) + + @staticmethod + def _read_syslog(last_read_line: int): + """Read recent lines in syslog, mark last line and return read lines as list.""" + log_lines = TestRun.executor.run_expect_success( + f"tail -qn +{last_read_line} {syslog_path}" + ).stdout.splitlines() + # mark last read line to continue next reading from here + Logs.last_read_line += len(log_lines) + + return log_lines + + @staticmethod + def _count_logs(log_lines: list, expected_log): + """Count specified log in list and return its amount.""" + logs_counter = 0 + + for line in log_lines: + is_log_in_line = expected_log.search(line) + if is_log_in_line is not None: + logs_counter += 1 + + return logs_counter + + @staticmethod + def _validate_logs_amount(logs_counter: int, log_type: str): + """Validate amount of logs and return""" + if logs_counter == 0: + if Logs._is_flush(log_type): + TestRun.LOGGER.error(f"{log_type} log not occured") + else: + TestRun.LOGGER.warning(f"{log_type} log not occured") + elif logs_counter == 1: + TestRun.LOGGER.warning(f"{log_type} log occured only once.") + else: + TestRun.LOGGER.info(f"{log_type} log occured {logs_counter} times.") + + @staticmethod + def _is_flush(log_type: str): + return log_type == "FLUSH" diff --git a/test/functional/test-framework/test_utils/singleton.py b/test/functional/test-framework/test_utils/singleton.py new file mode 100644 index 0000000..a484929 --- /dev/null +++ b/test/functional/test-framework/test_utils/singleton.py @@ -0,0 +1,16 @@ +# +# Copyright(c) 2019-2021 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# + + +class Singleton(type): + """ + Singleton class + """ + _instances = {} + + def __call__(cls, *args, **kwargs): + if cls not in cls._instances: + cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs) + return cls._instances[cls] diff --git a/test/functional/test-framework/test_utils/size.py b/test/functional/test-framework/test_utils/size.py new file mode 100644 index 0000000..8efcedf --- /dev/null +++ b/test/functional/test-framework/test_utils/size.py @@ -0,0 +1,211 @@ +# +# Copyright(c) 2019-2021 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# + +import enum +import math + +from multimethod import multimethod + + +def parse_unit(str_unit: str): + for u in Unit: + if str_unit == u.name: + return u + + if str_unit == "KiB": + return Unit.KibiByte + elif str_unit in ["4KiB blocks", "4KiB Blocks"]: + return Unit.Blocks4096 + elif str_unit == "MiB": + return Unit.MebiByte + elif str_unit == "GiB": + return Unit.GibiByte + elif str_unit == "TiB": + return Unit.TebiByte + + if str_unit == "B": + return Unit.Byte + elif str_unit == "KB": + return Unit.KiloByte + elif str_unit == "MB": + return Unit.MegaByte + elif str_unit == "GB": + return Unit.GigaByte + elif str_unit == "TB": + return Unit.TeraByte + + raise ValueError(f"Unable to parse {str_unit}") + + +class Unit(enum.Enum): + Byte = 1 + KiloByte = 1000 + KibiByte = 1024 + MegaByte = 1000 * KiloByte + MebiByte = 1024 * KibiByte + GigaByte = 1000 * MegaByte + GibiByte = 1024 * MebiByte + TeraByte = 1000 * GigaByte + TebiByte = 1024 * GibiByte + Blocks512 = 512 + Blocks4096 = 4096 + + KiB = KibiByte + KB = KiloByte + MiB = MebiByte + MB = MegaByte + GiB = GibiByte + GB = GigaByte + TiB = TebiByte + TB = TeraByte + + def get_value(self): + return self.value + + def __str__(self): + return self.get_name() + + def get_name(self): + return self.name + + def get_short_name(self): + if self == Unit.Byte: + return "B" + elif self == Unit.KibiByte: + return "KiB" + elif self == Unit.KiloByte: + return "KB" + elif self == Unit.MebiByte: + return "MiB" + elif self == Unit.MegaByte: + return "MB" + elif self == Unit.GibiByte: + return "GiB" + elif self == Unit.GigaByte: + return "GB" + elif self == Unit.TebiByte: + return "TiB" + elif self == Unit.TeraByte: + return "TB" + raise ValueError(f"Unable to get short unit name for {self}.") + + +class UnitPerSecond: + def __init__(self, unit): + self.value = unit.get_value() + self.name = unit.name + "/s" + + def get_value(self): + return self.value + + +class Size: + def __init__(self, value: float, unit: Unit = Unit.Byte): + if value < 0: + raise ValueError("Size has to be positive.") + self.value = value * unit.value + self.unit = unit + + def __str__(self): + return f"{self.get_value(self.unit)} {self.unit}" + + def __hash__(self): + return self.value.__hash__() + + def __int__(self): + return int(self.get_value()) + + def __add__(self, other): + return Size(self.get_value() + other.get_value()) + + def __lt__(self, other): + return self.get_value() < other.get_value() + + def __le__(self, other): + return self.get_value() <= other.get_value() + + def __eq__(self, other): + return self.get_value() == other.get_value() + + def __ne__(self, other): + return self.get_value() != other.get_value() + + def __gt__(self, other): + return self.get_value() > other.get_value() + + def __ge__(self, other): + return self.get_value() >= other.get_value() + + def __radd__(self, other): + return Size(other + self.get_value()) + + def __sub__(self, other): + if self < other: + raise ValueError("Subtracted value is too big. Result size cannot be negative.") + return Size(self.get_value() - other.get_value()) + + @multimethod + def __mul__(self, other: int): + return Size(math.ceil(self.get_value() * other)) + + @multimethod + def __rmul__(self, other: int): + return Size(math.ceil(self.get_value() * other)) + + @multimethod + def __mul__(self, other: float): + return Size(math.ceil(self.get_value() * other)) + + @multimethod + def __rmul__(self, other: float): + return Size(math.ceil(self.get_value() * other)) + + @multimethod + def __truediv__(self, other): + if other.get_value() == 0: + raise ValueError("Divisor must not be equal to 0.") + return self.get_value() / other.get_value() + + @multimethod + def __truediv__(self, other: int): + if other == 0: + raise ValueError("Divisor must not be equal to 0.") + return Size(math.ceil(self.get_value() / other)) + + def set_unit(self, new_unit: Unit): + new_size = Size(self.get_value(target_unit=new_unit), unit=new_unit) + + if new_size != self: + raise ValueError(f"{new_unit} is not precise enough for {self}") + + self.value = new_size.value + self.unit = new_size.unit + + return self + + def get_value(self, target_unit: Unit = Unit.Byte): + return self.value / target_unit.value + + def is_zero(self): + if self.value == 0: + return True + else: + return False + + def align_up(self, alignment): + if self == self.align_down(alignment): + return Size(int(self)) + return Size(int(self.align_down(alignment)) + alignment) + + def align_down(self, alignment): + if alignment <= 0: + raise ValueError("Alignment must be a positive value!") + if alignment & (alignment - 1): + raise ValueError("Alignment must be a power of two!") + return Size(int(self) & ~(alignment - 1)) + + @staticmethod + def zero(): + return Size(0) diff --git a/test/functional/test-framework/test_utils/systemd.py b/test/functional/test-framework/test_utils/systemd.py new file mode 100644 index 0000000..dc4bcfd --- /dev/null +++ b/test/functional/test-framework/test_utils/systemd.py @@ -0,0 +1,25 @@ +# +# Copyright(c) 2019-2022 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# + +from pathlib import Path + +from core.test_run import TestRun + +systemd_service_directory = Path("/usr/lib/systemd/system/") + +def enable_service(name): + TestRun.executor.run_expect_success(f"systemctl enable {name}") + + +def disable_service(name): + TestRun.executor.run_expect_success(f"systemctl disable {name}") + + +def reload_daemon(): + TestRun.executor.run_expect_success("systemctl daemon-reload") + + +def restart_service(name): + TestRun.executor.run_expect_success(f"systemctl restart {name}") diff --git a/test/functional/test-framework/test_utils/time.py b/test/functional/test-framework/test_utils/time.py new file mode 100644 index 0000000..af7268f --- /dev/null +++ b/test/functional/test-framework/test_utils/time.py @@ -0,0 +1,14 @@ +# +# Copyright(c) 2019-2021 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# + +from attotime import attotimedelta + + +class Time(attotimedelta): + def total_microseconds(self): + return self.total_nanoseconds() / 1_000 + + def total_milliseconds(self): + return self.total_nanoseconds() / 1_000_000