Skip to content

Commit 03bc212

Browse files
authored
Merge pull request #330 from xcp-ng/disks-rework
Disk fixtures rework
2 parents 7d55cf3 + e309b8f commit 03bc212

26 files changed

+306
-214
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,7 @@ Here's an example of selection we can do thanks to the markers:
205205

206206
```
207207
# Run storage driver tests that either need no VM at all, or advise to use a small VM. Exclude tests that reboot the hosts.
208-
pytest tests/storage -m "(small_vm or no_vm) and not reboot" --hosts=ip_of_poolmaster1,ip_of_poolmaster2 --vm=http://path/to/a_small_vm.xva --sr-disk=auto
208+
pytest tests/storage -m "(small_vm or no_vm) and not reboot" --hosts=ip_of_poolmaster1,ip_of_poolmaster2 --vm=http://path/to/a_small_vm.xva
209209
```
210210

211211
Another example:

conftest.py

Lines changed: 80 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from __future__ import annotations
2+
13
import pytest
24

35
import itertools
@@ -12,6 +14,8 @@
1214
from lib import pxe
1315
from lib.common import (
1416
callable_marker,
17+
DiskDevName,
18+
HostAddress,
1519
is_uuid,
1620
prefix_object_name,
1721
setup_formatted_and_mounted_disk,
@@ -21,6 +25,7 @@
2125
wait_for,
2226
)
2327
from lib.netutil import is_ipv6
28+
from lib.host import Host
2429
from lib.pool import Pool
2530
from lib.sr import SR
2631
from lib.vm import VM, vm_cache_key_from_def
@@ -31,7 +36,7 @@
3136
# need to import them in the global conftest.py so that they are recognized as fixtures.
3237
from pkgfixtures import formatted_and_mounted_ext4_disk, sr_disk_wiped
3338

34-
from typing import Dict
39+
from typing import Dict, Generator, Iterable
3540

3641
# Do we cache VMs?
3742
try:
@@ -80,19 +85,12 @@ def pytest_addoption(parser):
8085
help="Max lines to output in a ssh log (0 if no limit)"
8186
)
8287
parser.addoption(
83-
"--sr-disk",
84-
action="append",
85-
default=[],
86-
help="Name of an available disk (sdb) or partition device (sdb2) to be formatted and used in storage tests. "
87-
"Set it to 'auto' to let the fixtures auto-detect available disks."
88-
)
89-
parser.addoption(
90-
"--sr-disk-4k",
88+
"--disks",
9189
action="append",
9290
default=[],
93-
help="Name of an available disk (sdb) or partition device (sdb2) with "
94-
"4KiB blocksize to be formatted and used in storage tests. "
95-
"Set it to 'auto' to let the fixtures auto-detect available disks."
91+
help="HOST:DISKS to authorize for use by tests. "
92+
"DISKS is a possibly-empty comma-separated list. "
93+
"No mention of a given host authorizes use of all its disks."
9694
)
9795
parser.addoption(
9896
"--image-format",
@@ -129,8 +127,8 @@ def pytest_collection_modifyitems(items, config):
129127
'windows_vm',
130128
'hostA2',
131129
'hostB1',
132-
'sr_disk',
133-
'sr_disk_4k'
130+
'unused_512B_disks',
131+
'unused_4k_disks',
134132
]
135133

136134
for item in items:
@@ -170,7 +168,7 @@ def pytest_runtest_makereport(item, call):
170168
# fixtures
171169

172170
@pytest.fixture(scope='session')
173-
def hosts(pytestconfig):
171+
def hosts(pytestconfig) -> Generator[list[Host]]:
174172
nested_list = []
175173

176174
def setup_host(hostname_or_ip, *, config=None):
@@ -236,6 +234,14 @@ def cleanup_hosts():
236234

237235
cleanup_hosts()
238236

237+
@pytest.fixture(scope='session')
238+
def pools_hosts_by_name_or_ip(hosts: list[Host]) -> Generator[dict[HostAddress, Host]]:
239+
"""All hosts of all pools, each indexed by their hostname_or_ip."""
240+
yield {host.hostname_or_ip: host
241+
for pool_master in hosts
242+
for host in pool_master.pool.hosts
243+
}
244+
239245
@pytest.fixture(scope='session')
240246
def registered_xo_cli():
241247
# The fixture is not responsible for establishing the connection.
@@ -355,103 +361,71 @@ def local_sr_on_hostB1(hostB1):
355361
yield sr
356362

357363
@pytest.fixture(scope='session')
358-
def sr_disk(pytestconfig, host):
359-
disks = pytestconfig.getoption("sr_disk")
360-
if len(disks) != 1:
361-
pytest.fail("This test requires exactly one --sr-disk parameter")
362-
disk = disks[0]
363-
if disk == "auto":
364-
logging.info(">> Check for the presence of a free disk device on the master host")
365-
disks = host.available_disks()
366-
assert len(disks) > 0, "a free disk device is required on the master host"
367-
disk = disks[0]
368-
logging.info(f">> Found free disk device(s) on hostA1: {' '.join(disks)}. Using {disk}.")
369-
else:
370-
logging.info(f">> Check that disk or block device {disk} is available on the master host")
371-
assert disk in host.available_disks(), \
372-
f"disk or block device {disk} is either not present or already used on master host"
373-
yield disk
364+
def disks(pytestconfig, pools_hosts_by_name_or_ip: dict[HostAddress, Host]
365+
) -> dict[Host, list[Host.BlockDeviceInfo]]:
366+
"""Dict identifying names of all disks for on all hosts of first pool."""
367+
def _parse_disk_option(option_text: str) -> tuple[HostAddress, list[DiskDevName]]:
368+
parsed = option_text.split(sep=":", maxsplit=1)
369+
assert len(parsed) == 2, f"--disks option {option_text!r} is not <host>:<disk>[,<disk>]*"
370+
host_address, disks_string = parsed
371+
devices = disks_string.split(',') if disks_string else []
372+
return host_address, devices
373+
374+
cli_disks = dict(_parse_disk_option(option_text)
375+
for option_text in pytestconfig.getoption("disks"))
376+
377+
def _host_disks(host: Host, hosts_cli_disks: list[DiskDevName] | None) -> Iterable[Host.BlockDeviceInfo]:
378+
"""Filter host disks according to list from `--cli` if given."""
379+
host_disks = host.disks()
380+
# no disk specified = allow all
381+
if hosts_cli_disks is None:
382+
yield from host_disks
383+
return
384+
# check all disks in --disks=host:... exist
385+
for cli_disk in hosts_cli_disks:
386+
for disk in host_disks:
387+
if disk['name'] == cli_disk:
388+
yield disk
389+
break # names are unique, don't expect another one
390+
else:
391+
raise Exception(f"no {cli_disk!r} disk on host {host.hostname_or_ip}, "
392+
f"has {','.join(disk['name'] for disk in host_disks)}")
393+
394+
ret = {host: list(_host_disks(host, cli_disks.get(host.hostname_or_ip)))
395+
for host in pools_hosts_by_name_or_ip.values()
396+
}
397+
logging.debug("disks collected: %s", {host.hostname_or_ip: value for host, value in ret.items()})
398+
return ret
374399

375400
@pytest.fixture(scope='session')
376-
def sr_disk_4k(pytestconfig, host):
377-
disks = pytestconfig.getoption("sr_disk_4k")
378-
if len(disks) != 1:
379-
pytest.fail("This test requires exactly one --sr-disks-4k parameter")
380-
disk = disks[0]
381-
if disk == "auto":
382-
logging.info(">> Check for the presence of a free 4KiB block device on the master host")
383-
disks = host.available_disks(4096)
384-
assert len(disks) > 0, "a free 4KiB block device is required on the master host"
385-
disk = disks[0]
386-
logging.info(f">> Found free 4KiB block device(s) on hostA1: {' '.join(disks)}. Using {disk}.")
387-
else:
388-
logging.info(f">> Check that 4KiB block device {disk} is available on the master host")
389-
assert disk in host.available_disks(4096), \
390-
f"4KiB block device {disk} must be available for use on master host"
391-
yield disk
401+
def unused_512B_disks(disks: dict[Host, list[Host.BlockDeviceInfo]]
402+
) -> dict[Host, list[Host.BlockDeviceInfo]]:
403+
"""Dict identifying names of all 512-bytes-blocks disks for on all hosts of first pool."""
404+
ret = {host: [disk for disk in host_disks
405+
if disk["log-sec"] == "512" and host.disk_is_available(disk["name"])]
406+
for host, host_disks in disks.items()
407+
}
408+
logging.debug("available disks collected: %s", {host.hostname_or_ip: value for host, value in ret.items()})
409+
return ret
392410

393411
@pytest.fixture(scope='session')
394-
def sr_disk_for_all_hosts(pytestconfig, request, host):
395-
disks = pytestconfig.getoption("sr_disk")
396-
if len(disks) != 1:
397-
pytest.fail("This test requires exactly one --sr-disk parameter")
398-
disk = disks[0]
399-
master_disks = host.available_disks()
400-
assert len(master_disks) > 0, "a free disk device is required on the master host"
401-
402-
if disk != "auto":
403-
assert disk in master_disks, \
404-
f"disk or block device {disk} is either not present or already used on master host"
405-
master_disks = [disk]
406-
407-
candidates = list(master_disks)
408-
for h in host.pool.hosts[1:]:
409-
other_disks = h.available_disks()
410-
candidates = [d for d in candidates if d in other_disks]
411-
412-
if disk == "auto":
413-
assert len(candidates) > 0, \
414-
f"a free disk device is required on all pool members. Pool master has: {' '.join(master_disks)}."
415-
logging.info(f">> Found free disk device(s) on all pool hosts: {' '.join(candidates)}. Using {candidates[0]}.")
416-
else:
417-
assert len(candidates) > 0, \
418-
f"disk or block device {disk} was not found to be present and free on all hosts"
419-
logging.info(f">> Disk or block device {disk} is present and free on all pool members")
420-
yield candidates[0]
412+
def unused_4k_disks(disks: dict[Host, list[Host.BlockDeviceInfo]]
413+
) -> dict[Host, list[Host.BlockDeviceInfo]]:
414+
"""Dict identifying names of all 4K-blocks disks for on all hosts of first pool."""
415+
ret = {host: [disk for disk in host_disks
416+
if disk["log-sec"] == "4096" and host.disk_is_available(disk["name"])]
417+
for host, host_disks in disks.items()
418+
}
419+
logging.debug("available 4k disks collected: %s", {host.hostname_or_ip: value for host, value in ret.items()})
420+
return ret
421421

422422
@pytest.fixture(scope='session')
423-
def sr_disks_for_all_hosts(pytestconfig, request, host):
424-
disks = pytestconfig.getoption("sr_disk")
425-
assert len(disks) > 0, "This test requires at least one --sr-disk parameter"
426-
# Fetch available disks on the master host
427-
master_disks = host.available_disks()
428-
assert len(master_disks) > 0, "a free disk device is required on the master host"
429-
430-
if "auto" in disks:
431-
candidates = list(master_disks)
432-
else:
433-
# Validate that all specified disks exist on the master host
434-
for disk in disks:
435-
assert disk in master_disks, \
436-
f"Disk or block device {disk} is either not present or already used on the master host"
437-
candidates = list(disks)
438-
439-
# Check if all disks are available on all hosts in the pool
440-
for h in host.pool.hosts[1:]:
441-
other_disks = h.available_disks()
442-
candidates = [d for d in candidates if d in other_disks]
443-
444-
if "auto" in disks:
445-
# Automatically select disks if "auto" is passed
446-
assert len(candidates) > 0, \
447-
f"Free disk devices are required on all pool members. Pool master has: {' '.join(master_disks)}."
448-
logging.info(">> Using free disk device(s) on all pool hosts: %s.", candidates)
449-
else:
450-
# Ensure specified disks are free on all hosts
451-
assert len(candidates) == len(disks), \
452-
f"Some specified disks ({', '.join(disks)}) are not free or available on all hosts."
453-
logging.info(">> Disk(s) %s are present and free on all pool members", candidates)
454-
yield candidates
423+
def pool_with_unused_512B_disk(host: Host, unused_512B_disks: dict[Host, list[Host.BlockDeviceInfo]]) -> Pool:
424+
"""Returns the first pool, ensuring all hosts have at least one unused 512-bytes-blocks disk."""
425+
for h in host.pool.hosts:
426+
assert h in unused_512B_disks
427+
assert unused_512B_disks[h], f"host {h} does not have any unused 512B-block disk"
428+
return host.pool
455429

456430
@pytest.fixture(scope='module')
457431
def vm_ref(request):

0 commit comments

Comments
 (0)