|
| 1 | +from __future__ import annotations |
| 2 | + |
1 | 3 | import pytest |
2 | 4 |
|
3 | 5 | import itertools |
|
12 | 14 | from lib import pxe |
13 | 15 | from lib.common import ( |
14 | 16 | callable_marker, |
| 17 | + DiskDevName, |
| 18 | + HostAddress, |
15 | 19 | is_uuid, |
16 | 20 | prefix_object_name, |
17 | 21 | setup_formatted_and_mounted_disk, |
|
21 | 25 | wait_for, |
22 | 26 | ) |
23 | 27 | from lib.netutil import is_ipv6 |
| 28 | +from lib.host import Host |
24 | 29 | from lib.pool import Pool |
25 | 30 | from lib.sr import SR |
26 | 31 | from lib.vm import VM, vm_cache_key_from_def |
|
31 | 36 | # need to import them in the global conftest.py so that they are recognized as fixtures. |
32 | 37 | from pkgfixtures import formatted_and_mounted_ext4_disk, sr_disk_wiped |
33 | 38 |
|
34 | | -from typing import Dict |
| 39 | +from typing import Dict, Generator, Iterable |
35 | 40 |
|
36 | 41 | # Do we cache VMs? |
37 | 42 | try: |
@@ -80,19 +85,12 @@ def pytest_addoption(parser): |
80 | 85 | help="Max lines to output in a ssh log (0 if no limit)" |
81 | 86 | ) |
82 | 87 | 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", |
91 | 89 | action="append", |
92 | 90 | 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." |
96 | 94 | ) |
97 | 95 | parser.addoption( |
98 | 96 | "--image-format", |
@@ -129,8 +127,8 @@ def pytest_collection_modifyitems(items, config): |
129 | 127 | 'windows_vm', |
130 | 128 | 'hostA2', |
131 | 129 | 'hostB1', |
132 | | - 'sr_disk', |
133 | | - 'sr_disk_4k' |
| 130 | + 'unused_512B_disks', |
| 131 | + 'unused_4k_disks', |
134 | 132 | ] |
135 | 133 |
|
136 | 134 | for item in items: |
@@ -170,7 +168,7 @@ def pytest_runtest_makereport(item, call): |
170 | 168 | # fixtures |
171 | 169 |
|
172 | 170 | @pytest.fixture(scope='session') |
173 | | -def hosts(pytestconfig): |
| 171 | +def hosts(pytestconfig) -> Generator[list[Host]]: |
174 | 172 | nested_list = [] |
175 | 173 |
|
176 | 174 | def setup_host(hostname_or_ip, *, config=None): |
@@ -236,6 +234,14 @@ def cleanup_hosts(): |
236 | 234 |
|
237 | 235 | cleanup_hosts() |
238 | 236 |
|
| 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 | + |
239 | 245 | @pytest.fixture(scope='session') |
240 | 246 | def registered_xo_cli(): |
241 | 247 | # The fixture is not responsible for establishing the connection. |
@@ -355,103 +361,71 @@ def local_sr_on_hostB1(hostB1): |
355 | 361 | yield sr |
356 | 362 |
|
357 | 363 | @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 |
374 | 399 |
|
375 | 400 | @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 |
392 | 410 |
|
393 | 411 | @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 |
421 | 421 |
|
422 | 422 | @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 |
455 | 429 |
|
456 | 430 | @pytest.fixture(scope='module') |
457 | 431 | def vm_ref(request): |
|
0 commit comments