Skip to content

Commit c7753f9

Browse files
committed
feat(coalesce): Add tests for coalesce
Add a vdi-type parameter to parametrize the vdi_type fixture, it default to vhd at the moment, will be used to run test on other vdi type, e.g. qcow2. The tests create a VDI, connect it to Dom0 then use the tapdev to write random data somewhere in it. It then compare the data of the VDI to the original data we have to see if it has changed. The test create snapshot/clone before deleting them and waiting for the coalesce to have happened by observing sm-config:vhd-parent before checking the integrity of the data. Signed-off-by: Damien Thenot <[email protected]>
1 parent d1caf94 commit c7753f9

File tree

5 files changed

+187
-5
lines changed

5 files changed

+187
-5
lines changed

conftest.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,13 @@ def pytest_addoption(parser):
7575
"4KiB blocksize to be formatted and used in storage tests. "
7676
"Set it to 'auto' to let the fixtures auto-detect available disks."
7777
)
78+
parser.addoption(
79+
"--image-format",
80+
action="append",
81+
default=[],
82+
help="Format of VDI to execute tests on."
83+
"Example: vhd,qcow2"
84+
)
7885

7986
def pytest_configure(config):
8087
global_config.ignore_ssh_banner = config.getoption('--ignore-ssh-banner')
@@ -87,6 +94,12 @@ def pytest_generate_tests(metafunc):
8794
vms = [None] # no --vm parameter does not mean skip the test, for us, it means use the default
8895
metafunc.parametrize("vm_ref", vms, indirect=True, scope="module")
8996

97+
if "image_format" in metafunc.fixturenames:
98+
image_format = metafunc.config.getoption("image_format")
99+
if len(image_format) == 0:
100+
image_format = ["vhd"] # Not giving image-format will default to doing tests on vhd
101+
metafunc.parametrize("image_format", image_format, scope="session")
102+
90103
def pytest_collection_modifyitems(items, config):
91104
# Automatically mark tests based on fixtures they require.
92105
# Check pytest.ini or pytest --markers for marker descriptions.

lib/host.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import subprocess
77
import tempfile
88
import uuid
9+
from typing import Optional
910

1011
from packaging import version
1112
from typing import Dict, List, Literal, Optional, overload, TYPE_CHECKING, Union
@@ -658,3 +659,16 @@ def enable_hsts_header(self):
658659
def disable_hsts_header(self):
659660
self.ssh(['rm', '-f', f'{XAPI_CONF_DIR}/00-XCP-ng-tests-enable-hsts-header.conf'])
660661
self.restart_toolstack(verify=True)
662+
663+
def get_dom0_uuid(self):
664+
output = self.ssh(["grep", "-e", "\"CONTROL_DOMAIN_UUID=\"", "/etc/xensource-inventory"])
665+
return output.split("=")[1].replace("'", "")
666+
667+
def get_sr_from_vdi_uuid(self, vdi_uuid) -> Optional[SR]:
668+
sr_uuid = self.xe("vdi-param-get",
669+
{"param-name": "sr-uuid",
670+
"uuid": vdi_uuid,
671+
})
672+
if sr_uuid is None:
673+
return None
674+
return SR(sr_uuid, self.pool)

lib/vdi.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import logging
2+
from typing import Optional
23

34
from lib.common import _param_add, _param_clear, _param_get, _param_remove, _param_set, strtobool
45
from typing import Literal, Optional, overload, TYPE_CHECKING
@@ -23,11 +24,7 @@ def __init__(self, uuid, *, host=None, sr=None):
2324
# TODO: use a different approach when migration is possible
2425
if sr is None:
2526
assert host
26-
sr_uuid = host.pool.get_vdi_sr_uuid(uuid)
27-
# avoid circular import
28-
# FIXME should get it from Host instead
29-
from lib.sr import SR
30-
self.sr = SR(sr_uuid, host.pool)
27+
self.sr = host.get_sr_from_vdi_uuid(self.uuid)
3128
else:
3229
self.sr = sr
3330

@@ -48,6 +45,9 @@ def readonly(self) -> bool:
4845
def __str__(self):
4946
return f"VDI {self.uuid} on SR {self.sr.uuid}"
5047

48+
def get_parent(self) -> Optional[str]:
49+
return self.param_get("sm-config", key="vhd-parent", accept_unknown_key=True)
50+
5151
@overload
5252
def param_get(self, param_name: str, key: Optional[str] = ...,
5353
accept_unknown_key: Literal[False] = ...) -> str:

tests/storage/coalesce/conftest.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import pytest
2+
import logging
3+
4+
from lib.vdi import VDI
5+
6+
MAX_LENGTH = 1 * 1024 * 1024 * 1024 # 1GiB
7+
8+
@pytest.fixture(scope="module")
9+
def vdi_on_local_sr(host, local_sr_on_hostA1, image_format):
10+
sr_uuid = local_sr_on_hostA1.uuid
11+
vdi_uuid = host.xe("vdi-create",
12+
{"sr-uuid": sr_uuid,
13+
"name-label": "testVDI",
14+
"virtual-size": str(MAX_LENGTH),
15+
"sm-config:type": image_format,
16+
})
17+
logging.info(">> Created VDI {} of type {}".format(vdi_uuid, image_format))
18+
19+
vdi = VDI(vdi_uuid, host=host)
20+
21+
yield vdi
22+
23+
logging.info("<< Destroying VDI {}".format(vdi_uuid))
24+
host.xe("vdi-destroy", {"uuid": vdi_uuid})
25+
26+
@pytest.fixture(scope="module")
27+
def vdi_with_vbd_on_dom0(host, vdi_on_local_sr):
28+
vdi_uuid = vdi_on_local_sr.uuid
29+
dom0_uuid = host.get_dom0_uuid()
30+
logging.info(f">> Plugging VDI {vdi_uuid} on Dom0")
31+
vbd_uuid = host.xe("vbd-create",
32+
{"vdi-uuid": vdi_uuid,
33+
"vm-uuid": dom0_uuid,
34+
"device": "autodetect",
35+
})
36+
host.xe("vbd-plug", {"uuid": vbd_uuid})
37+
38+
yield vdi_on_local_sr
39+
40+
logging.info(f"<< Unplugging VDI {vdi_uuid} from Dom0")
41+
host.xe("vbd-unplug", {"uuid": vbd_uuid})
42+
host.xe("vbd-destroy", {"uuid": vbd_uuid})
43+
44+
@pytest.fixture(scope="class")
45+
def data_file_on_host(host):
46+
filename = "/root/data.bin"
47+
logging.info(f">> Creating data file {filename} on host")
48+
size = 1 * 1024 * 1024 # 1MiB
49+
assert size <= MAX_LENGTH, "Size of the data file bigger than the VDI size"
50+
51+
host.ssh(["dd", "if=/dev/urandom", f"of={filename}", f"bs={size}", "count=1"])
52+
53+
yield filename
54+
55+
logging.info("<< Deleting data file")
56+
host.ssh(["rm", filename])
57+
58+
@pytest.fixture(scope="module")
59+
def tapdev(local_sr_on_hostA1, vdi_with_vbd_on_dom0):
60+
sr_uuid = local_sr_on_hostA1.uuid
61+
vdi_uuid = vdi_with_vbd_on_dom0.uuid
62+
yield f"/dev/sm/backend/{sr_uuid}/{vdi_uuid}"
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import logging
2+
import time
3+
4+
from lib.host import Host
5+
6+
def copy_data_to_tapdev(host: Host, data_file: str, tapdev: str, offset: int, length: int):
7+
"""
8+
if offset == 0:
9+
off = "0"
10+
else:
11+
off = f"{offset}B" # Doesn't work with `dd` version of XCP-ng 8.3
12+
"""
13+
bs = 1
14+
off = int(offset / bs)
15+
count = length / bs
16+
count += length % bs
17+
count = int(count)
18+
cmd = ["dd", f"if={data_file}", f"of={tapdev}", f"bs={bs}", f"seek={off}", f"count={count}"]
19+
host.ssh(cmd)
20+
21+
def get_data(host: Host, file: str, offset: int, length: int, checksum: bool = False) -> str:
22+
cmd = ["xxd", "-p", "-seek", str(offset), "-len", str(length), file]
23+
if checksum:
24+
cmd = cmd + ["|", "sha256sum"]
25+
return host.ssh(cmd)
26+
27+
def get_hashed_data(host: Host, file: str, offset: int, length: int):
28+
return get_data(host, file, offset, length, True).split()[0]
29+
30+
def snapshot_vdi(host: Host, vdi_uuid: str):
31+
vdi_snap = host.xe("vdi-snapshot", {"uuid": vdi_uuid})
32+
logging.info(f"Snapshot VDI {vdi_uuid}: {vdi_snap}")
33+
return vdi_snap
34+
35+
def compare_data(host: Host, tapdev: str, data_file: str, offset: int, length: int) -> bool:
36+
logging.info("Getting data from VDI and file")
37+
vdi_checksum = get_hashed_data(host, tapdev, offset, length)
38+
file_checksum = get_hashed_data(host, data_file, 0, length)
39+
logging.info(f"VDI: {vdi_checksum}")
40+
logging.info(f"FILE: {file_checksum}")
41+
42+
return vdi_checksum == file_checksum
43+
44+
def test_write_data(host, tapdev, data_file_on_host):
45+
length = 1 * 1024 * 1024
46+
offset = 0
47+
48+
logging.info("Copying data to tapdev")
49+
copy_data_to_tapdev(host, data_file_on_host, tapdev, offset, length)
50+
51+
assert compare_data(host, tapdev, data_file_on_host, offset, length)
52+
53+
def test_coalesce(host, tapdev, vdi_with_vbd_on_dom0, data_file_on_host):
54+
vdi = vdi_with_vbd_on_dom0
55+
vdi_uuid = vdi.uuid
56+
length = 1 * 1024 * 1024
57+
offset = 0
58+
59+
vdi_snap = snapshot_vdi(host, vdi_uuid)
60+
61+
logging.info("Copying data to tapdev")
62+
copy_data_to_tapdev(host, data_file_on_host, tapdev, offset, length)
63+
64+
logging.info("Removing VDI snapshot")
65+
host.xe("vdi-destroy", {"uuid": vdi_snap})
66+
67+
logging.info("Waiting for coalesce")
68+
while vdi.get_parent() is not None:
69+
time.sleep(1)
70+
logging.info("Coalesce done")
71+
72+
assert compare_data(host, tapdev, data_file_on_host, offset, length)
73+
74+
def test_clone_coalesce(host, tapdev, vdi_with_vbd_on_dom0, data_file_on_host):
75+
vdi = vdi_with_vbd_on_dom0
76+
vdi_uuid = vdi.uuid
77+
length = 1 * 1024 * 1024
78+
offset = 0
79+
80+
clone_uuid = host.xe("vdi-clone", {"uuid": vdi_uuid})
81+
logging.info(f"Clone VDI {vdi_uuid}: {clone_uuid}")
82+
83+
logging.info("Copying data to tapdev")
84+
copy_data_to_tapdev(host, data_file_on_host, tapdev, offset, length)
85+
86+
host.xe("vdi-destroy", {"uuid": clone_uuid})
87+
88+
logging.info("Waiting for coalesce")
89+
while vdi.get_parent() is not None:
90+
time.sleep(1)
91+
logging.info("Coalesce done")
92+
93+
assert compare_data(host, tapdev, data_file_on_host, offset, length)

0 commit comments

Comments
 (0)