|
|
|
@@ -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."
|
|
|
|
|