#!/usr/bin/env sh set -eu RELEASE_VERSION="${RELEASE_VERSION:-20260518-7b32b21-11}" BASE_URL="${BASE_URL:-http://10.8.0.1/ai_deploy}" BUNDLE_NAME="${BUNDLE_NAME:-managed-portal-${RELEASE_VERSION}.zip}" WEIGHTS_ARCHIVE_NAME="${WEIGHTS_ARCHIVE_NAME:-people-flow-weights-${RELEASE_VERSION}.tar.gz}" YOLO_ARCHIVE_NAME="${YOLO_ARCHIVE_NAME:-people-flow-yolo11n-${RELEASE_VERSION}.tar.gz}" INSTALL_ROOT="${INSTALL_ROOT:-/opt/managed-portal-releases}" TARGET_DIR="${TARGET_DIR:-${INSTALL_ROOT}/managed-portal-${RELEASE_VERSION}}" SHARED_ROOT="${SHARED_ROOT:-${INSTALL_ROOT}/shared}" DEFAULT_PEOPLE_FLOW_WEIGHTS_DIR="${SHARED_ROOT}/people_flow_project/weights" require_command() { if ! command -v "$1" >/dev/null 2>&1; then echo "missing required command: $1" >&2 exit 1 fi } ensure_ubuntu_package() { package_name="$1" command_name="$2" if command -v "$command_name" >/dev/null 2>&1; then return 0 fi if [ ! -r /etc/os-release ]; then echo "missing required command: $command_name; unable to detect OS for automatic installation" >&2 exit 1 fi os_id="$(. /etc/os-release && printf '%s' "${ID:-}")" os_like="$(. /etc/os-release && printf '%s' "${ID_LIKE:-}")" case "$os_id:$os_like" in ubuntu:*|*:ubuntu*|debian:*|*:debian*) ;; *) echo "missing required command: $command_name; automatic installation is only supported on Ubuntu/Debian hosts" >&2 exit 1 ;; esac if [ "$(id -u)" -ne 0 ]; then echo "missing required command: $command_name; rerun installer as root on Ubuntu to auto-install $package_name" >&2 exit 1 fi export DEBIAN_FRONTEND=noninteractive apt-get update apt-get install -y "$package_name" if ! command -v "$command_name" >/dev/null 2>&1; then echo "failed to install required command: $command_name" >&2 exit 1 fi } pull_or_use_local() { image="$1" if docker pull "$image"; then return 0 fi echo "docker pull failed for $image, checking local image cache" >&2 if docker image inspect "$image" >/dev/null 2>&1; then echo "using existing local image $image" >&2 return 0 fi echo "image unavailable locally after pull failure: $image" >&2 exit 1 } run_compose() { if command -v docker-compose >/dev/null 2>&1; then docker-compose "$@" return 0 fi docker compose "$@" } dir_has_files() { directory="$1" [ -d "$directory" ] && [ -n "$(find "$directory" -mindepth 1 -print -quit 2>/dev/null)" ] } dir_has_payload_files() { directory="$1" [ -d "$directory" ] && [ -n "$(find "$directory" -type f ! -name '.gitkeep' -print -quit 2>/dev/null)" ] } people_flow_weights_complete() { directory="$1" people_flow_deepface_weights_complete "$directory" && people_flow_yolo_weight_present "$directory" } people_flow_deepface_weights_complete() { directory="$1" [ -f "$directory/deepface/age_model_weights.h5" ] && [ -f "$directory/deepface/gender_model_weights.h5" ] && [ -f "$directory/deepface/retinaface.h5" ] } people_flow_yolo_weight_present() { directory="$1" [ -f "$directory/yolo11n.pt" ] } copy_dir_contents() { source_dir="$1" target_dir="$2" mkdir -p "$target_dir" cp -R "$source_dir"/. "$target_dir"/ } download_bundle() { tmp_dir="$1" bundle_zip="$tmp_dir/$BUNDLE_NAME" bundle_url="${BASE_URL%/}/$BUNDLE_NAME" echo "downloading $bundle_url" >&2 curl -fL "$bundle_url" -o "$bundle_zip" echo "$bundle_zip" } download_weights_archive() { tmp_dir="$1" weights_archive="$tmp_dir/$WEIGHTS_ARCHIVE_NAME" weights_url="${BASE_URL%/}/$WEIGHTS_ARCHIVE_NAME" echo "downloading $weights_url" >&2 curl -fL "$weights_url" -o "$weights_archive" echo "$weights_archive" } download_yolo_archive() { tmp_dir="$1" yolo_archive="$tmp_dir/$YOLO_ARCHIVE_NAME" yolo_url="${BASE_URL%/}/$YOLO_ARCHIVE_NAME" echo "downloading $yolo_url" >&2 curl -fL "$yolo_url" -o "$yolo_archive" echo "$yolo_archive" } build_overlay_image() { overlay_name="$1" base_image="$2" overlay_root="$3" overlay_image="$4" overlay_context="$(dirname "$overlay_root")" if [ ! -d "$overlay_root" ]; then printf '%s\n' "$base_image" return 0 fi if [ -z "$(find "$overlay_root" -mindepth 1 -print -quit)" ]; then printf '%s\n' "$base_image" return 0 fi echo "building runtime overlay for $overlay_name" >&2 docker build \ -f "$TARGET_DIR/deploy/Dockerfile.runtime-overlay" \ --build-arg "BASE_IMAGE=$base_image" \ -t "$overlay_image" \ "$overlay_context" >/dev/null printf '%s\n' "$overlay_image" } clear_stale_runtime_state() { people_flow_status="$TARGET_DIR/managed/people_flow_project/outputs/rtsp_stream/worker_status.json" if [ -f "$people_flow_status" ]; then rm -f "$people_flow_status" fi } ensure_runtime_directories() { mkdir -p \ "$TARGET_DIR/managed/store_dwell_alert/config" \ "$TARGET_DIR/managed/store_dwell_alert/data" \ "$TARGET_DIR/managed/people_flow_project/config" \ "$TARGET_DIR/managed/people_flow_project/outputs/rtsp_stream" } find_existing_people_flow_weights() { if people_flow_weights_complete "$MANAGED_PEOPLE_FLOW_WEIGHTS_DIR"; then printf '%s\n' "$MANAGED_PEOPLE_FLOW_WEIGHTS_DIR" return 0 fi for candidate in "$INSTALL_ROOT"/managed-portal-*/managed/people_flow_project/weights; do if [ "$candidate" = "$TARGET_DIR/managed/people_flow_project/weights" ]; then continue fi if people_flow_weights_complete "$candidate"; then printf '%s\n' "$candidate" return 0 fi done return 1 } find_existing_people_flow_deepface_weights() { if people_flow_deepface_weights_complete "$MANAGED_PEOPLE_FLOW_WEIGHTS_DIR"; then printf '%s\n' "$MANAGED_PEOPLE_FLOW_WEIGHTS_DIR" return 0 fi if people_flow_deepface_weights_complete "$TARGET_DIR/managed/people_flow_project/weights"; then printf '%s\n' "$TARGET_DIR/managed/people_flow_project/weights" return 0 fi for candidate in "$INSTALL_ROOT"/managed-portal-*/managed/people_flow_project/weights; do if [ "$candidate" = "$TARGET_DIR/managed/people_flow_project/weights" ]; then continue fi if people_flow_deepface_weights_complete "$candidate"; then printf '%s\n' "$candidate" return 0 fi done return 1 } extract_people_flow_weights_archive() { tmp_dir="$1" bundle_weights_dir="$TARGET_DIR/managed/people_flow_project/weights" weights_archive="$(download_weights_archive "$tmp_dir")" mkdir -p "$TARGET_DIR/managed" tar -xzf "$weights_archive" -C "$TARGET_DIR/managed" if ! people_flow_weights_complete "$bundle_weights_dir"; then echo "downloaded weights archive did not populate all required people-flow weights under $bundle_weights_dir" >&2 exit 1 fi printf '%s\n' "$bundle_weights_dir" } ensure_people_flow_yolo_weight() { tmp_dir="$1" target_weights_dir="$2" if people_flow_yolo_weight_present "$target_weights_dir"; then return 0 fi yolo_extract_dir="$tmp_dir/yolo" yolo_archive="$(download_yolo_archive "$tmp_dir")" rm -rf "$yolo_extract_dir" mkdir -p "$yolo_extract_dir" tar -xzf "$yolo_archive" -C "$yolo_extract_dir" yolo_file="$yolo_extract_dir/people_flow_project/weights/yolo11n.pt" if [ ! -f "$yolo_file" ]; then echo "downloaded yolo archive did not populate people_flow_project/weights/yolo11n.pt" >&2 exit 1 fi mkdir -p "$target_weights_dir" cp "$yolo_file" "$target_weights_dir/yolo11n.pt" } prepare_people_flow_weights() { tmp_dir="$1" bundle_weights_dir="$TARGET_DIR/managed/people_flow_project/weights" source_weights_dir="" deepface_weights_dir="" if people_flow_weights_complete "$bundle_weights_dir"; then source_weights_dir="$bundle_weights_dir" echo "seeding shared people-flow weights from bundle" >&2 elif source_weights_dir="$(find_existing_people_flow_weights 2>/dev/null)"; then echo "reusing existing people-flow weights from $source_weights_dir" >&2 elif deepface_weights_dir="$(find_existing_people_flow_deepface_weights 2>/dev/null)"; then echo "reusing existing people-flow DeepFace weights from $deepface_weights_dir" >&2 if [ "$deepface_weights_dir" != "$MANAGED_PEOPLE_FLOW_WEIGHTS_DIR" ]; then rm -rf "$MANAGED_PEOPLE_FLOW_WEIGHTS_DIR" copy_dir_contents "$deepface_weights_dir" "$MANAGED_PEOPLE_FLOW_WEIGHTS_DIR" fi ensure_people_flow_yolo_weight "$tmp_dir" "$MANAGED_PEOPLE_FLOW_WEIGHTS_DIR" source_weights_dir="$MANAGED_PEOPLE_FLOW_WEIGHTS_DIR" elif source_weights_dir="$(extract_people_flow_weights_archive "$tmp_dir" 2>/dev/null)"; then echo "seeding shared people-flow weights from downloaded archive" >&2 else echo "people-flow weights not found; seed $MANAGED_PEOPLE_FLOW_WEIGHTS_DIR, publish $WEIGHTS_ARCHIVE_NAME and $YOLO_ARCHIVE_NAME, or include managed/people_flow_project/weights in the release zip" >&2 exit 1 fi if [ "$source_weights_dir" != "$MANAGED_PEOPLE_FLOW_WEIGHTS_DIR" ]; then rm -rf "$MANAGED_PEOPLE_FLOW_WEIGHTS_DIR" copy_dir_contents "$source_weights_dir" "$MANAGED_PEOPLE_FLOW_WEIGHTS_DIR" fi if ! people_flow_weights_complete "$MANAGED_PEOPLE_FLOW_WEIGHTS_DIR"; then echo "people-flow weights are incomplete under $MANAGED_PEOPLE_FLOW_WEIGHTS_DIR" >&2 exit 1 fi rm -rf "$bundle_weights_dir" mkdir -p "$(dirname "$bundle_weights_dir")" ln -s "$MANAGED_PEOPLE_FLOW_WEIGHTS_DIR" "$bundle_weights_dir" } require_command curl ensure_ubuntu_package unzip unzip require_command tar require_command docker tmp_dir="$(mktemp -d)" trap 'rm -rf "$tmp_dir"' EXIT INT TERM mkdir -p "$INSTALL_ROOT" bundle_zip="$(download_bundle "$tmp_dir")" rm -rf "$TARGET_DIR" unzip -oq "$bundle_zip" -d "$INSTALL_ROOT" if [ ! -f "$TARGET_DIR/release-manifest.env" ]; then echo "release-manifest.env not found in $TARGET_DIR" >&2 exit 1 fi set -a . "$TARGET_DIR/release-manifest.env" set +a MANAGED_PEOPLE_FLOW_WEIGHTS_DIR="${MANAGED_PEOPLE_FLOW_WEIGHTS_DIR:-$DEFAULT_PEOPLE_FLOW_WEIGHTS_DIR}" ensure_runtime_directories prepare_people_flow_weights "$tmp_dir" clear_stale_runtime_state echo "pulling release images" pull_or_use_local "$MANAGED_PORTAL_IMAGE" pull_or_use_local "$MANAGED_PORTAL_WEB_IMAGE" pull_or_use_local "$PEOPLE_FLOW_PROJECT_IMAGE" pull_or_use_local "$STORE_DWELL_ALERT_IMAGE" PEOPLE_FLOW_PROJECT_IMAGE="$(build_overlay_image \ people-flow-project \ "$PEOPLE_FLOW_PROJECT_IMAGE" \ "$TARGET_DIR/runtime-overlays/people-flow-project/rootfs" \ "managed-portal-runtime/people-flow-project:${RELEASE_VERSION}")" STORE_DWELL_ALERT_IMAGE="$(build_overlay_image \ store-dwell-alert \ "$STORE_DWELL_ALERT_IMAGE" \ "$TARGET_DIR/runtime-overlays/store-dwell-alert/rootfs" \ "managed-portal-runtime/store-dwell-alert:${RELEASE_VERSION}")" export MANAGED_PORTAL_IMAGE export MANAGED_PORTAL_WEB_IMAGE export PEOPLE_FLOW_PROJECT_IMAGE export STORE_DWELL_ALERT_IMAGE export MANAGED_PEOPLE_FLOW_WEIGHTS_DIR cd "$TARGET_DIR/deploy" run_compose \ --env-file managed-portal.release.env \ -f docker-compose.ota-release.yml \ up -d echo "release installed under $TARGET_DIR"