Skip to content

Commit

Permalink
Expose actual guest HW description in guest topology
Browse files Browse the repository at this point in the history
Related to #2402. The patch
bootstraps `hardware` field in the guest topology content, and populates
it it very basic content. Extending it with more info, e.g. `disk` or
`system`, will be task for future patches.
  • Loading branch information
happz committed Dec 31, 2023
1 parent c7ab83a commit 0101b5e
Show file tree
Hide file tree
Showing 13 changed files with 189 additions and 20 deletions.
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.31
Guest HW exposed via ``hardware`` key.

example:
- |
# A trivial pseudo-test script
Expand Down
46 changes: 46 additions & 0 deletions tmt/steps/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@

import tmt.base
import tmt.cli
import tmt.hardware
import tmt.plugins
import tmt.steps.discover
import tmt.steps.execute
Expand Down Expand Up @@ -1753,11 +1754,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.guest
self.hardware = guest.actual_hardware.to_spec()


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

return environment

@classmethod
def inject(
cls,
*,
environment: EnvironmentType,
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 QueuedPhase(GuestlessTask, Task, Generic[StepDataT]):
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 @@ -23,7 +23,7 @@
from tmt.steps import Action, PhaseQueue, QueuedPhase, 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 EnvironmentType, Path, ShellScript, Stopwatch, cached_property, field

if TYPE_CHECKING:
import tmt.cli
Expand Down Expand Up @@ -218,6 +218,22 @@ def reboot_requested(self) -> bool:
""" Whether a guest reboot has been requested by the test """
return self.reboot_request_path.exists()

def inject_topology(self, environment: EnvironmentType) -> 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_reboot(self) -> bool:
"""
Reboot the guest 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 @@ -309,13 +309,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,6 +67,8 @@ def go(
""" Perform finishing tasks on given guest """
super().go(guest=guest, environment=environment, logger=logger)

environment = environment or {}

# Give a short summary
scripts: list[tmt.utils.ShellScript] = self.get('script')
overview = fmf.utils.listed(scripts, 'script')
Expand All @@ -75,6 +77,16 @@ def go(
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 @@ -78,16 +78,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
17 changes: 17 additions & 0 deletions tmt/steps/provision/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -601,6 +601,12 @@ def is_ready(self) -> bool:

raise NotImplementedError

@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 @@ -708,6 +714,17 @@ 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(self.actual_hardware.to_spec()).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 @@ -491,6 +491,16 @@ def is_ready(self) -> bool:
# return True if self.guest is not None
return self.guest 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 Optional, Union

import tmt
import tmt.hardware
import tmt.steps
import tmt.steps.provision
import tmt.utils
Expand Down Expand Up @@ -68,6 +69,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
16 changes: 16 additions & 0 deletions tmt/steps/provision/local.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import dataclasses
import os
import platform
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 +29,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: {platform.machine()}
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 @@ -559,6 +559,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
12 changes: 12 additions & 0 deletions tmt/steps/provision/podman.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import dataclasses
import os
import platform
from shlex import quote
from typing import Any, Optional, Union, cast

import tmt
import tmt.base
import tmt.hardware
import tmt.log
import tmt.steps
import tmt.steps.provision
Expand Down Expand Up @@ -118,6 +120,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: {platform.machine()}
"""
)
)

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

0 comments on commit 0101b5e

Please sign in to comment.