Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expose actual guest HW description in guest topology #2586

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions spec/plans/guest-topology.fmf
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ description: |
The shell-friendly file contains arrays, therefore it's compatible
with Bash 4.x and newer.

.. note::

The ``hardware`` key, describing the actual HW configuration of the
guest, as understood by tmt, is using
:ref:`hardware specification </spec/hardware>`
and as such is still a work in progress. Not all hardware properties
might be available yet.

.. note::

The shell-friendly file is easy to ingest for a shell-based tests,
Expand All @@ -34,6 +42,7 @@ description: |
name: ...
role: ...
hostname: ...
hardware: ...

# List of names of all provisioned guests.
guest-names:
Expand All @@ -48,10 +57,12 @@ description: |
name: guest1
role: ...
hostname: ...
hardware: ...
guest2:
name: guest2
role: ...
hostname: ...
hardware: ...
...

# List of all known roles.
Expand Down Expand Up @@ -102,6 +113,9 @@ description: |
TMT_ROLES[role2]="guestN ..."
...

.. versionadded:: 1.33
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The next milestone is 1.34?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, and it seems we're not going to make that one either :/

Leaving this comment open till the PR gets ready for merge & reviewed.

Guest HW exposed via ``hardware`` key.

example:
- |
# A trivial pseudo-test script
Expand Down
20 changes: 20 additions & 0 deletions tmt/hardware.py
Original file line number Diff line number Diff line change
Expand Up @@ -1400,6 +1400,26 @@ def parse_hw_requirements(spec: Spec) -> BaseConstraint:
return _parse_block(spec)


def simplify_actual_hardware(hardware: BaseConstraint) -> Spec:
as_spec: Spec = {}

def _simplify_constraint(constraint: BaseConstraint) -> Spec:
constraint_spec = constraint.to_spec()

assert isinstance(constraint_spec, dict)

return cast(dict[str, str], constraint_spec)

if isinstance(hardware, And):
for constraint in hardware.constraints:
as_spec.update(_simplify_constraint(constraint))

else:
as_spec.update(_simplify_constraint(hardware))

return as_spec


@dataclasses.dataclass
class Hardware(SpecBasedContainer[Spec, Spec]):
constraint: Optional[BaseConstraint]
Expand Down
46 changes: 46 additions & 0 deletions tmt/steps/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
from click.core import ParameterSource

import tmt.export
import tmt.hardware
import tmt.log
import tmt.options
import tmt.queue
Expand Down Expand Up @@ -1916,11 +1917,13 @@ class GuestTopology(SerializableContainer):
name: str
role: Optional[str]
hostname: Optional[str]
hardware: Optional[tmt.hardware.Spec]

def __init__(self, guest: 'Guest') -> None:
self.name = guest.name
self.role = guest.role
self.hostname = guest.topology_address
self.hardware = tmt.hardware.simplify_actual_hardware(guest.actual_hardware)


@dataclasses.dataclass(init=False)
Expand Down Expand Up @@ -2110,6 +2113,49 @@ def push(

return environment

@classmethod
def inject(
cls,
*,
environment: Environment,
guests: list['Guest'],
guest: 'Guest',
dirpath: Path,
filename_base: Optional[Path] = None,
logger: tmt.log.Logger) -> 'Topology':
"""
Create, save and push topology to a given guest.

.. note::

A helper for simplified use from plugins. It delivers exactly what
:py:meth:`save` and :py:meth:`push` would do, but is easier to use
for a common plugin developer.

:param environment: environment to update with topology variables.
:param guests: list of all provisioned guests.
:param guest: a guest on which the plugin would run its actions.
:param dirpath: a directory to save the topology into.
:param filename_base: if set, it would be used as a base for filenames,
correct suffixes would be added.
:param logger: logger to use for logging.
:returns: instantiated topology container.
"""

topology = cls(guests)
topology.guest = GuestTopology(guest)

environment.update(
topology.push(
dirpath=dirpath,
guest=guest,
filename_base=filename_base,
logger=logger
)
)

return topology


@dataclasses.dataclass
class ActionTask(tmt.queue.GuestlessTask[None]):
Expand Down
18 changes: 17 additions & 1 deletion tmt/steps/execute/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
from tmt.steps import Action, ActionTask, PhaseQueue, PluginTask, Step
from tmt.steps.discover import Discover, DiscoverPlugin, DiscoverStepData
from tmt.steps.provision import Guest
from tmt.utils import Path, ShellScript, Stopwatch, cached_property, field
from tmt.utils import Environment, Path, ShellScript, Stopwatch, cached_property, field

if TYPE_CHECKING:
import tmt.cli
Expand Down Expand Up @@ -271,6 +271,22 @@ def is_guest_healthy(self) -> bool:

return True

def inject_topology(self, environment: Environment) -> tmt.steps.Topology:
"""
Collect and install the guest topology and make it visible to test.

:param environment: environment to update with topology variables.
:returns: instantiated topology container.
"""

return tmt.steps.Topology.inject(
environment=environment,
guests=self.phase.step.plan.provision.guests(),
guest=self.guest,
dirpath=self.path,
logger=self.logger
)

def handle_restart(self) -> bool:
"""
"Restart" the test if the test requested it.
Expand Down
8 changes: 1 addition & 7 deletions tmt/steps/execute/internal.py
Original file line number Diff line number Diff line change
Expand Up @@ -325,13 +325,7 @@ def execute(
options=["-s", "-p", "--chmod=755"])

# Create topology files
topology = tmt.steps.Topology(self.step.plan.provision.guests())
topology.guest = tmt.steps.GuestTopology(guest)

environment.update(topology.push(
dirpath=invocation.path,
guest=guest,
logger=logger))
invocation.inject_topology(environment)

command: str
if guest.become and not guest.facts.is_superuser:
Expand Down
12 changes: 12 additions & 0 deletions tmt/steps/finish/shell.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,25 @@ def go(
""" Perform finishing tasks on given guest """
super().go(guest=guest, environment=environment, logger=logger)

environment = environment or tmt.utils.Environment()

# Give a short summary
overview = fmf.utils.listed(self.data.script, 'script')
self.info('overview', f'{overview} found', 'green')

workdir = self.step.plan.worktree
assert workdir is not None # narrow type

if not self.is_dry_run:
tmt.steps.Topology.inject(
environment=environment,
guests=self.step.plan.provision.guests(),
guest=guest,
dirpath=workdir,
filename_base=safe_filename(tmt.steps.TEST_TOPOLOGY_FILENAME_BASE, self, guest),
logger=logger
)

finish_wrapper_filename = safe_filename(FINISH_WRAPPER_FILENAME, self, guest)
finish_wrapper_path = workdir / finish_wrapper_filename

Expand Down
18 changes: 8 additions & 10 deletions tmt/steps/prepare/shell.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,16 +76,14 @@ def go(
assert workdir is not None # narrow type

if not self.is_dry_run:
topology = tmt.steps.Topology(self.step.plan.provision.guests())
topology.guest = tmt.steps.GuestTopology(guest)

environment.update(
topology.push(
dirpath=workdir,
guest=guest,
logger=logger,
filename_base=safe_filename(tmt.steps.TEST_TOPOLOGY_FILENAME_BASE, self, guest)
))
tmt.steps.Topology.inject(
environment=environment,
guests=self.step.plan.provision.guests(),
guest=guest,
dirpath=workdir,
filename_base=safe_filename(tmt.steps.TEST_TOPOLOGY_FILENAME_BASE, self, guest),
logger=logger
)

prepare_wrapper_filename = safe_filename(PREPARE_WRAPPER_FILENAME, self, guest)
prepare_wrapper_path = workdir / prepare_wrapper_filename
Expand Down
19 changes: 19 additions & 0 deletions tmt/steps/provision/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -749,6 +749,12 @@ def package_manager(self) -> 'tmt.package_managers.PackageManager':
return tmt.package_managers.find_package_manager(
self.facts.package_manager)(guest=self, logger=self._logger)

@property
def actual_hardware(self) -> tmt.hardware.BaseConstraint:
""" An actual HW configuration expressed with tmt hardware specification """

raise NotImplementedError

@classmethod
def options(cls, how: Optional[str] = None) -> list[tmt.options.ClickOptionDecoratorType]:
""" Prepare command line options related to guests """
Expand Down Expand Up @@ -863,6 +869,19 @@ def show(self, show_multihost_name: bool = True) -> None:
elif key in GUEST_FACTS_VERBOSE_FIELDS:
self.verbose(key_formatted, value_formatted, color='green')

if self.hardware and self.hardware.constraint:
self.info(
'hardware',
tmt.utils.dict_to_yaml(self.hardware.to_spec()).strip(),
color='green')

self.info(
'actual hardware',
tmt.utils.dict_to_yaml(
tmt.hardware.simplify_actual_hardware(
self.actual_hardware)).strip(),
color='green')

def _ansible_verbosity(self) -> list[str]:
""" Prepare verbose level based on the --debug option count """
if self.debug_level < 3:
Expand Down
10 changes: 10 additions & 0 deletions tmt/steps/provision/artemis.py
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,16 @@ def is_ready(self) -> bool:
# return True if self.guest is not None
return self.primary_address is not None

@property
def actual_hardware(self) -> tmt.hardware.BaseConstraint:
return tmt.hardware.parse_hw_requirements(
tmt.utils.yaml_to_dict(
f"""
arch: {self.arch}
"""
)
)

def _create(self) -> None:
environment: dict[str, Any] = {
'hw': {
Expand Down
11 changes: 11 additions & 0 deletions tmt/steps/provision/connect.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from typing import Any, Optional, Union

import tmt
import tmt.hardware
import tmt.steps
import tmt.steps.provision
import tmt.utils
Expand Down Expand Up @@ -85,6 +86,16 @@ class GuestConnect(tmt.steps.provision.GuestSsh):
soft_reboot: Optional[ShellScript]
hard_reboot: Optional[ShellScript]

@property
def actual_hardware(self) -> tmt.hardware.BaseConstraint:
return tmt.hardware.parse_hw_requirements(
tmt.utils.yaml_to_dict(
f"""
arch: {self.facts.arch}
"""
)
)

def reboot(
self,
hard: bool = False,
Expand Down
15 changes: 15 additions & 0 deletions tmt/steps/provision/local.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import dataclasses
import os
from typing import Any, Optional, Union

import tmt
import tmt.base
import tmt.hardware
import tmt.log
import tmt.steps
import tmt.steps.provision
Expand All @@ -26,6 +28,19 @@ def is_ready(self) -> bool:
""" Local is always ready """
return True

@property
def actual_hardware(self) -> tmt.hardware.BaseConstraint:
memory = os.sysconf('SC_PAGE_SIZE') * os.sysconf('SC_PHYS_PAGES')

return tmt.hardware.parse_hw_requirements(
tmt.utils.yaml_to_dict(
f"""
arch: {self.facts.arch}
memory: {memory} bytes
"""
)
)

def _run_ansible(
self,
playbook: Path,
Expand Down
10 changes: 10 additions & 0 deletions tmt/steps/provision/mrack.py
Original file line number Diff line number Diff line change
Expand Up @@ -823,6 +823,16 @@ def is_ready(self) -> bool:
except mrack.errors.MrackError:
return False

@property
def actual_hardware(self) -> tmt.hardware.BaseConstraint:
return tmt.hardware.parse_hw_requirements(
tmt.utils.yaml_to_dict(
f"""
arch: {self.arch}
"""
)
)

def _create(self, tmt_name: str) -> None:
""" Create beaker job xml request and submit it to Beaker hub """

Expand Down
11 changes: 11 additions & 0 deletions tmt/steps/provision/podman.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import tmt
import tmt.base
import tmt.hardware
import tmt.log
import tmt.steps
import tmt.steps.provision
Expand Down Expand Up @@ -114,6 +115,16 @@ def is_ready(self) -> bool:
))
return str(cmd_output.stdout).strip() == 'true'

@property
def actual_hardware(self) -> tmt.hardware.BaseConstraint:
return tmt.hardware.parse_hw_requirements(
tmt.utils.yaml_to_dict(
f"""
arch: {self.facts.arch}
"""
)
)

def wake(self) -> None:
""" Wake up the guest """
self.debug(
Expand Down
Loading
Loading