#!/bin/bash # This script can be used to start, stop, create, and delete containers. # It is what confd use, with the Finit container@.conf template, to set # up, run, and delete containers. # # NOTE: when creating/deleting containers, remember 'initctl reload' to # activate the changes! In confd this is already handled. # DOWNLOADS=/var/lib/containers/oci BUILTIN=/lib/oci TMPDIR=/var/tmp container=$0 checksum="" extracted= timeout=30 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" } pidfn() { echo "/run/containers/${1}.pid" } 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 </dev/null # Clean up after ourselves if [ -n "$extracted" ]; then log "Cleaning up extracted $dir" rm -rf "$dir" fi # Retag image from podman default $dir:latest if [ -n "$tag" ]; then podman tag "$dir" "$tag" >/dev/null podman rmi "$dir" >/dev/null else tag=$dir fi echo "$tag" } running() { status=$(podman inspect -f '{{.State.Status}}' "$1" 2>/dev/null) [ "$status" = "running" ] && 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 syslog" fi # When we get here we've already fetched, or pulled, the image 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" pidfile=/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=$pidfile $args $image $*" if podman create --name "$name" --conmon-pidfile="$pidfile" $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 if [ -z "$name" ]; then echo "Usage:" echo " container delete NAME" exit 1 fi # Should already be stopped, but if not ... log "$name: should already be stopped, double checking ..." container stop "$name" >/dev/null while running "$name"; do log "$name: still running, waiting for it to stop ..." _=$((timeout -= 1)) if [ $timeout -le 0 ]; then err 1 "timed out waiting for container $1 to stop before deleting it." fi sleep 1 done log "$name: calling podman rm -vif ..." podman rm -vif "$name" >/dev/null 2>&1 [ -n "$quiet" ] || log "Container $name has been removed." } waitfor() { 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 start 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 stop container:$name # Real work is done by wrap() courtesy of finit sysv emulation } wrap() { name=$1 cmd=$2 pidfile=$(pidfn "$name") # Containers have three phases: setup, running, and teardown. # 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 rm -f "$pidfile" return 0 fi # Skip "echo $name" from podman start in log podman "$cmd" "$name" >/dev/null } # 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 } cleanup() { pidfile=$(pidfn "$name") log "Received signal, exiting." if [ -n "$name" ] && [ -f "$pidfile" ]; then log "$name: in setup phase, removing $pidfile ..." rm -f "$pidfile" fi exit 1 } usage() { cat </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 ;; setup) [ -n "$name" ] || err 1 "setup: missing container name." script=/run/containers/${name}.sh [ -x "$script" ] || err 1 "setup: $script does not exist or is not executable." # 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 ..." read -t 60 _ < <(ip monitor address route) # On IP address/route changes, wait a few seconds more to ensure # the system has ample time to react and set things up for us. log "${name}: retrying ..." sleep 2 done rm -f "$pidfile" ;; shell) if [ -z "$name" ]; then name="$1" shift fi if [ $# -gt 0 ]; then podman exec -i "$name" sh -c "$*" else podman exec -it "$name" sh -l fi ;; show) cmd=$1 [ -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" 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 '/^# 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" exit 1 fi echo ">> Done." ;; volume) cmd=$1 [ -n "$cmd" ] && shift case $cmd in prune) podman volume prune $force ;; *) false ;; esac ;; *) if [ -n "$SERVICE_SCRIPT_TYPE" ] && [ -n "$SERVICE_ID" ]; then case "$SERVICE_SCRIPT_TYPE" in pre) # Called as pre-script from Finit service exec $container -q -n "$SERVICE_ID" setup ;; cleanup) # Called as cleanup-script from Finit service log "Calling $container -n $SERVICE_ID delete" exec $container -q -n "$SERVICE_ID" delete ;; *) false ;; esac fi usage exit 1 ;; esac