Skip to content

Commit 73b7044

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 73b7044

3 files changed

Lines changed: 184 additions & 0 deletions

File tree

tests/storage/linstor/unhealthy/__init__.py

Whitespace-only changes.
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
from __future__ import annotations
2+
3+
import pytest
4+
5+
import json
6+
import logging
7+
8+
import lib.commands as commands
9+
10+
# explicit import for package-scope fixtures
11+
from tests.storage.linstor.pkgfixtures import (
12+
_linstor_config,
13+
linstor_redundancy,
14+
linstor_sr,
15+
lvm_disks,
16+
pool_with_linstor,
17+
storage_pool_name,
18+
)
19+
20+
from typing import TYPE_CHECKING, Generator, List
21+
22+
if TYPE_CHECKING:
23+
from lib.host import Host
24+
from lib.pool import Pool
25+
26+
DM_FLAKEY_DEV_NAME = 'linfail'
27+
28+
class FlakeyDisk:
29+
def __init__(
30+
self,
31+
host: Host,
32+
pool_hosts: List[Host],
33+
device: Host.BlockDeviceInfo,
34+
dm_dev_name: str,
35+
) -> None:
36+
self._host = host
37+
self._pool_hosts = pool_hosts
38+
self._device = device
39+
self._dm_dev_name = dm_dev_name
40+
41+
@property
42+
def path(self) -> str:
43+
return f'/dev/mapper/{self._dm_dev_name}'
44+
45+
def create(self) -> None:
46+
self._host.ssh(f'dmsetup create {self._dm_dev_name} --table "{self._build_dm_table(False)}"')
47+
48+
def remove(self) -> None:
49+
self._host.ssh(f'dmsetup remove {self._dm_dev_name}')
50+
51+
def fail(self) -> None:
52+
logging.info(f'Failing device {self._device.path} on {self._host.hostname_or_ip}')
53+
54+
self._apply_dm_table(self._build_dm_table(True))
55+
self._host.ssh('sync')
56+
self._host.ssh('echo 3 > /proc/sys/vm/drop_caches')
57+
58+
def repair(self) -> None:
59+
logging.info(f'Repairing device {self._device.path} on {self._host.hostname_or_ip}')
60+
61+
self._apply_dm_table(self._build_dm_table(False))
62+
cmd_res = self._host.ssh('linstor -m --controllers `xe host-list params=address --minimal` r l'
63+
' -n `hostname` --props DrbdOptions/SkipDisk')
64+
failing_resources = json.loads(cmd_res)
65+
66+
# Make sure we take care of the database first so repairing the other
67+
# resources doesn't hang or fail.
68+
failing_resource_names = [res['name'] for res in failing_resources[0]]
69+
failing_resource_names.sort(key=lambda x: x != 'xcp-persistent-database')
70+
71+
for failing_resource_name in failing_resource_names:
72+
try:
73+
self._host.ssh('linstor --controllers `xe host-list params=address --minimal` r sp'
74+
f' `hostname` {failing_resource_name} DrbdOptions/SkipDisk')
75+
except commands.SSHCommandFailed:
76+
drbd_res_status = json.loads(self._host.ssh(f'drbdsetup status --json {failing_resource_name}'))
77+
78+
if drbd_res_status[0]['devices'][0]['disk-state'] != 'Negotiating':
79+
raise
80+
81+
# The DRBD resource sometimes gets stuck in the "Negotiating"
82+
# state when repairing the device. This can lead to a split-brain
83+
# issue, so we try to fix it as one.
84+
logging.warning('DRBD resource stuck in Negotiating state; attempting fix')
85+
86+
self._host.ssh(f'drbdadm disconnect {failing_resource_name}')
87+
self._host.ssh(f'drbdadm connect {failing_resource_name}')
88+
89+
for other_host in filter(lambda x: x != self._host, self._pool_hosts):
90+
drbd_res_status = json.loads(other_host.ssh(f'drbdsetup status --json {failing_resource_name}'))
91+
92+
other_host.ssh(f'drbdadm disconnect {failing_resource_name}')
93+
94+
if drbd_res_status[0]['role'] == 'Primary':
95+
other_host.ssh(f'drbdadm connect {failing_resource_name}')
96+
else:
97+
other_host.ssh(f'drbdadm connect --discard-my-data {failing_resource_name}')
98+
99+
self._host.ssh(f'drbdadm wait-sync {failing_resource_name}')
100+
101+
def _build_dm_table(self, disk_failed: bool) -> str:
102+
disk_size = self._device.size // 512
103+
104+
if disk_failed:
105+
return f'0 {disk_size} flakey {self._device.path} 0 0 1'
106+
else:
107+
return f'0 {disk_size} flakey {self._device.path} 0 1 0'
108+
109+
def _apply_dm_table(self, table: str) -> None:
110+
self._host.ssh(f'dmsetup reload {self._dm_dev_name} --table "{table}"')
111+
self._host.ssh(f'dmsetup resume {self._dm_dev_name}')
112+
113+
@pytest.fixture(scope='package')
114+
def flakey_unused_512B_disk(
115+
pool_with_unused_512B_disk: Pool,
116+
unused_512B_disks: dict[Host, list[Host.BlockDeviceInfo]],
117+
) -> Generator[dict[Host, FlakeyDisk], None, None]:
118+
flakey_disks: dict[Host, FlakeyDisk] = {}
119+
hosts = pool_with_unused_512B_disk.hosts
120+
121+
for host in hosts:
122+
disk = unused_512B_disks[host][0]
123+
flakey_disk = FlakeyDisk(host, hosts, disk, DM_FLAKEY_DEV_NAME)
124+
flakey_disk.create()
125+
126+
flakey_disks[host] = flakey_disk
127+
128+
yield flakey_disks
129+
130+
for flakey_disk in flakey_disks.values():
131+
flakey_disk.remove()
132+
133+
@pytest.fixture(scope='package')
134+
def lvm_disk_paths(
135+
flakey_unused_512B_disk: dict[Host, FlakeyDisk],
136+
) -> dict[Host, list[str]]:
137+
# Overrides the `lvm_disk_paths` package-scoped fixture from the parent
138+
# package so we can transparently use other fixtures that depend on it
139+
# whilst having a dm-flakey device mapper underneath.
140+
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+
failed_host = sr.pool.hosts[0]
32+
33+
# Let xcp-persistent-database come in sync across the nodes.
34+
failed_host.ssh('drbdadm wait-sync xcp-persistent-database')
35+
36+
flakey_unused_512B_disk[failed_host].fail()
37+
defer(lambda: flakey_unused_512B_disk[failed_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)