Skip to content

Commit 5287d97

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 2ad5a24 commit 5287d97

File tree

6 files changed

+157
-5
lines changed

6 files changed

+157
-5
lines changed

conftest.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,13 @@ def pytest_addoption(parser):
9494
"4KiB blocksize to be formatted and used in storage tests. "
9595
"Set it to 'auto' to let the fixtures auto-detect available disks."
9696
)
97+
parser.addoption(
98+
"--image-format",
99+
action="append",
100+
default=[],
101+
help="Format of VDI to execute tests on."
102+
"Example: vhd,qcow2"
103+
)
97104

98105
def pytest_configure(config):
99106
global_config.ignore_ssh_banner = config.getoption('--ignore-ssh-banner')
@@ -106,6 +113,12 @@ def pytest_generate_tests(metafunc):
106113
vms = [None] # no --vm parameter does not mean skip the test, for us, it means use the default
107114
metafunc.parametrize("vm_ref", vms, indirect=True, scope="module")
108115

116+
if "image_format" in metafunc.fixturenames:
117+
image_format = metafunc.config.getoption("image_format")
118+
if len(image_format) == 0:
119+
image_format = ["vhd"] # Not giving image-format will default to doing tests on vhd
120+
metafunc.parametrize("image_format", image_format, scope="session")
121+
109122
def pytest_collection_modifyitems(items, config):
110123
# Automatically mark tests based on fixtures they require.
111124
# Check pytest.ini or pytest --markers for marker descriptions.

lib/vdi.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,7 @@ def __init__(self, uuid, *, host=None, sr=None):
2525
# TODO: use a different approach when migration is possible
2626
if sr is None:
2727
assert host
28-
sr_uuid = host.pool.get_vdi_sr_uuid(uuid)
29-
# avoid circular import
30-
# FIXME should get it from Host instead
31-
from lib.sr import SR
32-
self.sr = SR(sr_uuid, host.pool)
28+
self.sr = host.get_sr_from_vdi_uuid(self.uuid)
3329
else:
3430
self.sr = sr
3531

tests/storage/coalesce/__init__.py

Whitespace-only changes.

tests/storage/coalesce/conftest.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import pytest
2+
3+
import logging
4+
5+
from lib.vdi import VDI
6+
7+
MAX_LENGTH = 1 * 1024 * 1024 * 1024 # 1GiB
8+
9+
@pytest.fixture(scope="module")
10+
def vdi_on_local_sr(host, local_sr_on_hostA1, image_format):
11+
sr = local_sr_on_hostA1
12+
vdi = sr.create_vdi("testVDI", MAX_LENGTH, image_format=image_format)
13+
logging.info(">> Created VDI {} of type {}".format(vdi.uuid, image_format))
14+
15+
yield vdi
16+
17+
logging.info("<< Destroying VDI {}".format(vdi.uuid))
18+
vdi.destroy()
19+
20+
@pytest.fixture(scope="module")
21+
def vdi_with_vbd_on_dom0(host, vdi_on_local_sr):
22+
dom0 = host.get_dom0_VM()
23+
dom0.connect_vdi(vdi_on_local_sr)
24+
25+
yield vdi_on_local_sr
26+
27+
dom0.disconnect_vdi(vdi_on_local_sr)
28+
29+
@pytest.fixture(scope="function")
30+
def data_file_on_host(host):
31+
filename = "/root/data.bin"
32+
logging.info(f">> Creating data file {filename} on host")
33+
size = 1 * 1024 * 1024 # 1MiB
34+
assert size <= MAX_LENGTH, "Size of the data file bigger than the VDI size"
35+
36+
host.ssh(["dd", "if=/dev/urandom", f"of={filename}", f"bs={size}", "count=1"])
37+
38+
yield filename
39+
40+
logging.info("<< Deleting data file")
41+
host.ssh(["rm", filename])
42+
43+
@pytest.fixture(scope="module")
44+
def tapdev(local_sr_on_hostA1, vdi_with_vbd_on_dom0):
45+
"""
46+
A tapdev is a blockdevice allowing access to a VDI from the Dom0.
47+
48+
It is usually used to give access to the VDI to Qemu for emulating devices
49+
before PV driver are loaded in the guest.
50+
"""
51+
sr_uuid = local_sr_on_hostA1.uuid
52+
vdi_uuid = vdi_with_vbd_on_dom0.uuid
53+
yield f"/dev/sm/backend/{sr_uuid}/{vdi_uuid}"
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import pytest
2+
3+
import logging
4+
5+
from .utils import compare_data, copy_data_to_tapdev, operation_on_vdi, wait_for_vdi_coalesce
6+
7+
from typing import TYPE_CHECKING
8+
9+
if TYPE_CHECKING:
10+
from lib.host import Host
11+
from lib.vdi import VDI
12+
13+
class Test:
14+
def test_write_data(self, host: "Host", tapdev: str, data_file_on_host: str):
15+
length = 1 * 1024 * 1024
16+
offset = 0
17+
18+
logging.info("Copying data to tapdev")
19+
copy_data_to_tapdev(host, data_file_on_host, tapdev, offset, length)
20+
21+
logging.info("Comparing data to tapdev")
22+
assert compare_data(host, tapdev, data_file_on_host, offset, length)
23+
24+
@pytest.mark.parametrize("vdi_op", ["snapshot", "clone"])
25+
def test_coalesce(self, host: "Host", tapdev: str, vdi_with_vbd_on_dom0: "VDI", data_file_on_host: str, vdi_op):
26+
vdi = vdi_with_vbd_on_dom0
27+
vdi_uuid = vdi.uuid
28+
length = 1 * 1024 * 1024
29+
offset = 0
30+
31+
new_vdi = operation_on_vdi(host, vdi_uuid, vdi_op)
32+
33+
logging.info("Copying data to tapdev")
34+
copy_data_to_tapdev(host, data_file_on_host, tapdev, offset, length)
35+
36+
logging.info(f"Removing VDI {vdi_op}")
37+
host.xe("vdi-destroy", {"uuid": new_vdi})
38+
39+
wait_for_vdi_coalesce(vdi)
40+
41+
logging.info("Comparing data to tapdev")
42+
assert compare_data(host, tapdev, data_file_on_host, offset, length)

tests/storage/coalesce/utils.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import logging
2+
3+
from lib.common import wait_for_not
4+
from lib.host import Host
5+
from lib.vdi import VDI
6+
7+
from typing import Literal
8+
9+
def wait_for_vdi_coalesce(vdi: VDI):
10+
wait_for_not(lambda: vdi.get_parent(), msg="Waiting for coalesce")
11+
logging.info("Coalesce done")
12+
13+
def copy_data_to_tapdev(host: Host, data_file: str, tapdev: str, offset: int, length: int):
14+
# if offset == 0:
15+
# off = "0"
16+
# else:
17+
# off = f"{offset}B" # Doesn't work with `dd` version of XCP-ng 8.3
18+
19+
bs = 1
20+
off = int(offset / bs)
21+
count = length / bs
22+
count += length % bs
23+
count = int(count)
24+
cmd = ["dd", f"if={data_file}", f"of={tapdev}", f"bs={bs}", f"seek={off}", f"count={count}"]
25+
host.ssh(cmd)
26+
27+
def get_data(host: Host, file: str, offset: int, length: int, checksum: bool = False) -> str:
28+
cmd = ["xxd", "-p", "-seek", str(offset), "-len", str(length), file]
29+
if checksum:
30+
cmd = cmd + ["|", "sha256sum"]
31+
return host.ssh(cmd)
32+
33+
def get_hashed_data(host: Host, file: str, offset: int, length: int):
34+
return get_data(host, file, offset, length, True).split()[0]
35+
36+
def operation_on_vdi(host: Host, vdi_uuid: str, vdi_op: Literal["snapshot", "clone"]) -> str:
37+
new_vdi = host.xe(f"vdi-{vdi_op}", {"uuid": vdi_uuid})
38+
logging.info(f"{vdi_op.capitalize()} VDI {vdi_uuid}: {new_vdi}")
39+
return new_vdi
40+
41+
def compare_data(host: Host, tapdev: str, data_file: str, offset: int, length: int) -> bool:
42+
logging.info("Getting data from VDI and file")
43+
vdi_checksum = get_hashed_data(host, tapdev, offset, length)
44+
file_checksum = get_hashed_data(host, data_file, 0, length)
45+
logging.info(f"VDI: {vdi_checksum}")
46+
logging.info(f"FILE: {file_checksum}")
47+
48+
return vdi_checksum == file_checksum

0 commit comments

Comments
 (0)