Skip to content

Commit

Permalink
Add support for multiple disks to testcloud plugin
Browse files Browse the repository at this point in the history
```
TMT_SHOW_TRACEBACK=1 tmt -vv run plans --default \
    provision --how virtual --image fedora-39 \
              --hardware 'disk[1].size=20GB' \
              --hardware 'disk[0].size=15GB' \
    login -s provision \
    finish
```

Fixes #2765
  • Loading branch information
happz committed May 21, 2024
1 parent b782a60 commit 2f630e0
Show file tree
Hide file tree
Showing 3 changed files with 82 additions and 15 deletions.
2 changes: 1 addition & 1 deletion tmt/hardware.py
Original file line number Diff line number Diff line change
Expand Up @@ -595,7 +595,7 @@ def printable_name(self) -> str:

names: list[str] = []

if components.peer_index:
if components.peer_index is not None:
names.append(f'{components.name.replace("_", "-")}[{components.peer_index}]')

else:
Expand Down
19 changes: 18 additions & 1 deletion tmt/steps/provision/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -498,7 +498,24 @@ def normalize_hardware(
for raw_datum in raw_hardware:
components = tmt.hardware.ConstraintComponents.from_spec(raw_datum)

if components.name == 'cpu' and components.child_name == 'flag':
if components.peer_index is not None:
if components.child_name is None:
raise tmt.utils.SpecificationError(
f"Hardware requirement '{raw_datum}' lacks child property.")

if components.name not in merged:
merged[components.name] = []

# Fill in empty spots between the existing ones and the one we're adding.
if len(merged[components.name]) <= components.peer_index:
merged[components.name] += [
{} for _ in range(components.peer_index - len(merged[components.name]) + 1)
]

merged[components.name][components.peer_index][components.child_name] = \
f'{components.operator} {components.value}'

elif components.name == 'cpu' and components.child_name == 'flag':
if 'flag' not in merged['cpu']:
merged['cpu']['flag'] = []

Expand Down
76 changes: 63 additions & 13 deletions tmt/steps/provision/testcloud.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@

import collections
import dataclasses
import datetime
import itertools
import os
import platform
import re
import threading
import types
from collections.abc import Iterator
from typing import TYPE_CHECKING, Any, Optional, Union, cast

import click
Expand Down Expand Up @@ -690,21 +693,40 @@ def _apply_hw_memory(self, domain: 'DomainConfiguration') -> None:

domain.memory_size = int(constraint.value.to('kB').magnitude)

def _apply_hw_disk_size(self, domain: 'DomainConfiguration') -> 'QCow2StorageDevice':
def _apply_hw_disk_size(self, domain: 'DomainConfiguration') -> None:
""" Apply ``disk`` constraint to given VM domain """

final_size: 'Size' = DEFAULT_DISK

def _generate_disk_filepaths() -> Iterator[Path]:
""" Generate paths to use for files representing VM storage """

# Start with the path already decided by testcloud...
yield Path(domain.local_disk)

# ... and use it as a basis for remaining paths.
for i in itertools.count(1, 1):
yield Path(f'{domain.local_disk}.{i}')

disk_filepath_generator = _generate_disk_filepaths()

if not self.hardware or not self.hardware.constraint:
self.debug(
'disk[0].size',
f"set to '{final_size}' because of no constraints",
level=4)

return QCow2StorageDevice(domain.local_disk, int(final_size.to('GB').magnitude))
domain.storage_devices = [
QCow2StorageDevice(
str(next(disk_filepath_generator)),
int(final_size.to('GB').magnitude))
]

return

variant = self.hardware.constraint.variant()

# Collect all `disk.size` constraints, ignore the rest.
disk_size_constraints = [
constraint
for constraint in variant
Expand All @@ -718,7 +740,17 @@ def _apply_hw_disk_size(self, domain: 'DomainConfiguration') -> 'QCow2StorageDev
f"set to '{final_size}' because of no 'disk.size' constraints",
level=4)

return QCow2StorageDevice(domain.local_disk, int(final_size.to('GB').magnitude))
domain.storage_devices = [
QCow2StorageDevice(
str(next(disk_filepath_generator)),
int(final_size.to('GB').magnitude))
]

return

# Now sort them into groups by their `peer_index`, i.e. `disk[0]`,
# `disk[1]` and so on.
by_peer_index: dict[int, list[tmt.hardware.SizeConstraint]] = collections.defaultdict(list)

for constraint in disk_size_constraints:
if constraint.operator not in (
Expand All @@ -728,14 +760,31 @@ def _apply_hw_disk_size(self, domain: 'DomainConfiguration') -> 'QCow2StorageDev
raise ProvisionError(
f"Cannot apply hardware requirement '{constraint}', operator not supported.")

self.debug(
'disk[0].size',
f"set to '{constraint.value}' because of '{constraint}'",
level=4)
components = constraint.expand_name()

assert components.peer_index is not None # narrow type

by_peer_index[components.peer_index].append(constraint)

final_size = constraint.value
# Process each disk and its constraints, construct the
# corresponding storage device, and the last constraint wins
# & sets its size.
for peer_index in sorted(by_peer_index.keys()):
final_size = DEFAULT_DISK

return QCow2StorageDevice(domain.local_disk, int(final_size.to('GB').magnitude))
for constraint in by_peer_index[peer_index]:
self.debug(
f'disk[{peer_index}].size',
f"set to '{constraint.value}' because of '{constraint}'",
level=4)

final_size = constraint.value

domain.storage_devices.append(
QCow2StorageDevice(
str(next(disk_filepath_generator)),
int(final_size.to('GB').magnitude))
)

def _apply_hw_arch(self, domain: 'DomainConfiguration', kvm: bool, legacy_os: bool) -> None:
if self.arch == "x86_64":
Expand Down Expand Up @@ -826,11 +875,14 @@ def start(self) -> None:
self._logger.debug('effective hardware', line, level=4)

self._apply_hw_memory(self._domain)
storage_image = self._apply_hw_disk_size(self._domain)
self._apply_hw_disk_size(self._domain)
_apply_hw_tpm(self.hardware, self._domain, self._logger)

self.debug('final domain memory', str(self._domain.memory_size))
self.debug('final domain disk size', str(storage_image.size))
self.debug('final domain root disk size', str(self._domain.storage_devices[0].size))

for i, device in enumerate(self._domain.storage_devices):
self.debug(f'final domain disk #{i} size', str(device.size))

# Is this a CoreOS?
self._domain.coreos = self.is_coreos
Expand All @@ -852,8 +904,6 @@ def start(self) -> None:
else:
raise tmt.utils.ProvisionError("Only system, or session connection is supported.")

self._domain.storage_devices.append(storage_image)

if not self._domain.coreos:
seed_disk = RawStorageDevice(self._domain.seed_path)
self._domain.storage_devices.append(seed_disk)
Expand Down

0 comments on commit 2f630e0

Please sign in to comment.