Merge pull request #1151 from kernelkit/recontain
Container Lifecycle and Upgrade Improvements Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
@@ -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
|
||||
|
||||
@@ -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/<name>/ 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
|
||||
|
||||
@@ -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
|
||||
-------------------------
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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
|
||||
---------------------------
|
||||
|
||||
{ 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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
From 927acdbd7e19b1a9a0065ebdb88b663ec6626b88 Mon Sep 17 00:00:00 2001
|
||||
From: Joachim Wiberg <troglobit@gmail.com>
|
||||
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 <troglobit@gmail.com>
|
||||
---
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 <<EOF
|
||||
System environment....: ${sysconfig_path:-${sysconfig}}
|
||||
C Compiler............: $CC $CFLAGS $CPPFLAGS $LDFLAGS $LIBS
|
||||
Linker................: $LD $LLDP_LDFLAGS $LLDP_BIN_LDFLAGS $LDFLAGS $LIBS
|
||||
CFLAGS................: $libite_CFLAGS $sysrepo_CFLAGS
|
||||
LIBS..................: $libite_LIBS $sysrepo_LIBS
|
||||
CFLAGS................: $libite_CFLAGS $libyang_CFLAGS $sysrepo_CFLAGS
|
||||
LIBS..................: $libite_LIBS $libyang_LIBS $sysrepo_LIBS
|
||||
|
||||
------------- Compiler version --------------
|
||||
$($CC --version || true)
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
# bash completion for copy command
|
||||
# SPDX-License-Identifier: ISC
|
||||
|
||||
_copy_completion()
|
||||
{
|
||||
local cur prev opts
|
||||
COMPREPLY=()
|
||||
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
prev="${COMP_WORDS[COMP_CWORD-1]}"
|
||||
|
||||
# Options for the copy command
|
||||
opts="-h -n -q -s -t -u -v"
|
||||
|
||||
local datastores_dst="running-config startup-config"
|
||||
local datastores_src="running-config startup-config factory-config"
|
||||
|
||||
case "${prev}" in
|
||||
-t)
|
||||
# Timeout expects a number, no completion
|
||||
return 0
|
||||
;;
|
||||
-u)
|
||||
# Username, could complete with getent passwd but keep it simple
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
|
||||
# If current word starts with -, complete options
|
||||
if [[ ${cur} == -* ]]; then
|
||||
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Determine position (source or destination)
|
||||
# Count non-option arguments before current position
|
||||
local arg_count=0
|
||||
local i
|
||||
for ((i=1; i < COMP_CWORD; i++)); do
|
||||
case "${COMP_WORDS[i]}" in
|
||||
-h|-n|-q|-s|-v)
|
||||
# Flag without argument
|
||||
;;
|
||||
-t|-u)
|
||||
# Flag with argument, skip next word
|
||||
((i++))
|
||||
;;
|
||||
-*)
|
||||
# Unknown flag, might have argument
|
||||
;;
|
||||
*)
|
||||
# Non-option argument
|
||||
((arg_count++))
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Helper function to add non-hidden files/dirs, filtering out dotfiles unless explicitly requested
|
||||
_add_files() {
|
||||
local IFS=$'\n'
|
||||
local files
|
||||
|
||||
# If user is explicitly typing a dotfile path, include dotfiles
|
||||
if [[ ${cur} == .* || ${cur} == */.* ]]; then
|
||||
files=( $(compgen -f -- "${cur}") )
|
||||
else
|
||||
# Only show non-hidden files and directories
|
||||
files=( $(compgen -f -- "${cur}" | grep -v '/\.[^/]*$' | grep -v '^\.[^/]') )
|
||||
fi
|
||||
|
||||
COMPREPLY+=( "${files[@]}" )
|
||||
}
|
||||
|
||||
case ${arg_count} in
|
||||
0)
|
||||
# First argument (source): complete with files and all datastores including factory-config
|
||||
COMPREPLY=( $(compgen -W "${datastores_src}" -- ${cur}) )
|
||||
_add_files
|
||||
;;
|
||||
1)
|
||||
# Second argument (destination): complete with files and limited datastores (no factory-config)
|
||||
COMPREPLY=( $(compgen -W "${datastores_dst}" -- ${cur}) )
|
||||
_add_files
|
||||
;;
|
||||
*)
|
||||
# No more arguments expected
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
complete -F _copy_completion copy
|
||||
@@ -34,6 +34,9 @@ struct infix_ds infix_config[] = {
|
||||
|
||||
static const char *prognm = "copy";
|
||||
static int timeout;
|
||||
static int dry_run;
|
||||
static int quiet;
|
||||
static int sanitize;
|
||||
|
||||
|
||||
/*
|
||||
@@ -177,11 +180,52 @@ static const char *infix_ds(const char *text, struct infix_ds **ds)
|
||||
return text;
|
||||
}
|
||||
|
||||
/*
|
||||
* Load configuration from a file and replace running datastore.
|
||||
* This triggers all sysrepo change callbacks, unlike sr_copy_config().
|
||||
* In dry-run mode, only validates the configuration without applying.
|
||||
*/
|
||||
static int replace_running(sr_conn_ctx_t *conn, sr_session_ctx_t *sess,
|
||||
const char *file, int timeout)
|
||||
{
|
||||
uint32_t flags = LYD_PARSE_NO_STATE | LYD_PARSE_ONLY | LYD_PARSE_STORE_ONLY | LYD_PARSE_STRICT;
|
||||
struct lyd_node *data = NULL;
|
||||
const struct ly_ctx *ctx;
|
||||
LY_ERR err;
|
||||
int rc;
|
||||
|
||||
ctx = sr_acquire_context(conn);
|
||||
err = lyd_parse_data_path(ctx, file, LYD_JSON, flags, 0, &data);
|
||||
sr_release_context(conn);
|
||||
|
||||
if (err) {
|
||||
fprintf(stderr, ERRMSG "unable to load %s, error %d: %s\n", file, err, ly_errmsg(ctx));
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (dry_run) {
|
||||
if (!quiet) {
|
||||
printf("Configuration validated, %s OK\n", file);
|
||||
fflush(stdout);
|
||||
}
|
||||
lyd_free_all(data);
|
||||
rc = 0;
|
||||
} else {
|
||||
/* Replace running config, assumes ownership of 'data' */
|
||||
rc = sr_replace_config(sess, NULL, data, timeout * 1000);
|
||||
if (rc) {
|
||||
emsg(sess, ERRMSG "unable to replace configuration, err %d: %s\n",
|
||||
rc, sr_strerror(rc));
|
||||
lyd_free_all(data);
|
||||
}
|
||||
}
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
||||
static int copy(const char *src, const char *dst, const char *remote_user)
|
||||
{
|
||||
struct infix_ds *srcds = NULL, *dstds = NULL;
|
||||
char temp_file[20] = "/tmp/copy.XXXXXX";
|
||||
const char *tmpfn = NULL;
|
||||
sr_session_ctx_t *sess;
|
||||
const char *fn = NULL;
|
||||
@@ -208,9 +252,8 @@ static int copy(const char *src, const char *dst, const char *remote_user)
|
||||
|
||||
user = getuser();
|
||||
|
||||
/* 1. Regular ds copy */
|
||||
/* 1. Regular ds to ds copy */
|
||||
if (srcds && dstds) {
|
||||
/* Ensure the dst ds is writable */
|
||||
if (!dstds->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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <errno.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <termios.h>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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_ */
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- sh -*-
|
||||
MODULES=(
|
||||
"infix-interfaces -e containers"
|
||||
"infix-containers@2025-06-25.yang"
|
||||
"infix-containers@2025-10-12.yang"
|
||||
)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -102,9 +102,14 @@
|
||||
</COMMAND>
|
||||
|
||||
<COMMAND name="remove" help="Remove a container image from local storage">
|
||||
<PARAM name="name" ptype="/IMAGES" help="Image name" />
|
||||
<SWITCH name="optional" min="0">
|
||||
<PARAM name="name" ptype="/IMAGES" help="Image name" />
|
||||
</SWITCH>
|
||||
<ACTION sym="script" in="tty" out="tty" interrupt="true">
|
||||
doas container remove $KLISH_PARAM_name
|
||||
if [ -n "$KLISH_PARAM_name" ]; then
|
||||
all=-a
|
||||
fi
|
||||
doas container $all remove $KLISH_PARAM_name
|
||||
</ACTION>
|
||||
</COMMAND>
|
||||
|
||||
|
||||
@@ -188,6 +188,7 @@
|
||||
<PARAM name="src" ptype="/DATASTORE" help="Source datastore or file, e.g., tftp://ip/file"/>
|
||||
<PARAM name="dst" ptype="/RW_DATASTORE" help="Destination datastore or file, e.g., scp://ip/file"/>
|
||||
<SWITCH name="optional" min="0">
|
||||
<COMMAND name="validate" help="Validate configuration without applying"/>
|
||||
<PARAM name="user" ptype="/STRING" help="Remote username (interactive password)"/>
|
||||
</SWITCH>
|
||||
<ACTION sym="copy@infix" in="tty" out="tty" interrupt="true"/>
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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[]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
@@ -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
|
||||
|
||||
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
@@ -1 +0,0 @@
|
||||
../container_bridge/topology.dot
|
||||
@@ -1 +0,0 @@
|
||||
../container_bridge/topology.dot
|
||||
@@ -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
|
||||
|
||||
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
@@ -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
|
||||
|
||||
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
@@ -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
|
||||
|
||||
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.5 KiB |
@@ -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
|
||||
|
||||
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
@@ -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
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
../bridge/topology.dot
|
||||
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
@@ -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"
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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()
|
||||
@@ -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> mgmt | <data> data }",
|
||||
pos="0,12!",
|
||||
requires="controller",
|
||||
];
|
||||
|
||||
target [
|
||||
label="{ <mgmt> mgmt | <data> data } | target",
|
||||
pos="10,12!",
|
||||
|
||||
requires="infix",
|
||||
];
|
||||
|
||||
host:mgmt -- target:mgmt [requires="mgmt", color=lightgrey]
|
||||
host:data -- target:data [color=black]
|
||||
}
|
||||
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 6.0 KiB |
@@ -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
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
../bridge/topology.dot
|
||||
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
@@ -0,0 +1 @@
|
||||
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
|
||||
|
||||
@@ -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}"
|
||||
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
|
||||
<!-- Title: 1x2 Pages: 1 -->
|
||||
<svg width="424pt" height="55pt"
|
||||
viewBox="0.00 0.00 424.03 55.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 51)">
|
||||
<title>1x2</title>
|
||||
<polygon fill="white" stroke="transparent" points="-4,4 -4,-51 420.03,-51 420.03,4 -4,4"/>
|
||||
<!-- host -->
|
||||
<g id="node1" class="node">
|
||||
<title>host</title>
|
||||
<polygon fill="none" stroke="black" points="0,-0.5 0,-46.5 100,-46.5 100,-0.5 0,-0.5"/>
|
||||
<text text-anchor="middle" x="25" y="-19.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">host</text>
|
||||
<polyline fill="none" stroke="black" points="50,-0.5 50,-46.5 "/>
|
||||
<text text-anchor="middle" x="75" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
|
||||
<polyline fill="none" stroke="black" points="50,-23.5 100,-23.5 "/>
|
||||
<text text-anchor="middle" x="75" y="-8.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">data</text>
|
||||
</g>
|
||||
<!-- target -->
|
||||
<g id="node2" class="node">
|
||||
<title>target</title>
|
||||
<polygon fill="none" stroke="black" points="300.03,-0.5 300.03,-46.5 416.03,-46.5 416.03,-0.5 300.03,-0.5"/>
|
||||
<text text-anchor="middle" x="325.03" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
|
||||
<polyline fill="none" stroke="black" points="300.03,-23.5 350.03,-23.5 "/>
|
||||
<text text-anchor="middle" x="325.03" y="-8.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">data</text>
|
||||
<polyline fill="none" stroke="black" points="350.03,-0.5 350.03,-46.5 "/>
|
||||
<text text-anchor="middle" x="383.03" y="-19.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">target</text>
|
||||
</g>
|
||||
<!-- host--target -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>host:mgmt--target:mgmt</title>
|
||||
<path fill="none" stroke="lightgrey" stroke-width="2" d="M100,-35.5C100,-35.5 300.03,-35.5 300.03,-35.5"/>
|
||||
</g>
|
||||
<!-- host--target -->
|
||||
<g id="edge2" class="edge">
|
||||
<title>host:data--target:data</title>
|
||||
<path fill="none" stroke="black" stroke-width="2" d="M100,-11.5C100,-11.5 300.03,-11.5 300.03,-11.5"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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> mgmt | <data> data }",
|
||||
pos="1,1!",
|
||||
pos="10,10.95!",
|
||||
requires="controller"
|
||||
];
|
||||
|
||||
target [
|
||||
label="{ <mgmt> mgmt | <data> data | <unused> unused } | target",
|
||||
pos="3,1!",
|
||||
pos="30,10!",
|
||||
requires="infix",
|
||||
];
|
||||
|
||||
dummy [
|
||||
label="{ <link> link } | dummy",
|
||||
pos="5,1!",
|
||||
pos="29.8,00!",
|
||||
requires="infix",
|
||||
];
|
||||
|
||||
|
||||
@@ -1,45 +1,59 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
|
||||
<!-- Generated by graphviz version 2.43.0 (0)
|
||||
-->
|
||||
<!-- Title: 1x3 Pages: 1 -->
|
||||
<svg width="440pt" height="78pt"
|
||||
viewBox="0.00 0.00 440.03 78.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 74)">
|
||||
<svg width="381pt" height="191pt"
|
||||
viewBox="0.00 0.00 381.03 190.72" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 186.72)">
|
||||
<title>1x3</title>
|
||||
<polygon fill="white" stroke="transparent" points="-4,4 -4,-74 436.03,-74 436.03,4 -4,4"/>
|
||||
<polygon fill="white" stroke="transparent" points="-4,4 -4,-186.72 377.03,-186.72 377.03,4 -4,4"/>
|
||||
<!-- host -->
|
||||
<g id="node1" class="node">
|
||||
<title>host</title>
|
||||
<polygon fill="none" stroke="black" points="0,-12 0,-58 100,-58 100,-12 0,-12"/>
|
||||
<text text-anchor="middle" x="25" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">host</text>
|
||||
<polyline fill="none" stroke="black" points="50,-12 50,-58 "/>
|
||||
<text text-anchor="middle" x="75" y="-42.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
|
||||
<polyline fill="none" stroke="black" points="50,-35 100,-35 "/>
|
||||
<text text-anchor="middle" x="75" y="-19.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">data</text>
|
||||
<polygon fill="none" stroke="black" points="0,-136.22 0,-182.22 100,-182.22 100,-136.22 0,-136.22"/>
|
||||
<text text-anchor="middle" x="25" y="-155.52" font-family="DejaVu Sans Mono, Book" font-size="14.00">host</text>
|
||||
<polyline fill="none" stroke="black" points="50,-136.22 50,-182.22 "/>
|
||||
<text text-anchor="middle" x="75" y="-167.02" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
|
||||
<polyline fill="none" stroke="black" points="50,-159.22 100,-159.22 "/>
|
||||
<text text-anchor="middle" x="75" y="-144.02" font-family="DejaVu Sans Mono, Book" font-size="14.00">data</text>
|
||||
</g>
|
||||
<!-- target -->
|
||||
<g id="node2" class="node">
|
||||
<title>target</title>
|
||||
<polygon fill="none" stroke="black" points="300.03,-0.5 300.03,-69.5 432.03,-69.5 432.03,-0.5 300.03,-0.5"/>
|
||||
<text text-anchor="middle" x="333.03" y="-54.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
|
||||
<polyline fill="none" stroke="black" points="300.03,-46.5 366.03,-46.5 "/>
|
||||
<text text-anchor="middle" x="333.03" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">data</text>
|
||||
<polyline fill="none" stroke="black" points="300.03,-23.5 366.03,-23.5 "/>
|
||||
<text text-anchor="middle" x="333.03" y="-8.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">unused</text>
|
||||
<polyline fill="none" stroke="black" points="366.03,-0.5 366.03,-69.5 "/>
|
||||
<text text-anchor="middle" x="399.03" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">target</text>
|
||||
<polygon fill="none" stroke="black" points="241.03,-112.51 241.03,-181.51 373.03,-181.51 373.03,-112.51 241.03,-112.51"/>
|
||||
<text text-anchor="middle" x="274.03" y="-166.31" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
|
||||
<polyline fill="none" stroke="black" points="241.03,-158.51 307.03,-158.51 "/>
|
||||
<text text-anchor="middle" x="274.03" y="-143.31" font-family="DejaVu Sans Mono, Book" font-size="14.00">data</text>
|
||||
<polyline fill="none" stroke="black" points="241.03,-135.51 307.03,-135.51 "/>
|
||||
<text text-anchor="middle" x="274.03" y="-120.31" font-family="DejaVu Sans Mono, Book" font-size="14.00">unused</text>
|
||||
<polyline fill="none" stroke="black" points="307.03,-112.51 307.03,-181.51 "/>
|
||||
<text text-anchor="middle" x="340.03" y="-143.31" font-family="DejaVu Sans Mono, Book" font-size="14.00">target</text>
|
||||
</g>
|
||||
<!-- host--target -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>host:mgmt--target:mgmt</title>
|
||||
<path fill="none" stroke="lightgray" stroke-width="2" d="M100,-47C100,-47 300.03,-58 300.03,-58"/>
|
||||
<path fill="none" stroke="lightgray" stroke-width="2" d="M100,-171.22C100,-171.22 241.03,-170.01 241.03,-170.01"/>
|
||||
</g>
|
||||
<!-- host--target -->
|
||||
<g id="edge2" class="edge">
|
||||
<title>host:data--target:data</title>
|
||||
<path fill="none" stroke="black" stroke-width="2" d="M100,-23C100,-23 300.03,-35 300.03,-35"/>
|
||||
<text text-anchor="middle" x="159" y="-26.8" font-family="DejaVu Serif, Book" font-size="14.00">192.168.1.42/24</text>
|
||||
<path fill="none" stroke="black" stroke-width="2" d="M100,-147.22C100,-147.22 241.03,-147.01 241.03,-147.01"/>
|
||||
<text text-anchor="middle" x="159" y="-151.02" font-family="DejaVu Serif, Book" font-size="14.00">192.168.1.42/24</text>
|
||||
</g>
|
||||
<!-- dummy -->
|
||||
<g id="node3" class="node">
|
||||
<title>dummy</title>
|
||||
<polygon fill="none" stroke="black" points="250.46,-0.5 250.46,-36.5 358.46,-36.5 358.46,-0.5 250.46,-0.5"/>
|
||||
<text text-anchor="middle" x="275.46" y="-14.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">link</text>
|
||||
<polyline fill="none" stroke="black" points="300.46,-0.5 300.46,-36.5 "/>
|
||||
<text text-anchor="middle" x="329.46" y="-14.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">dummy</text>
|
||||
</g>
|
||||
<!-- target--dummy -->
|
||||
<g id="edge3" class="edge">
|
||||
<title>target:unused--dummy:link</title>
|
||||
<path fill="none" stroke="gray" stroke-width="2" stroke-dasharray="5,2" d="M274.03,-112.01C274.03,-112.01 275.46,-36.5 275.46,-36.5"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 3.4 KiB |
@@ -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"]
|
||||
|
||||
@@ -16,7 +16,8 @@ the image, e.g., with missing Alpine packages.
|
||||
here: <https://github.com/kernelkit/infix/pkgs/container/infix-test>
|
||||
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`)
|
||||
|
||||
@@ -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 \
|
||||
|
||||