Skip to content

Commit d6deac9

Browse files
authored
Merge pull request #360 from xcp-ng/mlr/629-xostor-drbdadm-verify
Add tests for corruption recovery on LINSTOR SR
2 parents c17c21a + 7b32adb commit d6deac9

2 files changed

Lines changed: 160 additions & 6 deletions

File tree

tests/storage/linstor/conftest.py

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,15 @@
33
import pytest
44

55
import functools
6+
import json
67
import logging
78
import os
9+
from contextlib import contextmanager
810
from dataclasses import dataclass
911

1012
import lib.commands as commands
1113
from lib import config
14+
from lib.common import safe_split
1215

1316
try:
1417
from data import LINSTOR_REDUNDANCY # type: ignore
@@ -186,9 +189,25 @@ def vdi_on_linstor_sr(linstor_sr: SR) -> Generator[VDI, None, None]:
186189
yield vdi
187190
vdi.destroy()
188191

192+
@contextmanager
193+
def _vm_on_linstor_sr(host: Host, linstor_sr: SR, vm_ref: str) -> Generator[VM]:
194+
"""
195+
Context manager to provide the fixture lifecycle on a VM on a Linstor SR
196+
with different scopes without repeating the code.
197+
"""
198+
vm = host.import_vm(vm_ref, sr_uuid=linstor_sr.uuid)
199+
try:
200+
yield vm
201+
finally:
202+
logging.info("<< Destroy VM")
203+
vm.destroy(verify=True)
204+
189205
@pytest.fixture(scope='module')
190206
def vm_on_linstor_sr(host: Host, linstor_sr: SR, vm_ref: str) -> Generator[VM, None, None]:
191-
vm = host.import_vm(vm_ref, sr_uuid=linstor_sr.uuid)
192-
yield vm
193-
logging.info("<< Destroy VM")
194-
vm.destroy(verify=True)
207+
with _vm_on_linstor_sr(host, linstor_sr, vm_ref) as vm:
208+
yield vm
209+
210+
@pytest.fixture(scope='function')
211+
def vm_on_linstor_sr_function(host: Host, linstor_sr: SR, vm_ref: str) -> Generator[VM]:
212+
with _vm_on_linstor_sr(host, linstor_sr, vm_ref) as vm:
213+
yield vm

tests/storage/linstor/test_linstor_sr.py

Lines changed: 137 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,77 @@
11
import pytest
22

3+
import json
34
import logging
5+
import shlex
46
import time
57

68
from lib.commands import SSHCommandFailed
7-
from lib.common import vm_image, wait_for
9+
from lib.common import safe_split, vm_image, wait_for
810
from lib.host import Host
911
from lib.pool import Pool
1012
from lib.sr import SR
1113
from lib.vdi import VDI
1214
from lib.vm import VM
1315
from tests.storage import vdi_is_open
1416

15-
from .conftest import LINSTOR_PACKAGE
17+
from .conftest import GROUP_NAME, LINSTOR_PACKAGE
18+
19+
from typing import Tuple
1620

1721
# Requirements:
1822
# - two or more XCP-ng hosts >= 8.2 with additional unused disk(s) for the SR
1923
# - access to XCP-ng RPM repository from the host
2024

25+
26+
def get_drbd_status(host: Host, resource: str):
27+
logging.debug("[%s] Fetching DRBD status for resource `%s`...", host, resource)
28+
return json.loads(host.ssh(shlex.join(["drbdsetup", "status", resource, "--json"])))
29+
30+
def get_corrupted_resources(host: Host, resource: str):
31+
return [
32+
(
33+
res.get("name", ""),
34+
conn.get("name", ""),
35+
peer.get("out-of-sync", 0),
36+
)
37+
for res in get_drbd_status(host, resource)
38+
for conn in res.get("connections", [])
39+
for peer in conn.get("peer_devices", [])
40+
if peer.get("out-of-sync", 0) > 0
41+
]
42+
43+
def wait_drbd_sync(host: Host, resource: str):
44+
logging.info("[%s] Waiting for DRBD sync on resource `%s`...", host, resource)
45+
host.ssh(shlex.join(["drbdadm", "wait-sync", resource]))
46+
47+
48+
def get_vdi_volume_name_from_linstor(master: Host, vdi_uuid: str) -> str:
49+
result = master.ssh(shlex.join([
50+
"linstor-kv-tool",
51+
"--dump-volumes",
52+
"-g",
53+
f"xcp-sr-{GROUP_NAME}_thin_device"
54+
]))
55+
volumes = json.loads(result)
56+
for k, v in volumes.items():
57+
path = safe_split(k, "/")
58+
if len(path) < 4:
59+
continue
60+
uuid = path[2]
61+
data_type = path[3]
62+
if uuid == vdi_uuid and data_type == "volume-name":
63+
return v
64+
raise FileNotFoundError(f"Could not find matching linstor volume for `{vdi_uuid}`")
65+
66+
67+
def get_vdi_host(pool: Pool, vdi_uuid: str, path: str) -> Host:
68+
for h in pool.hosts:
69+
result = h.ssh(shlex.join(["test", "-e", path]), simple_output=False, check=False)
70+
if result.returncode == 0:
71+
return h
72+
raise FileNotFoundError(f"Could not find matching host for `{vdi_uuid}`")
73+
74+
2175
@pytest.mark.usefixtures("linstor_sr")
2276
class TestLinstorSR:
2377
@pytest.mark.quicktest
@@ -54,6 +108,87 @@ def test_snapshot(self, vm_on_linstor_sr: VM) -> None:
54108
finally:
55109
vm.shutdown(verify=True)
56110

111+
@pytest.fixture(scope='function')
112+
def host_and_vm_with_corrupted_vdi_on_linstor_sr(self, host: Host, linstor_sr: SR, vm_on_linstor_sr_function: VM):
113+
vm: VM = vm_on_linstor_sr_function
114+
pool: Pool = host.pool
115+
master: Host = pool.master
116+
117+
vdi_uuid: str = next((
118+
vdi.uuid for vdi in vm.vdis if vdi.sr.uuid == linstor_sr.uuid
119+
))
120+
121+
volume_name = get_vdi_volume_name_from_linstor(master, vdi_uuid)
122+
lv_path = f"/dev/{GROUP_NAME}/{volume_name}_00000"
123+
vdi_host = get_vdi_host(pool, vdi_uuid, lv_path)
124+
logging.info("[%s]: corrupting `%s`", host, lv_path)
125+
vdi_host.ssh(shlex.join([
126+
"dd",
127+
"if=/dev/urandom",
128+
f"of={lv_path}",
129+
"bs=4096",
130+
# Lower values seem to go undetected sometimes
131+
"count=10000" # ~40MB
132+
]))
133+
yield vdi_host, vm, volume_name
134+
135+
@pytest.mark.small_vm
136+
def test_resynchronization(
137+
self, host_and_vm_with_corrupted_vdi_on_linstor_sr: Tuple[Host, VM, str]
138+
):
139+
(host, vm, resource_name) = host_and_vm_with_corrupted_vdi_on_linstor_sr
140+
hostname = host.hostname()
141+
142+
try:
143+
other_host = next(
144+
next(h for h in host.pool.hosts if h.hostname() == conn.get("name", ""))
145+
for res in get_drbd_status(host, resource_name)
146+
for conn in res.get("connections", [])
147+
for peer in conn.get("peer_devices", [])
148+
if peer.get("peer-disk-state", "") == "UpToDate"
149+
)
150+
logging.info("Elected `%s` as peer for verification and repair", other_host)
151+
except StopIteration:
152+
pytest.fail("Could not find an UpToDate peer host")
153+
154+
corrupted = None
155+
max_attempts = 3
156+
# Attempting several times since testing revealed `drbdadm verify` can be flaky
157+
for attempt in range(1, max_attempts + 1):
158+
logging.info("`drbdadm verify` attempt %d/%d", attempt, max_attempts)
159+
logging.info("[%s] Running DRBD verify for %s...", other_host, resource_name)
160+
other_host.ssh(shlex.join(["drbdadm", "verify", f"{resource_name}:{hostname}/0"]))
161+
wait_drbd_sync(other_host, resource_name)
162+
163+
corrupted_resources = get_corrupted_resources(other_host, resource_name)
164+
if not corrupted_resources:
165+
logging.warning("No corrupted resources found on attempt #%d", attempt)
166+
continue
167+
for res_name, peer_name, out_of_sync in corrupted_resources:
168+
if res_name == resource_name and peer_name == hostname:
169+
corrupted = (res_name, peer_name, out_of_sync)
170+
if corrupted:
171+
break
172+
if not corrupted:
173+
pytest.fail(f"Failed to identify corrupted resource after {max_attempts} attempts")
174+
175+
logging.info("Invalidating remote resource `%s`...", resource_name)
176+
other_host.ssh(shlex.join([
177+
"drbdadm", "invalidate-remote",
178+
f"{resource_name}:{hostname}/0",
179+
"--reset-bitmap=no"
180+
]))
181+
wait_drbd_sync(other_host, resource_name)
182+
if get_corrupted_resources(other_host, resource_name):
183+
pytest.fail("Corrupted resource did not get fixed")
184+
185+
vm.start(on=host.uuid)
186+
try:
187+
vm.wait_for_os_booted()
188+
vm.test_snapshot_on_running_vm()
189+
finally:
190+
vm.shutdown(verify=True)
191+
57192
# *** tests with reboots (longer tests).
58193

59194
@pytest.mark.reboot

0 commit comments

Comments
 (0)