Skip to content

Commit 7d55cf3

Browse files
authored
Merge pull request #290 from xcp-ng/dtt-coalesce
feat(coalesce): Add tests for coalesce
2 parents f8c93ce + 2019971 commit 7d55cf3

File tree

11 files changed

+286
-17
lines changed

11 files changed

+286
-17
lines changed

conftest.py

Lines changed: 23 additions & 3 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.
@@ -304,14 +317,21 @@ def host_no_ipv6(host):
304317
if is_ipv6(host.hostname_or_ip):
305318
pytest.skip(f"This test requires an IPv4 XCP-ng")
306319

320+
@pytest.fixture(scope="session")
321+
def shared_sr(host):
322+
sr = host.pool.first_shared_sr()
323+
assert sr, "No shared SR available on hosts"
324+
logging.info(">> Shared SR on host present: {} of type {}".format(sr.uuid, sr.get_type()))
325+
yield sr
326+
307327
@pytest.fixture(scope='session')
308328
def local_sr_on_hostA1(hostA1):
309329
""" A local SR on the pool's master. """
310330
srs = hostA1.local_vm_srs()
311331
assert len(srs) > 0, "a local SR is required on the pool's master"
312332
# use the first local SR found
313333
sr = srs[0]
314-
logging.info(">> local SR on hostA1 present : %s" % sr.uuid)
334+
logging.info(">> local SR on hostA1 present: {} of type {}".format(sr.uuid, sr.get_type()))
315335
yield sr
316336

317337
@pytest.fixture(scope='session')
@@ -321,7 +341,7 @@ def local_sr_on_hostA2(hostA2):
321341
assert len(srs) > 0, "a local SR is required on the pool's second host"
322342
# use the first local SR found
323343
sr = srs[0]
324-
logging.info(">> local SR on hostA2 present : %s" % sr.uuid)
344+
logging.info(">> local SR on hostA2 present: {} of type {}".format(sr.uuid, sr.get_type()))
325345
yield sr
326346

327347
@pytest.fixture(scope='session')
@@ -331,7 +351,7 @@ def local_sr_on_hostB1(hostB1):
331351
assert len(srs) > 0, "a local SR is required on the second pool's master"
332352
# use the first local SR found
333353
sr = srs[0]
334-
logging.info(">> local SR on hostB1 present : %s" % sr.uuid)
354+
logging.info(">> local SR on hostB1 present: {} of type {}".format(sr.uuid, sr.get_type()))
335355
yield sr
336356

337357
@pytest.fixture(scope='session')

lib/basevm.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import logging
22

3-
from typing import TYPE_CHECKING, Any, Literal, Optional, overload
3+
from typing import TYPE_CHECKING, Any, List, Literal, Optional, overload
44

55
if TYPE_CHECKING:
66
import lib.host
@@ -59,7 +59,7 @@ def name(self) -> str:
5959
def _disk_list(self):
6060
raise NotImplementedError()
6161

62-
def vdi_uuids(self, sr_uuid=None):
62+
def vdi_uuids(self, sr_uuid: Optional[str] = None) -> List[str]:
6363
output = self._disk_list()
6464
if output == '':
6565
return []

lib/host.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ def __init__(self, pool: 'lib.pool.Pool', hostname_or_ip):
6767
self.uuid = self.inventory['INSTALLATION_UUID']
6868
self.xcp_version = version.parse(self.inventory['PRODUCT_VERSION'])
6969
self.xcp_version_short = f"{self.xcp_version.major}.{self.xcp_version.minor}"
70+
self._dom0: Optional[VM] = None
7071

7172
def __str__(self):
7273
return self.hostname_or_ip
@@ -711,3 +712,20 @@ def enable_hsts_header(self):
711712
def disable_hsts_header(self):
712713
self.ssh(['rm', '-f', f'{XAPI_CONF_DIR}/00-XCP-ng-tests-enable-hsts-header.conf'])
713714
self.restart_toolstack(verify=True)
715+
716+
def get_dom0_uuid(self):
717+
return self.inventory["CONTROL_DOMAIN_UUID"]
718+
719+
def get_dom0_vm(self) -> VM:
720+
if not self._dom0:
721+
self._dom0 = VM(self.get_dom0_uuid(), self)
722+
return self._dom0
723+
724+
def get_sr_from_vdi_uuid(self, vdi_uuid: str) -> Optional[SR]:
725+
sr_uuid = self.xe("vdi-param-get", {
726+
"param-name": "sr-uuid",
727+
"uuid": vdi_uuid,
728+
})
729+
if not sr_uuid:
730+
return None
731+
return SR(sr_uuid, self.pool)

lib/snapshot.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,18 @@
22

33
from lib.basevm import BaseVM
44

5+
from typing import TYPE_CHECKING
6+
7+
if TYPE_CHECKING:
8+
from lib.host import Host
9+
from lib.vm import VM
10+
11+
512
class Snapshot(BaseVM):
13+
def __init__(self, uuid: str, host: "Host", vm: "VM"):
14+
self.basevm = vm
15+
super(Snapshot, self).__init__(uuid, host)
16+
617
def _disk_list(self):
718
return self.host.xe('snapshot-disk-list', {'uuid': self.uuid, 'vbd-params': ''},
819
minimal=True)
@@ -21,3 +32,4 @@ def exists(self):
2132
def revert(self):
2233
logging.info("Revert to snapshot %s", self.uuid)
2334
self.host.xe('snapshot-revert', {'uuid': self.uuid})
35+
self.basevm.create_vdis_list() # We reset the base VM object VDIs list because it changed following the revert

lib/sr.py

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,24 @@
22
import time
33

44
import lib.commands as commands
5-
from lib.common import prefix_object_name, safe_split, strtobool, wait_for, wait_for_not
5+
from lib.common import (
6+
prefix_object_name,
7+
safe_split,
8+
strtobool,
9+
wait_for,
10+
wait_for_not,
11+
)
612
from lib.vdi import VDI
713

14+
from typing import Optional
15+
816
class SR:
917
def __init__(self, uuid, pool):
1018
self.uuid = uuid
1119
self.pool = pool
1220
self._is_shared = None # cached value for is_shared()
1321
self._main_host = None # cached value for main_host()
22+
self._type = None # cache value for get_type()
1423

1524
def pbd_uuids(self):
1625
return safe_split(self.pool.master.xe('pbd-list', {'sr-uuid': self.uuid}, minimal=True))
@@ -152,13 +161,21 @@ def is_shared(self):
152161
{'uuid': self.uuid, 'param-name': 'shared'}))
153162
return self._is_shared
154163

155-
def create_vdi(self, name_label, virtual_size=64):
164+
def get_type(self) -> str:
165+
if self._type is None:
166+
self._type = self.pool.master.xe("sr-param-get", {"uuid": self.uuid, "param-name": "type"})
167+
return self._type
168+
169+
def create_vdi(self, name_label: str, virtual_size: int = 64, image_format: Optional[str] = None) -> VDI:
156170
logging.info("Create VDI %r on SR %s", name_label, self.uuid)
157-
vdi_uuid = self.pool.master.xe('vdi-create', {
171+
args = {
158172
'name-label': prefix_object_name(name_label),
159173
'virtual-size': str(virtual_size),
160-
'sr-uuid': self.uuid
161-
})
174+
'sr-uuid': self.uuid,
175+
}
176+
if image_format:
177+
args["sm-config:image-format"] = image_format
178+
vdi_uuid = self.pool.master.xe('vdi-create', args)
162179
return VDI(vdi_uuid, sr=self)
163180

164181
def run_quicktest(self):

lib/vdi.py

Lines changed: 7 additions & 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

@@ -50,6 +46,12 @@ def readonly(self) -> bool:
5046
def __str__(self):
5147
return f"VDI {self.uuid} on SR {self.sr.uuid}"
5248

49+
def get_parent(self) -> Optional[str]:
50+
return self.param_get("sm-config", key="vhd-parent", accept_unknown_key=True)
51+
52+
def get_image_format(self) -> Optional[str]:
53+
return self.param_get("sm-config", key="image-format", accept_unknown_key=True)
54+
5355
@overload
5456
def param_get(self, param_name: str, key: Optional[str] = ...,
5557
accept_unknown_key: Literal[False] = ...) -> str:

lib/vm.py

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
)
2020
from lib.snapshot import Snapshot
2121
from lib.vbd import VBD
22+
from lib.vdi import VDI
2223
from lib.vif import VIF
2324

2425
from typing import TYPE_CHECKING, List, Literal, Optional, Union, overload
@@ -33,6 +34,7 @@ def __init__(self, uuid: str, host: 'Host'):
3334
self.previous_host = None # previous host when migrated or being migrated
3435
self.is_windows = self.param_get('platform', 'device_id', accept_unknown_key=True) == '0002'
3536
self.is_uefi = self.param_get('HVM-boot-params', 'firmware', accept_unknown_key=True) == 'uefi'
37+
self.create_vdis_list()
3638

3739
def power_state(self) -> str:
3840
return self.param_get('power-state')
@@ -273,19 +275,74 @@ def migrate(self, target_host, sr=None, network=None):
273275

274276
self.previous_host = self.host
275277
self.host = target_host
278+
self.create_vdis_list()
276279

277280
def snapshot(self, ignore_vdis=None):
278281
logging.info("Snapshot VM")
279282
args = {'uuid': self.uuid, 'new-name-label': 'Snapshot of %s' % self.uuid}
280283
if ignore_vdis:
281284
args['ignore-vdi-uuids'] = ','.join(ignore_vdis)
282-
return Snapshot(self.host.xe('vm-snapshot', args), self.host)
285+
snap_uuid = self.host.xe('vm-snapshot', args)
286+
return Snapshot(snap_uuid, self.host, self)
283287

284288
def checkpoint(self) -> Snapshot:
285289
logging.info("Checkpoint VM")
286290
return Snapshot(self.host.xe('vm-checkpoint', {'uuid': self.uuid,
287291
'new-name-label': 'Checkpoint of %s' % self.uuid}),
288-
self.host)
292+
self.host, self)
293+
294+
def connect_vdi(self, vdi: VDI, device: str = "autodetect") -> str:
295+
logging.info(f">> Plugging VDI {vdi.uuid} on VM {self.uuid}")
296+
vbd_uuid = self.host.xe("vbd-create", {
297+
"vdi-uuid": vdi.uuid,
298+
"vm-uuid": self.uuid,
299+
"device": device,
300+
})
301+
try:
302+
self.host.xe("vbd-plug", {"uuid": vbd_uuid})
303+
except commands.SSHCommandFailed:
304+
self.host.xe("vbd-destroy", {"uuid": vbd_uuid})
305+
raise
306+
307+
self.vdis.append(vdi)
308+
309+
return vbd_uuid
310+
311+
def disconnect_vdi(self, vdi: VDI):
312+
logging.info(f"<< Unplugging VDI {vdi.uuid} from VM {self.uuid}")
313+
assert vdi in self.vdis, "VDI {vdi.uuid} not in VM {self.uuid} VDI list"
314+
vbd_uuid = self.host.xe("vbd-list", {
315+
"vdi-uuid": vdi.uuid,
316+
"vm-uuid": self.uuid
317+
}, minimal=True)
318+
try:
319+
self.host.xe("vbd-unplug", {"uuid": vbd_uuid})
320+
except commands.SSHCommandFailed as e:
321+
if e.stdout == f"The device is not currently attached\ndevice: {vbd_uuid}":
322+
logging.info(f"VBD {vbd_uuid} already unplugged")
323+
else:
324+
raise
325+
self.host.xe("vbd-destroy", {"uuid": vbd_uuid})
326+
self.vdis.remove(vdi)
327+
328+
def destroy_vdi(self, vdi_uuid: str) -> None:
329+
for vdi in self.vdis:
330+
if vdi.uuid == vdi_uuid:
331+
self.vdis.remove(vdi)
332+
super().destroy_vdi(vdi_uuid)
333+
break
334+
335+
def create_vdis_list(self) -> None:
336+
""" Used to redo the VDIs list of the VM when reverting a snapshot. """
337+
try:
338+
self.vdis = [VDI(vdi_uuid, host=self.host) for vdi_uuid in self.vdi_uuids()]
339+
except commands.SSHCommandFailed as e:
340+
# Doesn't work with Dom0 since `vm-disk-list` doesn't work on it so we create empty list
341+
if e.stdout == "Error: No matching VMs found":
342+
logging.info("Couldn't get disks list. We are Dom0. Continuing...")
343+
self.vdis = []
344+
else:
345+
raise
289346

290347
def vifs(self):
291348
_vifs = []

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}"

0 commit comments

Comments
 (0)