Files
scylladb/test/storage/conftest.py
Łukasz Paszkowski 5f6df4eb97 test/storage: Properly mount/clear volumes
Due to a missing functionality in PythonTest, `unshare` is never used
to mount volumes. As a consequence:
+ volumes are created with sudo which is undesired
+ they are not cleared automatically

Even having the missing support in place, the approach with mounting
volumes with `unshare` would not work as http server, a pool of clusters,
and scylla cluster manager are started outside of the new namespace.
Thus cluster would have no access to volumes created with `unshare`.

The new approach that works with and without dbuild and does not require
sudo, uses the following three commands to mount a volume:

truncate -s 100M /tmp/mydevice.img
mkfs.ext4 /tmp/mydevice.img
fuse2fs /tmp/mydevice.img test/

Additionally, a proper cleanup is performed, i.e. servers are stopped
gracefully and and volumes are unmounted after the tests using them are
completed.

Fixes: https://github.com/scylladb/scylladb/issues/25906

Closes scylladb/scylladb#26065
2025-09-25 11:05:50 +03:00

84 lines
3.3 KiB
Python

#!/usr/bin/python3
#
# Copyright (C) 2025-present ScyllaDB
#
# SPDX-License-Identifier: LicenseRef-ScyllaDB-Source-Available-1.0
#
import pytest
import os
import pathlib
import shutil
import subprocess
import uuid
from typing import Callable
from contextlib import asynccontextmanager, contextmanager
from dataclasses import dataclass
from test.pylib.manager_client import ManagerClient
from test.cluster.conftest import *
from test.pylib.util import gather_safely
@pytest.fixture(scope="function")
def volumes_factory(pytestconfig, build_mode, request):
hash = str(uuid.uuid4())
base = pathlib.Path(f"{pytestconfig.getoption("tmpdir")}/{build_mode}/volumes/{hash}")
volumes = []
@dataclass
class VolumeInfo:
img: pathlib.Path
mount: pathlib.Path
log: pathlib.Path
@contextmanager
def wrapper(sizes: list[str]):
try:
for id, size in enumerate(sizes):
path = base / f"scylla-{id}"
path.mkdir(parents=True)
volume = VolumeInfo(path.with_name(f"{path.name}.img"), path, path.with_name(f"{path.name}.log"))
subprocess.run(["truncate", "-s", size, volume.img], check=True)
subprocess.run(["mkfs.ext4", volume.img], check=True, stdout=subprocess.DEVNULL)
# -o uid=... and -o gid=... to avoid root:root ownership of mounted files
# -o fakeroot to avoid permission denied errors on creating files inside docker
subprocess.run(["fuse2fs", "-o", f"uid={os.getuid()}", "-o", f"gid={os.getgid()}", "-o", "fakeroot", volume.img, volume.mount], check=True)
volumes.append(volume)
yield volumes
finally:
pass
yield wrapper
# Unmount volumes and optionally preserve data. Copying cannot be done in the finally
# clause of the wrapper above as at that point test is not yet marked as failed. So the
# copy and consequently volumes cleanup have to be done here.
report = request.node.stash[PHASE_REPORT_KEY]
test_failed = report.when == "call" and report.failed
preserve_data = test_failed or request.config.getoption("save_log_on_success")
for id, volume in enumerate(volumes):
if preserve_data:
shutil.copytree(volume.mount, base.parent.parent / f"scylla-{hash}-{id}", ignore=shutil.ignore_patterns('commitlog*', 'lost+found*'))
shutil.copyfile(volume.log, base.parent.parent / f"scylla-{hash}-{id}.log")
subprocess.run(["fusermount3", "-u", volume.mount], check=True)
os.unlink(volume.img)
@asynccontextmanager
async def space_limited_servers(manager: ManagerClient, volumes_factory: Callable, sizes: list[str], **server_args):
servers = []
cmdline = server_args.pop("cmdline", [])
with volumes_factory(sizes) as volumes:
try:
servers = [await manager.server_add(cmdline = [*cmdline, '--workdir', str(volume.mount)],
property_file={"dc": "dc1", "rack": f"r{id}"},
**server_args) for id, volume in enumerate(volumes)]
yield servers
finally:
# Stop servers to be able to unmount volumes
await gather_safely(*(manager.server_stop(server.server_id) for server in servers))