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