Files
infix/board/common/rootfs/usr/sbin/container
T
Joachim Wiberg 904040098a board/common: refactor error logging and clean up on error
- Refactor logging to simplify code and get proper log level
 - Clean up lingering directories from any extracted tarball on error

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2024-11-20 08:50:45 +01:00

796 lines
18 KiB
Bash
Executable File

#!/bin/sh
# This script can be used to start, stop, create, and delete containers.
# It is primarily used by confd to create jobs for execd to run from its
# /run/containers/queue, but it can also be used manually.
#
# NOTE: when creating/deleting containers, remember 'initctl reload' to
# activate the changes! When called by confd, via execd, this is
# already handled.
#
DOWNLOADS=/var/lib/containers/oci
BUILTIN=/lib/oci
TMPDIR=/var/tmp
checksum=""
extracted=
dir=""
all=""
env=""
port=""
force=
log()
{
logger -I $PPID -t container -p local1.notice -- "$*"
}
err()
{
rc=$1; shift
logger -I $PPID -t container -p local1.err -- "Error: $*"
if [ -n "$extracted" ]; then
if [ -d "$TMPDIR/$dir" ]; then
log "Cleaning up extracted $dir"
rm -rf "$dir"
fi
fi
[ "$rc" -eq 0 ] || exit "$rc"
}
check()
{
file=$1
if [ -z "$checksum" ]; then
log "no checksum to verify $file against, continuing."
return 0
fi
if echo "${checksum} ${file}" | "$cmdsum" -c -s; then
log "$file checksum verified OK."
return 0
fi
got=$("$cmdsum" "${file}" | awk '{print $1}')
log "$file checksum mismatch, got $got, expected $checksum, removing file."
rm -f "$file"
return 1
}
# Fetch an OCI image over ftp/http/https. Use wget for FTP, which curl
# empirically does not work well with. Log progress+ & error to syslog.
fetch()
{
url=$1
file=$(basename "$url")
dst="$DOWNLOADS/$file"
cd "$DOWNLOADS" || return
if [ -e "$file" ]; then
log "$file already available."
if check "$file"; then
echo "$dst"
return 0
fi
fi
log "Fetching $url"
if echo "$url" | grep -qE "^ftp://"; then
cmd="wget -q $url"
elif echo "$url" | grep -qE "^https?://"; then
cmd="curl $creds -sSL --fail -o \"$file\" $url"
else
log "Unsupported URL scheme: $url"
return 1
fi
if out=$(eval "$cmd" 2>&1); then
log "$file downloaded successfully."
if check "$file"; then
echo "$dst"
return 0
fi
fi
# log error message from backend
while IFS= read -r line; do
log "$line"
done <<EOF
$out
EOF
return 1
}
# Unpacks a given oci-archive.tar[.gz] in the current directory. Sanity
# checks, at least one index.json in the top-level dir of the archive.
# If there are more index files, this function does not handle them.
unpack_archive()
{
image=$1
name=$2
# Supported transports for load and create
case "$image" in
oci:*) # Unpacked OCI image
file=${image#oci:}
;;
oci-archive:*) # Packed OCI image, .tar or .tar.gz format
file=${image#oci-archive:}
;;
ftp://* | http://* | https://*)
if ! file=$(fetch "$image"); then
return 1
fi
;;
*) # docker://*, docker-archive:*, or URL
echo "$image"
return 0
;;
esac
if [ ! -e "$file" ]; then
if [ -e "$DOWNLOADS/$file" ]; then
file="$DOWNLOADS/$file"
elif [ -e "$BUILTIN/$file" ]; then
file="$BUILTIN/$file"
else
err 1 "cannot find OCI archive $file in search path."
fi
fi
if [ -d "$file" ]; then
index=$(find "$file" -name index.json)
if [ -z "$index" ]; then
err 1 "cannot find index.json in OCI image $file"
fi
else
cd "$TMPDIR" || err 0 "failed cd $TMPDIR, wiill use $(pwd) for OCI archive extraction."
index=$(tar tf "$file" |grep index.json)
if [ -z "$index" ]; then
err 1 "invalid OCI archive, cannot find index.json in $file"
fi
[ -n "$quiet" ] || log "Extracting OCI archive $file ..."
tar xf "$file" || err 1 "failed unpacking $file in $(pwd)"
extracted=true
fi
dir=$(dirname "$index")
if echo "$dir" | grep -q ":"; then
if [ -z "$name" ]; then
name="$dir"
fi
sanitized_dir=$(echo "$dir" | cut -d':' -f1)
mv "$dir" "$sanitized_dir" || err 1 "failed renaming $dir to $sanitized_dir"
dir="$sanitized_dir"
fi
[ -n "$quiet" ] || log "Loading OCI image $dir ..."
podman load -qi "$dir" >/dev/null
# Clean up after ourselves
if [ -n "$extracted" ]; then
log "Cleaning up extracted $dir"
rm -rf "$dir"
fi
# Rename image from podman default $dir:latest
if [ -n "$name" ]; then
podman tag "$dir" "$name" >/dev/null
podman rmi "$dir" >/dev/null
else
name=$dir
fi
echo "$name"
}
running()
{
run=$(podman inspect "$1" 2>/dev/null |jq .[].State.Running)
[ "$run" = "true" ] && return 0
return 1
}
# shellcheck disable=SC2086
create()
{
name=$1
image=$2
shift 2
if [ -z "$name" ] || [ -z "$image" ]; then
echo "Usage:"
echo " container create NAME IMAGE"
exit 1
fi
# Unpack and load docker-archive/oci/oci-archive, returning image
# name, or return docker:// URL for download.
if ! image=$(unpack_archive "$image"); then
exit 1
fi
if [ -z "$logging" ]; then
logging="--log-driver k8s-file --log-opt path=/run/containers/$name.fifo"
fi
# Pull quietly and don't retry on failure, we use execd for this,
# or user retry manually when run interactively, we may have other
# containers waiting to start that have an image locally already.
# Use --retry=0 with Podman 5.0 or later.
args="$args --read-only --replace --quiet --cgroup-parent=containers $caps"
args="$args --restart=$restart --systemd=false --tz=local $privileged"
args="$args $vol $mount $hostname $entrypoint $env $port $logging"
pidfn=/run/container:${name}.pid
[ -n "$quiet" ] || log "---------------------------------------"
[ -n "$quiet" ] || log "Got name: $name image: $image"
[ -n "$quiet" ] || log "Got networks: $network"
if [ -n "$network" ]; then
for net in $network; do
args="$args --net=$net"
done
for srv in $dns; do
args="$args --dns=$srv"
done
for domain in $search; do
args="$args --dns-search=$domain"
done
else
args="$args --network=none"
fi
# shellcheck disable=SC2048
log "podman create --name $name --conmon-pidfile=$pidfn $args $image $*"
if podman create --name "$name" --conmon-pidfile="$pidfn" $args "$image" $*; then
[ -n "$quiet" ] || log "Successfully created container $name from $image"
[ -n "$manual" ] || start "$name"
# Should already be enabled by confd (this is for manual use)
initctl -bnq enable "container@${name}.conf"
exit 0
fi
err 1 "failed creating container $name, please check the configuration."
}
delete()
{
name=$1
image=$2
if [ -z "$name" ]; then
echo "Usage:"
echo " container delete NAME"
exit 1
fi
# Should already be disabled (and stopped) by confd (this is for manual use)
initctl -bnq disable "container@${name}.conf"
podman rm -vif "$name" >/dev/null 2>&1
[ -n "$quiet" ] || log "Container $name has been removed."
}
waitfor()
{
timeout=$2
while [ ! -f "$1" ]; do
_=$((timeout -= 1))
if [ $timeout -le 0 ]; then
err 1 "timed out waiting for $1, aborting!"
fi
sleep 1;
done
}
start()
{
name=$1
if running "$name"; then
[ -n "$quiet" ] || echo "$name: already running."
return
fi
initctl -bq cond set "container:$name"
# Real work is done by wrap() courtesy of finit sysv emulation
}
stop()
{
name=$1
if ! running "$name"; then
[ -n "$quiet" ] || echo "$name: not running."
return
fi
initctl -bq cond clr "container:$name"
# Real work is done by wrap() courtesy of finit sysv emulation
}
wrap()
{
name=$1
cmd=$2
podman "$cmd" "$name"
}
# Removes network $1 from all containers
netwrm()
{
net=$1
for c in $(podman ps $all --format "{{.Names}}"); do
for n in $(podman inspect "$c" |jq -r '.[].NetworkSettings.Networks | keys[]'); do
if [ "$n" = "$net" ]; then
podman network disconnect $force "$n" "$c" >/dev/null
fi
done
done
}
# Schedule restart of (any) container using network $1 to activate network changes
netrestart()
{
net=$1
for c in $(podman ps $all --format "{{.Names}}"); do
for n in $(podman inspect "$c" |jq -r '.[].NetworkSettings.Networks | keys[]'); do
if [ "$n" = "$net" ]; then
initctl -nbq touch "container@$c"
fi
done
done
}
usage()
{
cat <<EOF
usage:
container [opt] cmd [arg]
options:
-a, --all Show 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
--cap-drop CAP Drop capability, for privileged containter
--checksum TYPE:SUM Use md5/sha256/sha512 to verify ftp/http/https archives
-c, --creds USR[:PWD] Credentials to pass to curl -u for remote ops
-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
-h, --help Show this help text
--hostname NAME Set hostname when creating container
--net NETWORK Network interface(s) when creating or finding container
-l, --log-driver DRV Log driver to use
--log-opt OPT Logging options to log driver
--log-path PATH Path for k8s-file log pipe
-m, --mount HOST:DEST Bind mount a read-only file inside a container
--manual Do not start container automatically after creation
-n, --name NAME Alternative way of supplying name to start/stop/restart
--privileged Give container extended privileges
-p, --publish PORT Publish ports when creating container
Syntax: [[ip:][hostPort]:]containerPort[/protocol]
-q, --quiet Quiet operation, called from confd
-r, --restart POLICY One of "no", "always", or "on-failure:NUM"
-s, --simple Show output in simplified format
-v, --volume NAME:PATH Create named volume mounted inside container on PATH
commands:
create NAME IMAGE NET Create container NAME using IMAGE with networks NET
delete [network] NAME Remove container NAME or network NAME from all containers
exec NAME CMD Run a command inside a container
flush Clean up lingering containers and associated anonymous volumes
find [ifname PID] Find PID of container where '--net IFNAME' currently lives
or, find the name of our IFNAME inside the container @PID
help Show this help text
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
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]
shell Start a shell inside a container
show [image | volume] Show containers, images, or volumes
stat Show continuous stats about containers (Ctrl-C aborts)
start [NAME] Start a container, see -n
stop [NAME] Stop a container, see -n
volume [prune] Prune unused volumes
EOF
}
while [ "$1" != "" ]; do
case $1 in
-a | --all)
all="-a"
;;
--cap-add)
shift
caps="$caps --cap-add=$1"
;;
--cap-drop)
shift
caps="$caps --cap-drop=$1"
;;
--checksum)
shift
type="${1%%:*}"
checksum="${1#*:}"
case "$type" in
md5)
cmdsum=md5sum
;;
sha256)
cmdsum=sha256sum
;;
sha512)
cmdsum=sha512sum
;;
*)
err 1 "Unsupported checksum type: $type"
;;
esac
;;
-c | --creds)
shift
creds="-u $1"
;;
-d | --detach)
detach="-d"
;;
--dns)
shift
dns="$dns $1"
;;
--dns-search)
shift
search="$search $1"
;;
-e | --env)
shift
env="$env --env-file=$1"
;;
--entrypoint)
entrypoint="--entrypoint=\"\""
;;
-f | --force)
force="-f"
;;
-h | --help)
usage
exit 0
;;
--hostname)
shift
hostname="--hostname $1"
;;
-l | --log-driver)
shift
logging=" --log-driver=$1"
;;
--log-opt)
shift
logging="$logging --log-opt $1"
;;
--log-path)
shift
logging="$logging --log-opt path=$1"
;;
-m | --mount)
shift
mount="--mount=$1"
;;
--manual)
manual=true
;;
-n | --name)
shift
name="$1"
;;
--net)
shift
if [ -n "$network" ]; then
network="$network $1"
else
network=$1
fi
;;
--privileged)
privileged="--privileged=true"
;;
-p | --publish)
shift
port="$port -p $1"
;;
-q | --quiet)
quiet="-q"
;;
-r | --restart)
shift
restart=$1
;;
-s | --simple)
simple=true
;;
-v | --volume)
shift
vol="$vol -v $1"
;;
*)
break
;;
esac
shift
done
cmd=$1
if [ -n "$cmd" ]; then
shift
fi
case $cmd in
# Does not work atm., cannot attach to TTY because
# we monitor 'podman start -ai foo' with Finit.
# attach)
# podman attach "$1"
# ;;
create)
[ -n "$quiet" ] || log "Got create args: $*"
create "$@"
;;
delete)
cmd=$1
name=$2
if [ "$cmd" = "network" ] && [ -n "$name" ]; then
netwrm "$name"
else
delete "$@"
fi
;;
exec)
podman exec -it "$@"
;;
flush)
echo "Cleaning up any lingering containers";
podman rm -av
;;
find)
cmd=$1
pid=$2
if [ "$cmd" = "ifname" ] && [ -n "$pid" ]; then
nsenter -t "$pid" -n ip -d -j link | \
jq --arg ifname "$network" -r '.[] | select(.ifalias==$ifname) | .ifname'
else
containers=$(podman ps $all --format "{{.Names}}")
for c in $containers; do
json=$(podman inspect "$c")
nets=$(echo "$json" |jq -r '.[].NetworkSettings.Networks | keys[]' 2>/dev/null)
for n in $nets; do
if [ "$network" = "$n" ]; then
pid=$(echo "$json" | jq .[].State.Pid)
echo "$pid"
exit 0
fi
done
done
fi
;;
help)
usage
;;
load)
# shellcheck disable=SC2086
name=$(unpack_archive "$1" $2)
[ -n "$name" ] || exit 1
# Show resulting image(s) matching $name
podman images -n "$name"
;;
locate) # Find where the host's ifname lives
if [ -z "$network" ]; then
echo "Missing --net IFNAME option."
exit 1
fi
containers=$(podman ps $all --format "{{.Names}}")
for c in $containers; do
json=$(podman inspect "$c")
nets=$(echo "$json" |jq -r '.[].NetworkSettings.Networks | keys[]' 2>/dev/null)
for n in $nets; do
if [ "$network" = "$n" ]; then
echo "$c"
exit 0;
fi
done
done
;;
ls | list)
cmd=$1
[ -n "$cmd" ] && shift
case $cmd in
image*)
podman images $all --format "{{.Repository}}:{{.Tag}}"
;;
oci)
find $BUILTIN $DOWNLOADS -type f 2>/dev/null
;;
*)
podman ps $all --format "{{.Names}}"
;;
esac
;;
pull)
podman pull "$@"
;;
remove)
podman rmi $all $force -i "$1"
;;
run)
img=$1
cmd=$2
[ -n "$port" ] || port="-P"
if [ -n "$cmd" ]; then
shift 2
[ -n "$detach" ] || echo "Starting $img ENTRYPOINT $cmd :: use Ctrl-p Ctrl-q to detach"
podman run -it --rm $detach $port --entrypoint="$cmd" "$img" "$@"
else
[ -n "$detach" ] || echo "Starting $img :: use Ctrl-p Ctrl-q to detach"
podman run -it --rm $detach $port "$img"
fi
;;
save)
name=$1
file=$2
if echo "$file" | grep -q ".gz"; then
file=${file%%.gz}
gzip=true
fi
if ! echo "$file" | grep -q ".tar"; then
file=${file}.tar
gzip=true
fi
podman save -o "$file" "$name"
if [ -s "$file" ] && [ -n "$gzip" ]; then
gzip "$file"
fi
;;
shell)
podman exec -it "$1" sh -l
;;
show)
cmd=$1
[ -n "$cmd" ] && shift
case $cmd in
image*)
if [ -n "$simple" ]; then
podman images $all --format "{{.Names}} {{.Size}}" \
| sed 's/\[\(.*\)\] /\1 /g' \
| awk '{ printf "%-60s %s %s\n", $1, $2, $3}'
else
podman images $all
fi
;;
volume*)
printf "%-20s CONTAINER\n" "VOLUME"
for v in $(podman volume ls --format "{{.Name}}"); do
printf "%-20s" "$v"
podman ps -a --filter volume="$v" --format '{{.Names}}' | sed 's/^/ /'
done
;;
*)
if [ -n "$simple" ]; then
podman ps $all --format "{{.ID}} {{.Names}} {{.Image}}" \
| awk '{ printf "%s %-30s %s\n", $1, $2, $3}'
else
podman ps $all
fi
;;
esac
;;
start)
if [ -n "$name" ]; then
wrap "$name" start
elif [ -n "$1" ]; then
start "$1"
else
usage
exit 1
fi
;;
restart)
if [ -n "$name" ]; then
wrap "$name" restart
elif [ -n "$1" ]; then
cmd=$1
name=$2
if [ "$cmd" = "network" ] && [ -n "$name" ]; then
netrestart "$name"
else
name=$1
stop "$name"
timeout=20
while running "$name"; do
_=$((timeout -= 1))
if [ $timeout -le 0 ]; then
err 1 "timed out waiting for container $1 to stop before restarting it."
fi
sleep 1
done
start "$name"
fi
else
usage
exit 1
fi
;;
stop)
if [ -n "$name" ]; then
wrap "$name" stop
elif [ -n "$1" ]; then
stop "$1"
else
usage
exit 1
fi
;;
stat*)
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;
fi
# Likely an OCI archive, or local directory, assume user has updated image.
if echo "$img" | grep -Eq '^localhost/'; then
file=$(awk '{s=$NF} END{print s}' "$script")
echo "Upgrading container ${1} with local archive: $file ..."
else
printf ">> Stopping ... "
podman stop "$1"
printf ">> "
podman pull "$img" || (echo "Failed fetching $img, check your network (settings)."; exit 1)
fi
echo ">> Starting $1 ..."
if ! "$script"; then
echo ">> Failed recreating container $1"
exit 1
fi
echo ">> Done."
;;
volume)
cmd=$1
[ -n "$cmd" ] && shift
case $cmd in
prune)
podman volume $force prune
;;
*)
false
;;
esac
;;
*)
usage
exit 1
;;
esac