Skip to content

Commit

Permalink
Support container provisioner in toolbox (#3228)
Browse files Browse the repository at this point in the history
For Fedora Silverblue users it is common to run podman
via `flatpak-spawn --host` which runs podman on the host
system itself. This requires to pass the toolbox container
name when running `podman cp` to correctly copy stuff
from the toolbox container, where `tmt` is installed
to the provisioned container.

Fixes #1020

Signed-off-by: Miroslav Vadkerti <mvadkert@redhat.com>
  • Loading branch information
thrix authored and happz committed Jan 31, 2025
1 parent 921cdd3 commit 1742b14
Show file tree
Hide file tree
Showing 6 changed files with 174 additions and 2 deletions.
5 changes: 5 additions & 0 deletions docs/releases.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Expand Down
24 changes: 24 additions & 0 deletions tests/provision/container/toolbox/main.fmf
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 2 additions & 0 deletions tests/provision/container/toolbox/podman_wrapper
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/bin/bash
flatpak-spawn --host podman "$@"
84 changes: 84 additions & 0 deletions tests/provision/container/toolbox/test.sh
Original file line number Diff line number Diff line change
@@ -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
45 changes: 45 additions & 0 deletions tmt/steps/provision/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand Down
16 changes: 14 additions & 2 deletions tmt/steps/provision/podman.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down

0 comments on commit 1742b14

Please sign in to comment.