Skip to content

Commit 904bb05

Browse files
committed
linstor: test VM startup on disk failure
This adds a test to the LINSTOR SR test suite to make sure that a VM with a VDI on a shared LINSTOR SR can still start and shut down properly when a physical disk of that SR has failed. The test does the following: - Fails a physical disk of the LINSTOR SR pool on a random host. - Ensures a VM can still start up and shut down on all hosts. This uses a device mapper to avoid relying on the capabilities of the underlying block device. Signed-off-by: Alexandre Sollier <alexandre.sollier@vates.tech>
1 parent 46b1c18 commit 904bb05

3 files changed

Lines changed: 155 additions & 0 deletions

File tree

tests/storage/linstor/unhealthy/__init__.py

Whitespace-only changes.
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
from __future__ import annotations
2+
3+
import pytest
4+
5+
import json
6+
import logging
7+
8+
# explicit import for package-scope fixtures
9+
from tests.storage.linstor.pkgfixtures import (
10+
_linstor_config,
11+
linstor_redundancy,
12+
linstor_sr,
13+
lvm_disks,
14+
pool_with_linstor,
15+
storage_pool_name,
16+
)
17+
18+
from typing import TYPE_CHECKING, Generator
19+
20+
if TYPE_CHECKING:
21+
from lib.host import Host
22+
from lib.pool import Pool
23+
24+
DM_FLAKEY_DEV_NAME = 'linfail'
25+
26+
class FlakeyDisk:
27+
def __init__(
28+
self,
29+
host: Host,
30+
device: Host.BlockDeviceInfo,
31+
dm_dev_name: str,
32+
) -> None:
33+
self._host = host
34+
self._device = device
35+
self._dm_dev_name = dm_dev_name
36+
37+
@property
38+
def path(self) -> str:
39+
return f'/dev/mapper/{self._dm_dev_name}'
40+
41+
def create(self) -> None:
42+
self._host.ssh(f'dmsetup create {self._dm_dev_name} --table "{self._build_dm_table(False)}"')
43+
44+
def remove(self) -> None:
45+
self._host.ssh(f'dmsetup remove {self._dm_dev_name}')
46+
47+
def fail(self) -> None:
48+
logging.info(f'Failing device {self._device.path} on {self._host.hostname_or_ip}')
49+
50+
self._apply_dm_table(self._build_dm_table(True))
51+
self._host.ssh('sync')
52+
self._host.ssh('echo 3 > /proc/sys/vm/drop_caches')
53+
54+
def repair(self) -> None:
55+
logging.info(f'Repairing device {self._device.path} on {self._host.hostname_or_ip}')
56+
57+
self._apply_dm_table(self._build_dm_table(False))
58+
cmd_res = self._host.ssh('linstor -m --controllers `xe host-list params=address --minimal` r l'
59+
' -n `hostname` --props DrbdOptions/SkipDisk')
60+
failing_resources = json.loads(cmd_res)
61+
62+
# Make sure we take care of the database first so repairing the other
63+
# resources doesn't hang or fail.
64+
failing_resource_names = [res['name'] for res in failing_resources[0]]
65+
failing_resource_names.sort(key=lambda x: x != 'xcp-persistent-database')
66+
67+
for failing_resource_name in failing_resource_names:
68+
self._host.ssh('linstor --controllers `xe host-list params=address --minimal` r sp'
69+
f' `hostname` {failing_resource_name} DrbdOptions/SkipDisk')
70+
self._host.ssh(f'drbdadm wait-sync {failing_resource_name}')
71+
72+
def _build_dm_table(self, disk_failed: bool) -> str:
73+
disk_size = self._device.size // 512
74+
75+
if disk_failed:
76+
return f'0 {disk_size} flakey {self._device.path} 0 0 1'
77+
else:
78+
return f'0 {disk_size} flakey {self._device.path} 0 1 0'
79+
80+
def _apply_dm_table(self, table: str) -> None:
81+
self._host.ssh(f'dmsetup reload {self._dm_dev_name} --table "{table}"')
82+
self._host.ssh(f'dmsetup resume {self._dm_dev_name}')
83+
84+
@pytest.fixture(scope='package')
85+
def flakey_unused_512B_disk(
86+
pool_with_unused_512B_disk: Pool,
87+
unused_512B_disks: dict[Host, list[Host.BlockDeviceInfo]],
88+
) -> Generator[dict[Host, FlakeyDisk], None, None]:
89+
flakey_disks: dict[Host, FlakeyDisk] = {}
90+
hosts = pool_with_unused_512B_disk.hosts
91+
92+
for host in hosts:
93+
disk = unused_512B_disks[host][0]
94+
flakey_disk = FlakeyDisk(host, disk, DM_FLAKEY_DEV_NAME)
95+
flakey_disk.create()
96+
97+
flakey_disks[host] = flakey_disk
98+
99+
yield flakey_disks
100+
101+
for flakey_disk in flakey_disks.values():
102+
flakey_disk.remove()
103+
104+
@pytest.fixture(scope='package')
105+
def lvm_disk_paths(
106+
flakey_unused_512B_disk: dict[Host, FlakeyDisk],
107+
) -> dict[Host, list[str]]:
108+
# Overrides the `lvm_disk_paths` package-scoped fixture from the parent
109+
# package so we can transparently use other fixtures that depend on it
110+
# whilst having a dm-flakey device mapper underneath.
111+
return {host: [disk.path] for (host, disk) in flakey_unused_512B_disk.items()}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
from __future__ import annotations
2+
3+
import pytest
4+
5+
import logging
6+
import random
7+
8+
from lib.common import Defer, run_with_timeout
9+
from lib.host import Host
10+
from lib.sr import SR
11+
from lib.vm import VM
12+
13+
from .conftest import FlakeyDisk
14+
15+
# Requirements:
16+
# - three or more XCP-ng hosts >= 8.2 with additional unused disk(s) for the SR
17+
# - LINSTOR redundancy set to at least 2
18+
# - access to XCP-ng RPM repository from the host
19+
20+
class TestLinstorSRFailedDisk:
21+
@pytest.mark.small_vm # run with a small VM to test the features
22+
def test_linstor_sr_fail_disk(
23+
self,
24+
vm_on_linstor_sr: VM,
25+
flakey_unused_512B_disk: dict[Host, FlakeyDisk],
26+
linstor_sr: SR,
27+
defer: Defer,
28+
) -> None:
29+
sr = linstor_sr
30+
vm = vm_on_linstor_sr
31+
random_host = random.choice(sr.pool.hosts)
32+
33+
# Let xcp-persistent-database come in sync across the nodes.
34+
random_host.ssh('drbdadm wait-sync xcp-persistent-database')
35+
36+
flakey_unused_512B_disk[random_host].fail()
37+
defer(lambda: flakey_unused_512B_disk[random_host].repair())
38+
39+
for host in sr.pool.hosts:
40+
logging.info(f'Checking VM on host {host.hostname_or_ip}')
41+
42+
run_with_timeout(lambda: vm.start(on=host.uuid), timeout_secs=60)
43+
vm.wait_for_os_booted()
44+
vm.shutdown(verify=True)

0 commit comments

Comments
 (0)