|
1 | 1 | import pytest |
2 | 2 |
|
| 3 | +import json |
3 | 4 | import logging |
| 5 | +import shlex |
4 | 6 | import time |
5 | 7 |
|
6 | 8 | 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 |
8 | 10 | from lib.host import Host |
9 | 11 | from lib.pool import Pool |
10 | 12 | from lib.sr import SR |
11 | 13 | from lib.vdi import VDI |
12 | 14 | from lib.vm import VM |
13 | 15 | from tests.storage import vdi_is_open |
14 | 16 |
|
15 | | -from .conftest import LINSTOR_PACKAGE |
| 17 | +from .conftest import GROUP_NAME, LINSTOR_PACKAGE |
| 18 | + |
| 19 | +from typing import Tuple |
16 | 20 |
|
17 | 21 | # Requirements: |
18 | 22 | # - two or more XCP-ng hosts >= 8.2 with additional unused disk(s) for the SR |
19 | 23 | # - access to XCP-ng RPM repository from the host |
20 | 24 |
|
| 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 | + |
21 | 75 | @pytest.mark.usefixtures("linstor_sr") |
22 | 76 | class TestLinstorSR: |
23 | 77 | @pytest.mark.quicktest |
@@ -54,6 +108,87 @@ def test_snapshot(self, vm_on_linstor_sr: VM) -> None: |
54 | 108 | finally: |
55 | 109 | vm.shutdown(verify=True) |
56 | 110 |
|
| 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 | + |
57 | 192 | # *** tests with reboots (longer tests). |
58 | 193 |
|
59 | 194 | @pytest.mark.reboot |
|
0 commit comments