Merge pull request #955 from kernelkit/lingering-containers

Lingering containers
This commit is contained in:
Tobias Waldekranz
2025-02-25 13:41:55 +01:00
committed by GitHub
26 changed files with 457 additions and 251 deletions
+5 -5
View File
@@ -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
@@ -1,4 +1,7 @@
task name:container-%i :setup \
[2345] container -n %i setup -- Setup container %i
sysv <!usr/container:%i> :%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 \
pre:0,/usr/sbin/container cleanup:0,/usr/sbin/container \
[2345] <!> :%i container -n %i -- container %i
+75 -40
View File
@@ -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.
@@ -9,6 +9,7 @@
DOWNLOADS=/var/lib/containers/oci
BUILTIN=/lib/oci
TMPDIR=/var/tmp
container=$0
checksum=""
extracted=
timeout=30
@@ -109,33 +110,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 +149,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 +173,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,21 +190,21 @@ 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()
{
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
}
@@ -272,7 +274,6 @@ create()
delete()
{
name=$1
image=$2
if [ -z "$name" ]; then
echo "Usage:"
@@ -281,9 +282,11 @@ delete()
fi
# Should already be stopped, but if not ...
container stop "$name"
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."
@@ -291,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."
}
@@ -315,7 +319,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
}
@@ -328,7 +332,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
}
@@ -337,7 +341,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
@@ -426,11 +431,12 @@ 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
stop [NAME] Stop a container, see -n
upgrade NAME Upgrade a running container (stop, pull, restart)
volume [prune] Prune unused volumes
EOF
}
@@ -582,19 +588,24 @@ 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)
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
@@ -700,17 +711,25 @@ 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
;;
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
@@ -803,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
@@ -831,6 +850,22 @@ case $cmd in
esac
;;
*)
if [ -n "$SERVICE_SCRIPT_TYPE" ] && [ -n "$SERVICE_ID" ]; then
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
log "Calling $container -n $SERVICE_ID delete"
exec $container -q -n "$SERVICE_ID" delete
;;
*)
false
;;
esac
fi
usage
exit 1
;;
+9 -2
View File
@@ -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
-------------------------
+40 -31
View File
@@ -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
<string> 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 <string>
@@ -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
+10 -7
View File
@@ -17,17 +17,20 @@ run name:startup log:prio:user.notice norestart <pid/confd> env:/etc/default/con
-- Loading startup-config
# Run if loading startup-config fails for some reason
run name:failure log:prio:user.critical norestart <pid/confd> env:/etc/default/confd if:<run/startup/failure> \
[S] /usr/libexec/confd/load -t $CONFD_TIMEOUT failure-config \
run name:failure log:prio:user.critical norestart env:/etc/default/confd \
if:<run/startup/failure> \
[S] <pid/confd> /usr/libexec/confd/load -t $CONFD_TIMEOUT failure-config \
-- Loading failure-config
run name:error :2 log:console norestart if:<run/failure/failure> \
run name:error :2 log:console norestart \
if:<run/failure/failure> \
[S] /usr/libexec/confd/error --
service name:netopeer notify:none log <pid/confd> env:/etc/default/confd \
[12345] netopeer2-server -F -t $CONFD_TIMEOUT -v 1 \
service name:netopeer notify:none log env:/etc/default/confd \
[12345] <pid/confd> netopeer2-server -F -t $CONFD_TIMEOUT -v 1 \
-- NETCONF server
# Create initial /etc/resolv.conf after successful bootstrap
task name:resolv :conf norestart <pid/dnsmasq> if:<run/startup/success> \
[S] resolvconf -u -- Update DNS configuration
task name:resolv :conf norestart \
if:<run/startup/success> \
[S] <pid/dnsmasq> resolvconf -u -- Update DNS configuration
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
-3
View File
@@ -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;
+2 -6
View File
@@ -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 */
+28 -44
View File
@@ -21,12 +21,19 @@
#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;
const char *restart_policy, *string, *image;
struct lyd_node *node, *nets, *caps;
char script[strlen(name) + 5];
FILE *fp, *ap;
@@ -53,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));
@@ -197,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);
@@ -206,24 +220,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 +324,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 +347,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;
@@ -38,6 +38,7 @@
<COMMAND name="prune" help="Clean up all unused containers, images and volume data">
<ACTION sym="script" in="tty" out="tty" interrupt="true">
doas podman system prune
doas podman volume prune
</ACTION>
</COMMAND>
+3
View File
@@ -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[]
@@ -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
<<<
@@ -22,7 +22,7 @@ with infamy.Test() as test:
NAME = "web-docker0"
DUTIP = "10.0.0.2"
OURIP = "10.0.0.1"
BODY = "<html><body><p>Kilroy was here</p></body></html>"
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()
@@ -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
<<<
@@ -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"<html><body><p>{MESG}</p></body></html>"
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()
@@ -0,0 +1 @@
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
<<<
+71
View File
@@ -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()
@@ -0,0 +1 @@
../../../infamy/topologies/1x2.dot
@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Title: 1x2 Pages: 1 -->
<svg width="424pt" height="55pt"
viewBox="0.00 0.00 424.03 55.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 51)">
<title>1x2</title>
<polygon fill="white" stroke="transparent" points="-4,4 -4,-51 420.03,-51 420.03,4 -4,4"/>
<!-- host -->
<g id="node1" class="node">
<title>host</title>
<polygon fill="none" stroke="black" points="0,-0.5 0,-46.5 100,-46.5 100,-0.5 0,-0.5"/>
<text text-anchor="middle" x="25" y="-19.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">host</text>
<polyline fill="none" stroke="black" points="50,-0.5 50,-46.5 "/>
<text text-anchor="middle" x="75" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
<polyline fill="none" stroke="black" points="50,-23.5 100,-23.5 "/>
<text text-anchor="middle" x="75" y="-8.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">data</text>
</g>
<!-- target -->
<g id="node2" class="node">
<title>target</title>
<polygon fill="none" stroke="black" points="300.03,-0.5 300.03,-46.5 416.03,-46.5 416.03,-0.5 300.03,-0.5"/>
<text text-anchor="middle" x="325.03" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
<polyline fill="none" stroke="black" points="300.03,-23.5 350.03,-23.5 "/>
<text text-anchor="middle" x="325.03" y="-8.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">data</text>
<polyline fill="none" stroke="black" points="350.03,-0.5 350.03,-46.5 "/>
<text text-anchor="middle" x="383.03" y="-19.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">target</text>
</g>
<!-- host&#45;&#45;target -->
<g id="edge1" class="edge">
<title>host:mgmt&#45;&#45;target:mgmt</title>
<path fill="none" stroke="lightgrey" stroke-width="2" d="M100,-35.5C100,-35.5 300.03,-35.5 300.03,-35.5"/>
</g>
<!-- host&#45;&#45;target -->
<g id="edge2" class="edge">
<title>host:data&#45;&#45;target:data</title>
<path fill="none" stroke="black" stroke-width="2" d="M100,-11.5C100,-11.5 300.03,-11.5 300.03,-11.5"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

@@ -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
+14 -10
View File
@@ -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()
+2 -2
View File
@@ -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
+9 -9
View File
@@ -11,37 +11,37 @@ graph "ring-4-duts" {
host [
label=" { host | { <mgmt4> mgmt4 | <data4> data4 | <mgmt1> mgmt1 | <data1> data1 | <data2> data2 | <mgmt2> mgmt2 | <data3> data3 | <mgmt3> mgmt3 } }",
pos="150,200!"
kind="controller",
requires="controller",
];
R1 [
label="{ { <mgmt> mgmt | <data> data } | \n R1 \n 10.0.0.1/32 \n(lo) } | { <ring1> ring1 | <cross> cross | <ring2> ring2 }",
pos="50,150!",
kind="infix",
requires="infix",
];
R4 [
label="{ { <mgmt> mgmt | <data> data } | \n R4 \n 10.0.0.4/32 \n(lo) } | { <ring1> ring1 | <cross> cross | <ring2> ring2 }",
pos="50,50!"
kind="infix",
requires="infix",
];
R2 [
label="{ <ring2> ring2 | <cross> cross | <ring1> ring1 } | { { <data> data | <mgmt> mgmt } | \n R2 \n10.0.0.2/32 \n(lo) }",
pos="250,150!"
kind="infix",
requires="infix",
];
R3 [
label="{ <ring2> ring2 | <cross> cross | <ring1> ring1 } | { { <data> data | <mgmt> 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"]