From b6aa604cdd17e9f1b1e3ce786c5b505e03b7f008 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sun, 23 Feb 2025 11:11:57 +0100 Subject: [PATCH 01/16] package/finit: bump to v4.10-rc1 ChangeLog: https://github.com/troglobit/finit/releases/tag/4.10-rc1 Signed-off-by: Joachim Wiberg --- package/finit/finit.hash | 2 +- package/finit/finit.mk | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package/finit/finit.hash b/package/finit/finit.hash index 1a545235..813e0ad1 100644 --- a/package/finit/finit.hash +++ b/package/finit/finit.hash @@ -1,5 +1,5 @@ # From https://github.com/troglobit/finit/releases/ -sha256 7e4efaa4c165aabaf145ac3e6ae2ac907209e69758f4892c83cfccc579876c27 finit-4.9.tar.gz +sha256 ead37606012ced935c66dcfd88786d533d08964b00adb7899ea156ae29b7896a finit-4.10-rc1.tar.gz # Locally calculated sha256 2fd62c0fe6ea6d1861669f4c87bda83a0b5ceca64f4baa4d16dd078fbd218c14 LICENSE diff --git a/package/finit/finit.mk b/package/finit/finit.mk index 987c1945..eec7fede 100644 --- a/package/finit/finit.mk +++ b/package/finit/finit.mk @@ -4,7 +4,7 @@ # ################################################################################ -FINIT_VERSION = 4.9 +FINIT_VERSION = 4.10-rc1 FINIT_SITE = https://github.com/troglobit/finit/releases/download/$(FINIT_VERSION) FINIT_LICENSE = MIT FINIT_LICENSE_FILES = LICENSE From 417d48f015491f3290348373a83c4ccbfb847bcc Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Wed, 5 Feb 2025 05:42:08 +0100 Subject: [PATCH 02/16] test: update topology to new syntax Signed-off-by: Joachim Wiberg --- test/infamy/topologies/ring-4-duts.dot | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/test/infamy/topologies/ring-4-duts.dot b/test/infamy/topologies/ring-4-duts.dot index c4c14a5c..30c30904 100644 --- a/test/infamy/topologies/ring-4-duts.dot +++ b/test/infamy/topologies/ring-4-duts.dot @@ -11,37 +11,37 @@ graph "ring-4-duts" { host [ label=" { host | { mgmt4 | data4 | mgmt1 | data1 | data2 | mgmt2 | data3 | mgmt3 } }", pos="150,200!" - kind="controller", + requires="controller", ]; R1 [ label="{ { mgmt | data } | \n R1 \n 10.0.0.1/32 \n(lo) } | { ring1 | cross | ring2 }", pos="50,150!", - kind="infix", + requires="infix", ]; R4 [ label="{ { mgmt | data } | \n R4 \n 10.0.0.4/32 \n(lo) } | { ring1 | cross | ring2 }", pos="50,50!" - kind="infix", + requires="infix", ]; R2 [ label="{ ring2 | cross | ring1 } | { { data | mgmt } | \n R2 \n10.0.0.2/32 \n(lo) }", pos="250,150!" - kind="infix", + requires="infix", ]; R3 [ label="{ ring2 | cross | ring1 } | { { data | mgmt } | \n R3 \n10.0.0.3/32 \n(lo) }", pos="250,50!" - kind="infix", + requires="infix", ]; - host:mgmt1 -- R1:mgmt [kind=mgmt, color="lightgray"] - host:mgmt2 -- R2:mgmt [kind=mgmt, color="lightgray"] - host:mgmt3 -- R3:mgmt [kind=mgmt, color="lightgray"] - host:mgmt4 -- R4:mgmt [kind=mgmt, color="lightgray"] + host:mgmt1 -- R1:mgmt [requires="mgmt", color="lightgray"] + host:mgmt2 -- R2:mgmt [requires="mgmt", color="lightgray"] + host:mgmt3 -- R3:mgmt [requires="mgmt", color="lightgray"] + host:mgmt4 -- R4:mgmt [requires="mgmt", color="lightgray"] // host-Dut links host:data1 -- R1:data [color="darkgreen"] From ab3b389f69021334f1eaa0021ca280ec9a4bfe0a Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Wed, 5 Feb 2025 07:04:33 +0100 Subject: [PATCH 03/16] test: set up duts in parallel Signed-off-by: Joachim Wiberg --- test/case/use_case/ospf_container/test.py | 24 +++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/test/case/use_case/ospf_container/test.py b/test/case/use_case/ospf_container/test.py index dafa9b97..1568f8d9 100755 --- a/test/case/use_case/ospf_container/test.py +++ b/test/case/use_case/ospf_container/test.py @@ -572,6 +572,7 @@ def config_abr(target, data, link1, link2, link3): } }) + with infamy.Test() as test: with test.step("Set up topology and attach to target DUTs"): env = infamy.Env() @@ -585,6 +586,7 @@ with infamy.Test() as test: test.skip() if not R3.has_model("infix-containers"): test.skip() + with test.step("Configure DUTs"): _, R1ring1 = env.ltop.xlate("R1", "ring1") _, R1ring2 = env.ltop.xlate("R1", "ring2") @@ -607,7 +609,7 @@ with infamy.Test() as test: _, R1data = env.ltop.xlate("R1", "data") _, R3data = env.ltop.xlate("R3", "data") - _, ABRdata = env.ltop.xlate("ABR", "data") + _, ABRdata = env.ltop.xlate("ABR", "data") _, ABRlink1 = env.ltop.xlate("ABR", "link1") _, ABRlink2 = env.ltop.xlate("ABR", "link2") _, ABRlink3 = env.ltop.xlate("ABR", "link3") @@ -617,13 +619,15 @@ with infamy.Test() as test: lambda: config_generic(R3, 3, R3ring1, R3ring2, R3link), lambda: config_abr(ABR, ABRdata, ABRlink1, ABRlink2, ABRlink3)) - with infamy.IsolatedMacVlans({hostR1ring1: "iface1", hostR2ring2: "iface2"}) as sw1,\ + with infamy.IsolatedMacVlans({hostR1ring1: "iface1", hostR2ring2: "iface2"}) as sw1, \ infamy.IsolatedMacVlans({hostR2ring1: "iface1", hostR3ring2: "iface2"}) as sw2, \ infamy.IsolatedMacVlans({hostR3ring1: "iface1", hostR1ring2: "iface2"}) as sw3: - create_vlan_bridge(sw1) - create_vlan_bridge(sw2) - create_vlan_bridge(sw3) - #breakpoint() + + util.parallel(lambda: create_vlan_bridge(sw1), + lambda: create_vlan_bridge(sw2), + lambda: create_vlan_bridge(sw3)) + # breakpoint() + _, hport0 = env.ltop.xlate("host", "data4") with test.step("Wait for all routers to peer"): @@ -641,14 +645,14 @@ with infamy.Test() as test: with infamy.IsolatedMacVlan(hport0) as ns: ns.addip("192.168.100.2") ns.addroute("0.0.0.0/0", "192.168.100.1") - #breakpoint() + # breakpoint() with test.step("Verify ABR:data can access container A on R1 (10.1.1.101)"): - furl=Furl("http://10.1.1.101:8080") + furl = Furl("http://10.1.1.101:8080") util.until(lambda: furl.nscheck(ns, BODY)) with test.step("Verify ABR:data can access container A on R2 (10.1.2.101)"): - furl=Furl("http://10.1.2.101:8080") + furl = Furl("http://10.1.2.101:8080") util.until(lambda: furl.nscheck(ns, BODY)) with test.step("Verify ABR:data can access container A on R3 (10.1.3.101)"): - furl=Furl("http://10.1.3.101:8080") + furl = Furl("http://10.1.3.101:8080") util.until(lambda: furl.nscheck(ns, BODY)) test.succeed() From 73e1c158f59557dc6b95a8d1e6b0d4a59bf62780 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sun, 9 Feb 2025 19:33:10 +0100 Subject: [PATCH 04/16] confd: minor, rearrange for readability Signed-off-by: Joachim Wiberg --- package/confd/confd.conf | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/package/confd/confd.conf b/package/confd/confd.conf index dc62a60b..c8dcf4eb 100644 --- a/package/confd/confd.conf +++ b/package/confd/confd.conf @@ -17,17 +17,20 @@ run name:startup log:prio:user.notice norestart env:/etc/default/con -- Loading startup-config # Run if loading startup-config fails for some reason -run name:failure log:prio:user.critical norestart env:/etc/default/confd if: \ - [S] /usr/libexec/confd/load -t $CONFD_TIMEOUT failure-config \ +run name:failure log:prio:user.critical norestart env:/etc/default/confd \ + if: \ + [S] /usr/libexec/confd/load -t $CONFD_TIMEOUT failure-config \ -- Loading failure-config -run name:error :2 log:console norestart if: \ +run name:error :2 log:console norestart \ + if: \ [S] /usr/libexec/confd/error -- -service name:netopeer notify:none log env:/etc/default/confd \ - [12345] netopeer2-server -F -t $CONFD_TIMEOUT -v 1 \ +service name:netopeer notify:none log env:/etc/default/confd \ + [12345] netopeer2-server -F -t $CONFD_TIMEOUT -v 1 \ -- NETCONF server # Create initial /etc/resolv.conf after successful bootstrap -task name:resolv :conf norestart if: \ - [S] resolvconf -u -- Update DNS configuration +task name:resolv :conf norestart \ + if: \ + [S] resolvconf -u -- Update DNS configuration From 7a692fd57d2d28e8b6f55e77c00f7e8b1e826729 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sun, 9 Feb 2025 16:47:20 +0100 Subject: [PATCH 05/16] busybox: sync and add timeout tool - Add missing tool: timeout - Add extra flags to sync - Allow wc to work with large files - Update defconfig sync -> enable PAM Signed-off-by: Joachim Wiberg --- board/common/busybox_defconfig | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/board/common/busybox_defconfig b/board/common/busybox_defconfig index cf0a7c16..bd6f7bdc 100644 --- a/board/common/busybox_defconfig +++ b/board/common/busybox_defconfig @@ -1,7 +1,7 @@ # # Automatically generated make config: don't edit # Busybox version: 1.36.1 -# Tue Oct 22 13:12:02 2024 +# Sun Feb 9 12:25:37 2025 # CONFIG_HAVE_DOT_CONFIG=y @@ -17,7 +17,7 @@ CONFIG_SHOW_USAGE=y CONFIG_FEATURE_VERBOSE_USAGE=y # CONFIG_FEATURE_COMPRESS_USAGE is not set CONFIG_LFS=y -# CONFIG_PAM is not set +CONFIG_PAM=y CONFIG_FEATURE_DEVPTS=y CONFIG_FEATURE_UTMP=y CONFIG_FEATURE_WTMP=y @@ -325,7 +325,7 @@ CONFIG_FEATURE_STAT_FILESYSTEM=y CONFIG_STTY=y CONFIG_SUM=y CONFIG_SYNC=y -# CONFIG_FEATURE_SYNC_FANCY is not set +CONFIG_FEATURE_SYNC_FANCY=y CONFIG_FSYNC=y # CONFIG_TAC is not set CONFIG_TAIL=y @@ -336,7 +336,7 @@ CONFIG_TEST=y CONFIG_TEST1=y CONFIG_TEST2=y CONFIG_FEATURE_TEST_64=y -# CONFIG_TIMEOUT is not set +CONFIG_TIMEOUT=y CONFIG_TOUCH=y CONFIG_FEATURE_TOUCH_SUSV3=y CONFIG_TR=y @@ -357,7 +357,7 @@ CONFIG_BASE32=y CONFIG_BASE64=y CONFIG_UUENCODE=y CONFIG_WC=y -# CONFIG_FEATURE_WC_LARGE is not set +CONFIG_FEATURE_WC_LARGE=y CONFIG_WHO=y CONFIG_W=y CONFIG_USERS=y From 53d0995d0cee1451febc56b4814a34bf5356f25f Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sun, 9 Feb 2025 18:59:46 +0100 Subject: [PATCH 06/16] container: fix check if image exists already - Rename local variables - Extract image name, using basename, for podman image exists Fixes the following syslog error message: root@foo:~$ podman image exists docker://ghcr.io/kernelkit/curios:edge Error: unsupported transport "docker" for looking up local images Signed-off-by: Joachim Wiberg --- board/common/rootfs/usr/sbin/container | 43 +++++++++++++------------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/board/common/rootfs/usr/sbin/container b/board/common/rootfs/usr/sbin/container index 7819f8bd..537a7347 100755 --- a/board/common/rootfs/usr/sbin/container +++ b/board/common/rootfs/usr/sbin/container @@ -1,4 +1,4 @@ -#!/bin/sh +#!/bin/bash # This script can be used to start, stop, create, and delete containers. # It is what confd use, with the Finit container@.conf template, to set # up, run, and delete containers. @@ -109,33 +109,34 @@ EOF # If there are more index files, this function does not handle them. unpack_archive() { - image=$1 - name=$2 + uri=$1 + tag=$2 + img=$(basename "$uri") # Supported transports for load and create - case "$image" in + case "$uri" in oci:*) # Unpacked OCI image - file=${image#oci:} + file=${uri#oci:} ;; oci-archive:*) # Packed OCI image, .tar or .tar.gz format - file=${image#oci-archive:} + file=${uri#oci-archive:} ;; ftp://* | http://* | https://*) - if ! file=$(fetch "$image"); then + if ! file=$(fetch "$uri"); then return 1 fi ;; *) # docker://*, docker-archive:*, or URL - if podman image exists "$image"; then - echo "$image" + if podman image exists "$img"; then + echo "$img" return 0 fi # XXX: use --retry=0 with Podman 5.0 or later. - if ! id=$(podman pull --quiet "$image"); then - log "Failed pulling $image" + if ! id=$(podman pull --quiet "$uri"); then + log "Failed pulling $uri" return 1 fi - # Echo image name to caller + # Echo image tag to caller podman images --filter id="$id" --format "{{.Repository}}:{{.Tag}}" return 0 ;; @@ -147,7 +148,7 @@ unpack_archive() elif [ -e "$BUILTIN/$file" ]; then file="$BUILTIN/$file" else - err 1 "cannot find OCI archive $file in search path." + err 1 "cannot find OCI archive $file in URI $uri" fi fi @@ -171,8 +172,8 @@ unpack_archive() dir=$(dirname "$index") if echo "$dir" | grep -q ":"; then - if [ -z "$name" ]; then - name="$dir" + if [ -z "$tag" ]; then + tag="$dir" fi sanitized_dir=$(echo "$dir" | cut -d':' -f1) mv "$dir" "$sanitized_dir" || err 1 "failed renaming $dir to $sanitized_dir" @@ -188,15 +189,15 @@ unpack_archive() rm -rf "$dir" fi - # Rename image from podman default $dir:latest - if [ -n "$name" ]; then - podman tag "$dir" "$name" >/dev/null - podman rmi "$dir" >/dev/null + # Retag image from podman default $dir:latest + if [ -n "$tag" ]; then + podman tag "$dir" "$tag" >/dev/null + podman rmi "$dir" >/dev/null else - name=$dir + tag=$dir fi - echo "$name" + echo "$tag" } running() From 3811df9ab2d7087cf645cde86c899681d4d01d39 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Mon, 10 Feb 2025 14:53:03 +0100 Subject: [PATCH 07/16] container: refactor setup stage to use Finit pre: script When updating a container configuration we need to rerun the setup stage of the container. However, a run/task in Finit only runs once for each runlevel, meaning changes at runtime of a container was broken. This is a refactor of the container script to allow it to also run as a pre:script in the Finit setup stage of a sysv/service. Also included is a fix to the timeout/retry handling in case the container setup fails. We must call `read -t 60` *before* calling `ip monitor`, otherwise it will not be called until `ip monitor` returns. This patch requires Finit v4.9, or later. Fixes #930 Signed-off-by: Joachim Wiberg --- .../etc/finit.d/available/container@.conf | 11 ++++++---- board/common/rootfs/usr/sbin/container | 22 ++++++++++++------- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/board/common/rootfs/etc/finit.d/available/container@.conf b/board/common/rootfs/etc/finit.d/available/container@.conf index 19facfb0..1942816d 100644 --- a/board/common/rootfs/etc/finit.d/available/container@.conf +++ b/board/common/rootfs/etc/finit.d/available/container@.conf @@ -1,4 +1,7 @@ -task name:container-%i :setup \ - [2345] container -n %i setup -- Setup container %i -sysv :%i pid:!/run/container:%i.pid log:prio:local1,tag:%i kill:10 \ - [2345] container -n %i -- container %i +# 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 \ + [2345] :%i pre:0,/usr/sbin/container container -n %i \ + -- container %i diff --git a/board/common/rootfs/usr/sbin/container b/board/common/rootfs/usr/sbin/container index 537a7347..b6ead39d 100755 --- a/board/common/rootfs/usr/sbin/container +++ b/board/common/rootfs/usr/sbin/container @@ -9,6 +9,7 @@ DOWNLOADS=/var/lib/containers/oci BUILTIN=/lib/oci TMPDIR=/var/tmp +container=$0 checksum="" extracted= timeout=30 @@ -202,8 +203,8 @@ unpack_archive() running() { - run=$(podman inspect "$1" 2>/dev/null |jq .[].State.Running) - [ "$run" = "true" ] && return 0 + status=$(podman inspect -f '{{.State.Status}}' "$1" 2>/dev/null) + [ "$status" = "running" ] && return 0 return 1 } @@ -316,7 +317,7 @@ start() return fi - initctl -bq cond set "container:$name" + initctl start container:$name # Real work is done by wrap() courtesy of finit sysv emulation } @@ -329,7 +330,7 @@ stop() return fi - initctl -bq cond clr "container:$name" + initctl stop container:$name # Real work is done by wrap() courtesy of finit sysv emulation } @@ -338,7 +339,8 @@ wrap() name=$1 cmd=$2 - podman "$cmd" "$name" + # Skip "echo $name" from podman start in log + podman "$cmd" "$name" >/dev/null } # Removes network $1 from all containers @@ -701,12 +703,12 @@ case $cmd in script=/run/containers/${name}.sh [ -x "$script" ] || err 1 "setup: $script does not exist or is not executable." while ! "$script"; do - # Wait for address/route changes, or retry every 60 secods - # shellcheck disable=2162,3045 - ip monitor address route | while read -t 60 _; do break; done + log "${name}: setup failed, waiting for network changes ..." + read -t 60 _ < <(ip monitor address route) # 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. + log "${name}: retrying ..." sleep 2 done ;; @@ -832,6 +834,10 @@ case $cmd in esac ;; *) + if [ -n "$SERVICE_SCRIPT_TYPE" ] && [ -n "$SERVICE_ID" ]; then + # Called as pre-script from Finit service + exec $container -n "$SERVICE_ID" setup + fi usage exit 1 ;; From 67cb307f2db3d8688ae14cc30b50375cdbc3fade Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sun, 16 Feb 2025 13:13:12 +0100 Subject: [PATCH 08/16] container: refactor cleanup stage to use Finit cleanup:script Containers in Infix are read-only, any persistent data is either stored in volumes or content mounts. To prevent lingering containers, as have been seen on some test systems, we move cleanup stage from confd to use the Finit cleanup:script mechanism. The resulting code is simpler, shorter, and easier to debug and use. As an added benefit, confd no longer prunes "unused" volumes by itself on (any!) reconfiguration event, which with the new container life cycle in Infix was quite fragile. Instead the CLI command 'container prune' is extended with 'podman volume prune', which coincidentally the help text already alluded to. This patch requires Finit v4.10-rc1, or later. Fixes #902 Signed-off-by: Joachim Wiberg --- .../etc/finit.d/available/container@.conf | 4 +- board/common/rootfs/usr/sbin/container | 23 ++++++-- src/confd/src/core.c | 3 - src/confd/src/core.h | 8 +-- src/confd/src/infix-containers.c | 59 ++++++------------- src/klish-plugin-infix/xml/containers.xml | 1 + 6 files changed, 40 insertions(+), 58 deletions(-) diff --git a/board/common/rootfs/etc/finit.d/available/container@.conf b/board/common/rootfs/etc/finit.d/available/container@.conf index 1942816d..f3a5b0e3 100644 --- a/board/common/rootfs/etc/finit.d/available/container@.conf +++ b/board/common/rootfs/etc/finit.d/available/container@.conf @@ -3,5 +3,5 @@ # 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 \ - [2345] :%i pre:0,/usr/sbin/container container -n %i \ - -- container %i + pre:0,/usr/sbin/container cleanup:0,/usr/sbin/container \ + [2345] :%i container -n %i -- container %i diff --git a/board/common/rootfs/usr/sbin/container b/board/common/rootfs/usr/sbin/container index b6ead39d..8d1e4c67 100755 --- a/board/common/rootfs/usr/sbin/container +++ b/board/common/rootfs/usr/sbin/container @@ -274,7 +274,6 @@ create() delete() { name=$1 - image=$2 if [ -z "$name" ]; then echo "Usage:" @@ -283,7 +282,7 @@ delete() fi # Should already be stopped, but if not ... - container stop "$name" + container stop "$name" >/dev/null while running "$name"; do _=$((timeout -= 1)) @@ -585,11 +584,12 @@ case $cmd in ;; delete) cmd=$1 - name=$2 + [ -n "$name" ] || name=$2 if [ "$cmd" = "network" ] && [ -n "$name" ]; then netwrm "$name" else - delete "$@" + [ -n "$name" ] || name=$1 + delete "$name" fi ;; exec) @@ -835,8 +835,19 @@ case $cmd in ;; *) if [ -n "$SERVICE_SCRIPT_TYPE" ] && [ -n "$SERVICE_ID" ]; then - # Called as pre-script from Finit service - exec $container -n "$SERVICE_ID" setup + case "$SERVICE_SCRIPT_TYPE" in + pre) + # Called as pre-script from Finit service + exec $container -q -n "$SERVICE_ID" setup + ;; + cleanup) + # Called as cleanup-script from Finit service + exec $container -q -n "$SERVICE_ID" delete + ;; + *) + false + ;; + esac fi usage exit 1 diff --git a/src/confd/src/core.c b/src/confd/src/core.c index d3168c12..f8e6bc2e 100644 --- a/src/confd/src/core.c +++ b/src/confd/src/core.c @@ -75,9 +75,6 @@ int core_post_hook(sr_session_ctx_t *session, uint32_t sub_id, const char *modul return SR_ERR_SYS; } - /* Everything done, including interfaces, launch all container scripts */ - infix_containers_post_hook(session, priv); - /* skip reload in bootstrap, implicit reload in runlevel change */ if (systemf("runlevel >/dev/null 2>&1")) return SR_ERR_OK; diff --git a/src/confd/src/core.h b/src/confd/src/core.h index e2a8b0a8..bce84f7e 100644 --- a/src/confd/src/core.h +++ b/src/confd/src/core.h @@ -213,13 +213,9 @@ int hostnamefmt (struct confd *confd, const char *fmt, char *hostnm, size_t /* infix-containers.c */ #ifdef CONTAINERS -int infix_containers_init(struct confd *confd); -void infix_containers_pre_hook(sr_session_ctx_t *session, struct confd *confd); -void infix_containers_post_hook(sr_session_ctx_t *session, struct confd *confd); +int infix_containers_init(struct confd *confd); #else -static inline int infix_containers_init(struct confd *confd) { return 0; } -static inline void infix_containers_pre_hook(sr_session_ctx_t *session, struct confd *confd) {} -static inline void infix_containers_post_hook(sr_session_ctx_t *session, struct confd *confd) {} +static inline int infix_containers_init(struct confd *confd) { return 0; } #endif /* infix-dhcp-common.c */ diff --git a/src/confd/src/infix-containers.c b/src/confd/src/infix-containers.c index 9d26bc8a..431ece2b 100644 --- a/src/confd/src/infix-containers.c +++ b/src/confd/src/infix-containers.c @@ -21,9 +21,16 @@ #define CFG_XPATH "/infix-containers:containers" #define _PATH_CONT "/run/containers" -#define _PATH_INBOX _PATH_CONT "/INBOX" - +/* + * Create a setup/create/upgrade script and instantiate a new instance + * that Finit will start when all networking and other dependencies are + * out of the way. Finit calls the `/usr/sbin/container` wrapper script + * in the pre: hook to fetch and create the container instance. + * + * The script we create here, on every boot, contains all information + * needed to recreate and upgrade the user's container at runtime. + */ static int add(const char *name, struct lyd_node *cif) { const char *restart_policy, *string; @@ -206,24 +213,22 @@ static int add(const char *name, struct lyd_node *cif) fchmod(fileno(fp), 0700); fclose(fp); - /* Enable, or update, container -- both trigger setup script. */ + /* Enable, or update, container -- both trigger container setup. */ systemf("initctl -bnq enable container@%s.conf", name); systemf("initctl -bnq touch container@%s.conf", name); return 0; } +/* + * Remove setup/create/upgrade script and disable the currently running + * instance. The `/usr/sbin/container` wrapper script is called when + * Finit removes the instance, and it does not need the file we erase. + */ static int del(const char *name) { - char fn[strlen(_PATH_CONT) + strlen(name) + 10]; - - /* Remove container setup script */ - snprintf(fn, sizeof(fn), "%s/%s.sh", _PATH_CONT, name); - erase(fn); - - /* Stop and schedule for deletion */ - systemf("initctl -bnq stop container:%s", name); - writesf(name, "a", "%s", _PATH_INBOX); + erasef("%s/%s.sh", _PATH_CONT, name); + systemf("initctl -bnq disable container@%s.conf", name); return SR_ERR_OK; } @@ -312,7 +317,7 @@ static int action(sr_session_ctx_t *session, uint32_t sub_id, const char *xpath, return SR_ERR_INTERNAL; cmd += 2; - DEBUG("CALLING 'container %s %s' (xpath %s)", cmd, name, xpath); + DEBUG("RPC xpath %s, calling 'container %s %s'", xpath, cmd, name); if (systemf("container %s %s", cmd, name)) return SR_ERR_INTERNAL; @@ -335,34 +340,6 @@ static int oci_load(sr_session_ctx_t *session, uint32_t sub_id, const char *xpat return SR_ERR_OK; } -/* - * Containers depend on a lot of other system resources being properly - * set up, e.g., networking, which is run by dagger. So we need to wait - * for all that before we can launch new, or modified, containers. This - * post hook runs as (one of) the last actions on a config change/boot. - */ -void infix_containers_post_hook(sr_session_ctx_t *session, struct confd *confd) -{ - char name[256]; - FILE *fp; - - fp = fopen(_PATH_INBOX, "r"); - if (!fp) - return; /* nothing to delete */ - - while (fgets(name, sizeof(name), fp)) { - chomp(name); - systemf("initctl -bnq disable container@%s.conf", name); - systemf("container delete %s", name); - systemf("initctl -bnq cond clr container:%s", name); - } - - fclose(fp); - erase(_PATH_INBOX); - - systemf("podman volume prune -f"); -} - int infix_containers_init(struct confd *confd) { int rc; diff --git a/src/klish-plugin-infix/xml/containers.xml b/src/klish-plugin-infix/xml/containers.xml index 42a747fb..23becbae 100644 --- a/src/klish-plugin-infix/xml/containers.xml +++ b/src/klish-plugin-infix/xml/containers.xml @@ -38,6 +38,7 @@ doas podman system prune + doas podman volume prune From 4b2f14014094e933e2be79850d2901cb93b8b8e3 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sat, 22 Feb 2025 11:07:25 +0100 Subject: [PATCH 09/16] container: add support for running commands inside container - More logging when deleting a container - Update 'exec' and 'shell' commands to support running commands Signed-off-by: Joachim Wiberg --- board/common/rootfs/usr/sbin/container | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/board/common/rootfs/usr/sbin/container b/board/common/rootfs/usr/sbin/container index 8d1e4c67..ed1a1b4c 100755 --- a/board/common/rootfs/usr/sbin/container +++ b/board/common/rootfs/usr/sbin/container @@ -282,9 +282,11 @@ 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 + log "$name: still running, waiting for it to stop ..." _=$((timeout -= 1)) if [ $timeout -le 0 ]; then err 1 "timed out waiting for container $1 to stop before deleting it." @@ -292,6 +294,7 @@ 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." } @@ -428,7 +431,7 @@ commands: run NAME [CMD] Run a container interactively, with an optional command save IMAGE FILE Save a container image to an OCI tarball FILE[.tar.gz] setup NAME Create and set up container as a Finit task - shell Start a shell inside a container + shell [CMD] Start a shell, or run CMD, inside a container show [image | volume] Show containers, images, or volumes stat Show continuous stats about containers (Ctrl-C aborts) start [NAME] Start a container, see -n @@ -593,11 +596,15 @@ case $cmd in fi ;; exec) - podman exec -it "$@" + if [ -z "$name" ]; then + name="$1" + shift + fi + podman exec -i "$name" "$@" ;; flush) echo "Cleaning up any lingering containers"; - podman rm -av + podman rm -av $force ;; find) cmd=$1 @@ -713,7 +720,15 @@ case $cmd in done ;; shell) - podman exec -it "$1" sh -l + if [ -z "$name" ]; then + name="$1" + shift + fi + if [ $# -gt 0 ]; then + podman exec -i "$name" sh -c "$*" + else + podman exec -it "$name" sh -l + fi ;; show) cmd=$1 @@ -842,6 +857,7 @@ case $cmd in ;; cleanup) # Called as cleanup-script from Finit service + log "Calling $container -n $SERVICE_ID delete" exec $container -q -n "$SERVICE_ID" delete ;; *) From fc32d8a9db8ca36386f9c3eb167ad3f1039bf708 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sat, 22 Feb 2025 14:50:55 +0100 Subject: [PATCH 10/16] container: add meta data to container shell script For tasks like upgrade we need, e.g., the image URI. Instead of guessing where on the container command line the image is, use a meta-data field in the container shell script. Signed-off-by: Joachim Wiberg --- board/common/rootfs/usr/sbin/container | 7 ++++--- src/confd/src/infix-containers.c | 13 ++++++++++--- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/board/common/rootfs/usr/sbin/container b/board/common/rootfs/usr/sbin/container index ed1a1b4c..7905b712 100755 --- a/board/common/rootfs/usr/sbin/container +++ b/board/common/rootfs/usr/sbin/container @@ -436,6 +436,7 @@ commands: stat Show continuous stats about containers (Ctrl-C aborts) start [NAME] Start a container, see -n stop [NAME] Stop a container, see -n + upgrade NAME Upgrade a running container (stop, pull, restart) volume [prune] Prune unused volumes EOF } @@ -821,15 +822,15 @@ case $cmd in # Likely an OCI archive, or local directory, assume user has updated image. if echo "$img" | grep -Eq '^localhost/'; then - file=$(awk '{s=$NF} END{print s}' "$script") - echo "Upgrading container ${1} with local archive: $file ..." + file=$(awk '/^# meta-image:/ {print $3}' "$script") + echo ">> Upgrading container $1 using $file ..." else printf ">> Stopping ... " podman stop "$1" printf ">> " podman pull "$img" || (echo "Failed fetching $img, check your network (settings)."; exit 1) + echo ">> Starting $1 ..." fi - echo ">> Starting $1 ..." if ! "$script"; then echo ">> Failed recreating container $1" exit 1 diff --git a/src/confd/src/infix-containers.c b/src/confd/src/infix-containers.c index 431ece2b..9c4620cc 100644 --- a/src/confd/src/infix-containers.c +++ b/src/confd/src/infix-containers.c @@ -33,7 +33,7 @@ */ static int add(const char *name, struct lyd_node *cif) { - const char *restart_policy, *string; + const char *restart_policy, *string, *image; struct lyd_node *node, *nets, *caps; char script[strlen(name) + 5]; FILE *fp, *ap; @@ -60,10 +60,17 @@ static int add(const char *name, struct lyd_node *cif) * setup at creation/boot and for manual upgrade. The delete * command ensures any already running container is stopped and * deleted so that it releases all claimed resources. + * + * The odd meta data is not used by the script itself, instead + * it is used by the /usr/sbin/container wrapper when upgrading + * a running container instance. */ + image = lydx_get_cattr(cif, "image"); fprintf(fp, "#!/bin/sh\n" + "# meta-name: %s\n" + "# meta-image: %s\n" "container --quiet delete %s >/dev/null\n" - "container --quiet", name); + "container --quiet", name, image, name); LYX_LIST_FOR_EACH(lyd_child(cif), node, "dns") fprintf(fp, " --dns %s", lyd_get_value(node)); @@ -204,7 +211,7 @@ static int add(const char *name, struct lyd_node *cif) fprintf(fp, " --checksum sha512:%s", string); } - fprintf(fp, " create %s %s", name, lydx_get_cattr(cif, "image")); + fprintf(fp, " create %s %s", name, image); if ((string = lydx_get_cattr(cif, "command"))) fprintf(fp, " %s", string); From 58cf5abf0b2308d7deb6cdf0cade14e59ad89167 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Mon, 17 Feb 2025 05:46:14 +0100 Subject: [PATCH 11/16] test: verify modifying a running container takes Add new test step that adds a content mount to /var/www/index.html to verify that changes to a running container are activated. This test pass was previously part of the container bridge test, but was moved here to allow that test to focus on bridge specific checks. Issue #930 Signed-off-by: Joachim Wiberg --- .../container_phys/container_phys.adoc | 8 ++- .../infix_containers/container_phys/test.py | 72 ++++++++++++------- 2 files changed, 50 insertions(+), 30 deletions(-) diff --git a/test/case/infix_containers/container_phys/container_phys.adoc b/test/case/infix_containers/container_phys/container_phys.adoc index fc4220e1..eb02a4cb 100644 --- a/test/case/infix_containers/container_phys/container_phys.adoc +++ b/test/case/infix_containers/container_phys/container_phys.adoc @@ -17,10 +17,12 @@ endif::testgroup[] endif::topdoc[] ==== Test sequence . Set up topology and attach to target DUT -. Create container 'web-phys' from bundled OCI image -. Verify container 'web-phys' has started +. Create httpd container from bundled OCI image +. Verify container has started . Verify host:data can ping 10.0.0.2 -. Verify container 'web-phys' is reachable on http://10.0.0.2:91 +. Verify container is reachable on http://10.0.0.2:91 +. Add a content mount, overriding index.html +. Verify server is restarted and returns new content <<< diff --git a/test/case/infix_containers/container_phys/test.py b/test/case/infix_containers/container_phys/test.py index 08256649..fa54f7a8 100755 --- a/test/case/infix_containers/container_phys/test.py +++ b/test/case/infix_containers/container_phys/test.py @@ -11,12 +11,14 @@ given a physical interface instead of an end of a VETH pair. """ import base64 import infamy -from infamy.util import until +from infamy.util import until, to_binary with infamy.Test() as test: NAME = "web-phys" DUTIP = "10.0.0.2" OURIP = "10.0.0.1" + MESG = "Kilroy was here" + BODY = f"

{MESG}

" URL = f"http://{DUTIP}:91/index.html" with test.step("Set up topology and attach to target DUT"): @@ -26,43 +28,39 @@ with infamy.Test() as test: if not target.has_model("infix-containers"): test.skip() - with test.step("Create container 'web-phys' from bundled OCI image"): + with test.step("Create httpd container from bundled OCI image"): _, 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 - }] - }, - "container-network": {} - } - ] + "interface": [{ + "name": f"{ifname}", + "ipv4": { + "address": [{ + "ip": f"{DUTIP}", + "prefix-length": 24 + }] + }, + "container-network": {} + }] } }) target.put_config_dict("infix-containers", { "containers": { - "container": [ - { - "name": f"{NAME}", - "image": f"oci-archive:{infamy.Container.HTTPD_IMAGE}", - "command": "/usr/sbin/httpd -f -v -p 91", - "network": { - "interface": [ - { "name": f"{ifname}" } - ] - } + "container": [{ + "name": f"{NAME}", + "image": f"oci-archive:{infamy.Container.HTTPD_IMAGE}", + "command": "/usr/sbin/httpd -f -v -p 91", + "network": { + "interface": [ + {"name": f"{ifname}"} + ] } - ] + }] } }) - with test.step("Verify container 'web-phys' has started"): + with test.step("Verify container has started"): c = infamy.Container(target) until(lambda: c.running(NAME), attempts=10) @@ -74,7 +72,27 @@ with infamy.Test() as test: with test.step("Verify host:data can ping 10.0.0.2"): ns.must_reach(DUTIP) - with test.step("Verify container 'web-phys' is reachable on http://10.0.0.2:91"): + with test.step("Verify container is reachable on http://10.0.0.2:91"): until(lambda: url.nscheck(ns, "It works"), attempts=10) + with test.step("Add a content mount, overriding index.html"): + # Verify modifying a running container takes, issue #930 + data = to_binary(BODY) + + target.put_config_dict("infix-containers", { + "containers": { + "container": [{ + "name": f"{NAME}", + "mount": [{ + "name": "index.html", + "content": f"{data}", + "target": "/var/www/index.html" + }] + }] + } + }) + + with test.step("Verify server is restarted and returns new content"): + until(lambda: url.nscheck(ns, MESG), attempts=10) + test.succeed() From 5660f6239d9251a154fbd9a7870ba8d9f3fe4ebf Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sat, 22 Feb 2025 11:51:08 +0100 Subject: [PATCH 12/16] test: refine container bridge test The phys container test verifies custom mounts, so let's refine and consolidate this test to focus on docker bridge features. Signed-off-by: Joachim Wiberg --- .../container_bridge/container_bridge.adoc | 8 +- .../infix_containers/container_bridge/test.py | 100 +++++++++--------- 2 files changed, 52 insertions(+), 56 deletions(-) diff --git a/test/case/infix_containers/container_bridge/container_bridge.adoc b/test/case/infix_containers/container_bridge/container_bridge.adoc index a2b08bf5..9c86bf06 100644 --- a/test/case/infix_containers/container_bridge/container_bridge.adoc +++ b/test/case/infix_containers/container_bridge/container_bridge.adoc @@ -21,10 +21,10 @@ endif::testgroup[] endif::topdoc[] ==== Test sequence . Set up topology and attach to target DUT -. Create container 'web-docker0' from bundled OCI image -. Verify container 'web-docker0' has started -. Verify basic DUT connectivity, host:data can ping DUT 10.0.0.2 -. Verify container 'web-docker0' is reachable on http://10.0.0.2:8080 +. Create httpd container from bundled OCI image +. Verify container has started +. Verify DUT connectivity, host can reach 10.0.0.2 +. Verify container is reachable on http://10.0.0.2:8080 <<< diff --git a/test/case/infix_containers/container_bridge/test.py b/test/case/infix_containers/container_bridge/test.py index 627ca874..3025f9af 100755 --- a/test/case/infix_containers/container_bridge/test.py +++ b/test/case/infix_containers/container_bridge/test.py @@ -22,7 +22,7 @@ with infamy.Test() as test: NAME = "web-docker0" DUTIP = "10.0.0.2" OURIP = "10.0.0.1" - BODY = "

Kilroy was here

" + MESG = "It works" URL = f"http://{DUTIP}:8080/index.html" with test.step("Set up topology and attach to target DUT"): @@ -32,61 +32,55 @@ with infamy.Test() as test: if not target.has_model("infix-containers"): test.skip() - with test.step("Create container 'web-docker0' from bundled OCI image"): + with test.step("Create httpd container from bundled OCI image"): _, ifname = env.ltop.xlate("target", "data") - data = to_binary(BODY) - target.put_config_dict("ietf-interfaces", { - "interfaces": { - "interface": [ - { - "name": f"{ifname}", - "ipv4": { - "address": [{ - "ip": f"{DUTIP}", - "prefix-length": 24 - }] + target.put_config_dicts({ + "ietf-interfaces": { + "interfaces": { + "interface": [ + { + "name": f"{ifname}", + "ipv4": { + "address": [{ + "ip": f"{DUTIP}", + "prefix-length": 24 + }] + } + }, { + "name": "docker0", + "type": "infix-if-type:bridge", + "container-network": { + "type": "bridge", + "subnet": [ + {"subnet": "172.17.0.0/16"}, + {"subnet": "2a02:2789:724:eb8:1::/80"} + ] + } } - }, - { - "name": "docker0", - "type": "infix-if-type:bridge", - "container-network": { - "type": "bridge", - "subnet": [ - { "subnet": "172.17.0.0/16" }, - { "subnet": "2a02:2789:724:eb8:1::/80" } - ] - } - } - ] - } - }) - target.put_config_dict("infix-containers", { - "containers": { - "container": [ - { + ] + } + }, + "infix-containers": { + "containers": { + "container": [{ "name": f"{NAME}", "image": f"oci-archive:{infamy.Container.HTTPD_IMAGE}", - "command": "/usr/sbin/httpd -f -v -p 91", - "mount": [ - { - "name": "index.html", - "content": f"{data}", - "target": "/var/www/index.html" - } - ], + "command": "/usr/sbin/httpd -f -v", "network": { - "interface": [ - { "name": "docker0" } - ], - "publish": [ "8080:91" ] + "interface": [{ + "name": "docker0", + "option": [ + "interface_name=wan", + "ip=172.17.0.2" + ], + }], + "publish": ["8080:80"] } - } - ] - } - }) + }] + } + }}) - with test.step("Verify container 'web-docker0' has started"): + with test.step("Verify container has started"): c = infamy.Container(target) until(lambda: c.running(NAME), attempts=10) @@ -95,9 +89,11 @@ with infamy.Test() as test: with infamy.IsolatedMacVlan(hport) as ns: ns.addip(OURIP) - with test.step("Verify basic DUT connectivity, host:data can ping DUT 10.0.0.2"): + + with test.step("Verify DUT connectivity, host can reach 10.0.0.2"): ns.must_reach(DUTIP) - with test.step("Verify container 'web-docker0' is reachable on http://10.0.0.2:8080"): - until(lambda: url.nscheck(ns, "Kilroy was here"), attempts=10) + + with test.step("Verify container is reachable on http://10.0.0.2:8080"): + until(lambda: url.nscheck(ns, MESG), attempts=10) test.succeed() From 91f0094048ab3f2050eb213715dc2c9959577ce5 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sat, 22 Feb 2025 14:53:04 +0100 Subject: [PATCH 13/16] test: add missing path to container images Signed-off-by: Joachim Wiberg --- test/infamy/container.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/infamy/container.py b/test/infamy/container.py index 7b0392e5..7fedbd53 100644 --- a/test/infamy/container.py +++ b/test/infamy/container.py @@ -5,8 +5,8 @@ from infamy.util import warn class Container: """Helper methods""" - HTTPD_IMAGE = "curios-httpd-latest.tar.gz" - NFTABLES_IMAGE = "curios-nftables-latest.tar.gz" + HTTPD_IMAGE = "/lib/oci/curios-httpd-latest.tar.gz" + NFTABLES_IMAGE = "/lib/oci/curios-nftables-latest.tar.gz" def __init__(self, target): self.system = target From d68dacdc80fbde0696b75b64a9e4034f77b904d9 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sat, 22 Feb 2025 14:54:17 +0100 Subject: [PATCH 14/16] test: new test, verify volume content is not lost Signed-off-by: Joachim Wiberg --- test/case/infix_containers/Readme.adoc | 3 + .../container_volume/Readme.adoc | 1 + .../container_volume/container_volume.adoc | 30 ++++++++ .../infix_containers/container_volume/test.py | 71 +++++++++++++++++++ .../container_volume/topology.dot | 1 + .../container_volume/topology.svg | 42 +++++++++++ .../infix_containers/infix_containers.yaml | 3 + 7 files changed, 151 insertions(+) create mode 120000 test/case/infix_containers/container_volume/Readme.adoc create mode 100644 test/case/infix_containers/container_volume/container_volume.adoc create mode 100755 test/case/infix_containers/container_volume/test.py create mode 120000 test/case/infix_containers/container_volume/topology.dot create mode 100644 test/case/infix_containers/container_volume/topology.svg diff --git a/test/case/infix_containers/Readme.adoc b/test/case/infix_containers/Readme.adoc index e67eb1b0..f17fe352 100644 --- a/test/case/infix_containers/Readme.adoc +++ b/test/case/infix_containers/Readme.adoc @@ -7,6 +7,7 @@ Verifies Infix Docker container support: - 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 + - Upgrading a container with a volume, verifying volume data is intact - Basic firewall container running in host network mode and full privileges <<< @@ -19,4 +20,6 @@ include::container_phys/Readme.adoc[] include::container_veth/Readme.adoc[] +include::container_volume/Readme.adoc[] + include::container_firewall_basic/Readme.adoc[] diff --git a/test/case/infix_containers/container_volume/Readme.adoc b/test/case/infix_containers/container_volume/Readme.adoc new file mode 120000 index 00000000..9beaf0a3 --- /dev/null +++ b/test/case/infix_containers/container_volume/Readme.adoc @@ -0,0 +1 @@ +container_volume.adoc \ No newline at end of file diff --git a/test/case/infix_containers/container_volume/container_volume.adoc b/test/case/infix_containers/container_volume/container_volume.adoc new file mode 100644 index 00000000..20fbfc05 --- /dev/null +++ b/test/case/infix_containers/container_volume/container_volume.adoc @@ -0,0 +1,30 @@ +=== Container Volume Persistence +==== Description +Verify that a container created from a local OCI archive, with a volume +for persistent content, can be upgraded at runtime, without losing the +content in the volume on restart. + +==== Topology +ifdef::topdoc[] +image::{topdoc}../../test/case/infix_containers/container_volume/topology.svg[Container Volume Persistence topology] +endif::topdoc[] +ifndef::topdoc[] +ifdef::testgroup[] +image::container_volume/topology.svg[Container Volume Persistence topology] +endif::testgroup[] +ifndef::testgroup[] +image::topology.svg[Container Volume Persistence topology] +endif::testgroup[] +endif::topdoc[] +==== Test sequence +. Set up topology and attach to target DUT +. Create container with volume from bundled OCI image +. Verify container has started +. Modify container volume content +. Verify container volume content +. Upgrade container +. Verify container volume content survived upgrade + + +<<< + diff --git a/test/case/infix_containers/container_volume/test.py b/test/case/infix_containers/container_volume/test.py new file mode 100755 index 00000000..c8f5d1e0 --- /dev/null +++ b/test/case/infix_containers/container_volume/test.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +"""Container Volume Persistence + +Verify that a container created from a local OCI archive, with a volume +for persistent content, can be upgraded at runtime, without losing the +content in the volume on restart. + +""" +import infamy +from infamy.util import until + +with infamy.Test() as test: + NAME = "web-volume" + PORT = 8080 + MESG = "HEJ" + + with test.step("Set up topology and attach to target DUT"): + env = infamy.Env() + target = env.attach("target", "mgmt") + tgtssh = env.attach("target", "mgmt", "ssh") + addr = target.get_mgmt_ip() + url = infamy.Furl(f"http://[{addr}]:{PORT}/index.html") + + if not target.has_model("infix-containers"): + test.skip() + + with test.step("Create container with volume from bundled OCI image"): + target.put_config_dict("infix-containers", { + "containers": { + "container": [ + { + "name": f"{NAME}", + "image": f"oci-archive:{infamy.Container.HTTPD_IMAGE}", + "command": f"/usr/sbin/httpd -f -v -p {PORT}", + "network": { + "host": True + }, + "volume": [{ + "name": "www", + "target": "/var/www" + }] + } + ] + } + }) + + with test.step("Verify container has started"): + c = infamy.Container(target) + until(lambda: c.running(NAME), attempts=10) + + with test.step("Modify container volume content"): + cmd = f"sudo container shell {NAME} 'echo {MESG} >/var/www/index.html'" + tgtssh.runsh(cmd) + + with test.step("Verify container volume content"): + until(lambda: url.check(MESG), attempts=10) + + with test.step("Upgrade container"): + out = tgtssh.runsh(f"sudo container upgrade {NAME}") + if ">> Done." not in out.stdout: + msg = f"Failed upgrading container {NAME}:\n" \ + f"STDOUT:\n{out.stdout}\n" \ + f"STDERR:\n{out.stderr}" + test.fail(msg) + # else: + # print(f"Container {NAME} upgraded: {out.stdout}") + + with test.step("Verify container volume content survived upgrade"): + until(lambda: url.check(MESG), attempts=10) + + test.succeed() diff --git a/test/case/infix_containers/container_volume/topology.dot b/test/case/infix_containers/container_volume/topology.dot new file mode 120000 index 00000000..4f53d15a --- /dev/null +++ b/test/case/infix_containers/container_volume/topology.dot @@ -0,0 +1 @@ +../../../infamy/topologies/1x2.dot \ No newline at end of file diff --git a/test/case/infix_containers/container_volume/topology.svg b/test/case/infix_containers/container_volume/topology.svg new file mode 100644 index 00000000..ff3d246b --- /dev/null +++ b/test/case/infix_containers/container_volume/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 3d364de7..40e51a31 100644 --- a/test/case/infix_containers/infix_containers.yaml +++ b/test/case/infix_containers/infix_containers.yaml @@ -12,6 +12,9 @@ - name: container_veth case: container_veth/test.py +- name: container_volume + case: container_volume/test.py + - name: container_firewall_basic case: container_firewall_basic/test.py From c28ea1db19c30691935f181ef890bd50ddf83e9c Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Mon, 17 Feb 2025 05:32:45 +0100 Subject: [PATCH 15/16] doc: refresh container documentation Signed-off-by: Joachim Wiberg --- doc/container.md | 71 +++++++++++++++++++++++++++--------------------- 1 file changed, 40 insertions(+), 31 deletions(-) diff --git a/doc/container.md b/doc/container.md index c3045754..69b00108 100644 --- a/doc/container.md +++ b/doc/container.md @@ -31,9 +31,9 @@ Infix comes with native support for Docker containers using [podman][]. The [YANG model][1] describes the current level of support, complete enough to run both system and application containers. -Key design features, like using Linux switchdev, allow users to assign -switch ports directly to containers, not just bridged VETH pairs, this -is a rare and in many cases *unique* feature of Infix. +Key design features of Infix, like using Linux switchdev, allow users to +assign switch ports directly to containers, not just bridged VETH pairs. +This is a rare and in many cases *unique* feature of Infix. All network specific settings are done using the IETF interfaces YANG model, with augments for containers to ensure smooth integration with @@ -43,7 +43,7 @@ container networking in podman. > Even though the `podman` command can be used directly from a shell > prompt, we strongly recommend using the CLI commands instead. They > employ the services of a wrapper `container` script which handles the -> integration of containers in the system. +> integration of Docker containers in the system. Caution @@ -83,18 +83,20 @@ In the CLI, containers can be run in one of two ways: 1. `container run IMAGE [COMMAND]`, or 2. enter `configure` context, then `edit container NAME` -The former is useful mostly for testing, or running single commands in -an image. It is a wrapper for `podman run -it --rm ...`, while the -latter is a wrapper and adaptation of `podman create ...`. +The first is useful mostly for testing, or running single commands in +an image. It is a wrapper for `podman run -it --rm ...`. -The second creates a read-only container that is automatically started -at every boot. When non-volatile storage is needed, data stored in a -volume is persisted until explicitly removed from the configuration, -i.e., across host and container reboots and upgrades. +The second creates a read-only container that by default automatically +start at every boot. It basically wraps `podman create ...`. -Another option is [Content Mounts](#content-mounts), where the content -of a file mounted into the container is kept along with the container -configuration in the device's `startup-config`. +When non-volatile storage is needed two complementary options exist: + + - **Volumes:** data stored in a volume is persisted until explicitly + removed from the configuration, i.e., across host reboots and + container upgrades + - **[Content Mounts](#content-mounts):** where the content of a file + mounted into the container is kept along with the container + configuration in the device's `startup-config` Podman ensures (using tmpfs) all containers have writable directories for certain critical file system paths: `/dev`, `/dev/shm`, `/run`, @@ -127,24 +129,30 @@ Classic Hello World: Hello from Docker! This message shows that your installation appears to be working correctly. -Persistent web server using nginx, sharing the host's network: +A web server with [nginx][], using standard docker bridge. Podman will +automatically create a VETH pair for us, connecting the container to the +`docker0` bridge: admin@example:/> configure - admin@example:/config> edit container web - admin@example:/config/container/web> set image docker://nginx:alpine - admin@example:/config/container/web> set publish 80:80 - admin@example:/config/container/web> set network host - admin@example:/config/container/web> leave + admin@example:/config/> edit interface docker0 + admin@example:/config/interface/docker0/> set container-network + admin@example:/config/interface/docker0/> end + admin@example:/config/> edit container web + admin@example:/config/container/web/> set image docker://nginx:alpine + admin@example:/config/container/web/> set network publish 8080:80 + admin@example:/config/container/web/> set network interface docker0 + admin@example:/config/container/web/> set volume cache target /var/cache + admin@example:/config/container/web/> leave admin@example:/> show container Exit to the shell and verify the service with curl, or try to attach to your device's IP address using your browser: - admin@example:~$ curl http://localhost + admin@example:~$ curl http://localhost:8080 -or connect to port 80 of your running Infix system with a browser. See -the following sections for how to add more interfaces and manage your -container at runtime. +or connect to port 8080 of your running Infix system with a browser. +See the following sections for how to add more interfaces and manage +your container at runtime. Container Images @@ -456,11 +464,11 @@ in a `bridge`. Below an example of a system container calls `set network interface docker0`, here we show how to set options for that network: - admin@example:/config/container/ntpd/> edit network docker0 - admin@example:/config/container/ntpd/network/docker0/> - admin@example:/config/container/ntpd/network/docker0/> set option + admin@example:/config/container/ntpd/> edit network interface docker0 + admin@example:/config/container/ntpd/network/interface/docker0/> + admin@example:/config/container/ntpd/network/interface/docker0/> set option Options for masquerading container bridges. - admin@example:/config/container/ntpd/network/docker0/> help option + admin@example:/config/container/ntpd/network/interface/docker0/> help option NAME option @@ -471,9 +479,9 @@ network: mac=00:01:02:c0:ff:ee -- set fixed MAC address in container interface_name=foo0 -- set interface name inside container - admin@example:/config/container/ntpd/network/docker0/> set option ip=172.17.0.2 - admin@example:/config/container/ntpd/network/docker0/> set option interface_name=wan - admin@example:/config/container/ntpd/network/docker0/> leave + admin@example:/config/container/ntpd/network/interface/docker0/> set option ip=172.17.0.2 + admin@example:/config/container/ntpd/network/interface/docker0/> set option interface_name=wan + admin@example:/config/container/ntpd/network/interface/docker0/> leave ### Container Host Interface @@ -903,4 +911,5 @@ Container Image](#upgrading-a-container-image) (above). [14]: https://github.com/kernelkit/curiOS/ [15]: https://github.com/kernelkit/curiOS/blob/2e4748f65e356b2c117f586cd9420d7ba66f79d5/board/system/rootfs/etc/inittab [tini]: https://github.com/krallin/tini +[nginx]: https://hub.docker.com/_/nginx [podman]: https://podman.io From fd7b4bbe39aac0b509260949be33a06f22acc8ca Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sun, 23 Feb 2025 13:41:23 +0100 Subject: [PATCH 16/16] doc: update ChangeLog for v25.02 release Signed-off-by: Joachim Wiberg --- doc/ChangeLog.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/doc/ChangeLog.md b/doc/ChangeLog.md index 5b093521..320dbc78 100644 --- a/doc/ChangeLog.md +++ b/doc/ChangeLog.md @@ -10,17 +10,24 @@ All notable changes to the project are documented in this file. ### Changes - Upgrade Linux kernel to 6.12.16 (LTS) - Upgrade Buildroot to 2024.02.11 (LTS) - - YANG type for SSH private/public keys has changed, from - ietf-crypto-types to infix-crypto-types - Add support for link aggregation (lag), static (balance-xor) and LACP + - Add support for the [i.MX 8M Plus EVK][EVK] + - YANG type change for SSH private/public keys, from ietf-crypto-types + to infix-crypto-types - Drop automatic default route (interface route) for IPv4 autoconf, not necessary and causes more confusion than good. Issue #923 ### Fixes + - Fix #902: containers "linger" in the system (state 'exited') after + having removed them from the configuration + - Fix #930: container configuration changes does not apply at runtime + only when saved to `startup-config` and system is rebooted - Fix #956: CLI `copy` command complains it cannot change owner when copying `factory-config` to `running-config`. Bogus error, the latter is not really a file +[EVK]: https://www.nxp.com/design/design-center/development-boards-and-designs/8MPLUSLPD4-EVK + [v25.01.0][] - 2025-01-31 -------------------------