Skip to content

Commit 42217f2

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 30b9316 commit 42217f2

3 files changed

Lines changed: 142 additions & 0 deletions

File tree

tests/storage/linstor/unhealthy/__init__.py

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

0 commit comments

Comments
 (0)