From 4d0abe11838fb107977850f31960d287dbe26605 Mon Sep 17 00:00:00 2001 From: Miroslav Vadkerti Date: Fri, 20 Sep 2024 01:40:14 +0200 Subject: [PATCH] Support Fedora Image Mode To support Fedora Image mode the following changes were required: * Fix `rsync` installation. The tool is not included in the distribution and the installation method was completely broken, i.e. `rpm-ostree` has no `--install-root` option. We did not hit it as there is no good testing environment available that would cover this. * Introduce `TMT_SCRIPTS_DEST_DIR` environment variable set by default to `/var/tmp/tmt/bin` which hosts the `tmt` scripts. The original path is not writable on these systems. The `PATH` is set system wide via a new `/etc/profile.d/tmt.sh` script. Signed-off-by: Miroslav Vadkerti --- .github/workflows/shellcheck.yml | 1 + tmt/steps/execute/__init__.py | 91 ++++++++++++++++++++++++----- tmt/steps/execute/scripts/tmt.sh.j2 | 4 ++ tmt/steps/provision/__init__.py | 18 +----- 4 files changed, 83 insertions(+), 31 deletions(-) create mode 100644 tmt/steps/execute/scripts/tmt.sh.j2 diff --git a/.github/workflows/shellcheck.yml b/.github/workflows/shellcheck.yml index 40a31c0d4e..a29671f37f 100644 --- a/.github/workflows/shellcheck.yml +++ b/.github/workflows/shellcheck.yml @@ -28,6 +28,7 @@ jobs: exclude-path: | tests/** examples/**/test.sh + tmt/steps/execute/scripts/tmt.sh.j2 tmt/templates/** token: ${{ secrets.GITHUB_TOKEN }} diff --git a/tmt/steps/execute/__init__.py b/tmt/steps/execute/__init__.py index 2e54718c02..ce663a9547 100644 --- a/tmt/steps/execute/__init__.py +++ b/tmt/steps/execute/__init__.py @@ -5,6 +5,7 @@ import os import signal as _signal import subprocess +import tempfile import threading from contextlib import suppress from dataclasses import dataclass @@ -27,6 +28,7 @@ from tmt.steps.discover import Discover, DiscoverPlugin, DiscoverStepData from tmt.steps.provision import Guest from tmt.utils import ( + Command, Path, ShellScript, Stopwatch, @@ -34,6 +36,7 @@ format_duration, format_timestamp, ) +from tmt.utils.templates import render_template_file if TYPE_CHECKING: import tmt.cli @@ -58,6 +61,9 @@ # Scripts source directory SCRIPTS_SRC_DIR = tmt.utils.resource_files('steps/execute/scripts') +#: The default scripts destination directory +SCRIPTS_DEST_DIR = Path("/var/tmp/tmt/bin") # noqa: S108 insecure usage of temporary dir + @dataclass class Script: @@ -75,12 +81,36 @@ class ScriptCreatingFile(Script): created_file: str +@dataclass +class ScriptTemplate(Script): + """ + Represents a Jinja2 templated script. + The source filename must have a ``.j2`` suffix. + """ + + context: dict[str, str] + + +def effective_scripts_dest_dir() -> Path: + """ + Find out what the actual scripts destination directory is. + + If ``TMT_SCRIPTS_DEST_DIR`` variable is set, it is used as the scripts destination + directory. Otherwise, the default of :py:data:`SCRIPTS_DEST_DIR` is used. + """ + + if 'TMT_SCRIPTS_DEST_DIR' in os.environ: + return Path(os.environ['TMT_SCRIPTS_DEST_DIR']) + + return SCRIPTS_DEST_DIR + + # Script handling reboots, in restraint compatible fashion TMT_REBOOT_SCRIPT = ScriptCreatingFile( - path=Path("/usr/local/bin/tmt-reboot"), + path=effective_scripts_dest_dir() / 'tmt-reboot', aliases=[ - Path("/usr/local/bin/rstrnt-reboot"), - Path("/usr/local/bin/rhts-reboot")], + effective_scripts_dest_dir() / 'rstrnt-reboot', + effective_scripts_dest_dir() / 'rhts-reboot'], related_variables=[ "TMT_REBOOT_COUNT", "REBOOTCOUNT", @@ -89,43 +119,54 @@ class ScriptCreatingFile(Script): ) TMT_REBOOT_CORE_SCRIPT = Script( - path=Path("/usr/local/bin/tmt-reboot-core"), + path=effective_scripts_dest_dir() / 'tmt-reboot-core', aliases=[], related_variables=[]) # Script handling result reporting, in restraint compatible fashion TMT_REPORT_RESULT_SCRIPT = ScriptCreatingFile( - path=Path("/usr/local/bin/tmt-report-result"), + path=effective_scripts_dest_dir() / 'tmt-report-result', aliases=[ - Path("/usr/local/bin/rstrnt-report-result"), - Path("/usr/local/bin/rhts-report-result")], + effective_scripts_dest_dir() / 'rstrnt-report-result', + effective_scripts_dest_dir() / 'rhts-report-result'], related_variables=[], created_file="tmt-report-results.yaml" ) # Script for archiving a file, usable for BEAKERLIB_COMMAND_SUBMIT_LOG TMT_FILE_SUBMIT_SCRIPT = Script( - path=Path("/usr/local/bin/tmt-file-submit"), + path=effective_scripts_dest_dir() / 'tmt-file-submit', aliases=[ - Path("/usr/local/bin/rstrnt-report-log"), - Path("/usr/local/bin/rhts-submit-log"), - Path("/usr/local/bin/rhts_submit_log")], + effective_scripts_dest_dir() / 'rstrnt-report-log', + effective_scripts_dest_dir() / 'rhts-submit-log', + effective_scripts_dest_dir() / 'rhts_submit_log'], related_variables=[] ) # Script handling text execution abortion, in restraint compatible fashion TMT_ABORT_SCRIPT = ScriptCreatingFile( - path=Path("/usr/local/bin/tmt-abort"), + path=effective_scripts_dest_dir() / 'tmt-abort', aliases=[ - Path("/usr/local/bin/rstrnt-abort"), - Path("/usr/local/bin/rhts-abort")], + effective_scripts_dest_dir() / 'rstrnt-abort', + effective_scripts_dest_dir() / 'rhts-abort'], related_variables=[], created_file="abort" ) +# Profile script for adding SCRIPTS_DEST_DIR to executable pats system-wide +TMT_ETC_PROFILE_D = ScriptTemplate( + path=Path("/etc/profile.d/tmt.sh"), + aliases=[], + related_variables=[], + context={ + 'scripts_dest_dir': str(effective_scripts_dest_dir()) + }) + + # List of all available scripts SCRIPTS = ( TMT_ABORT_SCRIPT, + TMT_ETC_PROFILE_D, TMT_FILE_SUBMIT_SCRIPT, TMT_REBOOT_SCRIPT, TMT_REBOOT_CORE_SCRIPT, @@ -595,12 +636,30 @@ def prepare_tests(self, guest: Guest, logger: tmt.log.Logger) -> list[TestInvoca return invocations + def _render_script_template(self, source: Path, context: dict[str, str]) -> Path: + """ Render script template with given context """ + + with tempfile.NamedTemporaryFile(mode='w', delete=False) as rendered_script: + rendered_script.write(render_template_file(source, None, **context)) + + return Path(rendered_script.name) + def prepare_scripts(self, guest: "tmt.steps.provision.Guest") -> None: """ Prepare additional scripts for testing """ + # Create scripts directory + guest.execute(Command("mkdir", "-p", str(SCRIPTS_DEST_DIR))) + # Install all scripts on guest for script in self.scripts: source = SCRIPTS_SRC_DIR / script.path.name + # Render script template + if isinstance(script, ScriptTemplate): + source = self._render_script_template( + SCRIPTS_SRC_DIR / f"{script.path.name}.j2", + context=script.context + ) + for dest in [script.path, *script.aliases]: guest.push( source=source, @@ -608,6 +667,10 @@ def prepare_scripts(self, guest: "tmt.steps.provision.Guest") -> None: options=["-p", "--chmod=755"], superuser=guest.facts.is_superuser is not True) + # Remove script template source + if isinstance(script, ScriptTemplate): + os.unlink(source) + def _tmt_report_results_filepath(self, invocation: TestInvocation) -> Path: """ Create path to test's ``tmt-report-result`` file """ diff --git a/tmt/steps/execute/scripts/tmt.sh.j2 b/tmt/steps/execute/scripts/tmt.sh.j2 new file mode 100644 index 0000000000..a916794965 --- /dev/null +++ b/tmt/steps/execute/scripts/tmt.sh.j2 @@ -0,0 +1,4 @@ +# shellcheck shell=bash + +# tmt provides executable scripts under this path +export PATH={{ scripts_dest_dir }}:$PATH diff --git a/tmt/steps/provision/__init__.py b/tmt/steps/provision/__init__.py index 049ae27d84..619a78c4c6 100644 --- a/tmt/steps/provision/__init__.py +++ b/tmt/steps/provision/__init__.py @@ -1303,23 +1303,7 @@ def _check_rsync(self) -> CheckRsyncOutcome: except tmt.utils.RunError: pass - # Install under '/root/pkg' for read-only distros - # (for now the check is based on 'rpm-ostree' presence) - # FIXME: Find a better way how to detect read-only distros - # self.debug("Check for a read-only distro.") - if self.facts.package_manager == 'rpm-ostree': - self.package_manager.install( - Package('rsync'), - options=tmt.package_managers.Options( - install_root=Path('/root/pkg'), - release_version='/' - ) - ) - - self.execute(Command('ln', '-sf', '/root/pkg/bin/rsync', '/usr/local/bin/rsync')) - - else: - self.package_manager.install(Package('rsync')) + self.package_manager.install(Package('rsync')) return CheckRsyncOutcome.INSTALLED