GitHub action to use systemd-nspawn
to run commands in a (un)booted container on a Raspberry Pi SD card image
systemd-nspawn
is
used to run commands in a light-weight namespace container, like chroot but with full virtualization
of the file system hierarchy, the process tree, the various IPC subsystems, and the host and domain
name. It can also be used to boot the image's init program (which is usually systemd) as an OS; this
action makes it easy to run a set of shell commands whether or not the OS is booted in the
container.
Note that you cannot start or interact with the Docker daemon inside a container booted with
systemd-nspawn
; instead, you should perform those operations inside a booted QEMU VM attached to
your image, e.g. using the ethanjli/piqemu-action
GitHub action.
- name: Install and run cowsay
uses: ethanjli/pinspawn-action@v0.1.3
with:
image: rpi-os-image.img
run: |
apt-get update
apt-get install -y cowsay
/usr/games/cowsay 'I am running in a light-weight namespace container!'
- name: Run in Python
uses: ethanjli/pinspawn-action@v0.1.3
with:
image: rpi-os-image.img
shell: python
run: |
import platform
for word in reversed(['!', platform.python_version(), 'Python', 'in', 'running', 'am', 'I']):
print(word, end=' ')
- name: Run without root permissions
uses: ethanjli/pinspawn-action@v0.1.3
with:
image: rpi-os-image.img
user: pi
shell: sh
run: |
sudo apt-get update
sudo apt-get install -y figlet
figlet -f digital "I am $USER in $SHELL!"
- name: Make a script on the host
uses: 1arp/create-a-file-action@0.4.5
with:
file: figlet.sh
content: |
#!/usr/bin/env -S bash -eux
figlet -f digital "I am $USER in $SHELL!"
- name: Make the script executable
run: chmod a+x figlet.sh
- name: Run script directly
uses: ethanjli/pinspawn-action@v0.1.3
with:
image: rpi-os-image.img
args: --bind "$(pwd)":/run/external
user: pi
shell: /run/external/figlet.sh
- name: Make a bootloader configuration snippet
uses: 1arp/create-a-file-action@0.4.5
with:
file: boot-config.snippet
content: |
# Enable support for the RV3028 RTC
dtoverlay=i2c-rtc,rv3028,trickle-resistor-ohms=3000,backup-switchover-mode=1
- name: Modify bootloader configuration
uses: ethanjli/pinspawn-action@v0.1.3
with:
image: rpi-os-image.img
args: --bind "$(pwd)":/run/external
run: |
cat /run/external/boot-config.snippet >> /boot/config.txt
cp /boot/config.txt /run/external/boot.config
- name: Print the bootloader config
run: cat boot.config
Note: the system in the container will shut down after the specified commands finish running.
- name: Analyze systemd boot process
uses: ethanjli/pinspawn-action@v0.1.3
with:
image: rpi-os-image.img
args: --bind "$(pwd)":/run/external
boot: true
run: |
while ! systemd-analyze 2>/dev/null; do
echo "Waiting for boot to finish..."
sleep 5
done
systemd-analyze critical-chain | cat
systemd-analyze blame | cat
systemd-analyze plot > /run/external/bootup-timeline.svg
echo "Done!"
- name: Upload the bootup timeline to Job Artifacts
uses: actions/upload-artifact@v4
with:
name: bootup-timeline
path: bootup-timeline.svg
if-no-files-found: error
overwrite: true
Inputs:
Input | Allowed values | Required? | Description |
---|---|---|---|
image |
file path | yes | Path of the image to use for the container. |
args |
systemd-nspawn options/args |
no (default ``) | Options, args, and/or a command to pass to systemd-nspawn . |
shell |
``, bash , `sh`, `python`, etc. |
no (default ``) | The shell to use for running commands. |
run |
shell commands | no (default ``) | Commands to run in the shell. |
user |
name of user in image | no (default root ) |
The user to run commands as. |
boot |
false , true |
no (default false ) |
Boot the image's init program (usually systemd) as PID 1. |
run-service |
file path | no (default ``) | systemd service to run shell with the run commands. |
-
image
must be the path of an unmounted raw disk image (such as a Raspberry Pi OS SD card image), where partition 2 should be mounted as the root filesystem (i.e./
) and partition 1 should be mounted to/boot
. -
args
can be a list of command-line options/arguments forsystemd-nspawn
. You should not set the--user
or--boot
flags here; instead, you should set theuser
andboot
action inputs. -
If
run
is not left empty,shell
will be used to execute commands specified in therun
input. You can use built-inshell
keywords, or you can define a custom set of shell options. The shell command that is run internally executes a temporary file that contains the commands to run, like in GitHub Actions. Please refer to the GitHub Actions semantics of theshell
keyword of job steps for details about the behavior of this action'sshell
input.If you just want to run a single script, you can leave
run
empty and provide that script as theshell
input. However, you will need to set the appropriate permissions on the script file. -
If
boot
is enabled, this action will usesystemd-nspawn
to automatically search for an init program in the image (typically systemd) and invoke it as PID 1, instead of a shell.-
The provided
run
commands will be triggered by a temporary system service defined with the following template (unless you specify a different service file template using therun-service
input):[Unit] Description=Run commands in booted OS After=getty.target [Service] Type=exec ExecStart=bash -c "\ su - {user} -c '{command}; echo $? | tee {result}'; \ echo Shutting down...; \ shutdown now \ " & StandardOutput=tty [Install] WantedBy=getty.target
This service file template has string interpolation applied to the following strings:
{user}
will be replaced with the value of the action'suser
input.{command}
will be replaced with a command to run your specifiedrun
commands using your specifiedshell
{result}
will be replaced with the path of a temporary file whose contents will be checked after the container finishes running to determine whether the command finished successfully (in which case the file should be the string0
); this file is interpreted as holding a return code.
-
If this flag is enabled, then any arguments specified as the command line in
args
are used as arguments for the init program, i.e.systemd-nspawn
will be invoked likesystemd-nspawn --boot {args}
.
-