@@ -1,5 +1,7 @@
|
||||
FROM debian:trixie-slim
|
||||
|
||||
LABEL org.khodo.product=product-vamp-zero
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
RUN apt-get update \
|
||||
|
||||
+141
-10
@@ -8,6 +8,49 @@ config_dir=/etc/product-vamp
|
||||
state_dir=/var/lib/product-vamp
|
||||
run_dir=/run/product-vamp
|
||||
share_dir=/usr/share/product-vamp
|
||||
image_tag=
|
||||
connection=product-vamp-control
|
||||
replacement_started=false
|
||||
image_loaded=false
|
||||
previous_image=
|
||||
stage_dir=
|
||||
|
||||
cleanup() {
|
||||
status=$?
|
||||
trap - EXIT
|
||||
if [ "$status" -ne 0 ] && [ "$replacement_started" = true ]; then
|
||||
systemctl stop product-vamp-zero.service product-vamp-ap.service >/dev/null 2>&1 || true
|
||||
docker rm -f product-vamp-zero >/dev/null 2>&1 || true
|
||||
nmcli connection down "$connection" >/dev/null 2>&1 || true
|
||||
echo "Installation failed; Product Vamp services are stopped. Rerun the installer." >&2
|
||||
elif [ "$status" -ne 0 ] && [ "$image_loaded" = true ] && [ -n "$previous_image" ]; then
|
||||
docker tag "$previous_image" "$image_tag" || echo "Failed to restore the previous image tag." >&2
|
||||
fi
|
||||
if [ -n "$stage_dir" ]; then
|
||||
rm -rf -- "$stage_dir"
|
||||
fi
|
||||
exit "$status"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
validate_target() {
|
||||
target=$1
|
||||
if [ "$(realpath -m -- "$target")" != "$target" ] || [ -L "$target" ]; then
|
||||
echo "Refusing unsafe installation target: $target" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ -e "$target" ] && [ -d "$target" ]; then
|
||||
if [ -n "$(find "$target" -type l -print -quit)" ]; then
|
||||
echo "Refusing installation target containing symlinks: $target" >&2
|
||||
exit 1
|
||||
fi
|
||||
mounts=$(find "$target" -xdev -type d -exec mountpoint -q {} ';' -print)
|
||||
if [ -n "$mounts" ]; then
|
||||
echo "Refusing mounted installation target: $target" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
required_packages=(
|
||||
docker.io
|
||||
docker-cli
|
||||
@@ -51,25 +94,113 @@ else
|
||||
fi
|
||||
systemctl enable --now docker NetworkManager
|
||||
|
||||
install -d -m 0755 "$config_dir" "$state_dir" "$run_dir" "$share_dir"
|
||||
cp -R "$zero_dir/share/product-vamp/." "$share_dir/"
|
||||
image_details=$(python3 - "$bundle_dir/manifest.json" "$image_tar" <<'PY'
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
import tarfile
|
||||
with open(sys.argv[1], encoding='utf-8') as source:
|
||||
tag = json.load(source)['zero']['image_tag']
|
||||
if not isinstance(tag, str) or not re.fullmatch(r'[A-Za-z0-9][A-Za-z0-9._:/-]*', tag):
|
||||
raise ValueError('invalid bundle image tag')
|
||||
with tarfile.open(sys.argv[2]) as archive:
|
||||
with archive.extractfile('manifest.json') as source:
|
||||
images = [image for image in json.load(source) if tag in (image.get('RepoTags') or [])]
|
||||
if len(images) != 1:
|
||||
raise ValueError('Docker archive does not contain exactly one matching bundle image')
|
||||
with archive.extractfile(images[0]['Config']) as source:
|
||||
digest = hashlib.file_digest(source, 'sha256').hexdigest()
|
||||
print(tag)
|
||||
print('sha256:' + digest)
|
||||
PY
|
||||
)
|
||||
mapfile -t image_fields <<< "$image_details"
|
||||
image_tag=${image_fields[0]}
|
||||
expected_image=${image_fields[1]}
|
||||
|
||||
if [ ! -f "$config_dir/vamp.json" ]; then
|
||||
cp "$zero_dir/config/vamp.example.json" "$config_dir/vamp.json"
|
||||
fi
|
||||
for target in "$config_dir" "$state_dir" "$run_dir" "$share_dir" \
|
||||
/usr/local/bin/product-vamp-start-ap /usr/local/bin/product-vamp-start-zero \
|
||||
/etc/systemd/system/product-vamp-ap.service.d /etc/systemd/system/product-vamp-zero.service.d \
|
||||
/run/systemd/system/product-vamp-ap.service.d /run/systemd/system/product-vamp-zero.service.d \
|
||||
/etc/systemd/system/product-vamp-ap.service /etc/systemd/system/product-vamp-zero.service; do
|
||||
validate_target "$target"
|
||||
done
|
||||
|
||||
stage_dir=$(mktemp -d /tmp/product-vamp-install-XXXXXX)
|
||||
install -m 0644 "$zero_dir/config/vamp.example.json" "$stage_dir/vamp.json"
|
||||
install -m 0755 "$zero_dir/start-ap.sh" "$stage_dir/start-ap.sh"
|
||||
install -m 0755 "$zero_dir/start-zero.sh" "$stage_dir/start-zero.sh"
|
||||
install -m 0644 "$zero_dir/systemd/product-vamp-ap.service" "$stage_dir/product-vamp-ap.service"
|
||||
install -m 0644 "$zero_dir/systemd/product-vamp-zero.service" "$stage_dir/product-vamp-zero.service"
|
||||
bash -n "$stage_dir/start-ap.sh"
|
||||
bash -n "$stage_dir/start-zero.sh"
|
||||
previous_image=$(docker image inspect --format '{{.Id}}' "$image_tag" 2>/dev/null || true)
|
||||
previous_images=$(docker image ls --quiet --no-trunc --filter label=org.khodo.product=product-vamp-zero)
|
||||
image_loaded=true
|
||||
docker load -i "$image_tar"
|
||||
new_image=$(docker image inspect --format '{{.Id}}' "$image_tag")
|
||||
if [ "$new_image" != "$expected_image" ]; then
|
||||
echo "Loaded Docker image does not match the incoming archive." >&2
|
||||
exit 1
|
||||
fi
|
||||
# Probe the actual image and staged configuration without touching host state or hardware.
|
||||
docker run --rm --network none --mount "type=bind,src=$stage_dir,dst=/install,readonly" \
|
||||
"$new_image" /usr/bin/vamp-zero --config /install/vamp.json --once
|
||||
|
||||
install -m 0755 "$zero_dir/start-ap.sh" /usr/local/bin/product-vamp-start-ap
|
||||
install -m 0755 "$zero_dir/start-zero.sh" /usr/local/bin/product-vamp-start-zero
|
||||
install -m 0644 "$zero_dir/systemd/product-vamp-ap.service" /etc/systemd/system/product-vamp-ap.service
|
||||
install -m 0644 "$zero_dir/systemd/product-vamp-zero.service" /etc/systemd/system/product-vamp-zero.service
|
||||
echo "Resetting all Product Vamp data and settings, including control Wi-Fi credentials."
|
||||
echo "A connection through the control Wi-Fi may disconnect."
|
||||
replacement_started=true
|
||||
for service in product-vamp-zero.service product-vamp-ap.service; do
|
||||
if systemctl cat "$service" >/dev/null 2>&1; then
|
||||
systemctl stop "$service"
|
||||
fi
|
||||
done
|
||||
if docker container inspect product-vamp-zero >/dev/null 2>&1; then
|
||||
docker rm -f product-vamp-zero
|
||||
fi
|
||||
for target in "$config_dir" "$state_dir" "$run_dir" "$share_dir" \
|
||||
/etc/systemd/system/product-vamp-ap.service.d /etc/systemd/system/product-vamp-zero.service.d \
|
||||
/run/systemd/system/product-vamp-ap.service.d /run/systemd/system/product-vamp-zero.service.d; do
|
||||
validate_target "$target"
|
||||
rm -rf --one-file-system -- "$target"
|
||||
done
|
||||
install -d -m 0755 "$config_dir" "$run_dir"
|
||||
install -m 0644 "$stage_dir/vamp.json" "$config_dir/vamp.json"
|
||||
printf '%s\n' "$new_image" > "$config_dir/image-id"
|
||||
install -m 0755 "$stage_dir/start-ap.sh" /usr/local/bin/product-vamp-start-ap
|
||||
install -m 0755 "$stage_dir/start-zero.sh" /usr/local/bin/product-vamp-start-zero
|
||||
install -m 0644 "$stage_dir/product-vamp-ap.service" /etc/systemd/system/product-vamp-ap.service
|
||||
install -m 0644 "$stage_dir/product-vamp-zero.service" /etc/systemd/system/product-vamp-zero.service
|
||||
|
||||
if nmcli -t -f NAME connection show | grep -Fxq "$connection"; then
|
||||
nmcli connection delete "$connection"
|
||||
fi
|
||||
systemctl daemon-reload
|
||||
systemctl disable --now getty@tty1.service >/dev/null 2>&1 || true
|
||||
systemctl enable --now getty@tty2.service >/dev/null 2>&1 || true
|
||||
systemctl enable --now product-vamp-ap.service
|
||||
systemctl enable product-vamp-zero.service
|
||||
systemctl restart product-vamp-zero.service
|
||||
|
||||
healthy=false
|
||||
for attempt in $(seq 1 60); do
|
||||
if docker exec product-vamp-zero /usr/bin/vamp-zero --config /etc/product-vamp/vamp.json --health-check; then
|
||||
healthy=true
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
if [ "$healthy" != true ]; then
|
||||
echo "Product Vamp did not become healthy after installation." >&2
|
||||
exit 1
|
||||
fi
|
||||
for image in $(printf '%s\n' "$previous_image" "$previous_images" | sort -u); do
|
||||
if [ "$image" != "$new_image" ]; then
|
||||
if [ -z "$(docker ps -aq --filter "ancestor=$image")" ]; then
|
||||
docker image rm "$image" || echo "Warning: could not remove old Product Vamp image $image" >&2
|
||||
else
|
||||
echo "Keeping old Product Vamp image $image: another container still uses it." >&2
|
||||
fi
|
||||
fi
|
||||
done
|
||||
echo "Product Vamp Zero installed. Control AP defaults to SSID 'Vamp Control' at http://192.168.42.1:8080."
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
set -euo pipefail
|
||||
|
||||
container_name=${PRODUCT_VAMP_ZERO_CONTAINER:-product-vamp-zero}
|
||||
image_tag=${PRODUCT_VAMP_ZERO_IMAGE:-product-vamp-zero:dev}
|
||||
config_dir=${PRODUCT_VAMP_CONFIG_DIR:-/etc/product-vamp}
|
||||
state_dir=${PRODUCT_VAMP_STATE_DIR:-/var/lib/product-vamp}
|
||||
image_tag=${PRODUCT_VAMP_ZERO_IMAGE:-}
|
||||
if [ -z "$image_tag" ]; then
|
||||
read -r image_tag < "$config_dir/image-id"
|
||||
fi
|
||||
run_dir=${PRODUCT_VAMP_RUN_DIR:-/run/product-vamp}
|
||||
display_tty=${PRODUCT_VAMP_DISPLAY_TTY:-/dev/tty1}
|
||||
|
||||
@@ -36,7 +38,6 @@ echo "Starting Product Vamp Zero container '$container_name' from '$image_tag' o
|
||||
exec docker run --rm --name "$container_name" --network host --privileged \
|
||||
--env PRODUCT_VAMP_DISPLAY_TTY="$display_tty" \
|
||||
-v "$config_dir:/etc/product-vamp:ro" \
|
||||
-v "$state_dir:/var/lib/product-vamp" \
|
||||
-v "$run_dir:/run/product-vamp" \
|
||||
"$image_tag" \
|
||||
sh -lc 'display_tty=${PRODUCT_VAMP_DISPLAY_TTY:-/dev/tty1}; exec setsid --wait --ctty /usr/bin/vamp-zero --config /etc/product-vamp/vamp.json < "$display_tty"'
|
||||
|
||||
Reference in New Issue
Block a user