From 477f7aed7239dca45c84f3faa615d2f5a7bf979a Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sun, 31 Aug 2025 13:58:43 +0200 Subject: [PATCH 01/16] Follow-up to bb19d064, prune *all* unused images From the documentation: > 'podman image prune' removes all dangling images from local storage. > With the all option, all unused images are deleted (i.e., images not > in use by any container). > > The image prune command does not prune cache images that only use > layers that are necessary for other images. So, when the container script is called in the cleanup phase of the lifetime of a container, we can use the '--all' option to ensure we also remove this container's loaded image. In the case this happens before a reboot of the system, there will be no old version of the image loaded to /var/lib/containers after boot. Issue #1098 Signed-off-by: Joachim Wiberg --- board/common/rootfs/usr/sbin/container | 2 +- doc/ChangeLog.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/board/common/rootfs/usr/sbin/container b/board/common/rootfs/usr/sbin/container index b7cc4d36..48a368f1 100755 --- a/board/common/rootfs/usr/sbin/container +++ b/board/common/rootfs/usr/sbin/container @@ -303,7 +303,7 @@ delete() podman rm -vif "$name" >/dev/null 2>&1 [ -n "$quiet" ] || log "Container $name has been removed." - cnt=$(podman image prune -f | wc -l) + cnt=$(podman image prune -af | wc -l) log "Pruned $cnt image(s)" } diff --git a/doc/ChangeLog.md b/doc/ChangeLog.md index fb935fa3..b7d505e1 100644 --- a/doc/ChangeLog.md +++ b/doc/ChangeLog.md @@ -13,7 +13,7 @@ All notable changes to the project are documented in this file. - Add support for [Raspberry Pi touch display][RPI-TOUCH] on Raspberry Pi 4 ### Fixes -- containers: prune dangling images to reclaim disk space (#1098) +- Fix #1098: Prune dangling container images to reclaim disk space [RPI-TOUCH]: https://www.raspberrypi.com/products/raspberry-pi-touch-display/ From d65c478a288bbf67113f2c8d0994f21de3dbb5bc Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Fri, 29 Aug 2025 15:47:10 +0200 Subject: [PATCH 02/16] doc: update logo with new slogan Signed-off-by: Joachim Wiberg --- doc/logo-netconf.png | Bin 0 -> 18710 bytes doc/logo-small.svg | 338 ++++++++++++++++++++++++++++++++++++++++ doc/logo.png | Bin 18710 -> 28727 bytes doc/logo3.svg | 359 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 697 insertions(+) create mode 100644 doc/logo-netconf.png create mode 100644 doc/logo-small.svg create mode 100644 doc/logo3.svg diff --git a/doc/logo-netconf.png b/doc/logo-netconf.png new file mode 100644 index 0000000000000000000000000000000000000000..5e5299a350dc0d5d5e2853961fa7a112387f0e41 GIT binary patch literal 18710 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 DoU~Vd literal 0 HcmV?d00001 diff --git a/doc/logo-small.svg b/doc/logo-small.svg new file mode 100644 index 00000000..2b7b1f2e --- /dev/null +++ b/doc/logo-small.svg @@ -0,0 +1,338 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/doc/logo.png b/doc/logo.png index 5e5299a350dc0d5d5e2853961fa7a112387f0e41..169dfacce77524f9052a11bbddd5a2dd8fb17144 100644 GIT binary patch literal 28727 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%0ADGjn4Co@A{GK#mAWXGq8=+PI;`^T#}H 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 literal 18710 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 DoU~Vd diff --git a/doc/logo3.svg b/doc/logo3.svg new file mode 100644 index 00000000..03fab738 --- /dev/null +++ b/doc/logo3.svg @@ -0,0 +1,359 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From cfeefc1d86d60ffd0cfac2f27a52ec77d7717c44 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sun, 31 Aug 2025 23:59:56 +0200 Subject: [PATCH 03/16] doc: slight refresh and pivot As Infix matures as an operating system it is quickly becoming more and more useful also for end-device use-cases. The README should reflect this change in focus. Signed-off-by: Joachim Wiberg --- README.md | 172 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 95 insertions(+), 77 deletions(-) diff --git a/README.md b/README.md index bfb14744..83c07267 100644 --- a/README.md +++ b/README.md @@ -2,58 +2,69 @@ Infix - Linux <3 NETCONF -Infix is a free, Linux-based, immutable operating system built around -[Buildroot][1] and [sysrepo][2]. A powerful mix that ease porting to -different platforms, simplify long-term maintenance, and provide -made-easy management using NETCONF, RESTCONF[^2], or the built-in -command line interface (CLI) from a console or SSH login. +Turn any ARM or x86 device into a powerful, manageable network appliance +in minutes. From $35 Raspberry Pi boards to enterprise switches — deploy +routers, IoT gateways, edge devices, or custom network solutions that +just work. -> [!TIP] -> _Curious how it works?_ Click the **▶ Example CLI Session** below to see -> it in action -> — or jump into the comprehensive [Infix Documentation][4] to learn even more. +## Our Values -Geared for switches and routers — yet its core value fits plenty of -other use cases: +**🔒 Immutable** +Your system never breaks. Read-only filesystem with atomic upgrades +means no configuration drift, no corrupted updates, and instant rollback +if something goes wrong. Deploy once, trust forever. -- Runs from a squashfs image on a read-only partition -- Single configuration file on a separate partition -- Built around YANG with standard IETF models -- Linux switchdev provides open switch APIs -- Atomic upgrades to secondary partition -- Highly security focused +**🤝 Friendly** +Actually easy to use. Auto-generated CLI from standard YANG models comes +with built-in help for every command — just hit `?` or TAB for +context-aware assistance. Familiar NETCONF/RESTCONF APIs and +[comprehensive documentation][4] mean you're never stuck. Whether +you're learning networking or managing enterprise infrastructure. -An immutable[^1] operating system enhances security and inherently makes -it maintenance-free. Configuration and data, e.g, containers, is stored -on separate partitions to ensure complete separation from system files -and allow for seamless backup, restore, and provisioning. +**🛡️ Secure** +Built with security as a foundation, not an afterthought. Minimal +attack surface, separation between system and data, and container +isolation. Sleep better knowing your infrastructure is protected. -In itself, Infix is perfectly suited for dedicated networking tasks, -such as routing, switching, and monitoring. This is how it started, as -a network focused operating system. Now, with native support for Docker -containers, it provides a versatile platform that can easily be adapted -to any customer need. Be it legacy applications, network protocols, -process monitoring, or edge data analysis, it can run close to end -equipment. Either directly connected on dedicated Ethernet ports or -indirectly using virtual network cables to exist on the same LAN as -other connected equipment. +## Why Choose Infix -The simple design of Infix provides complete control over both system -and data, minimal cognitive burden, and makes it incredibly easy to get -started. +**Hardware Flexibility**: Start with a $35 Raspberry Pi, scale to +enterprise switching hardware. Same OS, same tools, same reliability. -
Example CLI Session +**Standards-Based**: Built around YANG models and IETF standards. Learn +once, use everywhere - no vendor lock-in. -The CLI configure context is automatically generated from the loaded -YANG models and their corresponding [sysrepo][2] plugins. The following -is brief example of how to set the IP address of an interface: +**Container Ready**: Run your applications alongside networking +functions. GPIO access, dedicated Ethernet ports, custom protocols — +your device, your rules. -``` +## Use Cases + +1. **Home Labs & Hobbyists**: + Transform a Raspberry Pi into a full-featured router with WiFi +1. **IoT & Edge Computing**: + Bridge devices to the cloud with reliable, updatable gateways +1. **Small Business Networks**: + Enterprise-grade features without the complexity or cost +1. **Developers & Makers**: + Test networking concepts, prototype IoT solutions, or build custom + appliances +1. **Network Professionals**: + Consistent tooling from development to production deployment. + How about a digital twin using raw Qemu or [GNS3](https://gns3.com/infix)! + +## See It In Action + +Configure an interface in seconds - the CLI guides you with built-in help: + +
Click Here for an example CLI Session + +```bash admin@infix-12-34-56:/> configure admin@infix-12-34-56:/config/> edit interface eth0 admin@infix-12-34-56:/config/interface/eth0/> set ipv4 address autoconf bind-ni-name enabled - forwarding mtu neighbor + forwarding mtu neighbor admin@infix-12-34-56:/config/interface/eth0/> set ipv4 address 192.168.2.200 prefix-length 24 admin@infix-12-34-56:/config/interface/eth0/> show type ethernet; @@ -62,7 +73,6 @@ ipv4 { prefix-length 24; } } -ipv6 admin@infix-12-34-56:/config/interface/eth0/> diff interfaces { interface eth0 { @@ -85,56 +95,64 @@ lo ethernet UP 00:00:00:00:00:00 admin@infix-12-34-56:/> copy running-config startup-config ``` -[Click here][3] for more details. +Notice how TAB completion shows available options, `show` displays +current config, and `diff` shows exactly what changed before you +commit your changes with the `leave` command. +
-Infix can run on many different types of architectures and boards, much -thanks to Linux and Buildroot. Currently the focus is on 64-bit ARM -devices, optionally with switching fabric supported by Linux switchdev. -The [following boards](board/aarch64/README.md) are fully supported: +> [Full CLI documentation →][3] - - Marvell CN9130 CRB - - Marvell EspressoBIN - - Microchip SparX-5i PCB135 (eMMC) - - NXP i.MX8MP EVK - - Raspberry Pi 4B - - NanoPi R2S +## Get Started -Additionally, StarFive VisionFive2, a RISC-V based two-port router, and -an x86_64 build is also available. The latter is primarily intended for -development and testing, but can also be used for evaluation and demo -purposes. For more information, see: [Infix in Virtual -Environments](doc/virtual.md). +Get [pre-built images][5] for your hardware. Use the CLI, web +interface, or standard NETCONF/RESTCONF tools, e.g., `curl`. Add +containers for any custom functionality you need. -> See the [GitHub Releases](https://github.com/kernelkit/infix/releases) -> page for our pre-built images. The *[Latest Build][]* has bleeding -> edge images, if possible we recommend using a versioned release. -> -> For *customer specific builds* of Infix, see your product repository. +### Supported Platforms +- **Raspberry Pi 4B** - Perfect for home labs, learning, and prototyping +- **NanoPi R2S** - Compact dual-port router in a tiny package +- **x86_64** - Run in VMs or on mini PCs for development and testing +- **Marvell CN9130 CRB, EspressoBIN** - High-performance ARM platforms +- **Microchip SparX-5i, NXP i.MX8MP EVK** - Enterprise switching capabilities +- **StarFive VisionFive2** - RISC-V architecture support ----- +*Why start with Raspberry Pi?* It's affordable, widely available, has +built-in WiFi + Ethernet, and runs the exact same Infix OS you'd deploy +in production. Perfect for learning, prototyping, or even small-scale +deployments. + +> 📖 **[Complete documentation][4]** • 💬 **[Join our Discord][discord-url]** + +## Technical Details + +Built on proven open-source foundations ([Buildroot][1] + [sysrepo][2]) +for reliability you can trust: + +- **Immutable OS**: Read-only filesystem, atomic updates, instant rollback +- **YANG Configuration**: Industry-standard models with auto-generated tooling +- **Hardware Acceleration**: Linux switchdev support for wire-speed packet processing +- **Container Integration**: Docker support with flexible network and hardware access +- **Memory Efficient**: Runs comfortably on devices with as little as 256 MB RAM + +Perfect for everything from resource-constrained edge devices to +high-throughput network appliances. + +> Check the *[Latest Build][]* for bleeding-edge features. + +---
- -
Infix development is sponsored by Wires + +
Infix development is sponsored by Wires
----- - -[^1]: An immutable operating system is one with read-only file systems, - atomic updates, rollbacks, declarative configuration, and workload - isolation. All to improve reliability, scalability, and security. - For more information, see this [survey paper][5] and [article][6]. -[^2]: Partial RESTCONF support, features like HTTP PATCH, OPTIONS, HEAD, - and copying between datastores are still missing. - [1]: https://buildroot.org/ "Buildroot Homepage" [2]: https://www.sysrepo.org/ "Sysrepo Homepage" -[3]: doc/cli/introduction.md -[4]: https://kernelkit.org/infix/ "Infix User's Guide" -[5]: https://ceur-ws.org/Vol-3386/paper9.pdf "Immutable Operating Systems: A Survey" -[6]: https://www.zdnet.com/article/what-is-immutable-linux-heres-why-youd-run-an-immutable-linux-distro/ "Why you should run an immutable Linux distro" +[3]: https://kernelkit.org/infix/latest/cli/introduction/ +[4]: https://kernelkit.org/infix/ +[5]: https://github.com/kernelkit/infix/releases [Latest Build]: https://github.com/kernelkit/infix/releases/tag/latest "Latest build" [License]: https://en.wikipedia.org/wiki/GPL_license [License Badge]: https://img.shields.io/badge/License-GPL%20v2-blue.svg From ab9b2f555b381a5d90abd47ade6c632afee748ec Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Mon, 14 Jul 2025 19:44:12 +0200 Subject: [PATCH 04/16] package/finit: bump to v4.14 Highlights: - fixes to systemd and s6 type services - bare-bones libsystemd replacement with #include - new reload:script mimicking systemd ExecReload, and - new stop:script mimicking systemd ExecStop - exit status/signal info when a process dies - service kill:SEC now support up to 300 sec. - the /tmp/norespawn trick now also covers service_retry() - the sysv 'stop' command process environment is now same as 'start' - State machine ordering issue: enter new config generation after services disabled in previous generation have been stopped Full changelog at: - - Fixes #1123 Signed-off-by: Joachim Wiberg --- package/finit/finit.hash | 4 ++-- package/finit/finit.mk | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/package/finit/finit.hash b/package/finit/finit.hash index 9f2bd759..04d754de 100644 --- a/package/finit/finit.hash +++ b/package/finit/finit.hash @@ -1,8 +1,8 @@ # From https://github.com/troglobit/finit/releases/ -sha256 b6a0a2f98c860cf9fe5dfe7e3601d922957ad7880ae29919176ab960b7b96e70 finit-4.12.tar.gz +sha256 7c128119129324050ff7e5b56d0f33fa152fe254d035c0d0c6f72dc75d6786f3 finit-4.14.tar.gz # Locally calculated -sha256 2fd62c0fe6ea6d1861669f4c87bda83a0b5ceca64f4baa4d16dd078fbd218c14 LICENSE +sha256 868cb6c5414933a48db11186042cfe65c87480d326734bc6cf0e4b19b4a2e52a LICENSE # GIT Snapshot sha256 8c880293409cf566f6256bff193f985c50bd2eb99d2ff964dcaa9590251ed27e finit-438d6b4e638418a2a22024a3cead2f47909d72b9.tar.gz diff --git a/package/finit/finit.mk b/package/finit/finit.mk index 50959fea..2e3b2be0 100644 --- a/package/finit/finit.mk +++ b/package/finit/finit.mk @@ -4,7 +4,7 @@ # ################################################################################ -FINIT_VERSION = 4.12 +FINIT_VERSION = 4.14 FINIT_SITE = https://github.com/troglobit/finit/releases/download/$(FINIT_VERSION) FINIT_LICENSE = MIT FINIT_LICENSE_FILES = LICENSE @@ -39,6 +39,7 @@ FINIT_CONF_OPTS = \ --disable-contrib \ --disable-rescue \ --disable-silent-rules \ + --without-libsystemd \ --with-group="$(FINIT_GROUP)" ifeq ($(BR2_ROOTFS_MERGED_USR),y) From b21043c7e52514de7df7f286c958d78e060d6f49 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Fri, 29 Aug 2025 15:33:28 +0200 Subject: [PATCH 05/16] package/podman: bump 4.5.0 -> 4.9.5 This major upgrade, along with the upgrade to Finit v4.14, is what is needed to fix #1123, which was caused by some odd futex locking bug in Podman that left lingering issues in /var/lib/containers state files. The root cause as fixed already in v4.7.x, but since CNI is supported up to and including 4.9.5, going with a later release seemd prudent. Full changelogs at: - - - - - - - - - - - - - - - - - Fixes #1123 Signed-off-by: Joachim Wiberg --- board/common/rootfs/usr/sbin/container | 85 +++++++++++++------ doc/ChangeLog.md | 1 + package/podman/podman.hash | 2 +- package/podman/podman.mk | 2 +- package/podman/unconfined.conf | 3 + .../0001-Disable-pull-retry.patch | 0 .../0002-Add-log-driver-for-syslog.patch | 0 7 files changed, 65 insertions(+), 28 deletions(-) rename patches/podman/{4.5.0 => 4.9.5}/0001-Disable-pull-retry.patch (100%) rename patches/podman/{4.5.0 => 4.9.5}/0002-Add-log-driver-for-syslog.patch (100%) diff --git a/board/common/rootfs/usr/sbin/container b/board/common/rootfs/usr/sbin/container index 48a368f1..64fab20e 100755 --- a/board/common/rootfs/usr/sbin/container +++ b/board/common/rootfs/usr/sbin/container @@ -8,7 +8,7 @@ # DOWNLOADS=/var/lib/containers/oci BUILTIN=/lib/oci -TMPDIR=/var/tmp +BASEDIR=/var/tmp container=$0 checksum="" extracted= @@ -29,10 +29,10 @@ err() rc=$1; shift logger -I $PPID -t container -p local1.err -- "Error: $*" - if [ -n "$extracted" ]; then - if [ -d "$TMPDIR/$dir" ]; then - log "Cleaning up extracted $dir" - rm -rf "$dir" + if [ -n "$extracted" ] && [ -n "$tmpdir" ]; then + if [ -d "$tmpdir" ]; then + log "Cleaning up temporary directory $tmpdir" + rm -rf "$tmpdir" fi fi @@ -110,10 +110,11 @@ EOF return 1 } -# Unpacks a given oci-archive.tar[.gz] in the current directory. Sanity -# checks, at least one index.json in the top-level dir of the archive. -# If there are more index files, this function does not handle them. -unpack_archive() +# Extracts an oci-archive.tar[.gz] in a temporary directory. Finds and +# sanity checks that at least one index.json exist in the archive. This +# is the OCI directory fed to `podman load` and also used as repo name. +# NOTE: if there are >1 index.json, this function does not handle them. +load_archive() { uri=$1 tag=$2 @@ -158,49 +159,81 @@ unpack_archive() fi fi + file=$(realpath "$file") if [ -d "$file" ]; then index=$(find "$file" -name index.json) if [ -z "$index" ]; then err 1 "cannot find index.json in OCI image $file" fi else - cd "$TMPDIR" || err 0 "failed cd $TMPDIR, wiill use $(pwd) for OCI archive extraction." + # Extract files in a temporary directory, because most OCI + # archives are flat/bare, all files in the root w/o a dir/ + tmpdir=$(mktemp -d -p "$BASEDIR") || err 1 "failed creating temporary directory" + cd "$tmpdir" || err 1 "failed cd to temporary directory $tmpdir" - index=$(tar tf "$file" |grep index.json) + index="$tmpdir/$(tar tf "$file" |grep index.json)" if [ -z "$index" ]; then err 1 "invalid OCI archive, cannot find index.json in $file" fi [ -n "$quiet" ] || log "Extracting OCI archive $file ..." - tar xf "$file" || err 1 "failed unpacking $file in $(pwd)" + tar xf "$file" || err 1 "failed unpacking $file in $tmpdir" extracted=true + cd - >/dev/null || err 0 "failed cd -" fi dir=$(dirname "$index") - if echo "$dir" | grep -q ":"; then + + # Handle flat tarballs without a sub-directory, because + # the $dir name is used as fallback when retagging below. + if [ -n "$extracted" ] && [ "$dir" = "$tmpdir" ]; then + parent=$(dirname "$dir") + dirnam=$(echo "$img" | sed 's/\(.*\)\.tar.*/\1/') + tmpdir="${parent}/${dirnam}" + mv "$dir" "$tmpdir" + dir="$tmpdir" + fi + + if basename "$dir" | grep -q ":"; then if [ -z "$tag" ]; then - tag="$dir" + tag=$(basename "$dir") fi + sanitized_dir=$(echo "$dir" | cut -d':' -f1) mv "$dir" "$sanitized_dir" || err 1 "failed renaming $dir to $sanitized_dir" dir="$sanitized_dir" fi [ -n "$quiet" ] || log "Loading OCI image $dir ..." - podman load -qi "$dir" >/dev/null + output=$(podman load -qi "$dir") + + # Extract image ID from podman load output: + # "Loaded image: sha256:cd9d0aaf81be..." + if echo "$output" | grep -q "sha256:"; then + img_id="${output##*sha256:}" + else + # Fallback to directory name if no SHA found + img_id="$dir" + fi + + # On podman < 4.7.0 we had to retag images from default $dir:latest + # From >= 4.7.0 we always tag since loads come in as : + if [ -z "$tag" ]; then + tag=$(basename "$dir") + fi + + # Repo names must be lowercase, and only '[a-z0-9._/-]+' and ':tag' + tag=$(printf "%s" "$tag" | tr '[:upper:]' '[:lower:]' | tr -c 'a-z0-9._/:-' '-') + + [ -n "$quiet" ] || log "Tagging loaded image $img_id as $tag" + if ! podman tag "$img_id" "$tag"; then + err 1 "failed tagging image as $tag" + fi # Clean up after ourselves if [ -n "$extracted" ]; then log "Cleaning up extracted $dir" - rm -rf "$dir" - fi - - # Retag image from podman default $dir:latest - if [ -n "$tag" ]; then - podman tag "$dir" "$tag" >/dev/null - podman rmi "$dir" >/dev/null - else - tag=$dir + rm -rf "$tmpdir" fi echo "$tag" @@ -228,7 +261,7 @@ create() # Unpack and load docker-archive/oci/oci-archive, returning image # name, or return docker:// URL for download. - if ! image=$(unpack_archive "$image"); then + if ! image=$(load_archive "$image"); then exit 1 fi @@ -666,7 +699,7 @@ case $cmd in ;; load) # shellcheck disable=SC2086 - name=$(unpack_archive "$1" $2) + name=$(load_archive "$1" $2) [ -n "$name" ] || exit 1 # Show resulting image(s) matching $name diff --git a/doc/ChangeLog.md b/doc/ChangeLog.md index b7d505e1..a11f3565 100644 --- a/doc/ChangeLog.md +++ b/doc/ChangeLog.md @@ -14,6 +14,7 @@ All notable changes to the project are documented in this file. ### Fixes - Fix #1098: Prune dangling container images to reclaim disk space +- Fix #1123: Disabling or removing a container may cause podman to hang [RPI-TOUCH]: https://www.raspberrypi.com/products/raspberry-pi-touch-display/ diff --git a/package/podman/podman.hash b/package/podman/podman.hash index e1d968cb..dade6f39 100644 --- a/package/podman/podman.hash +++ b/package/podman/podman.hash @@ -1,3 +1,3 @@ # Locally computed -sha256 830a633630bf6e61f2b8d4ca00efdd9a173ef25cdd49d4a4364c293e088561df podman-4.5.0-go2.tar.gz +sha256 53f6bf7a8e4b647b2378ea8bfee6c67e03e412bf027b4dc0ff37a3a764703405 podman-4.9.5-go2.tar.gz sha256 62fb8a3a9621dc2388174caaabe9c2317b694bb9a1d46c98bcf5655b68f51be3 LICENSE diff --git a/package/podman/podman.mk b/package/podman/podman.mk index 126e337b..2fa70f7a 100644 --- a/package/podman/podman.mk +++ b/package/podman/podman.mk @@ -4,7 +4,7 @@ # ################################################################################ -PODMAN_VERSION = 4.5.0 +PODMAN_VERSION = 4.9.5 PODMAN_SITE = $(call github,containers,podman,v$(PODMAN_VERSION)) PODMAN_LICENSE = Apache-2.0 PODMAN_LICENSE_FILES = LICENSE diff --git a/package/podman/unconfined.conf b/package/podman/unconfined.conf index 8171e9ea..9a6c6875 100644 --- a/package/podman/unconfined.conf +++ b/package/podman/unconfined.conf @@ -1,2 +1,5 @@ +[Network] +network_backend = "cni" + [containers] seccomp_profile = "unconfined" diff --git a/patches/podman/4.5.0/0001-Disable-pull-retry.patch b/patches/podman/4.9.5/0001-Disable-pull-retry.patch similarity index 100% rename from patches/podman/4.5.0/0001-Disable-pull-retry.patch rename to patches/podman/4.9.5/0001-Disable-pull-retry.patch diff --git a/patches/podman/4.5.0/0002-Add-log-driver-for-syslog.patch b/patches/podman/4.9.5/0002-Add-log-driver-for-syslog.patch similarity index 100% rename from patches/podman/4.5.0/0002-Add-log-driver-for-syslog.patch rename to patches/podman/4.9.5/0002-Add-log-driver-for-syslog.patch From 6b0145f92e8ddd3993998c863c41483e78a7e855 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Fri, 29 Aug 2025 15:32:28 +0200 Subject: [PATCH 06/16] board/common: minor fix to usage text, and silence logs Signed-off-by: Joachim Wiberg --- board/common/rootfs/usr/sbin/container | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/board/common/rootfs/usr/sbin/container b/board/common/rootfs/usr/sbin/container index 64fab20e..1b0746bb 100755 --- a/board/common/rootfs/usr/sbin/container +++ b/board/common/rootfs/usr/sbin/container @@ -320,7 +320,6 @@ delete() fi # Should already be stopped, but if not ... - log "$name: should already be stopped, double checking ..." container stop "$name" >/dev/null while running "$name"; do @@ -332,7 +331,6 @@ delete() sleep 1 done - log "$name: calling podman rm -vif ..." podman rm -vif "$name" >/dev/null 2>&1 [ -n "$quiet" ] || log "Container $name has been removed." @@ -478,7 +476,7 @@ options: -q, --quiet Quiet operation, called from confd -r, --restart POLICY One of "no", "always", or "on-failure:NUM" -s, --simple Show output in simplified format - -t, --timeout SEC Set timeout for delete/restart commands, default: 20 + -t, --timeout SEC Set timeout for delete/restart commands, default: 30 -v, --volume NAME:PATH Create named volume mounted inside container on PATH commands: From 12df83e6d2ab1286cfa59463bb6851cbf40fff8d Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Fri, 29 Aug 2025 15:41:46 +0200 Subject: [PATCH 07/16] board/common: allow containers to shut down properly The extended kill delay (10 sec) is sometimes not enough for complex system containers. Also, podman sometimes take the opportunity to do housekeeping tasks when stopping a container. So, allow for up to 30 sec. grace period before we send SIGKILL. With the latest image prune extension, set a 60 sec. timeout for the cleanup task, in case podman gets stuck. This to prevent any future mishaps. Signed-off-by: Joachim Wiberg --- .../rootfs/etc/finit.d/available/container@.conf | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/board/common/rootfs/etc/finit.d/available/container@.conf b/board/common/rootfs/etc/finit.d/available/container@.conf index f3a5b0e3..7a494c60 100644 --- a/board/common/rootfs/etc/finit.d/available/container@.conf +++ b/board/common/rootfs/etc/finit.d/available/container@.conf @@ -1,7 +1,7 @@ # Start a container instance (%i) and redirect logs to /log/container -# Give podman enough time to properly shut down the container. Every -# time we start a container we run the setup stage, disable the Finit -# timeout to allow the setup stage to run to completion. -sysv log:prio:local1,tag:%i kill:10 pid:!/run/container:%i.pid \ - pre:0,/usr/sbin/container cleanup:0,/usr/sbin/container \ +# Give podman enough time to properly shut down the container, kill:30 +# The pre:script, which is responsibe for fetching a remote image, must +# not have a timeout. The cleanup should take no longer than a minute. +sysv log:prio:local1,tag:%i kill:30 pid:!/run/container:%i.pid \ + pre:0,/usr/sbin/container cleanup:60,/usr/sbin/container \ [2345] :%i container -n %i -- container %i From 66a5e5304b83b328771d2c1d530941df3e552650 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sun, 31 Aug 2025 14:00:51 +0200 Subject: [PATCH 08/16] board/common: fix container network change detection on pull When a container's image is on an inaccessible remote server, the container wrapper script waits in the background for any netowrk changes to retry download of the image. This change avoids the dangerous previous construct, and is also easier to read: timeuot after 60 seconds unless ip monitor reads at least one event before that. Fixes #1124 Signed-off-by: Joachim Wiberg --- board/common/rootfs/usr/sbin/container | 6 +++++- doc/ChangeLog.md | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/board/common/rootfs/usr/sbin/container b/board/common/rootfs/usr/sbin/container index 1b0746bb..fe68c85a 100755 --- a/board/common/rootfs/usr/sbin/container +++ b/board/common/rootfs/usr/sbin/container @@ -782,7 +782,11 @@ case $cmd in while ! "$script"; do log "${name}: setup failed, waiting for network changes ..." - read -t 60 _ < <(ip monitor address route) + + # Timeout and retry after 60 seconds, on SIGTERM, or when + # any network event is caught. + timeout -s TERM -k 1 60 sh -c \ + 'ip monitor address route 2>/dev/null | head -n1 >/dev/null' || true # On IP address/route changes, wait a few seconds more to ensure # the system has ample time to react and set things up for us. diff --git a/doc/ChangeLog.md b/doc/ChangeLog.md index a11f3565..9916f5e8 100644 --- a/doc/ChangeLog.md +++ b/doc/ChangeLog.md @@ -15,6 +15,8 @@ All notable changes to the project are documented in this file. ### Fixes - Fix #1098: Prune dangling container images to reclaim disk space - Fix #1123: Disabling or removing a container may cause podman to hang +- Fix #1124: Container setup with unreachable remote image spawns + excessive `ip monitor` processes [RPI-TOUCH]: https://www.raspberrypi.com/products/raspberry-pi-touch-display/ From 8b39d3fdc7fbd022c6d0996ec35f36aa504a5f7b Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sun, 31 Aug 2025 13:52:03 +0200 Subject: [PATCH 09/16] confd: explicitly declare cni backends - the port-mapping plugin supports iptables or nftables - the firewall plugin support only iptables or firewalld Enforce use of iptables wrapper for nftables, for now, in both plugins. This all needs to be refactored to run podman with "unmanaged" networks in the future. Related to issue #1125 Signed-off-by: Joachim Wiberg --- src/confd/src/cni.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/confd/src/cni.c b/src/confd/src/cni.c index 701b1a4c..436a43fc 100644 --- a/src/confd/src/cni.c +++ b/src/confd/src/cni.c @@ -250,12 +250,15 @@ static int cni_bridge(struct lyd_node *net, const char *ifname) " },\n" /* /bridge */ " {\n" " \"type\": \"portmap\",\n" + " \"backend\": \"iptables\",\n" + " \"snat\": true,\n" " \"capabilities\": {\n" " \"portMappings\": true\n" " }\n" " },\n" /* /portmap */ " {\n" - " \"type\": \"firewall\"\n" + " \"type\": \"firewall\",\n" + " \"backend\": \"iptables\"\n" " },\n" /* /firewall */ " {\n" " \"type\": \"tuning\"\n" From 313554d12e6ab3d7d721e3ec3d5ddaed3879f9a6 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sun, 31 Aug 2025 13:44:30 +0200 Subject: [PATCH 10/16] test/case: minor, cleanup - Drop redundant comments - Drop redundant imports - PEP-8 fixes Signed-off-by: Joachim Wiberg --- test/case/ietf_system/hostname/hostname.adoc | 2 +- test/case/ietf_system/hostname/test.py | 16 +++++++--------- .../infix_containers/container_basic/test.py | 7 ------- .../infix_containers/container_bridge/test.py | 5 ----- .../case/infix_containers/container_phys/test.py | 5 ----- .../case/infix_containers/container_veth/test.py | 4 ---- 6 files changed, 8 insertions(+), 31 deletions(-) diff --git a/test/case/ietf_system/hostname/hostname.adoc b/test/case/ietf_system/hostname/hostname.adoc index 5ad6db49..69b2e2c9 100644 --- a/test/case/ietf_system/hostname/hostname.adoc +++ b/test/case/ietf_system/hostname/hostname.adoc @@ -26,7 +26,7 @@ endif::topdoc[] . Verify new hostname 'h0stn4m3' . Set hostname to '%h-%m' . Verify hostname is %h-%m in running configuration -. Verify hostname format in operational, according to format +. Verify hostname format in operational <<< diff --git a/test/case/ietf_system/hostname/test.py b/test/case/ietf_system/hostname/test.py index 6b4634da..74f9854b 100755 --- a/test/case/ietf_system/hostname/test.py +++ b/test/case/ietf_system/hostname/test.py @@ -10,8 +10,6 @@ where MAC is the last three bytes of the base MAC address. e.g. ix-01-01-01. """ -import random -import string import re import infamy @@ -20,33 +18,33 @@ with infamy.Test() as test: env = infamy.Env() target = env.attach("target", "mgmt") tgtssh = env.attach("target", "mgmt", "ssh") - fmt = "%h-%m" - new="h0stn4m3" + FMT = "%h-%m" + NEW = "h0stn4m3" with test.step("Set hostname to 'h0stn4m3'"): target.put_config_dict("ietf-system", { "system": { - "hostname": new, + "hostname": NEW, } }) with test.step("Verify new hostname 'h0stn4m3'"): running = target.get_config_dict("/ietf-system:system") - assert running["system"]["hostname"] == new + assert running["system"]["hostname"] == NEW with test.step("Set hostname to '%h-%m'"): target.put_config_dict("ietf-system", { "system": { - "hostname": fmt, + "hostname": FMT, } }) with test.step("Verify hostname is %h-%m in running configuration"): running = target.get_config_dict("/ietf-system:system") - if running["system"]["hostname"] != fmt: + if running["system"]["hostname"] != FMT: test.fail() - with test.step("Verify hostname format in operational, according to format"): + with test.step("Verify hostname format in operational"): cmd = tgtssh.runsh("sed -n s/^DEFAULT_HOSTNAME=//p /etc/os-release") default = cmd.stdout.rstrip() diff --git a/test/case/infix_containers/container_basic/test.py b/test/case/infix_containers/container_basic/test.py index 4415a902..6fce60cd 100755 --- a/test/case/infix_containers/container_basic/test.py +++ b/test/case/infix_containers/container_basic/test.py @@ -1,11 +1,4 @@ #!/usr/bin/env python3 -# -# Verify that a simple web server container can be configured to run -# with host networking, on port 80. Operation is verified using a -# simple GET request for index.html and checking for a key phrase. -# -# The RPC actions: stop + start, and restart are also verified. -# """ Container basic diff --git a/test/case/infix_containers/container_bridge/test.py b/test/case/infix_containers/container_bridge/test.py index 3025f9af..cd9fd4b7 100755 --- a/test/case/infix_containers/container_bridge/test.py +++ b/test/case/infix_containers/container_bridge/test.py @@ -1,9 +1,4 @@ #!/usr/bin/env python3 -# -# Verify connectivity with a simple web server container from behind a -# docker0 bridge. As an added twist, this test also verifies content -# mounts, i.e., custom index.html from running-config. -# """ Container with bridge network diff --git a/test/case/infix_containers/container_phys/test.py b/test/case/infix_containers/container_phys/test.py index fa54f7a8..48964cb3 100755 --- a/test/case/infix_containers/container_phys/test.py +++ b/test/case/infix_containers/container_phys/test.py @@ -1,15 +1,10 @@ #!/usr/bin/env python3 -# -# Verify connectivity with a simple web server container that's been -# given a physical interface instead of an end of a VETH pair. -# """ Container with physical interface Verify connectivity with a simple web server container that's been given a physical interface instead of an end of a VETH pair. """ -import base64 import infamy from infamy.util import until, to_binary diff --git a/test/case/infix_containers/container_veth/test.py b/test/case/infix_containers/container_veth/test.py index f67319c1..408c5bb1 100755 --- a/test/case/infix_containers/container_veth/test.py +++ b/test/case/infix_containers/container_veth/test.py @@ -1,8 +1,4 @@ #!/usr/bin/env python3 -# -# Verify connectivity with a simple web server container from behind a -# regular bridge, a VETH pair connects the container to the bridge. -# r""" Container with VETH pair From 34dead1a5028ade4e7e3fefae2c1965f282d29de Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Tue, 2 Sep 2025 10:53:02 +0200 Subject: [PATCH 11/16] test/case: disable mdns-allow-deny for release, unstable Signed-off-by: Joachim Wiberg --- test/case/infix_services/mdns/mdns.yaml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/case/infix_services/mdns/mdns.yaml b/test/case/infix_services/mdns/mdns.yaml index 248c901f..09c78546 100644 --- a/test/case/infix_services/mdns/mdns.yaml +++ b/test/case/infix_services/mdns/mdns.yaml @@ -2,5 +2,6 @@ - name: mdns_enable_disable case: mdns_enable_disable/test.py -- name: mdns_allow_deny - case: mdns_allow_deny/test.py +# Disabled for v25.08, unstable +# - name: mdns_allow_deny +# case: mdns_allow_deny/test.py From 86df54c1071dcaedcca08bc9629e1f8ba260993a Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sun, 31 Aug 2025 13:44:54 +0200 Subject: [PATCH 12/16] test/case/infix_containers: ensure IP forwarding is enabled Usually the CNI bridge plugin "takes care" of enabling IPv4 forwarding on all interfaces, see issue #1125, but when the container tests are run in a different order from the infix_containers.yaml, Infix may reset the IPv4 forwarding on this critical interface. This change is both future proof and also ensures the test works as it was intended even if tests are run out-of-order. Signed-off-by: Joachim Wiberg --- test/case/infix_containers/container_bridge/test.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test/case/infix_containers/container_bridge/test.py b/test/case/infix_containers/container_bridge/test.py index cd9fd4b7..4c070d1c 100755 --- a/test/case/infix_containers/container_bridge/test.py +++ b/test/case/infix_containers/container_bridge/test.py @@ -36,6 +36,7 @@ with infamy.Test() as test: { "name": f"{ifname}", "ipv4": { + "forwarding": True, "address": [{ "ip": f"{DUTIP}", "prefix-length": 24 From 7f829cfbdf4ba36fee8253f1fa926bbabe07bf6c Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Mon, 1 Sep 2025 00:13:56 +0200 Subject: [PATCH 13/16] test/case/infix_containers: new test, verify enable/disable Regression test for issue #1123 Signed-off-by: Joachim Wiberg --- test/case/infix_containers/Readme.adoc | 2 + .../container_enabled/Readme.adoc | 1 + .../container_enabled/container_enabled.adoc | 37 +++++++++ .../container_enabled/test.py | 82 +++++++++++++++++++ .../container_enabled/topology.dot | 23 ++++++ .../container_enabled/topology.svg | 33 ++++++++ .../infix_containers/infix_containers.yaml | 4 +- 7 files changed, 181 insertions(+), 1 deletion(-) create mode 120000 test/case/infix_containers/container_enabled/Readme.adoc create mode 100644 test/case/infix_containers/container_enabled/container_enabled.adoc create mode 100755 test/case/infix_containers/container_enabled/test.py create mode 100644 test/case/infix_containers/container_enabled/topology.dot create mode 100644 test/case/infix_containers/container_enabled/topology.svg diff --git a/test/case/infix_containers/Readme.adoc b/test/case/infix_containers/Readme.adoc index a530a6e6..89cb0ef9 100644 --- a/test/case/infix_containers/Readme.adoc +++ b/test/case/infix_containers/Readme.adoc @@ -14,6 +14,8 @@ Verifies Infix Docker container support: include::container_basic/Readme.adoc[] +include::container_enabled/Readme.adoc[] + include::container_bridge/Readme.adoc[] include::container_phys/Readme.adoc[] diff --git a/test/case/infix_containers/container_enabled/Readme.adoc b/test/case/infix_containers/container_enabled/Readme.adoc new file mode 120000 index 00000000..867ff7a3 --- /dev/null +++ b/test/case/infix_containers/container_enabled/Readme.adoc @@ -0,0 +1 @@ +container_enabled.adoc \ No newline at end of file diff --git a/test/case/infix_containers/container_enabled/container_enabled.adoc b/test/case/infix_containers/container_enabled/container_enabled.adoc new file mode 100644 index 00000000..69b698fa --- /dev/null +++ b/test/case/infix_containers/container_enabled/container_enabled.adoc @@ -0,0 +1,37 @@ +=== Container enabled/disabled +==== Description +Verify that a container can be enabled and disabled via configuration. +Tests the 'enabled' leaf functionality by: + +1. Creating an enabled container and verifying it starts +2. Disabling the container and verifying it stops +3. Re-enabling the container and verifying it starts again + +Uses operational datastore to verify container running status. + +==== Topology +ifdef::topdoc[] +image::{topdoc}../../test/case/infix_containers/container_enabled/topology.svg[Container enabled/disabled topology] +endif::topdoc[] +ifndef::topdoc[] +ifdef::testgroup[] +image::container_enabled/topology.svg[Container enabled/disabled topology] +endif::testgroup[] +ifndef::testgroup[] +image::topology.svg[Container enabled/disabled topology] +endif::testgroup[] +endif::topdoc[] +==== Test sequence +. Set up topology and attach to target DUT +. Set hostname to 'container-host' +. Create enabled container from bundled OCI image +. Verify container has started +. Let container settle +. Disable container +. Verify container has stopped +. Re-enable container +. Verify container has started again + + +<<< + diff --git a/test/case/infix_containers/container_enabled/test.py b/test/case/infix_containers/container_enabled/test.py new file mode 100755 index 00000000..0832e97a --- /dev/null +++ b/test/case/infix_containers/container_enabled/test.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +""" +Container enabled/disabled + +Verify that a container can be enabled and disabled via configuration. +Tests the 'enabled' leaf functionality by: + +1. Creating an enabled container and verifying it starts +2. Disabling the container and verifying it stops +3. Re-enabling the container and verifying it starts again + +Uses operational datastore to verify container running status. +""" +import infamy +from infamy.util import until + + +def set_container_enabled(target, name, enabled): + """Helper function to set container enabled state and verify the change""" + target.put_config_dict("infix-containers", { + "containers": { + "container": [ + { + "name": name, + "enabled": enabled + } + ] + } + }) + + +with infamy.Test() as test: + NAME = "web-enabled" + + with test.step("Set up topology and attach to target DUT"): + env = infamy.Env() + target = env.attach("target", "mgmt") + + if not target.has_model("infix-containers"): + test.skip() + + with test.step("Set hostname to 'container-host'"): + target.put_config_dict("ietf-system", { + "system": { + "hostname": "container-host" + } + }) + + with test.step("Create enabled container from bundled OCI image"): + target.put_config_dict("infix-containers", { + "containers": { + "container": [ + { + "name": f"{NAME}", + "enabled": True, + "image": f"oci-archive:{infamy.Container.HTTPD_IMAGE}", + "command": "/usr/sbin/httpd -f -v -p 91", + "network": { + "host": True + } + } + ] + } + }) + + with test.step("Verify container has started"): + c = infamy.Container(target) + until(lambda: c.running(NAME), attempts=60) + + with test.step("Disable container"): + set_container_enabled(target, NAME, False) + + with test.step("Verify container has stopped"): + until(lambda: not c.running(NAME), attempts=60) + + with test.step("Re-enable container"): + set_container_enabled(target, NAME, True) + + with test.step("Verify container has started again"): + until(lambda: c.running(NAME), attempts=60) + + test.succeed() diff --git a/test/case/infix_containers/container_enabled/topology.dot b/test/case/infix_containers/container_enabled/topology.dot new file mode 100644 index 00000000..e6a0d803 --- /dev/null +++ b/test/case/infix_containers/container_enabled/topology.dot @@ -0,0 +1,23 @@ +graph "1x1" { + layout="neato"; + overlap="false"; + esep="+80"; + + node [shape=record, fontname="DejaVu Sans Mono, Book"]; + edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"]; + + host [ + label="host | { mgmt }", + pos="0,12!", + requires="controller", + ]; + + target [ + label="{ mgmt } | target", + pos="10,12!", + + requires="infix", + ]; + + host:mgmt -- target:mgmt [requires="mgmt", color="lightgray"] +} diff --git a/test/case/infix_containers/container_enabled/topology.svg b/test/case/infix_containers/container_enabled/topology.svg new file mode 100644 index 00000000..6fc6f47a --- /dev/null +++ b/test/case/infix_containers/container_enabled/topology.svg @@ -0,0 +1,33 @@ + + + + + + +1x1 + + + +host + +host + +mgmt + + + +target + +mgmt + +target + + + +host:mgmt--target:mgmt + + + + diff --git a/test/case/infix_containers/infix_containers.yaml b/test/case/infix_containers/infix_containers.yaml index 484663e8..255799b9 100644 --- a/test/case/infix_containers/infix_containers.yaml +++ b/test/case/infix_containers/infix_containers.yaml @@ -3,6 +3,9 @@ - name: container_basic case: container_basic/test.py +- name: container_enabled + case: container_enabled/test.py + - name: container_bridge case: container_bridge/test.py @@ -20,4 +23,3 @@ - name: container_host_commands case: container_host_commands/test.py - From 950a6ef79472b6f36ded36d0f75251d85b72e4d7 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Mon, 1 Sep 2025 09:38:16 +0200 Subject: [PATCH 14/16] test/case/infix_containers: new test, verify environment Signed-off-by: Joachim Wiberg --- test/case/infix_containers/Readme.adoc | 4 + .../container_environment/Readme.adoc | 1 + .../container_environment.adoc | 36 ++++++ .../container_environment/test.py | 117 ++++++++++++++++++ .../container_environment/topology.dot | 24 ++++ .../container_environment/topology.svg | 42 +++++++ .../infix_containers/infix_containers.yaml | 4 + 7 files changed, 228 insertions(+) create mode 120000 test/case/infix_containers/container_environment/Readme.adoc create mode 100644 test/case/infix_containers/container_environment/container_environment.adoc create mode 100755 test/case/infix_containers/container_environment/test.py create mode 100644 test/case/infix_containers/container_environment/topology.dot create mode 100644 test/case/infix_containers/container_environment/topology.svg diff --git a/test/case/infix_containers/Readme.adoc b/test/case/infix_containers/Readme.adoc index 89cb0ef9..fc4c21b3 100644 --- a/test/case/infix_containers/Readme.adoc +++ b/test/case/infix_containers/Readme.adoc @@ -4,6 +4,8 @@ Verifies Infix Docker container support: - Basic web server container running in host network mode + - Container enable/disable functionality via configuration + - Container environment variable configuration and access - Common setup with a docker0 bridge, automatic VETH pairs to container(s) - Connecting a container with a VETH pair to a standard Linux bridge - Assigning a physical Ethernet interface to a container @@ -16,6 +18,8 @@ include::container_basic/Readme.adoc[] include::container_enabled/Readme.adoc[] +include::container_environment/Readme.adoc[] + include::container_bridge/Readme.adoc[] include::container_phys/Readme.adoc[] diff --git a/test/case/infix_containers/container_environment/Readme.adoc b/test/case/infix_containers/container_environment/Readme.adoc new file mode 120000 index 00000000..acda1027 --- /dev/null +++ b/test/case/infix_containers/container_environment/Readme.adoc @@ -0,0 +1 @@ +container_environment.adoc \ No newline at end of file diff --git a/test/case/infix_containers/container_environment/container_environment.adoc b/test/case/infix_containers/container_environment/container_environment.adoc new file mode 100644 index 00000000..d7050ef4 --- /dev/null +++ b/test/case/infix_containers/container_environment/container_environment.adoc @@ -0,0 +1,36 @@ +=== Container environment variables +==== Description +Verify that environment variables can be set in container configuration +and are available inside the running container. Tests the 'env' list +functionality by: + +1. Creating a container with multiple environment variables +2. Using a custom script to extract env vars and serve them via HTTP +3. Fetching the served content to verify environment variables are set correctly + +Uses the nftables container image with custom rc.local script. + +==== Topology +ifdef::topdoc[] +image::{topdoc}../../test/case/infix_containers/container_environment/topology.svg[Container environment variables topology] +endif::topdoc[] +ifndef::topdoc[] +ifdef::testgroup[] +image::container_environment/topology.svg[Container environment variables topology] +endif::testgroup[] +ifndef::testgroup[] +image::topology.svg[Container environment variables topology] +endif::testgroup[] +endif::topdoc[] +==== Test sequence +. Set up topology and attach to target DUT +. Configure data interface with static IPv4 +. Create container with environment variables +. Verify container has started +. Verify environment variables are available via HTTP +. Verify basic connectivity to data interface +. Verify environment variables in HTTP response + + +<<< + diff --git a/test/case/infix_containers/container_environment/test.py b/test/case/infix_containers/container_environment/test.py new file mode 100755 index 00000000..f571fcfb --- /dev/null +++ b/test/case/infix_containers/container_environment/test.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 +""" +Container environment variables + +Verify that environment variables can be set in container configuration +and are available inside the running container. Tests the 'env' list +functionality by: + +1. Creating a container with multiple environment variables +2. Using a custom script to extract env vars and serve them via HTTP +3. Fetching the served content to verify environment variables are set correctly + +Uses the nftables container image with custom rc.local script. +""" +import infamy +from infamy.util import until, to_binary + + +with infamy.Test() as test: + NAME = "web-env" + DUTIP = "10.0.0.2" + OURIP = "10.0.0.1" + + with test.step("Set up topology and attach to target DUT"): + env = infamy.Env() + target = env.attach("target", "mgmt") + + if not target.has_model("infix-containers"): + test.skip() + + with test.step("Configure data interface with static IPv4"): + _, ifname = env.ltop.xlate("target", "data") + target.put_config_dict("ietf-interfaces", { + "interfaces": { + "interface": [{ + "name": f"{ifname}", + "ipv4": { + "address": [{ + "ip": f"{DUTIP}", + "prefix-length": 24 + }] + } + }] + } + }) + + with test.step("Create container with environment variables"): + script = to_binary("""#!/bin/sh +# Create HTTP response with environment variables +printf "HTTP/1.1 200 OK\\r\\n" > /var/www/response.txt +printf "Content-Type: text/plain\\r\\n" >> /var/www/response.txt +printf "Connection: close\\r\\n\\r\\n" >> /var/www/response.txt + +# Add environment variables using printf to control encoding +printf "TEST_VAR=\\"%s\\"\\n" "$TEST_VAR" >> /var/www/response.txt +printf "APP_PORT=%s\\n" "$APP_PORT" >> /var/www/response.txt +printf "DEBUG_MODE=\\"%s\\"\\n" "$DEBUG_MODE" >> /var/www/response.txt +printf "PATH_WITH_SPACES=\\"%s\\"\\n" "$PATH_WITH_SPACES" >> /var/www/response.txt + +while true; do + nc -l -p 8080 < /var/www/response.txt 2>>/var/www/debug.log || sleep 1 +done +""") + + target.put_config_dict("infix-containers", { + "containers": { + "container": [ + { + "name": f"{NAME}", + "image": f"oci-archive:{infamy.Container.NFTABLES_IMAGE}", + "env": [ + {"key": "TEST_VAR", "value": "hello-world"}, + {"key": "APP_PORT", "value": "8080"}, + {"key": "DEBUG_MODE", "value": "true"}, + {"key": "PATH_WITH_SPACES", "value": "/path with spaces/test"} + ], + "network": { + "host": True + }, + "mount": [ + { + "name": "rc.local", + "content": script, + "target": "/etc/rc.local", + "mode": "0755" + } + ], + "volume": [{ + "name": "www", + "target": "/var/www" + }] + } + ] + } + }) + + with test.step("Verify container has started"): + c = infamy.Container(target) + until(lambda: c.running(NAME), attempts=60) + + with test.step("Verify environment variables are available via HTTP"): + _, hport = env.ltop.xlate("host", "data") + url = infamy.Furl(f"http://{DUTIP}:8080/env.html") + + with infamy.IsolatedMacVlan(hport) as ns: + ns.addip(OURIP) + + with test.step("Verify basic connectivity to data interface"): + ns.must_reach(DUTIP) + + with test.step("Verify environment variables in HTTP response"): + until(lambda: url.nscheck(ns, "TEST_VAR=\"hello-world\""), attempts=10) + until(lambda: url.nscheck(ns, "APP_PORT=8080"), attempts=10) + until(lambda: url.nscheck(ns, "DEBUG_MODE=\"true\""), attempts=10) + until(lambda: url.nscheck(ns, "PATH_WITH_SPACES=\"/path with spaces/test\""), attempts=10) + + test.succeed() diff --git a/test/case/infix_containers/container_environment/topology.dot b/test/case/infix_containers/container_environment/topology.dot new file mode 100644 index 00000000..a66dae68 --- /dev/null +++ b/test/case/infix_containers/container_environment/topology.dot @@ -0,0 +1,24 @@ +graph "1x2" { + layout="neato"; + overlap="false"; + esep="+80"; + + node [shape=record, fontname="DejaVu Sans Mono, Book"]; + edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"]; + + host [ + label="host | { mgmt | data }", + pos="0,12!", + requires="controller", + ]; + + target [ + label="{ mgmt | data } | target", + pos="10,12!", + + requires="infix", + ]; + + host:mgmt -- target:mgmt [requires="mgmt", color=lightgrey] + host:data -- target:data [color=black] +} \ No newline at end of file diff --git a/test/case/infix_containers/container_environment/topology.svg b/test/case/infix_containers/container_environment/topology.svg new file mode 100644 index 00000000..ff3d246b --- /dev/null +++ b/test/case/infix_containers/container_environment/topology.svg @@ -0,0 +1,42 @@ + + + + + + +1x2 + + + +host + +host + +mgmt + +data + + + +target + +mgmt + +data + +target + + + +host:mgmt--target:mgmt + + + + +host:data--target:data + + + + diff --git a/test/case/infix_containers/infix_containers.yaml b/test/case/infix_containers/infix_containers.yaml index 255799b9..83d07fd7 100644 --- a/test/case/infix_containers/infix_containers.yaml +++ b/test/case/infix_containers/infix_containers.yaml @@ -6,6 +6,10 @@ - name: container_enabled case: container_enabled/test.py +# Disabled for v25.08, new/unstable +# - name: container_environment +# case: container_environment/test.py + - name: container_bridge case: container_bridge/test.py From fc7e1d0745d64a92b46cd654403fd8b1eac733fe Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Mon, 1 Sep 2025 16:14:32 +0200 Subject: [PATCH 15/16] test/case/infix_containers: extend retry for containers For a heavily loaded system, 10 seconds/retries is not enough time to expect containers to have started up. Particularly after the changes done recently to do prune before and after a container is started. Signed-off-by: Joachim Wiberg --- test/case/infix_containers/container_basic/test.py | 6 +++--- test/case/infix_containers/container_bridge/test.py | 2 +- test/case/infix_containers/container_firewall_basic/test.py | 4 ++-- test/case/infix_containers/container_host_commands/test.py | 4 ++-- test/case/infix_containers/container_phys/test.py | 4 ++-- test/case/infix_containers/container_veth/test.py | 2 +- test/case/infix_containers/container_volume/test.py | 4 ++-- 7 files changed, 13 insertions(+), 13 deletions(-) diff --git a/test/case/infix_containers/container_basic/test.py b/test/case/infix_containers/container_basic/test.py index 6fce60cd..7c67a9fa 100755 --- a/test/case/infix_containers/container_basic/test.py +++ b/test/case/infix_containers/container_basic/test.py @@ -54,7 +54,7 @@ with infamy.Test() as test: with test.step("Verify container 'web' has started"): c = infamy.Container(target) - until(lambda: c.running(NAME), attempts=10) + until(lambda: c.running(NAME), attempts=60) with test.step("Verify container 'web' is reachable on http://container-host.local:91"): until(lambda: _verify(addr), attempts=10) @@ -64,12 +64,12 @@ with infamy.Test() as test: c.action(NAME, "stop") with test.step("Verify container 'web' is stopped"): - until(lambda: not c.running(NAME), attempts=10) + until(lambda: not c.running(NAME), attempts=30) with test.step("Restart container 'web'"): c.action(NAME, "restart") with test.step("Verify container 'web' is reachable on http://container-host.local:91"): # Wait for it to restart and respond, or fail - until(lambda: _verify(addr), attempts=10) + until(lambda: _verify(addr), attempts=60) test.succeed() diff --git a/test/case/infix_containers/container_bridge/test.py b/test/case/infix_containers/container_bridge/test.py index 4c070d1c..66584988 100755 --- a/test/case/infix_containers/container_bridge/test.py +++ b/test/case/infix_containers/container_bridge/test.py @@ -78,7 +78,7 @@ with infamy.Test() as test: with test.step("Verify container has started"): c = infamy.Container(target) - until(lambda: c.running(NAME), attempts=10) + until(lambda: c.running(NAME), attempts=60) _, hport = env.ltop.xlate("host", "data") url = infamy.Furl(URL) diff --git a/test/case/infix_containers/container_firewall_basic/test.py b/test/case/infix_containers/container_firewall_basic/test.py index 0f1c9d0a..d9252a70 100755 --- a/test/case/infix_containers/container_firewall_basic/test.py +++ b/test/case/infix_containers/container_firewall_basic/test.py @@ -176,11 +176,11 @@ table ip nat { with test.step("Verify firewall container has started"): c = infamy.Container(target) - until(lambda: c.running(NFTNM), attempts=10) + until(lambda: c.running(NFTNM), attempts=60) with test.step("Verify web container has started"): c = infamy.Container(target) - until(lambda: c.running(WEBNM), attempts=10) + until(lambda: c.running(WEBNM), attempts=60) with infamy.IsolatedMacVlan(hport) as ns: NEEDLE = "tiny web server from the curiOS docker" diff --git a/test/case/infix_containers/container_host_commands/test.py b/test/case/infix_containers/container_host_commands/test.py index f2dbe912..3cc76668 100755 --- a/test/case/infix_containers/container_host_commands/test.py +++ b/test/case/infix_containers/container_host_commands/test.py @@ -72,8 +72,8 @@ nsenter -m/1/ns/mnt -u/1/ns/uts -i/1/ns/ipc -n/1/ns/net hostname {hostname_new} with test.step("Verify container has started"): c = infamy.Container(target) - until(lambda: c.running(cont_name), attempts=10) + until(lambda: c.running(cont_name), attempts=60) with test.step("Verify the new hostname set by the container"): - until(lambda: c.running(cont_name) != target.get_data("/ietf-system:system")["system"]["hostname"], attempts=10) + until(lambda: c.running(cont_name) != target.get_data("/ietf-system:system")["system"]["hostname"], attempts=30) test.succeed() diff --git a/test/case/infix_containers/container_phys/test.py b/test/case/infix_containers/container_phys/test.py index 48964cb3..1459444f 100755 --- a/test/case/infix_containers/container_phys/test.py +++ b/test/case/infix_containers/container_phys/test.py @@ -57,7 +57,7 @@ with infamy.Test() as test: with test.step("Verify container has started"): c = infamy.Container(target) - until(lambda: c.running(NAME), attempts=10) + until(lambda: c.running(NAME), attempts=60) _, hport = env.ltop.xlate("host", "data") url = infamy.Furl(URL) @@ -88,6 +88,6 @@ with infamy.Test() as test: }) with test.step("Verify server is restarted and returns new content"): - until(lambda: url.nscheck(ns, MESG), attempts=10) + until(lambda: url.nscheck(ns, MESG), attempts=60) test.succeed() diff --git a/test/case/infix_containers/container_veth/test.py b/test/case/infix_containers/container_veth/test.py index 408c5bb1..fb72c47c 100755 --- a/test/case/infix_containers/container_veth/test.py +++ b/test/case/infix_containers/container_veth/test.py @@ -94,7 +94,7 @@ with infamy.Test() as test: with test.step("Verify container 'web-br0-veth' has started"): c = infamy.Container(target) - until(lambda: c.running(NAME), attempts=10) + until(lambda: c.running(NAME), attempts=60) _, hport = env.ltop.xlate("host", "data") url = infamy.Furl(URL) diff --git a/test/case/infix_containers/container_volume/test.py b/test/case/infix_containers/container_volume/test.py index c8f5d1e0..95be8fe0 100755 --- a/test/case/infix_containers/container_volume/test.py +++ b/test/case/infix_containers/container_volume/test.py @@ -46,7 +46,7 @@ with infamy.Test() as test: with test.step("Verify container has started"): c = infamy.Container(target) - until(lambda: c.running(NAME), attempts=10) + until(lambda: c.running(NAME), attempts=60) with test.step("Modify container volume content"): cmd = f"sudo container shell {NAME} 'echo {MESG} >/var/www/index.html'" @@ -66,6 +66,6 @@ with infamy.Test() as test: # print(f"Container {NAME} upgraded: {out.stdout}") with test.step("Verify container volume content survived upgrade"): - until(lambda: url.check(MESG), attempts=10) + until(lambda: url.check(MESG), attempts=60) test.succeed() From 1d0c2be5153b573c71ca0e08a003618c98d48fe2 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Mon, 1 Sep 2025 14:32:16 +0200 Subject: [PATCH 16/16] patches: silence libyang "Obsolete schema node" warnings in log Fixes #1127 Signed-off-by: Joachim Wiberg --- doc/ChangeLog.md | 6 +- ...olete-change-log-level-warning-debug.patch | 63 +++++++++++++++++++ 2 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 patches/libyang/3.12.2/0001-lyd_validate_obsolete-change-log-level-warning-debug.patch diff --git a/doc/ChangeLog.md b/doc/ChangeLog.md index 9916f5e8..da3571a2 100644 --- a/doc/ChangeLog.md +++ b/doc/ChangeLog.md @@ -3,7 +3,7 @@ Change Log All notable changes to the project are documented in this file. -[v25.08.0][UNRELEASED] - +[v25.08.0][] - 2025-09-01 ------------------------- ### Changes @@ -17,6 +17,7 @@ All notable changes to the project are documented in this file. - Fix #1123: Disabling or removing a container may cause podman to hang - Fix #1124: Container setup with unreachable remote image spawns excessive `ip monitor` processes +- Fix #1127: Silence libyang Obsolete schema node warnings in log [RPI-TOUCH]: https://www.raspberrypi.com/products/raspberry-pi-touch-display/ @@ -1611,7 +1612,8 @@ Supported YANG models in addition to those used by sysrepo and netopeer: - N/A [buildroot]: https://buildroot.org/ -[UNRELEASED]: https://github.com/kernelkit/infix/compare/v25.06.1...HEAD +[UNRELEASED]: https://github.com/kernelkit/infix/compare/v25.08.0...HEAD +[v25.08.0]: https://github.com/kernelkit/infix/compare/v25.06.1...v26.08.0 [v25.06.0]: https://github.com/kernelkit/infix/compare/v25.05.1...v26.06.0 [v25.05.1]: https://github.com/kernelkit/infix/compare/v25.05.0...v25.05.1 [v25.05.0]: https://github.com/kernelkit/infix/compare/v25.04.0...v25.05.0 diff --git a/patches/libyang/3.12.2/0001-lyd_validate_obsolete-change-log-level-warning-debug.patch b/patches/libyang/3.12.2/0001-lyd_validate_obsolete-change-log-level-warning-debug.patch new file mode 100644 index 00000000..33db1b88 --- /dev/null +++ b/patches/libyang/3.12.2/0001-lyd_validate_obsolete-change-log-level-warning-debug.patch @@ -0,0 +1,63 @@ +From 4603d09f5d1bd9ca0a1d4467d30a71528c8e2df4 Mon Sep 17 00:00:00 2001 +From: Joachim Wiberg +Date: Mon, 1 Sep 2025 14:24:03 +0200 +Subject: [PATCH] lyd_validate_obsolete: change log level warning -> debug +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Organization: Wires + +A library should not log directly to syslog, this is the responsibility +of the application. In our case sysrepo, with a knob to disable it when +you know what you're doing or have other ways of handling deprecated and +obsolete nodes — e.g., automatically migrating them to a new tree. + +Signed-off-by: Joachim Wiberg +--- + src/log.c | 3 +++ + src/log.h | 1 + + src/validation.c | 2 +- + 3 files changed, 5 insertions(+), 1 deletion(-) + +diff --git a/src/log.c b/src/log.c +index 13da500ef..553aefe55 100644 +--- a/src/log.c ++++ b/src/log.c +@@ -705,6 +705,9 @@ ly_log_dbg(uint32_t group, const char *format, ...) + case LY_LDGDEPSETS: + str_group = "DEPSETS"; + break; ++ case LY_LDGSCHEMA: ++ str_group = "SCHEMA"; ++ break; + default: + LOGINT(NULL); + return; +diff --git a/src/log.h b/src/log.h +index 85f2ac6d4..2bbb0b19b 100644 +--- a/src/log.h ++++ b/src/log.h +@@ -156,6 +156,7 @@ LIBYANG_API_DECL uint32_t *ly_temp_log_options(uint32_t *opts); + #define LY_LDGDICT 0x01 /**< Dictionary additions and deletions. */ + #define LY_LDGXPATH 0x02 /**< XPath parsing end evaluation. */ + #define LY_LDGDEPSETS 0x04 /**< Dependency module sets for schema compilation. */ ++#define LY_LDGSCHEMA 0x08 /**< Schema compilation and validation. */ + + /** + * @} +diff --git a/src/validation.c b/src/validation.c +index 25be0feeb..8e308c61f 100644 +--- a/src/validation.c ++++ b/src/validation.c +@@ -1624,7 +1624,7 @@ lyd_validate_obsolete(const struct lyd_node *node) + if (snode->flags & LYS_STATUS_OBSLT && + (!(snode->nodetype & LYD_NODE_INNER) || lyd_child(node))) { + LOG_LOCSET(NULL, node); +- LOGWRN(snode->module->ctx, "Obsolete schema node \"%s\" instantiated in data.", snode->name); ++ LOGDBG(LY_LDGSCHEMA, snode->module->ctx, "Obsolete schema node \"%s\" instantiated in data.", snode->name); + LOG_LOCBACK(0, 1); + break; + } +-- +2.43.0 +