Skip to content

Commit d8516eb

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 cbfcd2e commit d8516eb

File tree

5 files changed

+189
-5
lines changed

5 files changed

+189
-5
lines changed

conftest.py

+13
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+
"--vdi-type",
80+
action="append",
81+
default=[],
82+
help="Types 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 "vdi_type" in metafunc.fixturenames:
98+
vdi_type = metafunc.config.getoption("vdi_type")
99+
if len(vdi_type) == 0:
100+
vdi_type = ["vhd"] # Not giving vdi-type will default to doing tests on vhd
101+
metafunc.parametrize("vdi_type", vdi_type, 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

+14
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import shlex
55
import tempfile
66
import uuid
7+
from typing import Optional
78

89
from packaging import version
910

@@ -606,3 +607,16 @@ def enable_hsts_header(self):
606607
def disable_hsts_header(self):
607608
self.ssh(['rm', '-f', f'{XAPI_CONF_DIR}/00-XCP-ng-tests-enable-hsts-header.conf'])
608609
self.restart_toolstack(verify=True)
610+
611+
def get_dom0_uuid(self):
612+
output = self.ssh(["grep", "-e", "\"CONTROL_DOMAIN_UUID=\"", "/etc/xensource-inventory"])
613+
return output.split("=")[1].replace("'", "")
614+
615+
def get_vdi_sr_uuid(self, vdi_uuid) -> Optional[SR]:
616+
sr_uuid = self.xe("vdi-param-get",
617+
{"param-name": "sr-uuid",
618+
"uuid": vdi_uuid,
619+
})
620+
if sr_uuid is None:
621+
return None
622+
return SR(sr_uuid, self.pool)

lib/vdi.py

+5-5
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
45

@@ -9,11 +10,7 @@ def __init__(self, uuid, *, host=None, sr=None):
910
self.uuid = uuid
1011
# TODO: use a different approach when migration is possible
1112
if sr is None:
12-
sr_uuid = host.get_vdi_sr_uuid(uuid)
13-
# avoid circular import
14-
# FIXME should get it from Host instead
15-
from lib.sr import SR
16-
self.sr = SR(sr_uuid, host.pool)
13+
self.sr = host.get_vdi_sr_uuid(uuid)
1714
else:
1815
self.sr = sr
1916

@@ -34,6 +31,9 @@ def readonly(self):
3431
def __str__(self):
3532
return f"VDI {self.uuid} on SR {self.sr.uuid}"
3633

34+
def get_parent(self) -> Optional[str]:
35+
return self.param_get("sm-config", key="vhd-parent", accept_unknown_key=True)
36+
3737
def param_get(self, param_name, key=None, accept_unknown_key=False):
3838
return _param_get(self.sr.pool.master, self.xe_prefix, self.uuid,
3939
param_name, key, accept_unknown_key)

tests/storage/coalesce/conftest.py

+62
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, vdi_type):
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": vdi_type,
16+
})
17+
logging.info(">> Created VDI {} of type {}".format(vdi_uuid, vdi_type))
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}"
+95
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import logging
2+
from typing import Optional
3+
4+
import time
5+
6+
from lib.host import Host
7+
8+
def copy_data_to_tapdev(host: Host, data_file: str, tapdev: str, offset: int, length: int):
9+
"""
10+
if offset == 0:
11+
off = "0"
12+
else:
13+
off = f"{offset}B" # Doesn't work with `dd` version of XCP-ng 8.3
14+
"""
15+
bs = 1
16+
off = int(offset / bs)
17+
count = length / bs
18+
count += length % bs
19+
count = int(count)
20+
cmd = ["dd", f"if={data_file}", f"of={tapdev}", f"bs={bs}", f"seek={off}", f"count={count}"]
21+
host.ssh(cmd)
22+
23+
def get_data(host: Host, file: str, offset: int, length: int, checksum: bool = False) -> str:
24+
cmd = ["xxd", "-p", "-seek", str(offset), "-len", str(length), file]
25+
if checksum:
26+
cmd = cmd + ["|", "sha256sum"]
27+
return host.ssh(cmd)
28+
29+
def get_hashed_data(host: Host, file: str, offset: int, length: int):
30+
return get_data(host, file, offset, length, True).split()[0]
31+
32+
def snapshot_vdi(host: Host, vdi_uuid: str):
33+
vdi_snap = host.xe("vdi-snapshot", {"uuid": vdi_uuid})
34+
logging.info(f"Snapshot VDI {vdi_uuid}: {vdi_snap}")
35+
return vdi_snap
36+
37+
def compare_data(host: Host, tapdev: str, data_file: str, offset: int, length: int) -> bool:
38+
logging.info("Getting data from VDI and file")
39+
vdi_checksum = get_hashed_data(host, tapdev, offset, length)
40+
file_checksum = get_hashed_data(host, data_file, 0, length)
41+
logging.info(f"VDI: {vdi_checksum}")
42+
logging.info(f"FILE: {file_checksum}")
43+
44+
return vdi_checksum == file_checksum
45+
46+
def test_write_data(host, tapdev, data_file_on_host):
47+
length = 1 * 1024 * 1024
48+
offset = 0
49+
50+
logging.info("Copying data to tapdev")
51+
copy_data_to_tapdev(host, data_file_on_host, tapdev, offset, length)
52+
53+
assert compare_data(host, tapdev, data_file_on_host, offset, length)
54+
55+
def test_coalesce(host, tapdev, vdi_with_vbd_on_dom0, data_file_on_host):
56+
vdi = vdi_with_vbd_on_dom0
57+
vdi_uuid = vdi.uuid
58+
length = 1 * 1024 * 1024
59+
offset = 0
60+
61+
vdi_snap = snapshot_vdi(host, vdi_uuid)
62+
63+
logging.info("Copying data to tapdev")
64+
copy_data_to_tapdev(host, data_file_on_host, tapdev, offset, length)
65+
66+
logging.info("Removing VDI snapshot")
67+
host.xe("vdi-destroy", {"uuid": vdi_snap})
68+
69+
logging.info("Waiting for coalesce")
70+
while vdi.get_parent() is not None:
71+
time.sleep(1)
72+
logging.info("Coalesce done")
73+
74+
assert compare_data(host, tapdev, data_file_on_host, offset, length)
75+
76+
def test_clone_coalesce(host, tapdev, vdi_with_vbd_on_dom0, data_file_on_host):
77+
vdi = vdi_with_vbd_on_dom0
78+
vdi_uuid = vdi.uuid
79+
length = 1 * 1024 * 1024
80+
offset = 0
81+
82+
clone_uuid = host.xe("vdi-clone", {"uuid": vdi_uuid})
83+
logging.info(f"Clone VDI {vdi_uuid}: {clone_uuid}")
84+
85+
logging.info("Copying data to tapdev")
86+
copy_data_to_tapdev(host, data_file_on_host, tapdev, offset, length)
87+
88+
host.xe("vdi-destroy", {"uuid": clone_uuid})
89+
90+
logging.info("Waiting for coalesce")
91+
while vdi.get_parent() is not None:
92+
time.sleep(1)
93+
logging.info("Coalesce done")
94+
95+
assert compare_data(host, tapdev, data_file_on_host, offset, length)

0 commit comments

Comments
 (0)