diff --git a/board/common/rootfs/etc/finit.d/available/container@.conf b/board/common/rootfs/etc/finit.d/available/container@.conf index 7a494c60..4e6b813c 100644 --- a/board/common/rootfs/etc/finit.d/available/container@.conf +++ b/board/common/rootfs/etc/finit.d/available/container@.conf @@ -1,7 +1,8 @@ # Start a container instance (%i) and redirect logs to /log/container -# Give podman enough time to properly shut down the container, kill:30 -# The pre:script, which is responsibe for fetching a remote image, must -# not have a timeout. The cleanup should take no longer than a minute. +# Give podman enough time to properly shut down the container, kill:30, +# which is also matched in the container script (podman default is 10!) +# The pre:script, responsibe for fetching a remote image and calling on +# 'podman load', must not have a timeout. sysv log:prio:local1,tag:%i kill:30 pid:!/run/container:%i.pid \ - pre:0,/usr/sbin/container cleanup:60,/usr/sbin/container \ + 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 fe68c85a..e407c1ab 100755 --- a/board/common/rootfs/usr/sbin/container +++ b/board/common/rootfs/usr/sbin/container @@ -6,19 +6,33 @@ # NOTE: when creating/deleting containers, remember 'initctl reload' to # activate the changes! In confd this is already handled. # +# TODO: this script should be refactored to take a .cfg file instead for +# each container. For details, see this pull request discussion: +# https://github.com/kernelkit/infix/pull/1151#discussion_r2444851292 +# +# shellcheck disable=SC1001 +CLEANUP=/var/lib/containers/cleanup DOWNLOADS=/var/lib/containers/oci BUILTIN=/lib/oci BASEDIR=/var/tmp container=$0 checksum="" extracted= -timeout=30 +timeout=30 # NOTE: matched with container@.conf dir="" all="" env="" port="" force= +# Variable shared across subshells +export meta_sha="" + +dbg() +{ + logger -I $PPID -t container -p local1.debug -- "$*" +} + log() { logger -I $PPID -t container -p local1.notice -- "$*" @@ -44,6 +58,104 @@ pidfn() echo "/run/containers/${1}.pid" } +calc_sha() +{ + sha256sum "$1" 2>/dev/null | awk '{print $1}' +} + +# Check image transport, return 0 if remote, 1 if local +is_remote() +{ + image=$(awk '/^# meta-image:/ {print $3}' "$1" 2>/dev/null || echo "") + echo "$image" + + case "$image" in + oci\:* | oci-archive\:* | docker-archive\:* | docker-daemon\:* | \ + dir\:* | containers-storage\:* | ostree\:* | sif\:* | /*) + return 1 + ;; + *) + return 0 + ;; + esac +} + +# Check loaded image in container store vs archive, as well as verifying +# the container instance vs. the configuration. +# +# This is an optimization to cut down startup times. +is_uptodate() +{ + img_sha=$(awk '/^# meta-image-sha256:/ {print $3}' "$script" 2>/dev/null) + if [ -n "$img_sha" ]; then + # Local image optimization: check if archive SHA matches stored sidecar file + img=$(awk '/^# meta-image:/ {print $3}' "$script" 2>/dev/null) + if [ -n "$img" ]; then + case "$img" in + docker-archive\:*) + archive_path="${img#docker-archive:}" + ;; + oci-archive\:*) + archive_path="${img#oci-archive:}" + ;; + *) + archive_path="$img" + ;; + esac + + # Handle relative paths - check BUILTIN and DOWNLOADS + if [ ! -f "$archive_path" ]; then + if [ -f "$DOWNLOADS/$(basename "$archive_path")" ]; then + archive_path="$DOWNLOADS/$(basename "$archive_path")" + elif [ -f "$BUILTIN/$(basename "$archive_path")" ]; then + archive_path="$BUILTIN/$(basename "$archive_path")" + fi + fi + + # Check if the archive exists and compare SHA with sidecar file + if [ -f "$archive_path" ]; then + archive_basename=$(basename "$archive_path") + sha_file="$DOWNLOADS/${archive_basename}.sha256" + + if [ -f "$sha_file" ]; then + stored_sha=$(cat "$sha_file" 2>/dev/null) + current_sha=$(calc_sha "$archive_path") + + # If SHA matches, check container instance + if [ "$stored_sha" = "$current_sha" ]; then + if podman container exists "$name"; then + config_sha=$(calc_sha "$script") + container_sha=$(podman inspect "$name" --format '{{index .Config.Labels "config-sha256"}}' 2>/dev/null) + container_img_sha=$(podman inspect "$name" --format '{{index .Config.Labels "meta-image-sha256"}}' 2>/dev/null) + + if [ "$container_img_sha" = "$img_sha" ] && [ "$container_sha" = "$config_sha" ]; then + # Unmodified local image and configuration + return 0 + fi + fi + else + # Archive changed (e.g., rootfs upgrade) - need to reload + dbg "Archive SHA changed, will reload image and recreate container" + fi + fi + fi + fi + else + # Remote image optimization: check config-sha256 only + if podman container exists "$name"; then + config_sha=$(calc_sha "$script") + container_sha=$(podman inspect "$name" --format '{{index .Config.Labels "config-sha256"}}' 2>/dev/null) + + if [ "$container_sha" = "$config_sha" ]; then + # Unmodified local image and configuration + return 0 + fi + fi + fi + + return 1 +} + check() { file=$1 @@ -74,11 +186,16 @@ fetch() dst="$DOWNLOADS/$file" cd "$DOWNLOADS" || return - if [ -e "$file" ]; then - log "$file already available." - if check "$file"; then - echo "$dst" - return 0 + if [ -f "$file" ]; then + if [ -n "$force" ]; then + log "Force flag set, removing cached $file and re-downloading." + rm -f "$file" + else + log "$file already available." + if check "$file"; then + echo "$dst" + return 0 + fi fi fi @@ -118,6 +235,7 @@ load_archive() { uri=$1 tag=$2 + tmp=$3 img=$(basename "$uri") # Supported transports for load and create @@ -134,7 +252,8 @@ load_archive() fi ;; *) # docker://*, docker-archive:*, or URL - if podman image exists "$img"; then + # Skip existence check if force flag is set (e.g., for upgrade command) + if [ -z "$force" ] && podman image exists "$img"; then echo "$img" return 0 fi @@ -149,10 +268,10 @@ load_archive() ;; esac - if [ ! -e "$file" ]; then - if [ -e "$DOWNLOADS/$file" ]; then + if [ ! -f "$file" ]; then + if [ -f "$DOWNLOADS/$file" ]; then file="$DOWNLOADS/$file" - elif [ -e "$BUILTIN/$file" ]; then + elif [ -f "$BUILTIN/$file" ]; then file="$BUILTIN/$file" else err 1 "cannot find OCI archive $file in URI $uri" @@ -205,7 +324,7 @@ load_archive() fi [ -n "$quiet" ] || log "Loading OCI image $dir ..." - output=$(podman load -qi "$dir") + output=$(nice podman load -qi "$dir") # Extract image ID from podman load output: # "Loaded image: sha256:cd9d0aaf81be..." @@ -230,6 +349,23 @@ load_archive() err 1 "failed tagging image as $tag" fi + # Save archive SHA256 to sidecar file to enable optimization + # This applies to both local archives and downloaded remote archives + if [ -f "$file" ]; then + img_sha256=$(calc_sha "$file") + archive_basename=$(basename "$file") + sha_file="$DOWNLOADS/${archive_basename}.sha256" + + mkdir -p "$DOWNLOADS" + + echo "$img_sha256" > "$sha_file" + if [ -n "$tmp" ]; then + echo "$img_sha256" > "$tmp" + fi + + log "Saved archive checksum to $sha_file" + fi + # Clean up after ourselves if [ -n "$extracted" ]; then log "Cleaning up extracted $dir" @@ -261,10 +397,16 @@ create() # Unpack and load docker-archive/oci/oci-archive, returning image # name, or return docker:// URL for download. - if ! image=$(load_archive "$image"); then + sha_file=$(mktemp) + if ! image=$(load_archive "$image" "" "$sha_file"); then exit 1 fi + if [ -s "$sha_file" ]; then + img_sha=$(cat "$sha_file" 2>/dev/null || echo "") + rm "$sha_file" + fi + if [ -z "$logging" ]; then logging="--log-driver syslog" fi @@ -295,9 +437,24 @@ create() args="$args --network=none" fi + # Add optimization labels for meta-image-sha256 and config checksum + script="/run/containers/${name}.sh" + if [ -f "$script" ]; then + if [ -z "$img_sha" ]; then + # Extract meta-image-sha256 from script if present + img_sha=$(awk '/^# meta-image-sha256:/ {print $3}' "$script" 2>/dev/null) + fi + if [ -n "$img_sha" ]; then + args="$args --label meta-image-sha256=$img_sha" + fi + + # Add config checksum label + args="$args --label config-sha256=$(calc_sha "$script")" + fi + # shellcheck disable=SC2048 log "podman create --name $name --conmon-pidfile=$pidfile $args $image $*" - if podman create --name "$name" --conmon-pidfile="$pidfile" $args "$image" $*; then + if nice podman create --name "$name" --conmon-pidfile="$pidfile" $args "$image" $*; then [ -n "$quiet" ] || log "Successfully created container $name from $image" [ -n "$manual" ] || start "$name" @@ -331,11 +488,90 @@ delete() sleep 1 done + # NOTE: -v does not remove *named volumes* podman rm -vif "$name" >/dev/null 2>&1 [ -n "$quiet" ] || log "Container $name has been removed." +} - cnt=$(podman image prune -af | wc -l) - log "Pruned $cnt image(s)" +# Called by Finit cleanup:script when a container service is removed. +# Processes all container instance IDs found in $CLEANUP// directory +# and removes them. This handles the case where a container with the same +# name but different configuration replaces an old one. +# +# Note: Volumes are NOT automatically removed to prevent accidental data loss. +# Use 'admin-exec container prune' to clean up unused volumes manually. +cleanup() +{ + if [ -z "$name" ]; then + log "cleanup: missing container name" + return 1 + fi + + cleanup_dir="$CLEANUP/$name" + if [ ! -d "$cleanup_dir" ]; then + log "cleanup: no cleanup directory for $name, nothing to do" + return 0 + fi + + log "Cleaning up container instances for: $name" + + # Remove all container instances by ID from the cleanup directory + for id_file in "$cleanup_dir"/*; do + [ -f "$id_file" ] || continue + cid=$(basename "$id_file") + + # Extract image name and meta-image-sha256 from the container instance labels + # These are the image in container store and the trace to an oci-archive.tar.gz + img=$(podman inspect "$cid" 2>/dev/null | jq -r '.[].ImageName' 2>/dev/null || echo "") + sha=$(podman inspect "$cid" --format '{{index .Config.Labels "meta-image-sha256"}}' 2>/dev/null || echo "") + + log "Removing container instance with ID: ${cid:0:12}..." + podman rm -vif "$cid" >/dev/null 2>&1 + rm -f "$id_file" + + # Clean up the image if it exists and is not used by other containers + if [ -n "$img" ] && [ "$img" != "null" ]; then + if ! podman ps -a --format "{{.Image}}" | grep -q "^${img}$"; then + log "Removing unused image: $img" + podman rmi "$img" 2>/dev/null || true + + # Also remove archive and sidecar checksum file from $DOWNLOADS for remote images + # I.e., $DOWNLOADS/oci-archive.tar.gz and $DOWNLOADS/oci-archive.tar.gz.sha256 + if [ -n "$sha" ] && [ -d "$DOWNLOADS" ]; then + # Search for matching sidecar file containing this SHA + for sha_file in "$DOWNLOADS"/*.sha256; do + [ -f "$sha_file" ] || continue + stored_sha=$(cat "$sha_file" 2>/dev/null || echo "") + if [ "$stored_sha" = "$sha" ]; then + # Found matching sidecar - derive archive filename by stripping .sha256 + archive="${sha_file%.sha256}" + if [ -f "$archive" ]; then + # Safety check: verify archive SHA matches before removing + if [ "$(calc_sha "$archive")" = "$sha" ]; then + log "Removing archive: $archive" + rm -f "$archive" + else + log "Warning: archive SHA mismatch for $archive, not removing" + fi + fi + + log "Removing sidecar file: $sha_file" + rm -f "$sha_file" + break + fi + done + fi + else + log "Image $img still in use by other containers, not removing" + fi + fi + done + + # Remove the cleanup directory for this container, and parent if empty + rmdir "$cleanup_dir" 2>/dev/null + rmdir "$CLEANUP" 2>/dev/null + + exit 0 } waitfor() @@ -375,31 +611,42 @@ stop() # Real work is done by wrap() courtesy of finit sysv emulation } +# When called by Finit: `initctl stop foo`, the container script +# is called as `container -n $foo stop`. For the stop command +# we want to bypass the default 10 second timeout. +# +# NOTE: containers have three phases: setup, running, and teardown. +# the setup phase is this script trying to fetch the image. wrap() { name=$1 cmd=$2 + args= pidfile=$(pidfn "$name") - # Containers have three phases: setup, running, and teardown. + if [ "$cmd" = "stop" ]; then + # The setup phase may run forever in the background trying to fetch + # the image. It saves its PID in /run/containers/${name}.pid + if [ -f "$pidfile" ]; then + pid=$(cat "$pidfile") - # The setup phase may run forever in the background trying to fetch - # the image. It saves its PID in /run/containers/${name}.pid - if [ "$cmd" = "stop" ] && [ -f "$pidfile" ]; then - pid=$(cat "$pidfile") + # Check if setup is still running ... + if kill -0 "$pid" 2>/dev/null; then + kill "$pid" + wait "$pid" 2>/dev/null + fi - # Check if setup is still running ... - if kill -0 "$pid" 2>/dev/null; then - kill "$pid" - wait "$pid" 2>/dev/null - fi + rm -f "$pidfile" + return 0 + fi - rm -f "$pidfile" - return 0 + # Only the 'podman stop' command takes -i and --timeout + args="-i --timeout $timeout" fi # Skip "echo $name" from podman start in log - podman "$cmd" "$name" >/dev/null + # shellcheck disable=SC2086 + podman "$cmd" $args "$name" >/dev/null } # Removes network $1 from all containers @@ -430,7 +677,7 @@ netrestart() done } -cleanup() +atexit() { pidfile=$(pidfn "$name") @@ -450,7 +697,7 @@ usage: container [opt] cmd [arg] options: - -a, --all Show all, of something + -a, --all Show all, do all, remove all, of something --dns NAMESERVER Set nameserver(s) when creating a container --dns-search LIST Set host lookup search list when creating container --cap-add CAP Add capability to unprivileged container @@ -460,7 +707,7 @@ options: -d, --detach Detach a container started with 'run IMG [CMD]' -e, --env FILE Environment variables when creating container --entrypoint Disable container image's ENTRYPOINT, run cmd + arg - -f, --force Force operation, e.g. remove + -f, --force Force operation, e.g. remove, or force image re-fetch -h, --help Show this help text --hostname NAME Set hostname when creating container --net NETWORK Network interface(s) when creating or finding container @@ -490,7 +737,7 @@ commands: list [image | oci] List names (only) of containers, images, or OCI archives load [NAME | URL] NM Load OCI tarball fileNAME or URL to image NM locate Find container that currently owns '--net IFNAME' - remove IMAGE Remove an (unused) container image + remove [IMAGE] Remove an image, or prune unused images including OCI archives restart [network] NAME Restart a (crashed) container or container(s) using network 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] @@ -638,7 +885,7 @@ if [ -n "$cmd" ]; then shift fi -trap cleanup INT HUP TERM +trap atexit INT HUP TERM case $cmd in # Does not work atm., cannot attach to TTY because @@ -646,6 +893,9 @@ case $cmd in # attach) # podman attach "$1" # ;; + cleanup) # Hidden from public view, use remove or flush commands instead + cleanup "$@" + ;; create) [ -n "$quiet" ] || log "Got create args: $*" create "$@" @@ -739,7 +989,14 @@ case $cmd in podman pull "$@" ;; remove) - podman rmi $all $force -i "$1" + if [ -n "$all" ]; then + log "Removing all OCI archives from $DOWNLOADS directory ..." + find "${DOWNLOADS:?}" -mindepth 1 -exec rm -rf {} + + log "Removing all unused container images ..." + podman image prune $all $force + else + podman rmi $force -i "$1" + fi ;; run) img=$1 @@ -775,28 +1032,54 @@ case $cmd in script=/run/containers/${name}.sh [ -x "$script" ] || err 1 "setup: $script does not exist or is not executable." + if is_uptodate "$script"; then + log "Container $name config unchanged, skipping setup" + exit 0 + fi + + # Capture old image ID before recreation (for surgical cleanup after) + old_image_id="" + if podman container exists "$name"; then + old_image_id=$(podman inspect "$name" --format '{{.ImageID}}' 2>/dev/null) + fi + # Save our PID in case we get stuck here and someone wants to # stop us, e.g., due to reconfiguration or reboot. pidfile=$(pidfn "${name}") echo $$ > "$pidfile" - while ! "$script"; do - log "${name}: setup failed, waiting for network changes ..." + if image=$(is_remote "$script"); then + while ! "$script"; do + log "${name}: setup failed, waiting for network changes ..." - # Timeout and retry after 60 seconds, on SIGTERM, or when - # any network event is caught. - timeout -s TERM -k 1 60 sh -c \ - 'ip monitor address route 2>/dev/null | head -n1 >/dev/null' || true + # Timeout and retry after 60 seconds, on SIGTERM, or when + # any network event is caught. + timeout -s TERM -k 1 60 sh -c \ + 'ip monitor address route 2>/dev/null | head -n1 >/dev/null' || true - # On IP address/route changes, wait a few seconds more to ensure - # the system has ample time to react and set things up for us. - log "${name}: retrying ..." - sleep 2 - done + # 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 + elif ! "$script"; then + log "${name}: setup failed for local image $image" + rm -f "$pidfile" + exit 1 + fi rm -f "$pidfile" - cnt=$(podman image prune -f | wc -l) - log "setup: pruned $cnt image(s)" + + # Remove the old image if it's not used by any other containers + if [ -n "$old_image_id" ]; then + # Check if the old image is still in use by any containers + if ! podman ps -a --format '{{.ImageID}}' | grep -q "^${old_image_id}$"; then + log "Removing old image $old_image_id" + podman rmi "$old_image_id" 2>/dev/null || true + else + log "Old image $old_image_id still in use by other containers, keeping it" + fi + fi ;; shell) if [ -z "$name" ]; then @@ -888,32 +1171,89 @@ case $cmd in podman stats -i 2 ;; upgrade) - # Start script used to initially create container - script=/run/containers/${1}.sh - - # Find container image - img=$(podman inspect "$1" | jq -r .[].ImageName) - if [ -z "$img" ]; then - echo "No such container ($1), or invalid ImageName. Cannot upgrade." - exit 1; + if [ -z "$name" ]; then + name="$1" fi + script=/run/containers/${name}.sh - # Likely an OCI archive, or local directory, assume user has updated image. - if echo "$img" | grep -Eq '^localhost/'; then - 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 - if ! "$script"; then - echo ">> Failed recreating container $1" + # Verify container exists + if ! podman container exists "$name"; then + echo "No such container: $name" exit 1 fi - echo ">> Done." + + # Check if container uses a mutable tag (e.g., :latest) + # Get the actual image name from the running container + image_name=$(podman inspect "$name" | jq -r '.[].ImageName') + if [ -z "$image_name" ]; then + echo "Cannot determine ImageName for container $name" + exit 1 + fi + + # Check if image uses :latest or other mutable tag + # Images with immutable digests (@sha256:...) don't benefit from upgrade + if echo "$image_name" | grep -qE '@sha256:'; then + echo "Container $name uses an immutable image digest ($image_name)" + echo "Upgrade will have no effect. Update the configuration to use a mutable tag." + exit 1 + fi + + # Get image URI from the container script + img=$(awk '/^# meta-image:/ {print $3}' "$script") + if [ -z "$img" ]; then + echo "Cannot determine image for container $name" + exit 1 + fi + + echo ">> Upgrading container $name from image $img ..." + + # Capture the current image ID before upgrade so we can remove it after + old_image_id=$(podman inspect "$name" --format '{{.ImageID}}' 2>/dev/null) + + # Stop container using proper command (goes through Finit) + printf ">> Stopping container ... " + container stop "$name" + echo "done" + + # Set force flag to ensure fresh pull/fetch of image + force="-f" + + # For remote images, force re-pull + case "$img" in + docker://* | ftp://* | http://* | https://*) + printf ">> Pulling latest image ... " + if ! load_archive "$img" >/dev/null 2>&1; then + echo "failed" + echo "Failed fetching $img, check your network settings." + exit 1 + fi + echo "done" + ;; + *) + # Local archives - user must update the file manually + echo ">> Using local image $img (ensure file is updated) ..." + ;; + esac + + # Recreate container by running the script + echo ">> Recreating container ..." + if ! "$script"; then + echo ">> Failed recreating container $name" + exit 1 + fi + + # Remove the old image if it's not used by any other containers + if [ -n "$old_image_id" ]; then + # Check if the old image is still in use by any containers + if ! podman ps -a --format '{{.ImageID}}' | grep -q "^${old_image_id}$"; then + log "Removing old image $old_image_id" + podman rmi "$old_image_id" 2>/dev/null || true + else + log "Old image $old_image_id still in use by other containers, keeping it" + fi + fi + + echo ">> Container $name upgraded successfully." ;; volume) cmd=$1 @@ -936,8 +1276,8 @@ 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 + log "Calling $container -n $SERVICE_ID cleanup" + exec $container -q -n "$SERVICE_ID" cleanup ;; *) false diff --git a/doc/ChangeLog.md b/doc/ChangeLog.md index 8f389bdb..48e87457 100644 --- a/doc/ChangeLog.md +++ b/doc/ChangeLog.md @@ -13,12 +13,40 @@ All notable changes to the project are documented in this file. - Extend NETCONF and RESTCONF scripting documentation with operational data examples, discovery patterns, and common workflow examples, issue #1156 - Initial support for a zone-based firewall, based on `firewalld`, issue #448 +- Add `validate` option to CLI `copy` command. This can be used before doing a + restore of a backup, or when having edited configuration files manually. With + the validate flag (`-n` from the shell) the file is only loaded and validated + against the YANG models, it is *not* rolled in if validation is successful. + Example: `copy /media/backup/old.cfg running-config validate`, issue #373 - Automatically expand `/var` partition on SD card at first boot on RPi +- New `upgrade` RPC (action) for containers using images with mutable tags +- Optimize startup of preexisting containers by adding metadata to track all + OCI archives loaded into container store, and all container configurations + used to create container instances. Instances are now only recreated when + metadata from an existing instance does not match either the configuration + or the image — because of configuration changes or image upgrades +- Updated container documentation on volumes, image tags, and image upgrade ### Fixes +- Fix #981: copying any file, including `running-config`, to the persistent + back-end store for `startup-config`, does not take +- Fix #1146: Possible to set longer containers names than the system supports. + Root cause, a limit of 15 characters implicitly imposed by the service mgmt + daemon, Finit. The length has not been increased to 64 characters (min: 2) + and the YANG model now properly warns if the name is outside of these limits +- Fix #1147: Use container metadata to clean up lingering old container images + instead of using the too broad `podman image prune -af` command +- Fix #1148: Only retry container instance create on remote images +- Fix #1149: Increase `podman stop` timeout, from 10 to 30 seconds, needed with + bigger containers on heavily loaded systems - Fix #1194: CLI `text-editor` command does not do proper input sanitation - Fix #1197: RPi4 no longer boots after BPi-R3 merge, introduced in v25.09 +- Upgrade fixes for containers with mutable images, e.g., `:latest`. Infix now + always tries to fetch a new version of the OCI archive, for remote images, + regardless of the transport. After upgrade the old image is pruned +- Fix #1203: copying any file, including `startup-config`, to `running-config` + does not take [v25.09.0][] - 2025-09-30 ------------------------- diff --git a/doc/cli/introduction.md b/doc/cli/introduction.md index 5ddd2347..a104f8a4 100644 --- a/doc/cli/introduction.md +++ b/doc/cli/introduction.md @@ -191,4 +191,12 @@ change that, e.g., breaks networking, it is trivial to revert back by: admin@host-12-34-56:/> copy startup-config running-config ``` -Or restarting the device. +Or restart the device, for example if the change to the configuration +caused you to lose contact with the system (it happens to the best of +us). The system will start up from the last "save gave". + +> **Tip:** when restoring a backup of a configuration, or having manually +> edited a config file, you can validate it using system's YANG models, +> it is *not* applied if validation is successful: +> +> `copy /media/backup/old.cfg running-config validate` diff --git a/doc/container.md b/doc/container.md index 1f169185..1d66166d 100644 --- a/doc/container.md +++ b/doc/container.md @@ -246,80 +246,221 @@ archive, which helps greatly with container upgrades (see below): admin@example:/> show log ... - Nov 20 07:24:56 infix container[5040]: Fetching ftp://192.168.122.1/curios-oci-amd64-v24.05.0.tar.gz - Nov 20 07:24:56 infix container[5040]: curios-oci-amd64-v24.05.0.tar.gz downloaded successfully. - Nov 20 07:24:56 infix container[5040]: curios-oci-amd64-v24.05.0.tar.gz checksum verified OK. - Nov 20 07:24:57 infix container[5040]: Cleaning up extracted curios-oci-amd64-v24.05.0 - Nov 20 07:24:57 infix container[5040]: podman create --name sys --conmon-pidfile=/run/container:sys.pid --read-only --replace --quiet --cgroup-parent=containers --restart=always --systemd=false --tz=local --hostname sys --log-driver k8s-file --log-opt path=/run/containers/sys.fifo --network=none curios-oci-amd64-v24.05.0 - Nov 20 07:24:57 infix container[3556]: b02e945c43c9bce2c4be88e31d6f63cfdb1a3c8bdd02179376eb059a49ae05e4 + Nov 20 07:24:56 example container[5040]: Fetching ftp://192.168.122.1/curios-oci-amd64-v24.05.0.tar.gz + Nov 20 07:24:56 example container[5040]: curios-oci-amd64-v24.05.0.tar.gz downloaded successfully. + Nov 20 07:24:56 example container[5040]: curios-oci-amd64-v24.05.0.tar.gz checksum verified OK. + Nov 20 07:24:57 example container[5040]: Cleaning up extracted curios-oci-amd64-v24.05.0 + Nov 20 07:24:57 example container[5040]: podman create --name sys --conmon-pidfile=/run/container:sys.pid --read-only --replace --quiet --cgroup-parent=containers --restart=always --systemd=false --tz=local --hostname sys --log-driver k8s-file --log-opt path=/run/containers/sys.fifo --network=none curios-oci-amd64-v24.05.0 + Nov 20 07:24:57 example container[3556]: b02e945c43c9bce2c4be88e31d6f63cfdb1a3c8bdd02179376eb059a49ae05e4 -Upgrading a Container Image +Understanding Image Tags +------------------------ + +Docker images use tags to identify different versions of the same image. +Understanding the difference between *mutable* and *immutable* tags is +important for managing container upgrades effectively. + +### Mutable Tags + +Tags like `:latest`, `:edge`, or `:stable` are *mutable* — they point to +different images over time as new versions are published to the registry. + +**Advantages:** + + - Convenient: upgrade without changing configuration + - Simple: use the CLI command `container upgrade NAME` to get the latest version, + there is even a convenient RPC for controlling the remotely + - Good for: development, testing, and systems that auto-update + +**Trade-offs:** + + - Less reproducible: different systems may run different versions + - Less predictable: upgrades happen when you pull, not when you plan + - Harder to rollback: previous version may no longer be available + +**Example mutable tags:** + +``` +docker://nginx:latest # Always points to newest release +docker://myapp:edge # Development/bleeding edge version +oci-archive:/var/tmp/app.tar # Local archive that may be replaced +``` + +### Immutable Tags + +Version-specific tags like `:v1.0.1`, `:24.11.0`, or digest references +like `@sha256:abc123...` are *immutable* — they always reference the +exact same image content. + +**Advantages:** + + - Reproducible: all systems run identical versions + - Predictable: upgrades only happen when you change configuration + - Auditable: clear history of what ran when + - Good for: production, compliance, and controlled deployments + +**Trade-offs:** + + - More explicit: must update configuration to upgrade + - Requires planning: need to know which version to use + +**Example immutable tags:** + +``` +docker://nginx:1.25.3 # Specific version number +docker://myapp:v2.1.0 # Semantic version tag +docker://nginx@sha256:abc123 # Cryptographic digest (most immutable) +``` + +> [!TIP] +> **Best practice for production:** Use specific version tags (`:v1.0.1`) +> rather than mutable tags (`:latest`). This ensures all your systems run +> identical software and upgrades happen only when you decide. + +Upgrading Container Images --------------------------- + ![Up-to-date Shield](img/shield-checkmark.svg){ align=right width="100" } The applications in your container are an active part of the system as a whole, so make it a routine to keep your container images up-to-date! -Containers are created at first setup and at every boot. If the image -exists in the file system it is reused -- i.e., an image pulled from a -remote registry is not fetched again. +### How Container Lifecycle Works -To upgrade a versioned image: - - update your `running-config` to use the new `image:tag` - - `leave` to activate the change, if you are in the CLI - - Podman pulls the new image in the background - - Your container is recreated with the new image - - The container is started +Infix intelligently manages container lifecycles to provide a smooth +experience while minimizing unnecessary work: -For "unversioned" images, e.g., images using a `:latest` or `:edge` tag, -use the following CLI command (`NAME` is the name of your container): +**At first setup:** When you configure a container for the first time, +Infix fetches the image (if needed) and creates the container instance. - admin@example:/> container upgrade NAME +**At boot time:** Infix checks if the container needs to be recreated by +comparing checksums for: -This stops the container, does `container pull IMAGE`, and recreates it -with the new image. Upgraded containers are automatically restarted. + - The image archive that the container was built from + - The container configuration script + +**When configuration changes:** If you modify any container settings +(network, volumes, environment, etc.), the container is automatically +recreated with the new configuration. + +**When explicitly upgraded:** Using the `container upgrade` command forces +a fresh pull of the image and recreates the container. + +This means that in most cases, **containers persist across reboots** and +are only recreated when actually necessary. Your container's state stored +in volumes is preserved across recreations. Since Infix containers use a +read-only root filesystem, any changes written outside of volumes or the +writable paths provided by Podman (`/dev`, `/dev/shm`, `/run`, `/tmp`, +`/var/tmp`) will be lost when the container is recreated. + +### Method 1: Upgrading Immutable Tags + +When using version-specific tags, you upgrade by explicitly changing the +image reference in your configuration: + +``` +admin@example:/> configure +admin@example:/config/> edit container web +admin@example:/config/container/web/> set image docker://nginx:1.25.3 +admin@example:/config/container/web/> leave +``` + +**What happens:** + + 1. Podman pulls the new image in the background (if not already present) + 2. Your container is automatically stopped + 3. The container is recreated with the new image + 4. The container is started with your existing volumes intact + +**Example:** Upgrading from one version to another: + +``` +admin@example:/> configure +admin@example:/config/> edit container system +admin@example:/config/container/system/> show image +image ghcr.io/kernelkit/curios:v24.11.0; +admin@example:/config/container/system/> set image ghcr.io/kernelkit/curios:v24.12.0 +admin@example:/config/container/system/> leave +admin@example:/> show log +... +Dec 13 14:32:15 example container[1523]: Pulling ghcr.io/kernelkit/curios:v24.12.0... +Dec 13 14:32:18 example container[1523]: Stopping old container instance... +Dec 13 14:32:19 example container[1523]: Creating new container with updated image... +Dec 13 14:32:20 example container[1523]: Container system started successfully +``` + +### Method 2: Upgrading Mutable Tags + +For images using mutable tags like `:latest` or `:edge`, use the +`container upgrade` command: + +``` +admin@example:/> container upgrade NAME +``` + +This command: + + 1. Stops the running container + 2. Pulls the latest version of the image from the registry + 3. Recreates the container with the new image + 4. Starts the container automatically **Example using registry:** - admin@example:/> container upgrade system - system - Trying to pull ghcr.io/kernelkit/curios:edge... - Getting image source signatures - Copying blob 07bfba95fe93 done - Copying config 0cb6059c0f done - Writing manifest to image destination - Storing signatures - 0cb6059c0f4111650ddbc7dbc4880c64ab8180d4bdbb7269c08034defc348f17 - system: not running. - 59618cc3c84bef341c1f5251a62be1592e459cc990f0b8864bc0f5be70e60719 +``` +admin@example:/> container upgrade system +system +Trying to pull ghcr.io/kernelkit/curios:edge... +Getting image source signatures +Copying blob 07bfba95fe93 done +Copying config 0cb6059c0f done +Writing manifest to image destination +Storing signatures +0cb6059c0f4111650ddbc7dbc4880c64ab8180d4bdbb7269c08034defc348f17 +system: not running. +59618cc3c84bef341c1f5251a62be1592e459cc990f0b8864bc0f5be70e60719 +``` -An OCI archive image can be upgraded in a similar manner, the first step -is of course to get the new archive onto the system (see above), and -then, provided the `oci-archive:/path/to/archive` format is used, call -the upgrade command as +**Example using local OCI archive:** - admin@example:/> container upgrade system - Upgrading container system with local archive: oci-archive:/var/tmp/curios-oci-amd64.tar.gz ... - 7ab4a07ee0c6039837419b7afda4da1527a70f0c60c0f0ac21cafee05ba24b52 +An OCI archive image can be upgraded in a similar manner. First, get the +new archive onto the system (see Container Images section above), then, +provided the `oci-archive:/path/to/archive` format is used in your +configuration, call the upgrade command: -OCI archives can also be fetched from ftp/http/https URL, in that case -the upgrade can be done the same way as a registry image (above). +``` +admin@example:/> container upgrade system +Upgrading container system with local archive: oci-archive:/var/tmp/curios-oci-amd64.tar.gz ... +7ab4a07ee0c6039837419b7afda4da1527a70f0c60c0f0ac21cafee05ba24b52 +``` + +OCI archives can also be fetched from ftp/http/https URLs. In that case, +the upgrade works the same way as a registry image — Infix downloads the +new archive and recreates the container. + +### Embedded Container Images > [!TIP] > Containers running from OCI images embedded in the operating system, -> e.g., `/lib/oci/mycontainer.tar.gz`, always run from the version in -> the operating system. To upgrade, install the new container image at -> build time, after system upgrade the container is also upgraded. The -> system unpacks and loads the OCI images into Podman every boot, which -> ensures the running container always has known starting state. +> e.g., `/lib/oci/mycontainer.tar.gz`, are automatically kept in sync +> with the Infix system image version. +> +> **How it works:** When you build a custom Infix image with embedded OCI +> archives, those containers will be upgraded whenever you upgrade the +> Infix operating system itself. At boot, Infix checks if the embedded +> image has changed and automatically recreates the container if needed. > > **Example:** default builds of Infix include a couple of OCI images > for reference, one is `/lib/oci/curios-nftables-v24.11.0.tar.gz`, but > there is also a symlink called `curios-nftables-latest.tar.gz` in the > same directory, which is what the Infix regression tests use in the -> image configuration of the container. This is what enables easy -> upgrades of the container along with the system itself. +> image configuration of the container. When the system is upgraded and +> the embedded image changes, the test containers are automatically +> recreated with the new version. +> +> This approach ensures your embedded containers always match your system +> version without any manual intervention. Capabilities @@ -614,6 +755,50 @@ empty: then rsync". > volume between containers. All the tricks possible with volumes may > be added in a later release. +### Volume Management + +Volumes are persistent storage that survive container restarts and image +upgrades, making them ideal for application data. However, this also means +they **are not automatically removed** when a container is deleted from the +configuration. + +This design choice prevents accidental data loss, especially in scenarios +where: + + - A container is temporarily removed and re-added with the same name + - A container is replaced with a different configuration but same name + - System upgrades or configuration changes affect container definitions + +To clean up unused volumes and reclaim disk space, use the admin-exec +command: + + admin@example:/> container prune + Deleted Images + ... + Deleted Volumes + ntpd-varlib + system-data + + Total reclaimed space: 45.2MB + +The `container prune` command safely removes: + + - Unused container images + - Volumes not attached to any container (running or stopped) + - Other unused container resources + +> [!TIP] +> You can monitor container resource usage with the command: +> +> admin@example:/> show container usage +> +> This displays disk space used by images, containers, and volumes, +> helping you decide when to run the prune command. +> +> To see which volumes exist and which containers use them: +> +> admin@example:/> show container volumes + ### Content Mounts Content mounts are a special type of file mount where the file contents diff --git a/package/bin/bin.mk b/package/bin/bin.mk index 5ac94ac9..15d276a9 100644 --- a/package/bin/bin.mk +++ b/package/bin/bin.mk @@ -11,7 +11,7 @@ BIN_LICENSE = BSD-3-Clause BIN_LICENSE_FILES = LICENSE BIN_REDISTRIBUTE = NO BIN_DEPENDENCIES = sysrepo libite -BIN_CONF_OPTS = --prefix= --disable-silent-rules +BIN_CONF_OPTS = --disable-silent-rules BIN_AUTORECONF = YES define BIN_CONF_ENV diff --git a/package/finit/0001-Increase-MAX_ID_LEN-to-support-longer-service-identi.patch b/package/finit/0001-Increase-MAX_ID_LEN-to-support-longer-service-identi.patch new file mode 100644 index 00000000..98bc19b8 --- /dev/null +++ b/package/finit/0001-Increase-MAX_ID_LEN-to-support-longer-service-identi.patch @@ -0,0 +1,35 @@ +From 927acdbd7e19b1a9a0065ebdb88b663ec6626b88 Mon Sep 17 00:00:00 2001 +From: Joachim Wiberg +Date: Tue, 14 Oct 2025 11:49:57 +0200 +Subject: [PATCH] Increase MAX_ID_LEN to support longer service identifiers +Organization: Wires + +Allow service IDs up to 64 characters to support SHA-256 hashes, +UUIDs, and other long unique identifiers. This increases memory +usage by ~98 bytes per service instance, which is negligible for +typical deployments. + +The IDENT column width in initctl output adapts dynamically based +on actual ID lengths in use, so short IDs remain unaffected. + +Signed-off-by: Joachim Wiberg +--- + src/svc.h | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/src/svc.h b/src/svc.h +index 74cc3ca..1a7e6f2 100644 +--- a/src/svc.h ++++ b/src/svc.h +@@ -94,7 +94,7 @@ typedef enum { + SVC_NOTIFY_S6, + } svc_notify_t; + +-#define MAX_ID_LEN 16 ++#define MAX_ID_LEN 65 + #define MAX_ARG_LEN 64 + #define MAX_CMD_LEN 256 + #define MAX_IDENT_LEN (MAX_ARG_LEN + MAX_ID_LEN + 1) +-- +2.43.0 + diff --git a/src/bin/Makefile.am b/src/bin/Makefile.am index 15ede0cf..f33dd3e9 100644 --- a/src/bin/Makefile.am +++ b/src/bin/Makefile.am @@ -3,11 +3,15 @@ ACLOCAL_AMFLAGS = -I m4 bin_PROGRAMS = copy erase files +# Bash completion +bashcompdir = $(datadir)/bash-completion/completions +dist_bashcomp_DATA = copy.bash + copy_SOURCES = copy.c util.c util.h copy_CPPFLAGS = -D_DEFAULT_SOURCE -D_GNU_SOURCE copy_CFLAGS = -W -Wall -Wextra -copy_CFLAGS += $(libite_CFLAGS) $(sysrepo_CFLAGS) -copy_LDADD = $(libite_LIBS) $(sysrepo_LIBS) +copy_CFLAGS += $(libite_CFLAGS) $(libyang_CFLAGS) $(sysrepo_CFLAGS) +copy_LDADD = $(libite_LIBS) $(libyang_LIBS) $(sysrepo_LIBS) erase_SOURCES = erase.c util.c util.h erase_CPPFLAGS = -D_DEFAULT_SOURCE -D_GNU_SOURCE diff --git a/src/bin/configure.ac b/src/bin/configure.ac index 10aaf0a8..8b5c12c8 100644 --- a/src/bin/configure.ac +++ b/src/bin/configure.ac @@ -15,6 +15,7 @@ AC_PROG_INSTALL PKG_PROG_PKG_CONFIG PKG_CHECK_MODULES([libite], [libite >= 2.5.0]) +PKG_CHECK_MODULES([libyang], [libyang >= 3.0.0]) PKG_CHECK_MODULES([sysrepo], [sysrepo >= 2.2.36]) # Misc variable replacements for below Summary @@ -55,8 +56,8 @@ cat <rw) { fprintf(stderr, ERRMSG "not possible to write to \"%s\", skipping.\n", dst); rc = 1; @@ -229,12 +272,32 @@ static int copy(const char *src, const char *dst, const char *remote_user) fprintf(stderr, ERRMSG "unable to open transaction to %s\n", dst); } else { sr_nacm_set_user(sess, user); - rc = sr_copy_config(sess, NULL, srcds->datastore, timeout * 1000); - if (rc) - emsg(sess, ERRMSG "unable to copy configuration, err %d: %s\n", - rc, sr_strerror(rc)); - else - set_owner(dstds->path, user); + + /* + * When copying TO running-config, use sr_replace_config() + * to trigger change callbacks. Otherwise use sr_copy_config() + * for direct datastore copy without callbacks. + */ + if (dstds->datastore == SR_DS_RUNNING && srcds->path) { + rc = replace_running(conn, sess, srcds->path, timeout); + if (!rc) + set_owner(dstds->path, user); + } else { + /* Direct copy for other datastores (no callbacks needed) */ + rc = sr_copy_config(sess, NULL, srcds->datastore, timeout * 1000); + if (rc) { + emsg(sess, ERRMSG "unable to copy configuration, err %d: %s\n", rc, sr_strerror(rc)); + } else { + /* Export datastore to backing file if it has one */ + if (dstds->path) { + rc = systemf("sysrepocfg -d %s -X%s -f json", dstds->sysrepocfg, dstds->path); + if (rc) + fprintf(stderr, ERRMSG "failed saving %s to %s\n", srcds->name, dstds->path); + else + set_owner(dstds->path, user); + } + } + } } rc = sr_disconnect(conn); @@ -246,12 +309,14 @@ static int copy(const char *src, const char *dst, const char *remote_user) if (srcds) { /* 2. Export from a datastore somewhere else */ + if (strstr(dst, "://")) { if (srcds->path) fn = srcds->path; else { snprintf(adjust, sizeof(adjust), "/tmp/%s.cfg", srcds->name); fn = tmpfn = adjust; + remove(tmpfn); rc = systemf("sysrepocfg -d %s -X%s -f json", srcds->sysrepocfg, fn); } @@ -270,10 +335,10 @@ static int copy(const char *src, const char *dst, const char *remote_user) if (dstds && dstds->path) fn = dstds->path; else - fn = cfg_adjust(dst, src, adjust, sizeof(adjust)); + fn = cfg_adjust(dst, src, adjust, sizeof(adjust), sanitize); if (!fn) { - fprintf(stderr, ERRMSG "invalid destination path.\n"); + fprintf(stderr, ERRMSG "file not found.\n"); rc = -1; goto err; } @@ -292,6 +357,8 @@ static int copy(const char *src, const char *dst, const char *remote_user) else set_owner(fn, user); } else if (dstds) { + /* 3. Import from somewhere to a datastore */ + if (!dstds->sysrepocfg) { fprintf(stderr, ERRMSG "not possible to import to this datastore.\n"); rc = 1; @@ -302,14 +369,20 @@ static int copy(const char *src, const char *dst, const char *remote_user) goto err; } - /* 3. Import from somewhere to a datastore */ if (strstr(src, "://")) { - tmpfn = mktemp(temp_file); - fn = tmpfn; + fn = basenm(src); + if (fn[0] == 0) { + fprintf(stderr, ERRMSG "missing filename in sorce URI.\n"); + rc = 1; + goto err; + } + snprintf(adjust, sizeof(adjust), "/tmp/%s", fn); + fn = tmpfn = adjust; + remove(tmpfn); } else { - fn = cfg_adjust(src, NULL, adjust, sizeof(adjust)); + fn = cfg_adjust(src, NULL, adjust, sizeof(adjust), sanitize); if (!fn) { - fprintf(stderr, ERRMSG "invalid source file location.\n"); + fprintf(stderr, ERRMSG "file not found.\n"); rc = 1; goto err; } @@ -320,20 +393,49 @@ static int copy(const char *src, const char *dst, const char *remote_user) if (rc) { fprintf(stderr, ERRMSG "failed downloading %s", src); } else { - rc = systemf("sysrepocfg -d %s -I%s -f json", dstds->sysrepocfg, fn); - if (rc) - fprintf(stderr, ERRMSG "failed loading %s from %s", dst, src); + if (dstds->datastore == SR_DS_RUNNING) { + if (sr_connect(SR_CONN_DEFAULT, &conn)) { + fprintf(stderr, ERRMSG "connection to datastore failed\n"); + rc = 1; + } else { + sr_log_syslog("klishd", SR_LL_WRN); + if (sr_session_start(conn, SR_DS_RUNNING, &sess)) { + fprintf(stderr, ERRMSG "unable to open transaction to %s\n", dst); + rc = 1; + } else { + sr_nacm_set_user(sess, user); + rc = replace_running(conn, sess, fn, timeout); + if (!rc) + set_owner(dstds->path, user); + } + sr_disconnect(conn); + } + } else { + rc = systemf("sysrepocfg -d %s -I%s -f json", dstds->sysrepocfg, fn); + if (rc) + fprintf(stderr, ERRMSG "failed loading %s from %s\n", dst, src); + else if (dstds->path) { + rc = systemf("sysrepocfg -d %s -X%s -f json", dstds->sysrepocfg, dstds->path); + if (rc) + fprintf(stderr, ERRMSG "failed saving %s\n", dstds->path); + else + set_owner(dstds->path, user); + } + + } } } else { + /* 4. regular copy file -> file, either may be remote */ + if (strstr(src, "://") && strstr(dst, "://")) { fprintf(stderr, ERRMSG "copy from remote to remote is not supported.\n"); goto err; } if (strstr(src, "://")) { - fn = cfg_adjust(dst, src, adjust, sizeof(adjust)); + fn = cfg_adjust(dst, src, adjust, sizeof(adjust), sanitize); if (!fn) { - fprintf(stderr, ERRMSG "invalid destination file location.\n"); + fprintf(stderr, ERRMSG "file not found.\n"); rc = 1; goto err; } @@ -347,25 +449,42 @@ static int copy(const char *src, const char *dst, const char *remote_user) rc = systemf("curl %s -Lo %s %s", remote_user, fn, src); } else if (strstr(dst, "://")) { - fn = cfg_adjust(src, NULL, adjust, sizeof(adjust)); + fn = cfg_adjust(src, NULL, adjust, sizeof(adjust), sanitize); if (!fn) { - fprintf(stderr, ERRMSG "invalid source file location.\n"); + fprintf(stderr, ERRMSG "file not found.\n"); rc = 1; goto err; } if (access(fn, F_OK)) - fprintf(stderr, ERRMSG "no such file %s, aborting.", fn); + fprintf(stderr, ERRMSG "no such file %s.", fn); else rc = systemf("curl %s -LT %s %s", remote_user, fn, dst); } else { - if (!access(dst, F_OK)) { - if (!yorn("Overwrite existing file %s", dst)) { + char bufa[256], bufb[256]; + const char *from, *to; + + from = cfg_adjust(src, NULL, bufa, sizeof(bufa), sanitize); + if (!from) { + fprintf(stderr, ERRMSG "no such file %s.", src); + rc = 1; + goto err; + } + + to = cfg_adjust(dst, src, bufb, sizeof(bufb), sanitize); + if (!to) { + fprintf(stderr, ERRMSG "no such file %s.", dst); + rc = 1; + goto err; + } + + if (!access(to, F_OK)) { + if (!yorn("Overwrite existing file %s", to)) { fprintf(stderr, "OK, aborting.\n"); return 0; } } - rc = systemf("cp %s %s", src, dst); + rc = systemf("cp %s %s", from, to); } } @@ -384,10 +503,23 @@ static int usage(int rc) printf("Usage: %s [OPTIONS] SRC DST\n" "\n" "Options:\n" - " -h This help text\n" - " -u USER Username for remote commands, like scp\n" - " -t SEEC Timeout for the operation, or default %d sec\n" - " -v Show version\n", prognm, timeout); + " -h This help text\n" + " -n Dry-run, validate configuration without applying\n" + " -q Quiet mode, suppress informational messages\n" + " -s Sanitize paths for CLI use (restrict path traversal)\n" + " -t SEC Timeout for the operation, or default %d sec\n" + " -u USER Username for remote commands, like scp\n" + " -v Show version\n" + "\n" + "Files:\n" + " SRC JSON configuration file, or a datastore\n" + " DST A file or datastore, except factory-config\n" + "\n" + "Datastores:\n" + " running-config The running datastore, current active config\n" + " startup-config The non-volatile config used at startup\n" + " factory-config The device's factory default configuration\n" + "\n", prognm, timeout); return rc; } @@ -399,10 +531,19 @@ int main(int argc, char *argv[]) timeout = fgetint("/etc/default/confd", "=", "CONFD_TIMEOUT"); - while ((c = getopt(argc, argv, "ht:u:v")) != EOF) { + while ((c = getopt(argc, argv, "hnqst:u:v")) != EOF) { switch(c) { case 'h': return usage(0); + case 'n': + dry_run = 1; + break; + case 'q': + quiet = 1; + break; + case 's': + sanitize = 1; + break; case 't': timeout = atoi(optarg); break; diff --git a/src/bin/erase.c b/src/bin/erase.c index 716c310b..acfe8c4a 100644 --- a/src/bin/erase.c +++ b/src/bin/erase.c @@ -10,31 +10,25 @@ #include "util.h" static const char *prognm = "erase"; - +static int sanitize; static int do_erase(const char *path) { - char *fn; + char buf[PATH_MAX]; + const char *fn; - if (access(path, F_OK)) { - size_t len = strlen(path) + 10; + fn = cfg_adjust(path, NULL, buf, sizeof(buf), sanitize); + if (!fn) { + fprintf(stderr, ERRMSG "file not found.\n"); + return 1; + } - fn = alloca(len); - if (!fn) { - fprintf(stderr, ERRMSG "failed allocating memory.\n"); - return -1; - } - - cfg_adjust(path, NULL, fn, len); - } else - fn = (char *)path; - - if (!yorn("Remove %s, are you sure", fn)) + if (!yorn("Remove %s, are you sure", path)) return 0; if (remove(fn)) { - fprintf(stderr, ERRMSG "failed removing %s: %s\n", fn, strerror(errno)); - return -1; + fprintf(stderr, ERRMSG "failed removing %s: %s\n", path, strerror(errno)); + return 11; } return 0; @@ -46,6 +40,7 @@ static int usage(int rc) "\n" "Options:\n" " -h This help text\n" + " -s Sanitize paths for CLI use (restrict path traversal)\n" " -v Show version\n", prognm); return rc; @@ -55,10 +50,13 @@ int main(int argc, char *argv[]) { int c; - while ((c = getopt(argc, argv, "hv")) != EOF) { + while ((c = getopt(argc, argv, "hsv")) != EOF) { switch(c) { case 'h': return usage(0); + case 's': + sanitize = 1; + break; case 'v': puts(PACKAGE_VERSION); return 0; diff --git a/src/bin/util.c b/src/bin/util.c index 0e739b1e..629ecfde 100644 --- a/src/bin/util.c +++ b/src/bin/util.c @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -67,7 +68,7 @@ int has_ext(const char *fn, const char *ext) return 0; } -static const char *basenm(const char *fn) +const char *basenm(const char *fn) { const char *ptr; @@ -75,44 +76,65 @@ static const char *basenm(const char *fn) return ""; ptr = strrchr(fn, '/'); - if (!ptr) + if (ptr) + ptr++; + else ptr = fn; return ptr; } -char *cfg_adjust(const char *fn, const char *tmpl, char *buf, size_t len) +static int path_allowed(const char *path) { - if (strstr(fn, "../")) - return NULL; /* relative paths not allowed */ + const char *accepted[] = { + "/media/", + "/cfg/", + getenv("HOME"), + NULL + }; - if (fn[0] == '/') { - strlcpy(buf, fn, len); - return buf; /* allow absolute paths */ + for (int i = 0; accepted[i]; i++) { + if (!strncmp(path, accepted[i], strlen(accepted[i]))) + return 1; } - /* Files in /cfg must end in .cfg */ - if (!strncmp(fn, "/cfg/", 5)) { - strlcpy(buf, fn, len); - if (!has_ext(fn, ".cfg")) - strlcat(buf, ".cfg", len); + return 0; +} - return buf; +char *cfg_adjust(const char *fn, const char *tmpl, char *buf, size_t len, int sanitize) +{ + char tmp[256], resolved[PATH_MAX]; + + if (strlen(basenm(fn)) == 0) { + if (!tmpl) + return NULL; + + fn = basenm(tmpl); + /* Fall through */ } - /* Files ending with .cfg belong in /cfg */ - if (has_ext(fn, ".cfg")) { - snprintf(buf, len, "/cfg/%s", fn); - return buf; + if (sanitize) { + if (strstr(fn, "../")) + return NULL; + + if (fn[0] == '/') { + if (!path_allowed(fn)) + return NULL; + } else { + snprintf(tmp, sizeof(tmp), "/cfg/%s", fn); + if (!has_ext(tmp, ".cfg")) + strlcat(tmp, ".cfg", sizeof(tmp)); + fn = tmp; + } + + /* If file exists, resolve symlinks and verify still in whitelist */ + if (!access(fn, F_OK) && realpath(fn, resolved)) { + if (!path_allowed(resolved)) + return NULL; + fn = resolved; + } } - if (strlen(fn) > 0 && fn[0] == '.' && tmpl) { - if (fn[1] == '/' && fn[2] != 0) - strlcpy(buf, fn, len); - else - strlcpy(buf, basenm(tmpl), len); - } else - strlcpy(buf, fn, len); - + strlcpy(buf, fn, len); return buf; } diff --git a/src/bin/util.h b/src/bin/util.h index 6c5ded9d..2b34e25b 100644 --- a/src/bin/util.h +++ b/src/bin/util.h @@ -7,11 +7,12 @@ #define ERRMSG "Error: " #define INFMSG "Note: " -int yorn (const char *fmt, ...); +int yorn (const char *fmt, ...); -int files (const char *path, const char *stripext); +int files (const char *path, const char *stripext); -int has_ext (const char *fn, const char *ext); -char *cfg_adjust (const char *fn, const char *tmpl, char *buf, size_t len); +const char *basenm (const char *fn); +int has_ext (const char *fn, const char *ext); +char *cfg_adjust (const char *fn, const char *tmpl, char *buf, size_t len, int sanitize); #endif /* BIN_UTIL_H_ */ diff --git a/src/confd/src/core.c b/src/confd/src/core.c index 49fb59fe..719d6228 100644 --- a/src/confd/src/core.c +++ b/src/confd/src/core.c @@ -82,8 +82,12 @@ int core_post_hook(sr_session_ctx_t *session, uint32_t sub_id, const char *modul return SR_ERR_OK; } - if (systemf("initctl -b reload")) + if (systemf("initctl -b reload")) { + EMERG("initctl reload: failed applying new configuration!"); return SR_ERR_SYS; + } + + AUDIT("The new configuration has been applied."); return SR_ERR_OK; } diff --git a/src/confd/src/infix-containers.c b/src/confd/src/infix-containers.c index 22736992..1c579930 100644 --- a/src/confd/src/infix-containers.c +++ b/src/confd/src/infix-containers.c @@ -21,6 +21,46 @@ #define CFG_XPATH "/infix-containers:containers" #define _PATH_CONT "/run/containers" +#define _PATH_CLEAN "/var/lib/containers/cleanup" + +/* + * Check if image is a local archive and return the file path. + * Returns NULL if not a recognized local archive format. + */ +static const char *local_path(const char *image) +{ + const char *prefixes[] = { + "docker-archive:", + "oci-archive:", + NULL, + }; + + for (int i = 0; prefixes[i]; i++) { + size_t len = strlen(prefixes[i]); + + if (!strncmp(image, prefixes[i], len)) + return image + len; + } + + return NULL; +} + +static int calc_sha(const char *path, char *sha256, size_t len) +{ + int rc = 1; + FILE *pp; + + pp = popenf("r", "sha256sum \"%s\" 2>/dev/null | awk '{print $1}'", path); + if (pp) { + if (fgets(sha256, len, pp)) { + chomp(sha256); + rc = 0; + } + pclose(pp); + } + + return rc; +} /* * Create a setup/create/upgrade script and instantiate a new instance @@ -36,18 +76,9 @@ static int add(const char *name, struct lyd_node *cif) const char *restart_policy, *string, *image; struct lyd_node *node, *nets, *caps; char script[strlen(name) + 5]; + const char *path; FILE *fp, *ap; - /* - * If running already, disable the service, keeping the created - * container and any volumes for later if the user re-enables - * it again. - */ - if (!lydx_is_enabled(cif, "enabled")) { - systemf("initctl -bnq disable container@%s.conf", name); - return 0; - } - snprintf(script, sizeof(script), "%s.sh", name); fp = fopenf("w", "%s/%s", _PATH_CONT, script); if (!fp) { @@ -66,11 +97,27 @@ static int add(const char *name, struct lyd_node *cif) * a running container instance. */ image = lydx_get_cattr(cif, "image"); + + /* + * TODO: this script should be refactored to be a .cfg file to + * the container helper script, or in the future, even a + * .cfg file to another runtime/engine. For details, see + * https://github.com/kernelkit/infix/pull/1151#discussion_r2444851292 + */ fprintf(fp, "#!/bin/sh\n" "# meta-name: %s\n" - "# meta-image: %s\n" - "container --quiet delete %s >/dev/null\n" - "container --quiet", name, image, name); + "# meta-image: %s\n", name, image); + + path = local_path(image); + if (path) { + char sha256[65] = { 0 }; + + if (!calc_sha(path, sha256, sizeof(sha256))) + fprintf(fp, "# meta-image-sha256: %s\n", sha256); + } + + fprintf(fp, "container --quiet delete %s >/dev/null\n" + "container --quiet", name); LYX_LIST_FOR_EACH(lyd_child(cif), node, "dns") fprintf(fp, " --dns %s", lyd_get_value(node)); @@ -256,9 +303,9 @@ static int add(const char *name, struct lyd_node *cif) fchmod(fileno(fp), 0700); fclose(fp); - /* Enable, or update, container -- both trigger container setup. */ - systemf("initctl -bnq enable container@%s.conf", name); systemf("initctl -bnq touch container@%s.conf", name); + systemf("initctl -bnq %s container@%s.conf", lydx_is_enabled(cif, "enabled") + ? "enable" : "disable", name); return 0; } @@ -270,9 +317,35 @@ static int add(const char *name, struct lyd_node *cif) */ static int del(const char *name) { + char prune_dir[sizeof(_PATH_CLEAN) + strlen(name) + 3]; + char buf[256]; + FILE *pp; + erasef("%s/%s.sh", _PATH_CONT, name); systemf("initctl -bnq disable container@%s.conf", name); + /* Schedule a cleanup job for this container as soon as it has stopped */ + snprintf(prune_dir, sizeof(prune_dir), "%s/%s", _PATH_CLEAN, name); + systemf("mkdir -p %s", prune_dir); + + /* Finit cleanup:script runs when container is deleted, it will remove any image by-ID */ + pp = popenf("r", "podman inspect %s 2>/dev/null | jq -r '.[].Id' 2>/dev/null", name); + if (!pp) { + /* Nothing to do, if we can't get the Id we cannot safely remove anything */ + ERROR("Cannot find any container instance named '%s' to delete", name); + rmdir(prune_dir); + return SR_ERR_OK; + } + + if (fgets(buf, sizeof(buf), pp)) { + chomp(buf); + /* The return sha should be at least 64 chars long (sha256) */ + if (strlen(buf) >= 64) + touchf("%s/%s", prune_dir, buf); + } + + pclose(pp); + return SR_ERR_OK; } @@ -419,6 +492,7 @@ int infix_containers_init(struct confd *confd) REGISTER_RPC(confd->session, CFG_XPATH "/container/start", action, NULL, &confd->sub); REGISTER_RPC(confd->session, CFG_XPATH "/container/stop", action, NULL, &confd->sub); REGISTER_RPC(confd->session, CFG_XPATH "/container/restart", action, NULL, &confd->sub); + REGISTER_RPC(confd->session, CFG_XPATH "/container/upgrade", action, NULL, &confd->sub); REGISTER_RPC(confd->session, "/infix-containers:oci-load", oci_load, NULL, &confd->sub); return SR_ERR_OK; diff --git a/src/confd/yang/confd/infix-containers.yang b/src/confd/yang/confd/infix-containers.yang index 9226bc24..07746d0e 100644 --- a/src/confd/yang/confd/infix-containers.yang +++ b/src/confd/yang/confd/infix-containers.yang @@ -22,6 +22,13 @@ module infix-containers { prefix infix-sys; } + revision 2025-10-12 { + description "Two major changes: + - Add dedicated 'ident' type for container and volume names. + - New upgrade action (RPC) for containers with :latest image."; + reference "internal"; + } + revision 2025-06-25 { description "Add file mode option to content mounts, allows creating scripts."; reference "internal"; @@ -62,6 +69,14 @@ module infix-containers { * Typedefs */ + typedef ident { + description "Container name or volume identifiers may not contain spaces."; + type string { + length "2..64"; + pattern '[a-zA-Z0-9][a-zA-Z0-9_.-]+'; + } + } + typedef mount-type { type enumeration { enum bind { @@ -128,7 +143,7 @@ module infix-containers { leaf name { description "Name of the container"; - type string; + type ident; } leaf id { @@ -344,7 +359,7 @@ module infix-containers { For persistent writable directories, *volumes* may be a better fit for your container and easier to set up."; - type string; + type ident; } leaf type { @@ -436,10 +451,7 @@ module infix-containers { with the contents of the container's file system on first use. Hence, upgrading the container image will not update the volume if the image has new/removed files at 'path'."; - type string { - pattern '[a-zA-Z_][a-zA-Z0-9_]*'; - length "1..64"; - } + type ident; } leaf target { @@ -474,6 +486,18 @@ module infix-containers { action restart { description "Restart a running, or start, a stopped container."; } + + action upgrade { + description "Upgrade container to latest version of its image. + + This action fetches the latest version of the container's image + and recreates the container with the new image. Any named volumes + are preserved. + + Note: This action is primarily for containers using mutable tags + like ':latest'. For containers using immutable tags (e.g., + ':25.06.0'), the upgrade action will have no effect."; + } } } diff --git a/src/confd/yang/confd/infix-containers@2025-06-25.yang b/src/confd/yang/confd/infix-containers@2025-10-12.yang similarity index 100% rename from src/confd/yang/confd/infix-containers@2025-06-25.yang rename to src/confd/yang/confd/infix-containers@2025-10-12.yang diff --git a/src/confd/yang/containers.inc b/src/confd/yang/containers.inc index 4b2e3f30..11dfc5bc 100644 --- a/src/confd/yang/containers.inc +++ b/src/confd/yang/containers.inc @@ -1,5 +1,5 @@ # -*- sh -*- MODULES=( "infix-interfaces -e containers" - "infix-containers@2025-06-25.yang" + "infix-containers@2025-10-12.yang" ) diff --git a/src/klish-plugin-infix/src/infix.c b/src/klish-plugin-infix/src/infix.c index 41c17df5..47c821b1 100644 --- a/src/klish-plugin-infix/src/infix.c +++ b/src/klish-plugin-infix/src/infix.c @@ -28,7 +28,7 @@ const uint8_t kplugin_infix_major = 1; const uint8_t kplugin_infix_minor = 0; -static void cd_home(kcontext_t *ctx) +static const char *cd_home(kcontext_t *ctx) { const char *user = "root"; ksession_t *session; @@ -43,6 +43,8 @@ static void cd_home(kcontext_t *ctx) pw = getpwnam(user); chdir(pw->pw_dir); + + return user; } static int systemf(const char *fmt, ...) @@ -118,7 +120,7 @@ int infix_erase(kcontext_t *ctx) cd_home(ctx); - return systemf("erase %s", path); + return systemf("erase -s %s", path); } int infix_files(kcontext_t *ctx) @@ -201,8 +203,8 @@ int infix_copy(kcontext_t *ctx) { kpargv_t *pargv = kcontext_pargv(ctx); const char *src, *dst; - const char *username; char user[256] = ""; + char validate[8] = ""; kparg_t *parg; src = kparg_value(kpargv_find(pargv, "src")); @@ -214,26 +216,20 @@ int infix_copy(kcontext_t *ctx) if (parg) snprintf(user, sizeof(user), "-u %s", kparg_value(parg)); - cd_home(ctx); - username = ksession_user(kcontext_session(ctx)); + parg = kpargv_find(pargv, "validate"); + if (parg) + strlcpy(validate, "-n", sizeof(validate)); - return systemf("sudo -u %s copy %s %s %s", username, user, src, dst); + /* Ensure we run the copy command as the logged-in user, not root (klishd) */ + return systemf("doas -u %s copy -s %s %s %s %s", cd_home(ctx), validate, user, src, dst); } int infix_shell(kcontext_t *ctx) { - const char *user = "root"; - ksession_t *session; + const char *user = cd_home(ctx); pid_t pid; int rc; - session = kcontext_session(ctx); - if (session) { - user = ksession_user(session); - if (!user) - user = "root"; - } - pid = fork(); if (pid == -1) return -1; diff --git a/src/klish-plugin-infix/xml/containers.xml b/src/klish-plugin-infix/xml/containers.xml index 23becbae..4d697c8a 100644 --- a/src/klish-plugin-infix/xml/containers.xml +++ b/src/klish-plugin-infix/xml/containers.xml @@ -102,9 +102,14 @@ - + + + - doas container remove $KLISH_PARAM_name + if [ -n "$KLISH_PARAM_name" ]; then + all=-a + fi + doas container $all remove $KLISH_PARAM_name diff --git a/src/klish-plugin-infix/xml/infix.xml b/src/klish-plugin-infix/xml/infix.xml index c40e249d..6e2137e3 100644 --- a/src/klish-plugin-infix/xml/infix.xml +++ b/src/klish-plugin-infix/xml/infix.xml @@ -188,6 +188,7 @@ + diff --git a/test/.env b/test/.env index 962afdf3..b6968427 100644 --- a/test/.env +++ b/test/.env @@ -2,7 +2,7 @@ # shellcheck disable=SC2034,SC2154 # Current container image -INFIX_TEST=ghcr.io/kernelkit/infix-test:2.5 +INFIX_TEST=ghcr.io/kernelkit/infix-test:2.6 ixdir=$(readlink -f "$testdir/..") logdir=$(readlink -f "$testdir/.log") diff --git a/test/case/infix_containers/Readme.adoc b/test/case/infix_containers/Readme.adoc index 726d2c1a..1a4c3a6c 100644 --- a/test/case/infix_containers/Readme.adoc +++ b/test/case/infix_containers/Readme.adoc @@ -10,38 +10,39 @@ Tests verifying Infix Docker container support: - Connecting containers with VETH pairs to standard Linux bridges - Assigning physical Ethernet interfaces to containers - Container upgrades with persistent volume data + - Container upgrade using RPC with cleanup of old image - Firewall container running in host network mode with full privileges -include::container_basic/Readme.adoc[] +include::basic/Readme.adoc[] <<< -include::container_enabled/Readme.adoc[] +include::enabled/Readme.adoc[] <<< -include::container_environment/Readme.adoc[] +include::environment/Readme.adoc[] <<< -include::container_bridge/Readme.adoc[] +include::bridge/Readme.adoc[] <<< -include::container_phys/Readme.adoc[] +include::phys/Readme.adoc[] <<< -include::container_veth/Readme.adoc[] +include::veth/Readme.adoc[] <<< -include::container_volume/Readme.adoc[] +include::volume/Readme.adoc[] <<< -include::container_firewall_basic/Readme.adoc[] +include::firewall_basic/Readme.adoc[] <<< -include::container_host_commands/Readme.adoc[] +include::host_commands/Readme.adoc[] diff --git a/test/case/infix_containers/all.yaml b/test/case/infix_containers/all.yaml index 53ec2915..d50db46a 100644 --- a/test/case/infix_containers/all.yaml +++ b/test/case/infix_containers/all.yaml @@ -1,28 +1,31 @@ --- - name: Container basic - case: container_basic/test.py + case: basic/test.py - name: Container enabled/disabled - case: container_enabled/test.py + case: enabled/test.py - name: Container environment variables - case: container_environment/test.py + case: environment/test.py - name: Container with bridge network - case: container_bridge/test.py + case: bridge/test.py - name: Container with physical interface - case: container_phys/test.py + case: phys/test.py - name: Container with VETH pair - case: container_veth/test.py + case: veth/test.py - name: Container Volume Persistence - case: container_volume/test.py + case: volume/test.py + +- name: Container Upgrade + case: upgrade/test.py - name: Basic Firewall Container - case: container_firewall_basic/test.py + case: firewall_basic/test.py - name: Host Command Execution from Container - case: container_host_commands/test.py + case: host_commands/test.py diff --git a/test/case/infix_containers/container_basic/Readme.adoc b/test/case/infix_containers/basic/Readme.adoc similarity index 100% rename from test/case/infix_containers/container_basic/Readme.adoc rename to test/case/infix_containers/basic/Readme.adoc diff --git a/test/case/infix_containers/container_basic/test.adoc b/test/case/infix_containers/basic/test.adoc similarity index 97% rename from test/case/infix_containers/container_basic/test.adoc rename to test/case/infix_containers/basic/test.adoc index 8205adbb..e76cd18c 100644 --- a/test/case/infix_containers/container_basic/test.adoc +++ b/test/case/infix_containers/basic/test.adoc @@ -1,6 +1,6 @@ === Container basic -ifdef::topdoc[:imagesdir: {topdoc}../../test/case/infix_containers/container_basic] +ifdef::topdoc[:imagesdir: {topdoc}../../test/case/infix_containers/basic] ==== Description diff --git a/test/case/infix_containers/container_basic/test.py b/test/case/infix_containers/basic/test.py similarity index 100% rename from test/case/infix_containers/container_basic/test.py rename to test/case/infix_containers/basic/test.py diff --git a/test/case/infix_containers/container_basic/topology.dot b/test/case/infix_containers/basic/topology.dot similarity index 100% rename from test/case/infix_containers/container_basic/topology.dot rename to test/case/infix_containers/basic/topology.dot diff --git a/test/case/infix_containers/container_basic/topology.svg b/test/case/infix_containers/basic/topology.svg similarity index 100% rename from test/case/infix_containers/container_basic/topology.svg rename to test/case/infix_containers/basic/topology.svg diff --git a/test/case/infix_containers/container_bridge/Readme.adoc b/test/case/infix_containers/bridge/Readme.adoc similarity index 100% rename from test/case/infix_containers/container_bridge/Readme.adoc rename to test/case/infix_containers/bridge/Readme.adoc diff --git a/test/case/infix_containers/container_bridge/test.adoc b/test/case/infix_containers/bridge/test.adoc similarity index 97% rename from test/case/infix_containers/container_bridge/test.adoc rename to test/case/infix_containers/bridge/test.adoc index faf9160f..c50ff86f 100644 --- a/test/case/infix_containers/container_bridge/test.adoc +++ b/test/case/infix_containers/bridge/test.adoc @@ -1,6 +1,6 @@ === Container with bridge network -ifdef::topdoc[:imagesdir: {topdoc}../../test/case/infix_containers/container_bridge] +ifdef::topdoc[:imagesdir: {topdoc}../../test/case/infix_containers/bridge] ==== Description diff --git a/test/case/infix_containers/container_bridge/test.py b/test/case/infix_containers/bridge/test.py similarity index 100% rename from test/case/infix_containers/container_bridge/test.py rename to test/case/infix_containers/bridge/test.py diff --git a/test/case/infix_containers/container_bridge/topology.dot b/test/case/infix_containers/bridge/topology.dot similarity index 100% rename from test/case/infix_containers/container_bridge/topology.dot rename to test/case/infix_containers/bridge/topology.dot diff --git a/test/case/infix_containers/container_bridge/topology.svg b/test/case/infix_containers/bridge/topology.svg similarity index 100% rename from test/case/infix_containers/container_bridge/topology.svg rename to test/case/infix_containers/bridge/topology.svg diff --git a/test/case/infix_containers/container_phys/topology.dot b/test/case/infix_containers/container_phys/topology.dot deleted file mode 120000 index 5130457b..00000000 --- a/test/case/infix_containers/container_phys/topology.dot +++ /dev/null @@ -1 +0,0 @@ -../container_bridge/topology.dot \ No newline at end of file diff --git a/test/case/infix_containers/container_veth/topology.dot b/test/case/infix_containers/container_veth/topology.dot deleted file mode 120000 index 5130457b..00000000 --- a/test/case/infix_containers/container_veth/topology.dot +++ /dev/null @@ -1 +0,0 @@ -../container_bridge/topology.dot \ No newline at end of file diff --git a/test/case/infix_containers/container_enabled/Readme.adoc b/test/case/infix_containers/enabled/Readme.adoc similarity index 100% rename from test/case/infix_containers/container_enabled/Readme.adoc rename to test/case/infix_containers/enabled/Readme.adoc diff --git a/test/case/infix_containers/container_enabled/test.adoc b/test/case/infix_containers/enabled/test.adoc similarity index 97% rename from test/case/infix_containers/container_enabled/test.adoc rename to test/case/infix_containers/enabled/test.adoc index 047b08c8..ea10606a 100644 --- a/test/case/infix_containers/container_enabled/test.adoc +++ b/test/case/infix_containers/enabled/test.adoc @@ -1,6 +1,6 @@ === Container enabled/disabled -ifdef::topdoc[:imagesdir: {topdoc}../../test/case/infix_containers/container_enabled] +ifdef::topdoc[:imagesdir: {topdoc}../../test/case/infix_containers/enabled] ==== Description diff --git a/test/case/infix_containers/container_enabled/test.py b/test/case/infix_containers/enabled/test.py similarity index 100% rename from test/case/infix_containers/container_enabled/test.py rename to test/case/infix_containers/enabled/test.py diff --git a/test/case/infix_containers/container_enabled/topology.dot b/test/case/infix_containers/enabled/topology.dot similarity index 100% rename from test/case/infix_containers/container_enabled/topology.dot rename to test/case/infix_containers/enabled/topology.dot diff --git a/test/case/infix_containers/container_enabled/topology.svg b/test/case/infix_containers/enabled/topology.svg similarity index 100% rename from test/case/infix_containers/container_enabled/topology.svg rename to test/case/infix_containers/enabled/topology.svg diff --git a/test/case/infix_containers/container_environment/Readme.adoc b/test/case/infix_containers/environment/Readme.adoc similarity index 100% rename from test/case/infix_containers/container_environment/Readme.adoc rename to test/case/infix_containers/environment/Readme.adoc diff --git a/test/case/infix_containers/container_environment/test.adoc b/test/case/infix_containers/environment/test.adoc similarity index 96% rename from test/case/infix_containers/container_environment/test.adoc rename to test/case/infix_containers/environment/test.adoc index 5e67da5a..2de6e5c4 100644 --- a/test/case/infix_containers/container_environment/test.adoc +++ b/test/case/infix_containers/environment/test.adoc @@ -1,6 +1,6 @@ === Container environment variables -ifdef::topdoc[:imagesdir: {topdoc}../../test/case/infix_containers/container_environment] +ifdef::topdoc[:imagesdir: {topdoc}../../test/case/infix_containers/environment] ==== Description diff --git a/test/case/infix_containers/container_environment/test.py b/test/case/infix_containers/environment/test.py similarity index 100% rename from test/case/infix_containers/container_environment/test.py rename to test/case/infix_containers/environment/test.py diff --git a/test/case/infix_containers/container_environment/topology.dot b/test/case/infix_containers/environment/topology.dot similarity index 100% rename from test/case/infix_containers/container_environment/topology.dot rename to test/case/infix_containers/environment/topology.dot diff --git a/test/case/infix_containers/container_environment/topology.svg b/test/case/infix_containers/environment/topology.svg similarity index 100% rename from test/case/infix_containers/container_environment/topology.svg rename to test/case/infix_containers/environment/topology.svg diff --git a/test/case/infix_containers/container_firewall_basic/Readme.adoc b/test/case/infix_containers/firewall_basic/Readme.adoc similarity index 100% rename from test/case/infix_containers/container_firewall_basic/Readme.adoc rename to test/case/infix_containers/firewall_basic/Readme.adoc diff --git a/test/case/infix_containers/container_firewall_basic/test.adoc b/test/case/infix_containers/firewall_basic/test.adoc similarity index 98% rename from test/case/infix_containers/container_firewall_basic/test.adoc rename to test/case/infix_containers/firewall_basic/test.adoc index 1f986fa6..3b1d8e33 100644 --- a/test/case/infix_containers/container_firewall_basic/test.adoc +++ b/test/case/infix_containers/firewall_basic/test.adoc @@ -1,6 +1,6 @@ === Basic Firewall Container -ifdef::topdoc[:imagesdir: {topdoc}../../test/case/infix_containers/container_firewall_basic] +ifdef::topdoc[:imagesdir: {topdoc}../../test/case/infix_containers/firewall_basic] ==== Description diff --git a/test/case/infix_containers/container_firewall_basic/test.py b/test/case/infix_containers/firewall_basic/test.py similarity index 100% rename from test/case/infix_containers/container_firewall_basic/test.py rename to test/case/infix_containers/firewall_basic/test.py diff --git a/test/case/infix_containers/container_firewall_basic/topology.dot b/test/case/infix_containers/firewall_basic/topology.dot similarity index 100% rename from test/case/infix_containers/container_firewall_basic/topology.dot rename to test/case/infix_containers/firewall_basic/topology.dot diff --git a/test/case/infix_containers/container_firewall_basic/topology.svg b/test/case/infix_containers/firewall_basic/topology.svg similarity index 100% rename from test/case/infix_containers/container_firewall_basic/topology.svg rename to test/case/infix_containers/firewall_basic/topology.svg diff --git a/test/case/infix_containers/container_host_commands/Readme.adoc b/test/case/infix_containers/host_commands/Readme.adoc similarity index 100% rename from test/case/infix_containers/container_host_commands/Readme.adoc rename to test/case/infix_containers/host_commands/Readme.adoc diff --git a/test/case/infix_containers/container_host_commands/test.adoc b/test/case/infix_containers/host_commands/test.adoc similarity index 96% rename from test/case/infix_containers/container_host_commands/test.adoc rename to test/case/infix_containers/host_commands/test.adoc index d6f88d8a..ac74d43e 100644 --- a/test/case/infix_containers/container_host_commands/test.adoc +++ b/test/case/infix_containers/host_commands/test.adoc @@ -1,6 +1,6 @@ === Host Command Execution from Container -ifdef::topdoc[:imagesdir: {topdoc}../../test/case/infix_containers/container_host_commands] +ifdef::topdoc[:imagesdir: {topdoc}../../test/case/infix_containers/host_commands] ==== Description diff --git a/test/case/infix_containers/container_host_commands/test.py b/test/case/infix_containers/host_commands/test.py similarity index 100% rename from test/case/infix_containers/container_host_commands/test.py rename to test/case/infix_containers/host_commands/test.py diff --git a/test/case/infix_containers/container_host_commands/topology.dot b/test/case/infix_containers/host_commands/topology.dot similarity index 100% rename from test/case/infix_containers/container_host_commands/topology.dot rename to test/case/infix_containers/host_commands/topology.dot diff --git a/test/case/infix_containers/container_host_commands/topology.svg b/test/case/infix_containers/host_commands/topology.svg similarity index 100% rename from test/case/infix_containers/container_host_commands/topology.svg rename to test/case/infix_containers/host_commands/topology.svg diff --git a/test/case/infix_containers/container_phys/Readme.adoc b/test/case/infix_containers/phys/Readme.adoc similarity index 100% rename from test/case/infix_containers/container_phys/Readme.adoc rename to test/case/infix_containers/phys/Readme.adoc diff --git a/test/case/infix_containers/container_phys/test.adoc b/test/case/infix_containers/phys/test.adoc similarity index 97% rename from test/case/infix_containers/container_phys/test.adoc rename to test/case/infix_containers/phys/test.adoc index 977854bc..6d0390f2 100644 --- a/test/case/infix_containers/container_phys/test.adoc +++ b/test/case/infix_containers/phys/test.adoc @@ -1,6 +1,6 @@ === Container with physical interface -ifdef::topdoc[:imagesdir: {topdoc}../../test/case/infix_containers/container_phys] +ifdef::topdoc[:imagesdir: {topdoc}../../test/case/infix_containers/phys] ==== Description diff --git a/test/case/infix_containers/container_phys/test.py b/test/case/infix_containers/phys/test.py similarity index 100% rename from test/case/infix_containers/container_phys/test.py rename to test/case/infix_containers/phys/test.py diff --git a/test/case/infix_containers/phys/topology.dot b/test/case/infix_containers/phys/topology.dot new file mode 120000 index 00000000..1f2e45e2 --- /dev/null +++ b/test/case/infix_containers/phys/topology.dot @@ -0,0 +1 @@ +../bridge/topology.dot \ No newline at end of file diff --git a/test/case/infix_containers/container_phys/topology.svg b/test/case/infix_containers/phys/topology.svg similarity index 100% rename from test/case/infix_containers/container_phys/topology.svg rename to test/case/infix_containers/phys/topology.svg diff --git a/test/case/infix_containers/container_veth/Readme.adoc b/test/case/infix_containers/upgrade/Readme.adoc similarity index 100% rename from test/case/infix_containers/container_veth/Readme.adoc rename to test/case/infix_containers/upgrade/Readme.adoc diff --git a/test/case/infix_containers/upgrade/download.sh b/test/case/infix_containers/upgrade/download.sh new file mode 100755 index 00000000..7bd28b1a --- /dev/null +++ b/test/case/infix_containers/upgrade/download.sh @@ -0,0 +1,40 @@ +#!/bin/sh +# Download curios-httpd container images for upgrade testing +# This script is called during Docker image build to pre-populate +# the test container with the necessary OCI archives. + +set -e + +DEST_DIR="${1:-/srv}" +IMAGE_BASE="ghcr.io/kernelkit/curios-httpd" +VERSIONS="24.05.0 24.11.0" +ARCHS="linux/amd64 linux/arm64" + +echo "Downloading curios-httpd images to $DEST_DIR..." +mkdir -p "$DEST_DIR" + +for ver in $VERSIONS; do + for arch in $ARCHS; do + # Create architecture-specific filename + arch_suffix=$(echo "$arch" | sed 's|linux/||') + output="$DEST_DIR/curios-httpd-${ver}-${arch_suffix}.tar" + + echo "Fetching ${IMAGE_BASE}:${ver} for ${arch}..." + skopeo copy --override-arch "${arch#linux/}" \ + "docker://${IMAGE_BASE}:${ver}" \ + "oci-archive:${output}" + + # Check if already gzipped and compress if needed + output_gz="${output}.gz" + if file "$output" | grep -q "gzip compressed"; then + echo "File ${output} is already gzipped, renaming..." + mv "$output" "$output_gz" + else + echo "Compressing ${output}..." + gzip "${output}" + fi + done +done + +echo "Download complete!" +ls -lh "$DEST_DIR" diff --git a/test/case/infix_containers/upgrade/test.adoc b/test/case/infix_containers/upgrade/test.adoc new file mode 100644 index 00000000..59c24940 --- /dev/null +++ b/test/case/infix_containers/upgrade/test.adoc @@ -0,0 +1,34 @@ +=== Container Upgrade + +ifdef::topdoc[:imagesdir: {topdoc}../../test/case/infix_containers/upgrade] + +==== Description + +Verify container upgrade functionality by testing the optimization +that skips recreation when the container configuration hasn't changed. + +This test uses two versions of curios-httpd (24.05.0 and 24.11.0) served +over HTTP with :latest tag. The test verifies that: +1. Container starts successfully from the :latest image +2. When the :latest tag points to a new version, upgrade is triggered +3. The container runs the new version after upgrade + +==== Topology + +image::topology.svg[Container Upgrade topology, align=center, scaledwidth=75%] + +==== Sequence + +. Set up topology and attach to target DUT +. Detect target architecture +. Set up isolated network and file server +. Create symlink for curios-httpd:latest -> 24.05.0 +. Create container 'web' from curios-httpd:latest (24.05.0) +. Verify container 'web' has started with version 24.05.0 +. Update symlink to point to curios-httpd:24.11.0 +. Trigger container upgrade by calling upgrade action +. Wait for container 'web' to complete uprgade +. Verify container 'web' is running new version 24.11.0 +. Verify old image was pruned and disk usage is reasonable + + diff --git a/test/case/infix_containers/upgrade/test.py b/test/case/infix_containers/upgrade/test.py new file mode 100755 index 00000000..7193c87e --- /dev/null +++ b/test/case/infix_containers/upgrade/test.py @@ -0,0 +1,182 @@ +#!/usr/bin/env python3 +"""Container Upgrade + +Verify container upgrade functionality by testing the optimization +that skips recreation when the container configuration hasn't changed. + +This test uses two versions of curios-httpd (24.05.0 and 24.11.0) served +over HTTP with :latest tag. The test verifies that: +1. Container starts successfully from the :latest image +2. When the :latest tag points to a new version, upgrade is triggered +3. The container runs the new version after upgrade +""" +import os +import time +import infamy +import infamy.file_server as srv +from infamy.util import until + +SRVPORT = 8008 +SRVDIR = "/srv" + +with infamy.Test() as test: + NAME = "web" + + 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") + + if not target.has_model("infix-containers"): + test.skip() + + _, hport = env.ltop.xlate("host", "data") + _, tport = env.ltop.xlate("target", "data") + + with test.step("Detect target architecture"): + # Query operational datastore for machine architecture + system_state = target.get_data("/ietf-system:system-state") + arch = system_state["system-state"]["platform"]["machine"] + + # Map kernel arch to our image naming + arch_map = { + "x86_64": "amd64", + "aarch64": "arm64", + "armv7l": "arm64", # Fallback for ARM variants + } + image_arch = arch_map.get(arch, "amd64") + print(f"Detected architecture: {arch} -> using {image_arch} images") + + with test.step("Set up isolated network and file server"): + netns = infamy.IsolatedMacVlan(hport).start() + netns.addip("192.168.0.1") + + target.put_config_dicts({ + "ietf-interfaces": { + "interfaces": { + "interface": [ + { + "name": tport, + "ipv4": { + "address": [ + { + "ip": "192.168.0.2", + "prefix-length": 24 + } + ] + } + } + ] + } + } + }) + netns.must_reach("192.168.0.2") + + with srv.FileServer(netns, "192.168.0.1", SRVPORT, SRVDIR): + with test.step("Create symlink for curios-httpd:latest -> 24.05.0"): + # Create symlink in the file server directory + old_img = f"{SRVDIR}/curios-httpd-24.05.0-{image_arch}.tar.gz" + new_img = f"{SRVDIR}/curios-httpd-24.11.0-{image_arch}.tar.gz" + latest_link = f"{SRVDIR}/curios-httpd-latest.tar.gz" + + # Remove any existing symlink + if os.path.exists(latest_link): + os.unlink(latest_link) + + # Point to old version + os.symlink(old_img, latest_link) + print(f"Created symlink: {latest_link} -> {old_img}") + + with test.step("Create container 'web' from curios-httpd:latest (24.05.0)"): + target.put_config_dict("infix-containers", { + "containers": { + "container": [ + { + "name": f"{NAME}", + "image": f"http://192.168.0.1:{SRVPORT}/curios-httpd-latest.tar.gz", + "command": "/usr/sbin/httpd -f -v -p 91", + "network": { + "host": True + } + } + ] + } + }) + + with test.step("Verify container 'web' has started with version 24.05.0"): + c = infamy.Container(target) + until(lambda: c.running(NAME), attempts=60) + + # Get initial operational data to capture container ID and image ID + containers_data = target.get_data("/infix-containers:containers") + container = containers_data["containers"]["container"][NAME] + initial_container_id = container["id"] + initial_image_id = container["image-id"] + + print(f"Container started with container-id: {initial_container_id[:12]}...") + print(f" and image-id: {initial_image_id[:12]}...") + + # Get baseline disk usage for /var/lib/containers + result = tgtssh.runsh("doas du -s /var/lib/containers") + initial_disk_usage = int(result.stdout.split()[0]) + print(f"Disk usage after initial creation: {initial_disk_usage} KiB") + + with test.step("Update symlink to point to curios-httpd:24.11.0"): + # Remove old symlink and point to new version + os.unlink(latest_link) + os.symlink(new_img, latest_link) + print(f"Updated symlink: {latest_link} -> {new_img}") + + with test.step("Trigger container upgrade by calling upgrade action"): + c = infamy.Container(target) + c.action(NAME, "upgrade") + + with test.step("Wait for container 'web' to complete uprgade"): + time.sleep(3) + until(lambda: c.running(NAME), attempts=30) + + with test.step("Verify container 'web' is running new version 24.11.0"): + c = infamy.Container(target) + # Wait for upgrade to complete and container to restart + until(lambda: c.running(NAME), attempts=60) + + # Get operational data after upgrade + containers_data = target.get_data("/infix-containers:containers") + container = containers_data["containers"]["container"][NAME] + new_container_id = container["id"] + new_image_id = container["image-id"] + + print(f"After upgrade container-id: {new_container_id[:12]}...") + print(f" image-id: {new_image_id[:12]}...") + + # Verify that both IDs have changed + if new_container_id == initial_container_id: + test.fail("Container ID did not change after upgrade!") + if new_image_id == initial_image_id: + test.fail("Image ID did not change after upgrade!") + + print("✓ Both container ID and image ID changed after upgrade") + + with test.step("Verify old image was pruned and disk usage is reasonable"): + # We expect minimal growth — the new image replaces old + # image, and they are each around 500-600 KiB. We allow for + # some overhead, but should be < 200 KiB. If the old image + # is not pruned properly, we'll see ~600 KiB extra growth. + MAX_ACCEPTABLE_GROWTH = 1000 + + # Get disk usage after upgrade + result = tgtssh.runsh("doas du -s /var/lib/containers") + final_disk_usage = int(result.stdout.split()[0]) + print(f"Disk usage after upgrade: {final_disk_usage} KiB") + + # Calculate the difference + disk_growth = final_disk_usage - initial_disk_usage + print(f"Disk usage growth: {disk_growth} KiB") + + if disk_growth > MAX_ACCEPTABLE_GROWTH: + test.fail(f"Disk usage grew by {disk_growth} KiB (expected < {MAX_ACCEPTABLE_GROWTH} KiB). " + f"Old image may not have been pruned!") + + print(f"✓ Disk usage growth is acceptable ({disk_growth} KiB < {MAX_ACCEPTABLE_GROWTH} KiB)") + + test.succeed() diff --git a/test/case/infix_containers/upgrade/topology.dot b/test/case/infix_containers/upgrade/topology.dot new file mode 100644 index 00000000..ebb673d5 --- /dev/null +++ b/test/case/infix_containers/upgrade/topology.dot @@ -0,0 +1,24 @@ +graph "1x2" { + layout="neato"; + overlap="false"; + esep="+80"; + + node [shape=record, fontname="DejaVu Sans Mono, Book"]; + edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"]; + + host [ + label="host | { mgmt | data }", + pos="0,12!", + requires="controller", + ]; + + target [ + label="{ mgmt | data } | target", + pos="10,12!", + + requires="infix", + ]; + + host:mgmt -- target:mgmt [requires="mgmt", color=lightgrey] + host:data -- target:data [color=black] +} diff --git a/test/case/infix_containers/container_volume/topology.svg b/test/case/infix_containers/upgrade/topology.svg similarity index 100% rename from test/case/infix_containers/container_volume/topology.svg rename to test/case/infix_containers/upgrade/topology.svg diff --git a/test/case/infix_containers/container_volume/Readme.adoc b/test/case/infix_containers/veth/Readme.adoc similarity index 100% rename from test/case/infix_containers/container_volume/Readme.adoc rename to test/case/infix_containers/veth/Readme.adoc diff --git a/test/case/infix_containers/container_veth/network_diagram.svg b/test/case/infix_containers/veth/network_diagram.svg similarity index 100% rename from test/case/infix_containers/container_veth/network_diagram.svg rename to test/case/infix_containers/veth/network_diagram.svg diff --git a/test/case/infix_containers/container_veth/test.adoc b/test/case/infix_containers/veth/test.adoc similarity index 98% rename from test/case/infix_containers/container_veth/test.adoc rename to test/case/infix_containers/veth/test.adoc index fa4edc70..c5de9312 100644 --- a/test/case/infix_containers/container_veth/test.adoc +++ b/test/case/infix_containers/veth/test.adoc @@ -1,6 +1,6 @@ === Container with VETH pair -ifdef::topdoc[:imagesdir: {topdoc}../../test/case/infix_containers/container_veth] +ifdef::topdoc[:imagesdir: {topdoc}../../test/case/infix_containers/veth] ==== Description diff --git a/test/case/infix_containers/container_veth/test.py b/test/case/infix_containers/veth/test.py similarity index 100% rename from test/case/infix_containers/container_veth/test.py rename to test/case/infix_containers/veth/test.py diff --git a/test/case/infix_containers/veth/topology.dot b/test/case/infix_containers/veth/topology.dot new file mode 120000 index 00000000..1f2e45e2 --- /dev/null +++ b/test/case/infix_containers/veth/topology.dot @@ -0,0 +1 @@ +../bridge/topology.dot \ No newline at end of file diff --git a/test/case/infix_containers/container_veth/topology.svg b/test/case/infix_containers/veth/topology.svg similarity index 100% rename from test/case/infix_containers/container_veth/topology.svg rename to test/case/infix_containers/veth/topology.svg diff --git a/test/case/infix_containers/volume/Readme.adoc b/test/case/infix_containers/volume/Readme.adoc new file mode 120000 index 00000000..ae32c841 --- /dev/null +++ b/test/case/infix_containers/volume/Readme.adoc @@ -0,0 +1 @@ +test.adoc \ No newline at end of file diff --git a/test/case/infix_containers/container_volume/test.adoc b/test/case/infix_containers/volume/test.adoc similarity index 97% rename from test/case/infix_containers/container_volume/test.adoc rename to test/case/infix_containers/volume/test.adoc index e56587a7..b5be4eda 100644 --- a/test/case/infix_containers/container_volume/test.adoc +++ b/test/case/infix_containers/volume/test.adoc @@ -1,6 +1,6 @@ === Container Volume Persistence -ifdef::topdoc[:imagesdir: {topdoc}../../test/case/infix_containers/container_volume] +ifdef::topdoc[:imagesdir: {topdoc}../../test/case/infix_containers/volume] ==== Description diff --git a/test/case/infix_containers/container_volume/test.py b/test/case/infix_containers/volume/test.py similarity index 96% rename from test/case/infix_containers/container_volume/test.py rename to test/case/infix_containers/volume/test.py index 95be8fe0..896b8ea8 100755 --- a/test/case/infix_containers/container_volume/test.py +++ b/test/case/infix_containers/volume/test.py @@ -57,7 +57,7 @@ with infamy.Test() as test: with test.step("Upgrade container"): out = tgtssh.runsh(f"sudo container upgrade {NAME}") - if ">> Done." not in out.stdout: + if f">> Container {NAME} upgraded successfully." not in out.stdout: msg = f"Failed upgrading container {NAME}:\n" \ f"STDOUT:\n{out.stdout}\n" \ f"STDERR:\n{out.stderr}" diff --git a/test/case/infix_containers/container_volume/topology.dot b/test/case/infix_containers/volume/topology.dot similarity index 100% rename from test/case/infix_containers/container_volume/topology.dot rename to test/case/infix_containers/volume/topology.dot diff --git a/test/case/infix_containers/volume/topology.svg b/test/case/infix_containers/volume/topology.svg new file mode 100644 index 00000000..ff3d246b --- /dev/null +++ b/test/case/infix_containers/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_firewall/basic/test.adoc b/test/case/infix_firewall/basic/test.adoc index bf6aef8b..015e9a4c 100644 --- a/test/case/infix_firewall/basic/test.adoc +++ b/test/case/infix_firewall/basic/test.adoc @@ -11,7 +11,7 @@ image::basic.svg[align=center, scaledwidth=50%] - Single zone configuration, "public", with action=drop - Allowed services: SSH (port 22), DHCPv6-client, mySSH (custom, port 222) - All other ports (HTTP, HTTPS, Telnet, etc.) blocked -- Verifies unused interfaces automatically assigned to default zone +- Check that unused interfaces are automatically assigned to default zone ==== Topology @@ -26,6 +26,7 @@ image::topology.svg[Basic Firewall for End Devices topology, align=center, scale . Verify ICMPv6 is dropped . Verify SSH service is allowed . Verify custom mySSH service is allowed +. Verify HTTP service override (8080 allowed, 80 blocked) . Verify other ports are blocked diff --git a/test/case/infix_firewall/basic/topology.dot b/test/case/infix_firewall/basic/topology.dot index c75e3bbf..52174355 100644 --- a/test/case/infix_firewall/basic/topology.dot +++ b/test/case/infix_firewall/basic/topology.dot @@ -1,26 +1,26 @@ graph "1x3" { layout = "neato"; overlap = false; - esep = "+80"; + esep = "+30"; node [shape=record, fontname="DejaVu Sans Mono, Book"]; edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"]; host [ label="host | { mgmt | data }", - pos="1,1!", + pos="10,10.95!", requires="controller" ]; target [ label="{ mgmt | data | unused } | target", - pos="3,1!", + pos="30,10!", requires="infix", ]; dummy [ label="{ link } | dummy", - pos="5,1!", + pos="29.8,00!", requires="infix", ]; diff --git a/test/case/infix_firewall/basic/topology.svg b/test/case/infix_firewall/basic/topology.svg index 644d700d..22a6325f 100644 --- a/test/case/infix_firewall/basic/topology.svg +++ b/test/case/infix_firewall/basic/topology.svg @@ -1,45 +1,59 @@ - + - - + + 1x3 - + host - -host - -mgmt - -data + +host + +mgmt + +data target - -mgmt - -data - -unused - -target + +mgmt + +data + +unused + +target host:mgmt--target:mgmt - + host:data--target:data - -192.168.1.42/24 + +192.168.1.42/24 + + + +dummy + +link + +dummy + + + +target:unused--dummy:link + diff --git a/test/docker/Dockerfile b/test/docker/Dockerfile index 81a07e08..78aa3a8b 100644 --- a/test/docker/Dockerfile +++ b/test/docker/Dockerfile @@ -8,6 +8,7 @@ RUN apk add --no-cache \ e2tools \ ethtool \ fakeroot \ + file \ gcc \ git \ graphviz \ @@ -25,6 +26,7 @@ RUN apk add --no-cache \ qemu-img \ qemu-system-x86_64 \ ruby-mustache \ + skopeo \ socat \ squashfs-tools \ sshpass \ @@ -38,20 +40,24 @@ RUN cd /tmp/mtools-$MTOOL_VERSION && make && make install # Alpine's QEMU package does not bundle this for some reason, copied # from Ubuntu -COPY qemu-ifup /etc +COPY docker/qemu-ifup /etc # Needed to let qeneth find mustache(1) ENV PATH="${PATH}:/usr/lib/ruby/gems/3.2.0/bin" # Install all python packages used by the tests -COPY init-venv.sh /root -COPY pip-requirements.txt /root +COPY docker/init-venv.sh /root +COPY docker/pip-requirements.txt /root # Add bootstrap YANG models, the rest will be downloaded from the device -ADD yang /root/yang +ADD docker/yang /root/yang RUN ~/init-venv.sh ~/pip-requirements.txt -COPY entrypoint.sh /entrypoint.sh +# Download container images for upgrade testing +COPY case/infix_containers/upgrade/download.sh /tmp/download.sh +RUN /tmp/download.sh /srv && rm /tmp/download.sh + +COPY docker/entrypoint.sh /entrypoint.sh ENTRYPOINT ["/entrypoint.sh"] CMD ["/bin/sh"] diff --git a/test/docker/README.md b/test/docker/README.md index 83b09004..c28b084c 100644 --- a/test/docker/README.md +++ b/test/docker/README.md @@ -16,7 +16,8 @@ the image, e.g., with missing Alpine packages. here: in this example we use version 0.4: - docker build -t ghcr.io/kernelkit/infix-test:0.4 . + cd test/ + docker build -f docker/Dockerfile -t ghcr.io/kernelkit/infix-test:0.4 . 3. Update the `test/.env` file to use the new version 4. Verify your new image works properly (remember to remove your `~/.infix/venv`) diff --git a/test/test.mk b/test/test.mk index 78e74a5b..85f3f4bb 100644 --- a/test/test.mk +++ b/test/test.mk @@ -36,11 +36,18 @@ test: test-sh: $(test-dir)/env $(base) $(mode) $(binaries) $(pkg-$(ARCH)) -i /bin/sh +SPEC_DEBUG := +SPEC_Q := @ +ifeq ($(V),1) +SPEC_DEBUG := -d +SPEC_Q := +endif + test-spec: @esc_infix_name="$(echo $(INFIX_NAME) | sed 's/\//\\\//g')"; \ sed 's/{REPLACE}/$(subst ",,$(esc_infix_name)) $(INFIX_VERSION)/' \ $(spec-dir)/Readme.adoc.in > $(spec-dir)/Readme.adoc - @$(spec-dir)/generate_spec.py -s $(test-dir)/case/all.yaml -r $(BR2_EXTERNAL_INFIX_PATH) + $(SPEC_Q)$(spec-dir)/generate_spec.py -s $(test-dir)/case/all.yaml -r $(BR2_EXTERNAL_INFIX_PATH) $(SPEC_DEBUG) @asciidoctor-pdf --failure-level INFO --theme $(spec-dir)/theme.yml \ -a logo="image:$(LOGO)" \ -a pdf-fontsdir=$(spec-dir)/fonts \