From 61eb829715b5da462ddd680c0410a069bc5c039f Mon Sep 17 00:00:00 2001 From: Christian Breunig Date: Thu, 3 Sep 2026 22:23:32 +0200 Subject: oci: T9269: mask systemd services for container startup Adjust systemd units for containerized operation - this is what the containerlab documentation asks users to do in their Dockerfile, but as we ship a ready made rootfs there is no build stage where "systemctl" could be run. Masking and disabling is nothing but symlink handling below /etc/systemd, so we can do it offline (and cross-architecture) right here. --- scripts/iso-to-oci | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) (limited to 'scripts') diff --git a/scripts/iso-to-oci b/scripts/iso-to-oci index 86dd8687..2505d3fd 100755 --- a/scripts/iso-to-oci +++ b/scripts/iso-to-oci @@ -69,6 +69,29 @@ sed -i 's/^LANG=.*$/LANG=C.UTF-8/' "${UNSQUASHFS}/etc/default/locale" printf 'Welcome to VyOS - \\n \\l\n\n' > "${UNSQUASHFS}/etc/issue" : > "${UNSQUASHFS}/etc/issue.net" +# adjust systemd units for containerized operation - this is what the +# containerlab documentation asks users to do in their Dockerfile, but as we +# ship a ready made rootfs there is no build stage where "systemctl" could be +# run. Masking and disabling is nothing but symlink handling below /etc/systemd, +# so we can do it offline (and cross-architecture) right here. +# +# masked units: getty(8) would fight with the container console, auditd(8) has +# no business inside a container as it requires the audit netlink socket and +# atopacctd(8) enables BSD process accounting via acct(2) which is global to +# the kernel and not namespaced - the first container to start it wins, every +# other one fails and turns "systemctl is-system-running" into "degraded", +# which is exactly what the containerlab healthcheck looks at +for unit in getty.target auditd.service atopacct.service; do + ln -sf /dev/null "${UNSQUASHFS}/etc/systemd/system/${unit}" +done + +# disabled units: kea-dhcp-ddns-server(8) is started via a "WantedBy" symlink +# and would fail on boot, VyOS enables it on demand from the CLI +for unit in kea-dhcp-ddns-server.service; do + find "${UNSQUASHFS}/etc/systemd/system" -name "${unit}" \ + \( -path '*.wants/*' -o -path '*.requires/*' \) -delete +done + # optional step: Decrease docker image size by deleting not necessary files for container rm -rf "${UNSQUASHFS}/boot" rm -rf "${UNSQUASHFS}/lib/firmware/" -- cgit v1.2.3 From 511e0333f7d6d1126c5154cad6c011de2374130d Mon Sep 17 00:00:00 2001 From: Christian Breunig Date: Sun, 6 Sep 2026 21:15:58 +0200 Subject: oci: T9269: add alive healthcheck command for image import containerlab uses the image healthcheck to determine when a VyOS node is ready, see https://containerlab.dev/manual/kinds/vyosnetworks_vyos/ A rootfs tarball carries no OCI image configuration, so this can only be attached when the tarball is imported. --- scripts/iso-to-oci | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) (limited to 'scripts') diff --git a/scripts/iso-to-oci b/scripts/iso-to-oci index 2505d3fd..32efbbbf 100755 --- a/scripts/iso-to-oci +++ b/scripts/iso-to-oci @@ -119,9 +119,16 @@ ln -s /opt/vyatta/etc/config "${UNSQUASHFS}/config" echo "I: generate OCI container image ${OCI_IMAGE}" XZ_OPT="-T0" tar --directory "${UNSQUASHFS}" --create . --xz --file "${OCI_IMAGE}" +# containerlab uses the image healthcheck to determine when a VyOS node is +# ready, see https://containerlab.dev/manual/kinds/vyosnetworks_vyos/ +# A rootfs tarball carries no OCI image configuration, so this can only be +# attached when the tarball is imported +HEALTHCHECK='HEALTHCHECK --start-period=10s CMD systemctl is-system-running' + echo "I: to import the previously generated OCI image to your local images run:" echo "" -echo " docker import --platform=linux/$ARCH $OCI_IMAGE vyos/vyos:$VERSION --change 'CMD [\"/sbin/init\"]'" +echo " docker import --platform=linux/$ARCH $OCI_IMAGE vyos/vyos:$VERSION \\" +echo " --change 'CMD [\"/sbin/init\"]' --change '$HEALTHCHECK'" echo "" cleanup -- cgit v1.2.3 From ffa0733bcc7f512fb0e1ab1a0bf4836952406057 Mon Sep 17 00:00:00 2001 From: Christian Breunig Date: Sun, 6 Sep 2026 19:37:24 +0000 Subject: oci: T9269: remove podman runtime from container image Running containers inside the VyOS container is not supported and the container CLI nodes are stripped from the image anyway, so podman(8) and its helpers are dead weight - about 110 MiB of the uncompressed rootfs (podman 43 MiB, podman-remote 32 MiB, netavark and aardvark-dns 21 MiB, runc 8 MiB), which is roughly 32 MiB of the compressed tarball. Remove the binaries along with everything that would otherwise dangle: the podman and netavark systemd units - podman.service is enabled via a "WantedBy" symlink and would fail on boot - the quadlet generators, the tmpfiles.d snippet and the configuration shipped by containers-common. The container operational mode templates are removed as well, they would only greet the user with a traceback now that podman is gone. --- scripts/iso-to-oci | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) (limited to 'scripts') diff --git a/scripts/iso-to-oci b/scripts/iso-to-oci index 32efbbbf..a0e5e87c 100755 --- a/scripts/iso-to-oci +++ b/scripts/iso-to-oci @@ -99,9 +99,38 @@ rm -rf "${UNSQUASHFS}/usr/lib/x86_64-linux-gnu/libwireshark.so*" rm -rf "${UNSQUASHFS}/lib/modules/*-vyos" rm -rf "${UNSQUASHFS}/root/.gnupg" +# podman(8) is useless inside the container as we do not support running +# containers in containers - the CLI nodes are removed below anyway. Dropping +# the runtime and its network helpers (netavark, aardvark-dns) saves about +# 110 MiB of the uncompressed rootfs. Nothing but the container CLI calls +# these, so they can go entirely - including the systemd units, the quadlet +# generators and the configuration shipped by containers-common +rm -f "${UNSQUASHFS}/etc/systemd/system/default.target.wants/podman.service" +rm -f "${UNSQUASHFS}/usr/lib/systemd/system/podman.service" +rm -f "${UNSQUASHFS}/usr/lib/systemd/system/podman.socket" +rm -f "${UNSQUASHFS}/usr/lib/systemd/system/netavark-dhcp-proxy.service" +rm -f "${UNSQUASHFS}/usr/lib/systemd/system/netavark-dhcp-proxy.socket" +rm -f "${UNSQUASHFS}/usr/lib/systemd/system/netavark-firewalld-reload.service" +rm -f "${UNSQUASHFS}/usr/lib/systemd/system-generators/podman-system-generator" +rm -f "${UNSQUASHFS}/usr/lib/systemd/user-generators/podman-user-generator" +rm -f "${UNSQUASHFS}/usr/lib/tmpfiles.d/podman.conf" +rm -rf "${UNSQUASHFS}/usr/lib/podman" +rm -rf "${UNSQUASHFS}/usr/libexec/podman" +rm -f "${UNSQUASHFS}/usr/bin/podman" "${UNSQUASHFS}/usr/bin/podman-remote" \ + "${UNSQUASHFS}/usr/bin/podmansh" "${UNSQUASHFS}/usr/bin/conmon" \ + "${UNSQUASHFS}/usr/bin/fuse-overlayfs" "${UNSQUASHFS}/usr/bin/runc" \ + "${UNSQUASHFS}/usr/sbin/runc" +rm -rf "${UNSQUASHFS}/etc/containers" +rm -rf "${UNSQUASHFS}/usr/share/containers" + # delete features not supported in container - only remove the node.def files, -# this is sufficient to not make the feature pop up on the CLI +# this is sufficient to not make the feature pop up on the CLI. The container +# operational mode commands go as well, they would only greet the user with a +# traceback now that podman is gone rm -rf "${UNSQUASHFS}/opt/vyatta/share/vyatta-cfg/templates/container" +for tree in add connect delete generate restart show update; do + rm -rf "${UNSQUASHFS}/opt/vyatta/share/vyatta-op/templates/${tree}/container" +done rm -rf "${UNSQUASHFS}/opt/vyatta/share/vyatta-cfg/templates/system/console" rm -rf "${UNSQUASHFS}/opt/vyatta/share/vyatta-cfg/templates/system/option/kernel" rm -rf "${UNSQUASHFS}/opt/vyatta/share/vyatta-cfg/templates/system/option/startup-beep" -- cgit v1.2.3 From f4a7506710b172267e02ce6dd146066f61b5850c Mon Sep 17 00:00:00 2001 From: Christian Breunig Date: Sun, 6 Sep 2026 20:12:50 +0000 Subject: oci: T9269: show a progress spinner for long running steps Unpacking the squashfs and compressing the rootfs keep the script silent for minutes - it looks hung. Animate a spinner, but only when stdout is a terminal so CI runs and piped logs keep their single "I: ..." line per step. --- scripts/iso-to-oci | 63 +++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 55 insertions(+), 8 deletions(-) (limited to 'scripts') diff --git a/scripts/iso-to-oci b/scripts/iso-to-oci index a0e5e87c..9f1b2a82 100755 --- a/scripts/iso-to-oci +++ b/scripts/iso-to-oci @@ -1,11 +1,58 @@ #!/bin/bash cleanup() { + # spin() hides the cursor while animating - make sure it comes back if we + # are interrupted mid step + if [[ -t 1 ]]; then + tput cnorm 2>/dev/null || true + fi if [[ -n "${WORKDIR:-}" && -d "${WORKDIR:-}" ]]; then rm -rf "${WORKDIR}" fi } +# Run a long running command while animating a spinner - unpacking an ISO and +# compressing the rootfs keep the script silent for minutes and it looks hung. +# Only animate when stdout is a terminal, "make oci" in CI or a piped log gets +# the plain message instead. Output of the wrapped command is discarded as +# before, but unlike before its exit code is checked. +spin() { + # exit codes accepted as success in addition to 0 - unsquashfs(1) returns 2 + # for non fatal errors, which is what we always get when running as a + # regular user (device nodes and xattrs can not be restored) + local allow_rc="" + if [[ "$1" == "--allow-rc" ]]; then + allow_rc="$2"; shift 2 + fi + local message="$1"; shift + local frames='|/-\' + local rc=0 i=0 + + if [[ ! -t 1 ]]; then + echo "I: ${message}" + "$@" >/dev/null 2>&1 || rc=$? + else + "$@" >/dev/null 2>&1 & + local pid=$! + tput civis 2>/dev/null + while kill -0 "${pid}" 2>/dev/null; do + printf '\r%s %s' "${frames:i++%4:1}" "${message}" + sleep 0.1 + done + wait "${pid}" || rc=$? + tput cnorm 2>/dev/null + # replace the spinner with the very same line the non interactive run + # would have printed + printf '\r\033[K' + echo "I: ${message}" + fi + + if (( rc )) && [[ " ${allow_rc} " != *" ${rc} "* ]]; then + echo "E: ${message} failed with exit code ${rc}" + exit 1 + fi +} + if [[ "$#" -ne 1 ]]; then echo "Usage: $0 " exit 2 @@ -44,15 +91,15 @@ ROOTFS="${WORKDIR}/iso" UNSQUASHFS="${WORKDIR}/unsquashfs" mkdir -p "${ROOTFS}/live" "${UNSQUASHFS}" -echo "I: extracting ISO metadata" -xorriso -osirrox on -indev "${ISO}" -extract /version.json "${ROOTFS}/version.json" >/dev/null 2>&1 +spin "extracting ISO metadata - version.json" \ + xorriso -osirrox on -indev "${ISO}" -extract /version.json "${ROOTFS}/version.json" -echo "I: extracting squashfs image" -xorriso -osirrox on -indev "${ISO}" -extract /live/filesystem.squashfs "${ROOTFS}/live/filesystem.squashfs" >/dev/null 2>&1 +spin "extracting ISO data - squashfs image" \ + xorriso -osirrox on -indev "${ISO}" -extract /live/filesystem.squashfs "${ROOTFS}/live/filesystem.squashfs" # create directory, unpack squashfs filesystem, get ISO version -echo "I: extracting squashfs content" -unsquashfs -follow -dest "${UNSQUASHFS}/" "${ROOTFS}/live/filesystem.squashfs" >/dev/null 2>&1 +spin --allow-rc 2 "extracting squashfs content" \ + unsquashfs -follow -dest "${UNSQUASHFS}/" "${ROOTFS}/live/filesystem.squashfs" VERSION="$(jq --raw-output .version "${ROOTFS}/version.json")" # older ISOs predate ARM64 support and carry no architecture in version.json ARCH="$(jq --raw-output '.architecture // "amd64"' "${ROOTFS}/version.json")" @@ -145,8 +192,8 @@ rmdir "${UNSQUASHFS}/config" || { echo "E: /config is not an empty directory"; e ln -s /opt/vyatta/etc/config "${UNSQUASHFS}/config" # create docker image -echo "I: generate OCI container image ${OCI_IMAGE}" -XZ_OPT="-T0" tar --directory "${UNSQUASHFS}" --create . --xz --file "${OCI_IMAGE}" +spin "generating OCI container image ${OCI_IMAGE}" \ + env XZ_OPT=-T0 tar --directory "${UNSQUASHFS}" --create . --xz --file "${OCI_IMAGE}" # containerlab uses the image healthcheck to determine when a VyOS node is # ready, see https://containerlab.dev/manual/kinds/vyosnetworks_vyos/ -- cgit v1.2.3