diff --git a/test/functional/api/cas/ioctl/__init__.py b/test/functional/api/cas/ioctl/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/test/functional/api/cas/ioctl/cas_requests.py b/test/functional/api/cas/ioctl/cas_requests.py new file mode 100644 index 000000000..ffa24dbcd --- /dev/null +++ b/test/functional/api/cas/ioctl/cas_requests.py @@ -0,0 +1,68 @@ +# +# Copyright(c) 2021 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause-Clear +# + +from api.cas.ioctl.cas_structs import * +from api.cas.ioctl.ioctl import IOWR + + +class IORequest: + def __init__(self, + command_number: RequestCode, + command_direction=None): + self.command_number = command_number.value + self.command_struct = self.get_struct() + self.command = command_direction(self.command_number, self.command_struct) + + def get_struct(self): + pass + + +class StartCacheRequest(IORequest): + def __init__(self, + cache_path_name: str, + cache_id: int = 1, + init_cache: InitCache = InitCache.CACHE_INIT_NEW, + caching_mode: CacheMode = CacheMode.default, + line_size: CacheLineSize = CacheLineSize.default, + force: int = 1): + self.cache_id = ctypes.c_uint16(cache_id).value + self.init_cache = init_cache.value + self.cache_path_name = ctypes.create_string_buffer( + bytes(cache_path_name, encoding='ascii'), MAX_STR_LEN).value + self.caching_mode = caching_mode.value + self.line_size = line_size.value + self.force = ctypes.c_uint8(force).value + super().__init__(RequestCode.START_CACHE_CODE, IOWR) + + def get_struct(self): + return StartCacheStructure( + cache_id=self.cache_id, + init_cache=self.init_cache, + cache_path_name=self.cache_path_name, + caching_mode=self.caching_mode, + line_size=self.line_size, + force=self.force + ) + + def __repr__(self): + return f'{self.command_struct}' + + +class StopCacheRequest(IORequest): + def __init__(self, + cache_id: int = 1, + flush_data: int = 1): + self.cache_id = ctypes.c_uint16(cache_id).value + self.flush_data = ctypes.c_uint8(flush_data).value + super().__init__(RequestCode.STOP_CACHE_CODE, IOWR) + + def get_struct(self): + return StopCacheStructure( + cache_id=self.cache_id, + flush_data=self.flush_data + ) + + def __repr__(self): + return f'{self.command_struct}' diff --git a/test/functional/api/cas/ioctl/cas_structs.py b/test/functional/api/cas/ioctl/cas_structs.py new file mode 100644 index 000000000..b3e2b1b74 --- /dev/null +++ b/test/functional/api/cas/ioctl/cas_structs.py @@ -0,0 +1,108 @@ +# +# Copyright(c) 2021 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause-Clear +# + +import ctypes +from enum import Enum + + +class RequestCode(Enum): + START_CACHE_CODE = ctypes.c_uint(21).value + STOP_CACHE_CODE = ctypes.c_uint(2).value + SET_CACHE_STATE_CODE = ctypes.c_uint(3).value + INSERT_CORE_CODE = ctypes.c_uint(22).value + REMOVE_CORE_CODE = ctypes.c_uint(23).value + RESET_STATS_CODE = ctypes.c_uint(6).value + FLUSH_CACHE_CODE = ctypes.c_uint(9).value + INTERRUPT_FLUSHING_CODE = ctypes.c_uint(10).value + FLUSH_CORE_CODE = ctypes.c_uint(11).value + CACHE_INFO_CODE = ctypes.c_uint(24).value + CORE_INFO_CODE = ctypes.c_uint(25).value + PARTITION_INFO_CODE = ctypes.c_uint(14).value + PARTITION_SET_CODE = ctypes.c_uint(15).value + GET_CACHE_COUNT_CODE = ctypes.c_uint(16).value + LIST_CACHE_CODE = ctypes.c_uint(17).value + UPGRADE_CODE = ctypes.c_uint(19).value + GET_CORE_POOL_COUNT_CODE = ctypes.c_uint(26).value + GET_CORE_POOL_PATHS_CODE = ctypes.c_uint(27).value + CORE_POOL_REMOVE_CODE = ctypes.c_uint(28).value + CACHE_CHECK_DEVICE_CODE = ctypes.c_uint(29).value + SET_CORE_PARAM_CODE = ctypes.c_uint(30).value + GET_CORE_PARAM_CODE = ctypes.c_uint(31).value + SET_CACHE_PARAM_CODE = ctypes.c_uint(32).value + GET_CACHE_PARAM_CODE = ctypes.c_uint(33).value + GET_STATS_CODE = ctypes.c_uint(34).value + PURGE_CACHE_CODE = ctypes.c_uint(35).value + PURGE_CORE_CODE = ctypes.c_uint(36).value + + +KiB = ctypes.c_ulonglong(1024).value +MAX_STR_LEN = 4096 +MAX_ELEVATOR_NAME = 16 + + +class InitCache(Enum): + CACHE_INIT_NEW = ctypes.c_uint8(0).value + CACHE_INIT_LOAD = ctypes.c_uint8(1).value + + +class CacheMode(Enum): + ocf_cache_mode_wt = ctypes.c_int(0).value + ocf_cache_mode_wb = ctypes.c_int(1).value + ocf_cache_mode_wa = ctypes.c_int(2).value + ocf_cache_mode_pt = ctypes.c_int(3).value + ocf_cache_mode_wi = ctypes.c_int(4).value + ocf_cache_mode_wo = ctypes.c_int(5).value + default = ocf_cache_mode_wt + + +class CacheLineSize(Enum): + ocf_cache_line_size_4 = ctypes.c_ulonglong(4).value * KiB + ocf_cache_line_size_8 = ctypes.c_ulonglong(8).value * KiB + ocf_cache_line_size16 = ctypes.c_ulonglong(16).value * KiB + ocf_cache_line_size_32 = ctypes.c_ulonglong(32).value * KiB + ocf_cache_line_size_64 = ctypes.c_ulonglong(64).value * KiB + default = ocf_cache_line_size_4 + + +class StartCacheStructure(ctypes.Structure): + _fields_ = [ + ('cache_id', ctypes.c_uint16), + ('init_cache', ctypes.c_uint8), + ('cache_path_name', ctypes.c_char * MAX_STR_LEN), + ('caching_mode', ctypes.c_int), + ('flush_data', ctypes.c_uint8), + ('line_size', ctypes.c_ulonglong), + ('force', ctypes.c_uint8), + ('min_free_ram', ctypes.c_uint64), + ('metadata_mode_optimal', ctypes.c_uint8), + ('cache_elevator', ctypes.c_char * MAX_ELEVATOR_NAME), + ('ext_err_code', ctypes.c_int) + ] + + def __repr__(self): + return (f'cache_id: {self.cache_id}\n' + f'init_cache: {self.init_cache}\n' + f'cache_path_name: {self.cache_path_name}\n' + f'caching_mode: {self.caching_mode}\n' + f'flush_data: {self.flush_data}\n' + f'line_size: {self.line_size}\n' + f'force: {self.force}\n' + f'min_free_ram: {self.min_free_ram}\n' + f'metadata_mode_optimal: {self.metadata_mode_optimal}\n' + f'cache_elevator: {self.cache_elevator}\n' + f'ext_err_code: {self.ext_err_code}\n') + + +class StopCacheStructure(ctypes.Structure): + _fields_ = [ + ('cache_id', ctypes.c_uint16), + ('flush_data', ctypes.c_uint8), + ('ext_err_code', ctypes.c_int) + ] + + def __repr__(self): + return (f'cache_id: {self.cache_id}\n' + f'flush_data: {self.flush_data}\n' + f'ext_err_code: {self.ext_err_code}\n') diff --git a/test/functional/api/cas/ioctl/ioctl.py b/test/functional/api/cas/ioctl/ioctl.py new file mode 100644 index 000000000..26bd753c6 --- /dev/null +++ b/test/functional/api/cas/ioctl/ioctl.py @@ -0,0 +1,147 @@ +# +# Copyright(c) 2021 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause-Clear +# + +import ctypes +import marshal +import os +from time import sleep + +from core.test_run import TestRun +from test_tools.fs_utils import chmod_numerical, remove, check_if_directory_exists, \ + create_directory + +IOC_NRBITS = 8 +IOC_TYPEBITS = 8 +IOC_SIZEBITS = 14 +IOC_DIRBITS = 2 + +IOC_NRMASK = (1 << IOC_NRBITS) - 1 # 255 +IOC_TYPEMASK = (1 << IOC_TYPEBITS) - 1 # 255 +IOC_SIZEMASK = (1 << IOC_SIZEBITS) - 1 # 16 383 +IOC_DIRMASK = (1 << IOC_DIRBITS) - 1 # 3 + +IOC_NRSHIFT = 0 # 0 +IOC_TYPESHIFT = IOC_NRSHIFT + IOC_NRBITS # 8 +IOC_SIZESHIFT = IOC_TYPESHIFT + IOC_TYPEBITS # 16 +IOC_DIRSHIFT = IOC_SIZESHIFT + IOC_SIZEBITS # 30 + +IOC_NONE = 0 +IOC_WRITE = 1 +IOC_READ = 2 + +KCAS_IOCTL_MAGIC = 0xBA # 186 + +IOC_IN = IOC_WRITE << IOC_DIRSHIFT # 1 073 741 824 +IOC_OUT = IOC_READ << IOC_DIRSHIFT # 2 147 483 648 +IOC_INOUT = (IOC_WRITE | IOC_READ) << IOC_DIRSHIFT # 3 221 225 472 +IOCSIZE_MASK = IOC_SIZEMASK << IOC_SIZESHIFT # 1 073 676 288 +IOCSIZE_SHIFT = IOC_SIZESHIFT # 16 + + +def IOC(dir, type, nr, size): + if dir > IOC_DIRMASK: + raise OverflowError(f"IO direction value {dir} exceeds {IOC_DIRMASK}") + dir <<= IOC_DIRSHIFT + + if type > IOC_TYPEMASK: + raise OverflowError(f"IO type value {type} exceeds {IOC_TYPEMASK}") + type <<= IOC_TYPESHIFT + + if nr > IOC_NRMASK: + raise OverflowError(f"IO command value {nr} exceeds {IOC_NRMASK}") + nr <<= IOC_NRSHIFT + + if size > IOC_SIZEMASK: + raise OverflowError(f"IO size value {size} exceeds {IOC_SIZEMASK}") + size <<= IOC_SIZESHIFT + + return dir | type | nr | size + + +def IOC_TYPECHECK(item): + return ctypes.sizeof(item) + + +def IO(nr): + return IOC(IOC_NONE, KCAS_IOCTL_MAGIC, nr, 0) + + +def IOR(nr, size): + return IOC(IOC_READ, KCAS_IOCTL_MAGIC, nr, IOC_TYPECHECK(size)) + + +def IOW(nr, size): + return IOC(IOC_WRITE, KCAS_IOCTL_MAGIC, nr, IOC_TYPECHECK(size)) + + +def IOWR(nr, size): + return IOC(IOC_READ | IOC_WRITE, KCAS_IOCTL_MAGIC, nr, IOC_TYPECHECK(size)) + + +def IOR_BAD(nr, size): + return IOC(IOC_READ, KCAS_IOCTL_MAGIC, nr, ctypes.sizeof(size)) + + +def IOW_BAD(nr, size): + return IOC(IOC_WRITE, KCAS_IOCTL_MAGIC, nr, ctypes.sizeof(size)) + + +def IOWR_BAD(nr, size): + return IOC(IOC_READ | IOC_WRITE, KCAS_IOCTL_MAGIC, nr, ctypes.sizeof(size)) + + +def IOC_DIR(nr): + return (nr >> IOC_DIRSHIFT) & IOC_DIRMASK + + +def IOC_TYPE(nr): + return (nr >> IOC_TYPESHIFT) & IOC_TYPEMASK + + +def IOC_NR(nr): + return (nr >> IOC_NRSHIFT) & IOC_NRMASK + + +def IOC_SIZE(nr): + return (nr >> IOC_SIZESHIFT) & IOC_SIZEMASK + + +temp_dir = '/tmp/cas' +struct_path = os.path.join(temp_dir, 'dump_file') +script_source = os.path.join(f'{os.path.dirname(__file__)}', 'send_ioctl_script.py') +script_dest = os.path.join(temp_dir, 'send_ioctl_script.py') + + +def send_script_with_dumped_args(): + if not check_if_directory_exists(temp_dir): + create_directory(temp_dir, True) + + TestRun.executor.rsync_to(script_source, script_dest) + chmod_numerical(script_dest, 550) + + TestRun.executor.rsync_to(struct_path, struct_path) + chmod_numerical(struct_path, 440) + + +def cas_ioctl(cas_ioctl_request, interrupt: bool = False): + if not os.path.exists(temp_dir): + os.mkdir(temp_dir) + + with open(struct_path, 'wb') as dump_file: + marshal.dump(cas_ioctl_request.command_struct, dump_file) + + send_script_with_dumped_args() + if interrupt: + pid = TestRun.executor.run_in_background( + f"{script_dest} -c {cas_ioctl_request.command} -s {struct_path}" + ) + sleep(2) + TestRun.executor.kill_process(pid) + else: + TestRun.executor.run(f"{script_dest} -c {cas_ioctl_request.command} -s {struct_path}") + if check_if_directory_exists(temp_dir): + remove(temp_dir, True, True, True) + if os.path.exists(struct_path): + os.remove(struct_path) diff --git a/test/functional/api/cas/ioctl/send_ioctl_script.py b/test/functional/api/cas/ioctl/send_ioctl_script.py new file mode 100644 index 000000000..ad0f78a26 --- /dev/null +++ b/test/functional/api/cas/ioctl/send_ioctl_script.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 +# +# Copyright(c) 2021 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause-Clear +# + +import argparse +import marshal +import os +import sys +from fcntl import ioctl + + +def main(): + CAS_DEVICE = '/dev/cas_ctrl' + + parser = argparse.ArgumentParser(description=f'Send ioctl request to {CAS_DEVICE}') + + parser.add_argument('-c', '--command', action='store', dest='command', type=str, + required=True, help=f"Specific request code to send to {CAS_DEVICE}") + parser.add_argument('-s', '--struct', action='store', dest='struct', type=str, + required=True, help="Structure of CAS request") + args = parser.parse_args() + + with open(args.struct, 'rb') as struct_file: + struct = bytearray(marshal.load(struct_file)) + + fd = os.open(CAS_DEVICE, os.O_RDWR) + try: + ioctl(fd, int(args.command), struct) + except OSError as err: + print(f"IOCTL request returned error: {err}", sys.stdout) + finally: + os.close(fd) + + +if __name__ == '__main__': + main() diff --git a/test/functional/tests/ioctl/__init__.py b/test/functional/tests/ioctl/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/test/functional/tests/ioctl/common_utils.py b/test/functional/tests/ioctl/common_utils.py new file mode 100644 index 000000000..1d7e212c1 --- /dev/null +++ b/test/functional/tests/ioctl/common_utils.py @@ -0,0 +1,29 @@ +# +# Copyright(c) 2021 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause-Clear +# + +from api.cas.cli_messages import __check_string_msg +from core.test_run import TestRun + + +interrupt_stop = [ + r"Waiting for cache stop interrupted\. Stop will finish asynchronously\." +] + +interrupt_start = [ + r"Cache added successfully, but waiting interrupted\. Rollback" +] + +load_and_force = [ + r"cache\d+: Using \'force\' flag is forbidden for load operation\." +] + + +def clear_dmesg(): + TestRun.executor.run_expect_success('dmesg -C') + + +def check_dmesg(searched_phrase: str): + dmesg_out = TestRun.executor.run_expect_success("dmesg").stdout + __check_string_msg(dmesg_out, searched_phrase) diff --git a/test/functional/tests/ioctl/test_ioctl_start.py b/test/functional/tests/ioctl/test_ioctl_start.py new file mode 100644 index 000000000..c79360dc7 --- /dev/null +++ b/test/functional/tests/ioctl/test_ioctl_start.py @@ -0,0 +1,113 @@ +# +# Copyright(c) 2021 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause-Clear +# + +from time import sleep + +import pytest + +from api.cas.casadm import start_cache +from api.cas.casadm_parser import get_caches +from api.cas.ioctl.cas_requests import StartCacheRequest +from api.cas.ioctl.cas_structs import CacheMode, CacheLineSize, InitCache +from api.cas.ioctl.ioctl import cas_ioctl +from core.test_run import TestRun +from storage_devices.disk import DiskType, DiskTypeSet +from test_utils.size import Size, Unit +from tests.ioctl import common_utils + +cache_id = 3 + + +@pytest.mark.parametrizex("force", [0, 1]) +@pytest.mark.parametrizex("load", InitCache) +@pytest.mark.parametrizex("cache_mode", CacheMode) +@pytest.mark.parametrizex("cache_line_size", CacheLineSize) +@pytest.mark.require_disk("cache", DiskTypeSet([DiskType.optane, DiskType.nand])) +def test_ioctl_start(cache_mode, cache_line_size, load, force): + """ + title: Start the cache without casadm. + description: | + Test of the ability to start the cache with IOCTL request bypassing native + OpenCAS manager - casadm. + pass_criteria: + - Cache started successfully without any errors if 'force' and 'load' flags are not used. + - When 'force' and 'load' flags are used cache is not started. + """ + with TestRun.step("Prepare cache device."): + cache_dev = TestRun.disks['cache'] + cache_dev.create_partitions([Size(2, Unit.GiB)]) + cache_part = cache_dev.partitions[0] + + with TestRun.step("Create IOCTL request for cache start."): + start_config = StartCacheRequest( + cache_path_name=cache_part.path, caching_mode=cache_mode, cache_id=cache_id, + init_cache=load, force=force, line_size=cache_line_size + ) + + if load.value: + with TestRun.step("Start and stop cache before loading."): + cache = start_cache(cache_part, cache_id=cache_id, force=True) + cache.stop() + + with TestRun.step("Start cache with IOCTL request bypassing casadm."): + cas_ioctl(start_config) + + with TestRun.step( + f"Check if cache is {'running' if not (load.value and force) else 'not running'}." + ): + if not (load.value and force): + if len(get_caches()) != 1: + TestRun.fail("Cache is missing!") + else: + if len(get_caches()) != 0: + TestRun.fail("Cache should not be loaded!") + + +@pytest.mark.parametrizex("force", [0, 1]) +@pytest.mark.parametrizex("load", InitCache) +@pytest.mark.parametrizex("cache_mode", CacheMode) +@pytest.mark.parametrizex("cache_line_size", CacheLineSize) +@pytest.mark.require_disk("cache", DiskTypeSet([DiskType.optane, DiskType.nand])) +def test_ioctl_start_interrupt(cache_mode, cache_line_size, load, force): + """ + title: Interrupt cache start without casadm. + description: | + Negative test of the ability to interrupt the cache start with IOCTL request sent + outside native OpenCAS manager - casadm. + pass_criteria: + - Cache is not started. + """ + with TestRun.step("Prepare cache device."): + cache_dev = TestRun.disks['cache'] + cache_dev.create_partitions([Size(128, Unit.GiB)]) + cache_part = cache_dev.partitions[0] + + with TestRun.step("Create IOCTL request for cache start."): + start_config = StartCacheRequest( + cache_path_name=cache_part.path, caching_mode=cache_mode, cache_id=cache_id, + init_cache=load, force=force, line_size=cache_line_size + ) + + if load.value: + with TestRun.step("Start and stop cache before loading."): + cache = start_cache(cache_part, cache_id=cache_id, force=True) + cache.stop() + + with TestRun.step("Clear dmesg."): + common_utils.clear_dmesg() + + with TestRun.step("Interrupt starting cache with IOCTL request bypassing casadm."): + cas_ioctl(start_config, True) + sleep(8) # wait for rollback after interruption + + with TestRun.step(f"Check if cache is not running."): + if len(get_caches()) > 0: + TestRun.fail('Start process finished. Interruption failed.') + else: + TestRun.LOGGER.info('Start process is dead as expected.') + + with TestRun.step("Check dmesg for interruption log."): + common_utils.check_dmesg(common_utils.load_and_force if (load.value and force) + else common_utils.interrupt_start) diff --git a/test/functional/tests/ioctl/test_ioctl_stop.py b/test/functional/tests/ioctl/test_ioctl_stop.py new file mode 100644 index 000000000..29c1ad23c --- /dev/null +++ b/test/functional/tests/ioctl/test_ioctl_stop.py @@ -0,0 +1,89 @@ +# +# Copyright(c) 2021 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause-Clear +# + +from time import sleep + +import pytest + +from api.cas.casadm import start_cache +from api.cas.casadm_parser import get_caches +from api.cas.ioctl.cas_requests import StopCacheRequest +from api.cas.ioctl.ioctl import cas_ioctl +from core.test_run import TestRun +from storage_devices.disk import DiskType, DiskTypeSet +from test_utils.size import Size, Unit +from tests.ioctl import common_utils + +cache_id = 4 + + +@pytest.mark.parametrizex("flush_data", [0, 1]) +@pytest.mark.require_disk("cache", DiskTypeSet([DiskType.optane, DiskType.nand])) +def test_ioctl_stop_clean_cache(flush_data): + """ + title: Stop the cache without casadm. + description: | + Test of the ability to stop the cache with IOCTL request bypassing native + OpenCAS manager - casadm. The cache is clean and no flush will be triggered during stop. + pass_criteria: + - Cache stopped successfully without any errors. + """ + with TestRun.step("Prepare cache device."): + cache_dev = TestRun.disks['cache'] + cache_dev.create_partitions([Size(2, Unit.GiB)]) + cache_part = cache_dev.partitions[0] + + with TestRun.step("Create IOCTL request for cache stopping."): + stop_config = StopCacheRequest(cache_id=cache_id, flush_data=flush_data) + + with TestRun.step("Start cache before stop."): + start_cache(cache_part, cache_id=cache_id, force=True) + + with TestRun.step("Stop cache with IOCTL request bypassing casadm."): + cas_ioctl(stop_config) + + with TestRun.step(f"Check if cache is not running."): + if len(get_caches()) != 0: + TestRun.fail("Cache should be stopped despite the interruption!") + + +@pytest.mark.parametrizex("flush_data", [0, 1]) +@pytest.mark.require_disk("cache", DiskTypeSet([DiskType.optane, DiskType.nand])) +def test_ioctl_stop_interrupt_clean_cache(flush_data): + """ + title: Interrupt cache stop without casadm. + description: | + Negative test of the ability to interrupt the cache stop with IOCTL request sent + outside native OpenCAS manager - casadm. The cache is clean and no flush can be + interrupted during stop. + pass_criteria: + - Cache is stopped. + """ + with TestRun.step("Prepare cache device."): + cache_dev = TestRun.disks['cache'] + cache_dev.create_partitions([Size(128, Unit.GiB)]) + cache_part = cache_dev.partitions[0] + + with TestRun.step("Create IOCTL request for cache stop."): + stop_config = StopCacheRequest(cache_id=cache_id, flush_data=flush_data) + + with TestRun.step("Start cache before stopping."): + start_cache(cache_part, cache_id=cache_id, force=True) + + with TestRun.step("Clear dmesg."): + common_utils.clear_dmesg() + + with TestRun.step("Interrupt stopping cache with IOCTL request bypassing casadm."): + cas_ioctl(stop_config, True) + sleep(8) # wait for thread to finish asynchronously after interruption + + with TestRun.step(f"Check if cache is not running."): + if len(get_caches()) > 0: + TestRun.fail("Cache should be stopped despite the interruption!") + else: + TestRun.LOGGER.info('Stop process finished as expected.') + + with TestRun.step("Check dmesg for interruption log."): + common_utils.check_dmesg(common_utils.interrupt_stop)