diff --git a/docs/releases.rst b/docs/releases.rst index 63b1d8a2fb..48c188dad5 100644 --- a/docs/releases.rst +++ b/docs/releases.rst @@ -36,6 +36,11 @@ contrast to the :ref:`/spec/core/contact` key, this field is not supposed to be updated and can be useful when trying to track down the original author for consultation. +The ``container`` executor now works in `Fedora Toolbx`__ when Podman is run +using ``flatpak-spawn --host`` on the host system. + +__ https://docs.fedoraproject.org/en-US/fedora-silverblue/toolbox/ + tmt-1.41.0 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests/provision/container/toolbox/main.fmf b/tests/provision/container/toolbox/main.fmf new file mode 100644 index 0000000000..5dc7b4f4f4 --- /dev/null +++ b/tests/provision/container/toolbox/main.fmf @@ -0,0 +1,24 @@ +summary: Test container provisioner in toolbox +description: + Verify that container provisioner works well when tmt is run from + a toolbox container and podman is run on the host system using + `flatpak-spawn --host`. This is a common setup used in Fedora + Silverblue. + +require: + - toolbox +tag+: + - provision-only + - provision-container +require+: + - toolbox +adjust+: + - enabled: false + when: distro != fedora + because: Setting up toolbox on CS9 with default UBI9 toolbox image is a pain. + - check: + - how: avc + result: xfail + when: distro >= fedora-41 + because: | + We are not interested in AVCs for this test due to complicated setup. diff --git a/tests/provision/container/toolbox/podman_wrapper b/tests/provision/container/toolbox/podman_wrapper new file mode 100755 index 0000000000..a1792117bf --- /dev/null +++ b/tests/provision/container/toolbox/podman_wrapper @@ -0,0 +1,2 @@ +#!/bin/bash +flatpak-spawn --host podman "$@" diff --git a/tests/provision/container/toolbox/test.sh b/tests/provision/container/toolbox/test.sh new file mode 100755 index 0000000000..a1d7e3a452 --- /dev/null +++ b/tests/provision/container/toolbox/test.sh @@ -0,0 +1,84 @@ +#!/bin/bash +. /usr/share/beakerlib/beakerlib.sh || exit 1 + +# Use `tmt try` to run this test locally, running directly the script will not work. + +rlJournalStart + rlPhaseStartSetup + rlRun "toolbox_container_name=\$(uuidgen)" 0 "Generate toolbox container name" + rlRun "toolbox_user=toolbox" 0 "Set user for running toolbox" + rlPhaseEnd + + rlPhaseStartTest "Create toolbox container" + # Add a toolbox user. Running toolbox under root user does not work well, + # so a separate user account is created. + rlRun "useradd $toolbox_user" + rlRun "toolbox_user_id=$(id -u $toolbox_user)" + + # Make sure systemd user session runs for the new user. The user session + # hosts a dbus session, which is required for toolbox. + rlRun "loginctl enable-linger $toolbox_user" + + # Add required environment variables for toolbox to the user's environment. + rlRun "echo export XDG_RUNTIME_DIR=/run/user/$toolbox_user_id >> /home/$toolbox_user/.bashrc" + rlRun "echo export DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$toolbox_user_id/bus >> /home/$toolbox_user/.bashrc" + + rlRun "sudo -iu $toolbox_user toolbox create -y $toolbox_container_name" + rlPhaseEnd + + toolbox_run() { + local command="sudo -iu $toolbox_user toolbox run --container $toolbox_container_name $*" + echo "Command: $command" + eval "$command" + } + + rlPhaseStartTest "Local execution via tmt: Install tmt from TMT_TREE" + TOOLBOX_TREE="/var/tmp/tree" + TMT_COMMAND="env -C ${TOOLBOX_TREE} hatch -e dev run env -C /tmp tmt" + + rlRun "type toolbox_run" + + # Install make and hatch + rlRun "toolbox_run sudo dnf -y install make hatch" + + # Create a copy of the tmt tree, to mitigate possible permission issues + rlRun "cp -Rf ${TMT_TREE} ${TOOLBOX_TREE}" + + # Copy tmt project into the toolbox container + rlRun "sudo -iu ${toolbox_user} podman cp ${TOOLBOX_TREE} $toolbox_container_name:${TOOLBOX_TREE}" + + # Fix permissions for the toolbox user + rlRun "toolbox_run sudo chown -Rf ${toolbox_user}:${toolbox_user} ${TOOLBOX_TREE}" + + # Initialize git in tmt tree, it is required for development installation + # and the tmt tree is not a git repository. + rlRun "toolbox_run git -C ${TOOLBOX_TREE} init" + + # Install additional development dependencies + rlRun "toolbox_run make -C ${TOOLBOX_TREE} develop" + rlPhaseEnd + + rlPhaseStartTest "Print tmt version installed in toolbox" + rlRun "toolbox_run $TMT_COMMAND --version" + rlPhaseEnd + + rlPhaseStartTest "Add podman wrapper" + # Copy the wrapper from the toolbox user, the containers are local to the user. + # Need to use a copy of the wrapper, the TMT_TREE is a volume mount and thus + # it is not accessible to the toolbox user. + rlRun "cp podman_wrapper /tmp/podman_wrapper" + rlRun "sudo -iu ${toolbox_user} podman cp /tmp/podman_wrapper $toolbox_container_name:/usr/bin/podman" + rlRun "toolbox_run podman --version" + rlPhaseEnd + + rlPhaseStartTest "Verify container provisioner works from toolbox" + rlRun RUNID="$(mktemp -u)" + rlRun -s "toolbox_run env -C /tmp ${TMT_COMMAND} run -i ${RUNID} -a -vvv provision -h container -i registry.fedoraproject.org/fedora:latest execute -h tmt -s \\\"echo hello from container\\\"" + rlAssertGrep "content: hello from container" $rlRun_LOG + rlPhaseEnd + + rlPhaseStartCleanup + rlRun "toolbox rm -f $toolbox_container_name" 0 "Remove toolbox container" + rlRun "userdel -rf toolbox" + rlPhaseEnd +rlJournalEnd diff --git a/tmt/steps/provision/__init__.py b/tmt/steps/provision/__init__.py index 1c04d4c556..2499382bfc 100644 --- a/tmt/steps/provision/__init__.py +++ b/tmt/steps/provision/__init__.py @@ -329,6 +329,8 @@ class GuestFacts(SerializableContainer): has_selinux: Optional[bool] = None is_superuser: Optional[bool] = None is_ostree: Optional[bool] = None + is_toolbox: Optional[bool] = None + toolbox_container_name: Optional[str] = None #: Various Linux capabilities and whether they are permitted to #: commands executed on this guest. @@ -589,6 +591,47 @@ def _query_is_ostree(self, guest: 'Guest') -> Optional[bool]: return output.stdout.strip() == 'yes' + def _query_is_toolbox(self, guest: 'Guest') -> Optional[bool]: + # https://www.reddit.com/r/Fedora/comments/g6flgd/toolbox_specific_environment_variables/ + output = self._execute( + guest, + Command( + tmt.utils.DEFAULT_SHELL, + '-c', + 'if [ -e /run/.toolboxenv ]; then echo yes; else echo no; fi')) + + if output is None or output.stdout is None: + return None + + return output.stdout.strip() == 'yes' + + def _query_toolbox_container_name(self, guest: 'Guest') -> Optional[str]: + output = self._execute( + guest, + Command( + tmt.utils.DEFAULT_SHELL, + '-c', + 'if [ -e /run/.containerenv ]; then echo yes; else echo no; fi')) + + if output is None or output.stdout is None: + return None + + if output.stdout.strip() == 'no': + return None + + output = self._execute( + guest, + Command('cat', '/run/.containerenv')) + + if output is None or output.stdout is None: + return None + + for line in output.stdout.splitlines(): + if line.startswith('name="'): + return line[6:-1] + + return None + def _query_capabilities(self, guest: 'Guest') -> dict[GuestCapability, bool]: # TODO: there must be a canonical way of getting permitted capabilities. # For now, we're interested in whether we can access kernel message buffer. @@ -610,6 +653,8 @@ def sync(self, guest: 'Guest') -> None: self.has_selinux = self._query_has_selinux(guest) self.is_superuser = self._query_is_superuser(guest) self.is_ostree = self._query_is_ostree(guest) + self.is_toolbox = self._query_is_toolbox(guest) + self.toolbox_container_name = self._query_toolbox_container_name(guest) self.capabilities = self._query_capabilities(guest) self.in_sync = True diff --git a/tmt/steps/provision/podman.py b/tmt/steps/provision/podman.py index f5021e570d..92514d4ed4 100644 --- a/tmt/steps/provision/podman.py +++ b/tmt/steps/provision/podman.py @@ -383,10 +383,22 @@ def push( self._run_guest_command(Command( "chcon", "--recursive", "--type=container_file_t", self.parent.plan.workdir ), shell=False, silent=True) + # In case explicit destination is given, use `podman cp` to copy data - # to the container + # to the container. If running in toolbox, make sure to copy from the toolbox + # container instead of localhost. if source and destination: - self.podman(Command("cp", source, f"{self.container}:{destination}")) + container_name: Optional[str] = None + if self.parent.plan.my_run.runner.facts.is_toolbox: + container_name = self.parent.plan.my_run.runner.facts.toolbox_container_name + self.podman( + Command( + "cp", + f"{container_name}:{source}" + if container_name else source, + f"{self.container}:{destination}" + ) + ) def pull( self,