779 lines
24 KiB
Bash
Executable File
779 lines
24 KiB
Bash
Executable File
#!/usr/bin/env sh
|
|
set -eu
|
|
|
|
INSTALL_DIR="${PLATFORM_AGENT_DIR:-/opt/platform-agent}"
|
|
SERVICE_NAME="${PLATFORM_AGENT_SERVICE:-platform-agent}"
|
|
REPO_URL="${PLATFORM_AGENT_REPO_URL:-}"
|
|
RUN_USER="${PLATFORM_AGENT_USER:-$(id -un)}"
|
|
INSTALL_TAILSCALE="${INSTALL_TAILSCALE:-false}"
|
|
INSTALL_OPENSSH_SERVER="${INSTALL_OPENSSH_SERVER:-true}"
|
|
INSTALL_GIT="${INSTALL_GIT:-true}"
|
|
TAILSCALE_AUTH_KEY="${TAILSCALE_AUTH_KEY:-}"
|
|
TAILSCALE_LOGIN_SERVER="${TAILSCALE_LOGIN_SERVER:-}"
|
|
TAILSCALE_HOSTNAME="${TAILSCALE_HOSTNAME:-}"
|
|
TAILSCALE_EXTRA_ARGS="${TAILSCALE_EXTRA_ARGS:-}"
|
|
PLATFORM_PROVISION_URL="${PLATFORM_PROVISION_URL:-https://portal-api.autonomystudio.net/api/provision}"
|
|
PLATFORM_ACCOUNT="${PLATFORM_ACCOUNT:-}"
|
|
PLATFORM_REGISTRATION_CODE="${PLATFORM_REGISTRATION_CODE:-}"
|
|
WORKSPACE_ROOT="${WORKSPACE_ROOT:-~/autonomy-studio/workspaces}"
|
|
export WORKSPACE_ROOT
|
|
ROBOT_STACK="${ROBOT_STACK:-autoware}"
|
|
ROS_DOMAIN_ID="${ROS_DOMAIN_ID:-0}"
|
|
CAMERA_ENABLED="${CAMERA_ENABLED:-false}"
|
|
CAMERA_VIDEO_DEVICE="${CAMERA_VIDEO_DEVICE:-/dev/video0}"
|
|
CAMERA_SIGNALING_URL="${CAMERA_SIGNALING_URL:-ws://127.0.0.1:3000}"
|
|
KASMVNC_GEOMETRY="${KASMVNC_GEOMETRY:-1600x900}"
|
|
AGENT_GATEWAY_TOKEN="${AGENT_GATEWAY_TOKEN:-}"
|
|
PLATFORM_AGENT_INTERACTIVE="${PLATFORM_AGENT_INTERACTIVE:-auto}"
|
|
PLATFORM_INSTALLER_TUI_URL="${PLATFORM_INSTALLER_TUI_URL:-}"
|
|
PLATFORM_SKIP_TUI="${PLATFORM_SKIP_TUI:-false}"
|
|
PLATFORM_AGENT_REINSTALL="${PLATFORM_AGENT_REINSTALL:-false}"
|
|
PLATFORM_AGENT_SETTINGS_ONLY="${PLATFORM_AGENT_SETTINGS_ONLY:-false}"
|
|
STEP_CURRENT=0
|
|
STEP_TOTAL=8
|
|
TUI_SKIP_REASON=""
|
|
|
|
log() {
|
|
printf '%s\n' "$*"
|
|
}
|
|
|
|
need_cmd() {
|
|
command -v "$1" >/dev/null 2>&1
|
|
}
|
|
|
|
has_tty() {
|
|
[ -t 0 ] || { [ -r /dev/tty ] && [ -w /dev/tty ]; }
|
|
}
|
|
|
|
tty_available() {
|
|
( : < /dev/tty > /dev/tty ) 2>/dev/null
|
|
}
|
|
|
|
progress_bar() {
|
|
current="$1"
|
|
total="$2"
|
|
label="$3"
|
|
width=24
|
|
filled=$((current * width / total))
|
|
empty=$((width - filled))
|
|
bar=""
|
|
i=0
|
|
while [ "$i" -lt "$filled" ]; do bar="${bar}#"; i=$((i + 1)); done
|
|
i=0
|
|
while [ "$i" -lt "$empty" ]; do bar="${bar}-"; i=$((i + 1)); done
|
|
printf '[%s] %s/%s %s\n' "$bar" "$current" "$total" "$label"
|
|
}
|
|
|
|
run_step() {
|
|
STEP_CURRENT=$((STEP_CURRENT + 1))
|
|
progress_bar "$STEP_CURRENT" "$STEP_TOTAL" "$1"
|
|
shift
|
|
"$@"
|
|
}
|
|
|
|
as_root() {
|
|
if [ "$(id -u)" -eq 0 ]; then
|
|
"$@"
|
|
elif need_cmd sudo; then
|
|
sudo "$@"
|
|
else
|
|
log "Root privileges are required: $*"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
prompt_yes_no() {
|
|
label="$1"
|
|
default="${2:-no}"
|
|
suffix="[y/N]"
|
|
[ "$default" = "yes" ] && suffix="[Y/n]"
|
|
if tty_available; then
|
|
printf '%s %s ' "$label" "$suffix" > /dev/tty
|
|
IFS= read -r answer < /dev/tty || answer=""
|
|
else
|
|
printf '%s %s ' "$label" "$suffix"
|
|
IFS= read -r answer || answer=""
|
|
fi
|
|
answer="$(printf '%s' "$answer" | tr '[:upper:]' '[:lower:]')"
|
|
if [ -z "$answer" ]; then
|
|
[ "$default" = "yes" ]
|
|
return
|
|
fi
|
|
[ "$answer" = "y" ] || [ "$answer" = "yes" ]
|
|
}
|
|
|
|
agent_already_installed() {
|
|
[ -f "/etc/systemd/system/${SERVICE_NAME}.service" ] || [ -f "${INSTALL_DIR}/agent.config.json" ]
|
|
}
|
|
|
|
handle_existing_install() {
|
|
case "$PLATFORM_AGENT_REINSTALL" in
|
|
true|1|yes|y|on)
|
|
return
|
|
;;
|
|
esac
|
|
case "$PLATFORM_AGENT_SETTINGS_ONLY" in
|
|
true|1|yes|y|on)
|
|
if [ -x "${INSTALL_DIR}/scripts/setting.sh" ]; then
|
|
exec "${INSTALL_DIR}/scripts/setting.sh"
|
|
fi
|
|
exec "$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)/setting.sh"
|
|
;;
|
|
esac
|
|
if ! agent_already_installed; then
|
|
return
|
|
fi
|
|
|
|
log "Autonomy Studio Platform Agent is already installed."
|
|
log "Install dir : ${INSTALL_DIR}"
|
|
log "Service : ${SERVICE_NAME}"
|
|
log ""
|
|
|
|
if ! interactive_allowed; then
|
|
log "Set PLATFORM_AGENT_REINSTALL=true to reinstall, or PLATFORM_AGENT_SETTINGS_ONLY=true to edit settings."
|
|
exit 0
|
|
fi
|
|
|
|
log "1. Open settings"
|
|
log "2. Reinstall / update"
|
|
log "3. Cancel"
|
|
log ""
|
|
if tty_available; then
|
|
printf 'Select [1]: ' > /dev/tty
|
|
IFS= read -r choice < /dev/tty || choice=""
|
|
else
|
|
printf 'Select [1]: '
|
|
IFS= read -r choice || choice=""
|
|
fi
|
|
case "${choice:-1}" in
|
|
1)
|
|
if [ -x "${INSTALL_DIR}/scripts/setting.sh" ]; then
|
|
exec "${INSTALL_DIR}/scripts/setting.sh"
|
|
fi
|
|
exec "$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)/setting.sh"
|
|
;;
|
|
2)
|
|
if prompt_yes_no "Continue reinstall/update?" "no"; then
|
|
return
|
|
fi
|
|
log "Canceled."
|
|
exit 0
|
|
;;
|
|
*)
|
|
log "Canceled."
|
|
exit 0
|
|
;;
|
|
esac
|
|
}
|
|
|
|
install_bun() {
|
|
if need_cmd bun; then
|
|
return
|
|
fi
|
|
log "Installing Bun."
|
|
curl -fsSL https://bun.sh/install | bash
|
|
export PATH="$HOME/.bun/bin:$PATH"
|
|
}
|
|
|
|
interactive_allowed() {
|
|
case "$PLATFORM_AGENT_INTERACTIVE" in
|
|
false|0|no|n|off)
|
|
return 1
|
|
;;
|
|
true|1|yes|y|on)
|
|
has_tty
|
|
return
|
|
;;
|
|
*)
|
|
has_tty
|
|
return
|
|
;;
|
|
esac
|
|
}
|
|
|
|
prompt_value() {
|
|
var_name="$1"
|
|
label="$2"
|
|
current_value="$3"
|
|
secret="${4:-false}"
|
|
if [ "$secret" = "true" ]; then
|
|
if tty_available; then
|
|
printf '%s [%s]: ' "$label" "hidden" > /dev/tty
|
|
stty -echo < /dev/tty || true
|
|
IFS= read -r answer < /dev/tty || answer=""
|
|
stty echo < /dev/tty || true
|
|
printf '\n' > /dev/tty
|
|
else
|
|
printf '%s [%s]: ' "$label" "hidden"
|
|
stty -echo || true
|
|
IFS= read -r answer || answer=""
|
|
stty echo || true
|
|
printf '\n'
|
|
fi
|
|
else
|
|
if tty_available; then
|
|
printf '%s [%s]: ' "$label" "$current_value" > /dev/tty
|
|
IFS= read -r answer < /dev/tty || answer=""
|
|
else
|
|
printf '%s [%s]: ' "$label" "$current_value"
|
|
IFS= read -r answer || answer=""
|
|
fi
|
|
fi
|
|
if [ -n "$answer" ]; then
|
|
eval "$var_name=\"\$answer\""
|
|
fi
|
|
}
|
|
|
|
run_shell_tui() {
|
|
if ! interactive_allowed; then
|
|
return 1
|
|
fi
|
|
|
|
clear
|
|
log "┌────────────────────────────────────────────────────────────────────┐"
|
|
log "│ Autonomy Studio Platform Agent Installer │"
|
|
log "└────────────────────────────────────────────────────────────────────┘"
|
|
log ""
|
|
|
|
prompt_value PLATFORM_ACCOUNT "Autonomy Studio account" "$PLATFORM_ACCOUNT"
|
|
prompt_value PLATFORM_REGISTRATION_CODE "6-digit registration code" "$PLATFORM_REGISTRATION_CODE"
|
|
prompt_value WORKSPACE_ROOT "Workspace root" "$WORKSPACE_ROOT"
|
|
prompt_value ROS_DOMAIN_ID "ROS_DOMAIN_ID" "$ROS_DOMAIN_ID"
|
|
prompt_value CAMERA_ENABLED "Enable camera true/false" "$CAMERA_ENABLED"
|
|
prompt_value CAMERA_VIDEO_DEVICE "Camera device" "$CAMERA_VIDEO_DEVICE"
|
|
prompt_value CAMERA_SIGNALING_URL "Camera signaling URL" "$CAMERA_SIGNALING_URL"
|
|
prompt_value KASMVNC_GEOMETRY "KasmVNC resolution" "$KASMVNC_GEOMETRY"
|
|
provision_tailscale_from_backend
|
|
return 0
|
|
}
|
|
|
|
run_bubbletea_tui() {
|
|
case "$PLATFORM_SKIP_TUI" in
|
|
true|1|yes|y|on)
|
|
TUI_SKIP_REASON="PLATFORM_SKIP_TUI is enabled."
|
|
return 1
|
|
;;
|
|
esac
|
|
if ! interactive_allowed; then
|
|
TUI_SKIP_REASON="Interactive terminal is not available or PLATFORM_AGENT_INTERACTIVE=false."
|
|
return 1
|
|
fi
|
|
|
|
env_file="$(mktemp)"
|
|
tui_bin=""
|
|
current_dir="$(CDPATH= cd -- "$(dirname -- "$0")/.." 2>/dev/null && pwd || printf '')"
|
|
tui_arch=""
|
|
case "$(uname -m 2>/dev/null || true)" in
|
|
x86_64|amd64) tui_arch="amd64" ;;
|
|
aarch64|arm64) tui_arch="arm64" ;;
|
|
esac
|
|
|
|
if [ -n "$PLATFORM_INSTALLER_TUI_URL" ]; then
|
|
tui_url="$PLATFORM_INSTALLER_TUI_URL"
|
|
if [ -n "$tui_arch" ]; then
|
|
tui_url="$(printf '%s' "$tui_url" | sed "s/{arch}/$tui_arch/g")"
|
|
fi
|
|
tui_bin="$(mktemp)"
|
|
curl -fsSL "$tui_url" -o "$tui_bin"
|
|
chmod +x "$tui_bin"
|
|
set +e
|
|
if tty_available; then
|
|
"$tui_bin" "$env_file" < /dev/tty > /dev/tty
|
|
else
|
|
"$tui_bin" "$env_file"
|
|
fi
|
|
status=$?
|
|
set -e
|
|
if [ "$status" -ne 0 ]; then
|
|
if [ "$status" -eq 130 ]; then
|
|
rm -f "$env_file" "$tui_bin"
|
|
exit 130
|
|
fi
|
|
TUI_SKIP_REASON="Failed to run the TUI downloaded from PLATFORM_INSTALLER_TUI_URL."
|
|
rm -f "$env_file" "$tui_bin"
|
|
return 1
|
|
fi
|
|
elif [ -n "$current_dir" ] && [ -n "$tui_arch" ] && [ -x "$current_dir/scripts/installer-tui/installer-tui-linux-$tui_arch" ]; then
|
|
set +e
|
|
if tty_available; then
|
|
"$current_dir/scripts/installer-tui/installer-tui-linux-$tui_arch" "$env_file" < /dev/tty > /dev/tty
|
|
else
|
|
"$current_dir/scripts/installer-tui/installer-tui-linux-$tui_arch" "$env_file"
|
|
fi
|
|
status=$?
|
|
set -e
|
|
if [ "$status" -ne 0 ]; then
|
|
if [ "$status" -eq 130 ]; then
|
|
rm -f "$env_file"
|
|
exit 130
|
|
fi
|
|
TUI_SKIP_REASON="Failed to run the local installer-tui binary."
|
|
rm -f "$env_file"
|
|
return 1
|
|
fi
|
|
elif [ -n "$current_dir" ] && [ -x "$current_dir/scripts/installer-tui/installer-tui" ]; then
|
|
# Compatibility with existing single-architecture installer packages.
|
|
set +e
|
|
if tty_available; then
|
|
"$current_dir/scripts/installer-tui/installer-tui" "$env_file" < /dev/tty > /dev/tty
|
|
else
|
|
"$current_dir/scripts/installer-tui/installer-tui" "$env_file"
|
|
fi
|
|
status=$?
|
|
set -e
|
|
if [ "$status" -ne 0 ]; then
|
|
if [ "$status" -eq 130 ]; then
|
|
rm -f "$env_file"
|
|
exit 130
|
|
fi
|
|
TUI_SKIP_REASON="Failed to run the local installer-tui binary."
|
|
rm -f "$env_file"
|
|
return 1
|
|
fi
|
|
elif [ -n "$current_dir" ] && [ -f "$current_dir/scripts/installer-tui/main.go" ] && need_cmd go; then
|
|
set +e
|
|
if tty_available; then
|
|
(cd "$current_dir/scripts/installer-tui" && go run . "$env_file") < /dev/tty > /dev/tty
|
|
else
|
|
(cd "$current_dir/scripts/installer-tui" && go run . "$env_file")
|
|
fi
|
|
status=$?
|
|
set -e
|
|
if [ "$status" -ne 0 ]; then
|
|
if [ "$status" -eq 130 ]; then
|
|
rm -f "$env_file"
|
|
exit 130
|
|
fi
|
|
TUI_SKIP_REASON="Failed to run the Bubble Tea TUI with go run."
|
|
rm -f "$env_file"
|
|
return 1
|
|
fi
|
|
else
|
|
TUI_SKIP_REASON="No TUI binary or Go runtime found. curl | sh installs require PLATFORM_INSTALLER_TUI_URL."
|
|
rm -f "$env_file"
|
|
return 1
|
|
fi
|
|
|
|
# shellcheck disable=SC1090
|
|
. "$env_file"
|
|
INSTALL_DIR="${PLATFORM_AGENT_DIR:-$INSTALL_DIR}"
|
|
SERVICE_NAME="${PLATFORM_AGENT_SERVICE:-$SERVICE_NAME}"
|
|
REPO_URL="${PLATFORM_AGENT_REPO_URL:-$REPO_URL}"
|
|
RUN_USER="${PLATFORM_AGENT_USER:-$RUN_USER}"
|
|
rm -f "$env_file"
|
|
[ -n "${tui_bin:-}" ] && rm -f "$tui_bin"
|
|
return 0
|
|
}
|
|
|
|
collect_install_config() {
|
|
if run_bubbletea_tui; then
|
|
return
|
|
fi
|
|
if run_shell_tui; then
|
|
return
|
|
fi
|
|
if ! interactive_allowed; then
|
|
return
|
|
fi
|
|
|
|
log "Bubble Tea TUI is unavailable. Falling back to shell prompts."
|
|
if [ -n "$TUI_SKIP_REASON" ]; then
|
|
log "Reason: $TUI_SKIP_REASON"
|
|
fi
|
|
prompt_value PLATFORM_ACCOUNT "Autonomy Studio account" "$PLATFORM_ACCOUNT"
|
|
prompt_value PLATFORM_REGISTRATION_CODE "6-digit registration code" "$PLATFORM_REGISTRATION_CODE"
|
|
prompt_value WORKSPACE_ROOT "Workspace root" "$WORKSPACE_ROOT"
|
|
prompt_value ROS_DOMAIN_ID "ROS_DOMAIN_ID" "$ROS_DOMAIN_ID"
|
|
prompt_value CAMERA_ENABLED "Enable camera true/false" "$CAMERA_ENABLED"
|
|
prompt_value CAMERA_VIDEO_DEVICE "Camera device" "$CAMERA_VIDEO_DEVICE"
|
|
prompt_value CAMERA_SIGNALING_URL "Camera signaling URL" "$CAMERA_SIGNALING_URL"
|
|
prompt_value KASMVNC_GEOMETRY "KasmVNC resolution" "$KASMVNC_GEOMETRY"
|
|
provision_tailscale_from_backend
|
|
}
|
|
|
|
provision_tailscale_from_backend() {
|
|
if [ -z "$PLATFORM_ACCOUNT" ] || [ -z "$PLATFORM_REGISTRATION_CODE" ]; then
|
|
log "Provisioning requires both account and registration code."
|
|
exit 1
|
|
fi
|
|
if [ -z "$PLATFORM_PROVISION_URL" ]; then
|
|
log "PLATFORM_PROVISION_URL is required for account provisioning."
|
|
exit 1
|
|
fi
|
|
if ! need_cmd curl; then
|
|
log "Provisioning requires curl."
|
|
exit 1
|
|
fi
|
|
if ! need_cmd python3; then
|
|
log "Shell fallback provisioning requires python3. Use the TUI binary or pass environment variables directly."
|
|
exit 1
|
|
fi
|
|
|
|
body_file="$(mktemp)"
|
|
resp_file="$(mktemp)"
|
|
env_file="$(mktemp)"
|
|
legacy_body_file="$(mktemp)"
|
|
chmod 600 "$body_file" "$legacy_body_file" "$resp_file" "$env_file"
|
|
PLATFORM_ACCOUNT="$PLATFORM_ACCOUNT" PLATFORM_REGISTRATION_CODE="$PLATFORM_REGISTRATION_CODE" RUN_USER="$RUN_USER" WORKSPACE_ROOT="$WORKSPACE_ROOT" python3 -c 'import json, os, platform
|
|
hostname = os.uname().nodename
|
|
distro = platform.platform()
|
|
ssh_user = os.environ.get("RUN_USER") or os.environ.get("USER") or ""
|
|
workspace_root = os.environ.get("WORKSPACE_ROOT") or "~/autonomy-studio/workspaces"
|
|
try:
|
|
with open("/etc/os-release", "r", encoding="utf-8") as fp:
|
|
for line in fp:
|
|
if line.startswith("PRETTY_NAME="):
|
|
distro = line.split("=", 1)[1].strip().strip("\"")
|
|
break
|
|
except OSError:
|
|
pass
|
|
device = {
|
|
"hostname": hostname,
|
|
"os": os.uname().sysname.lower(),
|
|
"arch": {"x86_64": "amd64", "AMD64": "amd64", "aarch64": "arm64", "armv8": "arm64"}.get(platform.machine(), platform.machine()),
|
|
"kernel": os.uname().release,
|
|
"distro": distro,
|
|
"sshUser": ssh_user,
|
|
"workspaceRoot": workspace_root,
|
|
}
|
|
print(json.dumps({
|
|
"account": os.environ["PLATFORM_ACCOUNT"],
|
|
"userCode": os.environ["PLATFORM_REGISTRATION_CODE"],
|
|
"hostname": hostname,
|
|
"os": device["os"],
|
|
"arch": device["arch"],
|
|
"kernel": device["kernel"],
|
|
"distro": device["distro"],
|
|
"sshUser": ssh_user,
|
|
"workspaceRoot": workspace_root,
|
|
"device": device,
|
|
}))' > "$body_file"
|
|
PLATFORM_ACCOUNT="$PLATFORM_ACCOUNT" PLATFORM_REGISTRATION_CODE="$PLATFORM_REGISTRATION_CODE" RUN_USER="$RUN_USER" WORKSPACE_ROOT="$WORKSPACE_ROOT" python3 -c 'import json, os
|
|
hostname = os.uname().nodename
|
|
print(json.dumps({
|
|
"account": os.environ["PLATFORM_ACCOUNT"],
|
|
"userCode": os.environ["PLATFORM_REGISTRATION_CODE"],
|
|
"hostname": hostname,
|
|
"sshUser": os.environ.get("RUN_USER") or os.environ.get("USER") or "",
|
|
"workspaceRoot": os.environ.get("WORKSPACE_ROOT") or "~/autonomy-studio/workspaces",
|
|
}))' > "$legacy_body_file"
|
|
http_status="$(curl -sS -H "Content-Type: application/json" --data-binary "@$body_file" "$PLATFORM_PROVISION_URL/device-authorizations/consume" -o "$resp_file" -w "%{http_code}" || printf '000')"
|
|
if [ "$http_status" = "400" ]; then
|
|
http_status="$(curl -sS -H "Content-Type: application/json" --data-binary "@$legacy_body_file" "$PLATFORM_PROVISION_URL/device-authorizations/consume" -o "$resp_file" -w "%{http_code}" || printf '000')"
|
|
fi
|
|
if [ "$http_status" -lt 200 ] || [ "$http_status" -ge 300 ]; then
|
|
rm -f "$body_file" "$legacy_body_file" "$resp_file" "$env_file"
|
|
log "Provisioning request failed."
|
|
exit 1
|
|
fi
|
|
python3 - "$resp_file" > "$env_file" <<'PY'
|
|
import json
|
|
import shlex
|
|
import sys
|
|
|
|
with open(sys.argv[1], "r", encoding="utf-8") as fp:
|
|
data = json.load(fp)
|
|
|
|
def first(*keys):
|
|
for key in keys:
|
|
value = data.get(key)
|
|
if value is not None and value != "":
|
|
if isinstance(value, bool):
|
|
return "true" if value else "false"
|
|
return str(value)
|
|
return ""
|
|
|
|
values = {
|
|
"INSTALL_TAILSCALE": first("installTailscale", "install_tailscale", "tailscaleEnabled", "tailscale_enabled"),
|
|
"TAILSCALE_AUTH_KEY": first("tailscaleAuthKey", "tailscale_auth_key", "authKey", "auth_key"),
|
|
"TAILSCALE_LOGIN_SERVER": first("tailscaleLoginServer", "tailscale_login_server", "loginServer", "login_server"),
|
|
"TAILSCALE_HOSTNAME": first("tailscaleHostname", "tailscale_hostname", "hostname"),
|
|
"TAILSCALE_EXTRA_ARGS": first("tailscaleExtraArgs", "tailscale_extra_args", "extraArgs", "extra_args"),
|
|
"AGENT_GATEWAY_TOKEN": first("agentGatewayToken", "agent_gateway_token", "gatewayToken", "gateway_token"),
|
|
}
|
|
if not values["INSTALL_TAILSCALE"] and values["TAILSCALE_AUTH_KEY"]:
|
|
values["INSTALL_TAILSCALE"] = "true"
|
|
for key, value in values.items():
|
|
if value:
|
|
print(f"{key}={shlex.quote(value)}")
|
|
PY
|
|
# shellcheck disable=SC1090
|
|
. "$env_file"
|
|
rm -f "$body_file" "$legacy_body_file" "$resp_file" "$env_file"
|
|
}
|
|
|
|
install_tailscale() {
|
|
case "$INSTALL_TAILSCALE" in
|
|
true|1|yes|y|on) ;;
|
|
*)
|
|
return
|
|
;;
|
|
esac
|
|
|
|
if [ "$(uname -s)" != "Linux" ]; then
|
|
log "Automatic Tailscale installation is supported only on Linux. Skipping."
|
|
return
|
|
fi
|
|
|
|
if ! need_cmd tailscale; then
|
|
log "Installing Tailscale."
|
|
curl -fsSL https://tailscale.com/install.sh | sh
|
|
fi
|
|
|
|
if need_cmd systemctl; then
|
|
as_root systemctl enable --now tailscaled
|
|
elif need_cmd service; then
|
|
as_root service tailscaled start || true
|
|
fi
|
|
|
|
if [ -n "$TAILSCALE_AUTH_KEY" ]; then
|
|
log "Joining the Tailscale network."
|
|
set -- up --auth-key "$TAILSCALE_AUTH_KEY"
|
|
if [ -n "$TAILSCALE_LOGIN_SERVER" ]; then
|
|
set -- "$@" --login-server "$TAILSCALE_LOGIN_SERVER"
|
|
fi
|
|
if [ -n "$TAILSCALE_HOSTNAME" ]; then
|
|
set -- "$@" --hostname "$TAILSCALE_HOSTNAME"
|
|
fi
|
|
if [ -n "$TAILSCALE_EXTRA_ARGS" ]; then
|
|
# shellcheck disable=SC2086
|
|
set -- "$@" $TAILSCALE_EXTRA_ARGS
|
|
fi
|
|
tailscale_error_file="$(mktemp)"
|
|
if ! as_root tailscale "$@" 2>"$tailscale_error_file"; then
|
|
tailscale_error="$(cat "$tailscale_error_file" 2>/dev/null || true)"
|
|
rm -f "$tailscale_error_file"
|
|
case "$tailscale_error" in
|
|
*"requires mentioning all"*|*"--reset"*)
|
|
log "Existing Tailscale settings detected. Retrying with --reset."
|
|
set -- "$@" --reset
|
|
as_root tailscale "$@"
|
|
;;
|
|
*)
|
|
printf '%s\n' "$tailscale_error" >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
else
|
|
rm -f "$tailscale_error_file"
|
|
fi
|
|
else
|
|
log "TAILSCALE_AUTH_KEY is empty. tailscaled was enabled but not joined."
|
|
fi
|
|
|
|
if [ "$RUN_USER" != "root" ]; then
|
|
log "Allowing ${RUN_USER} to run Tailscale without root."
|
|
as_root tailscale set --operator="$RUN_USER" || log "Failed to set Tailscale operator for ${RUN_USER}."
|
|
fi
|
|
}
|
|
|
|
install_openssh_server() {
|
|
case "$INSTALL_OPENSSH_SERVER" in
|
|
true|1|yes|y|on) ;;
|
|
*)
|
|
return
|
|
;;
|
|
esac
|
|
|
|
if [ "$(uname -s)" != "Linux" ]; then
|
|
log "Automatic OpenSSH server installation is supported only on Linux. Skipping."
|
|
return
|
|
fi
|
|
|
|
if need_cmd sshd || [ -x /usr/sbin/sshd ]; then
|
|
log "OpenSSH server is already installed."
|
|
elif need_cmd apt-get; then
|
|
log "Installing OpenSSH server with apt."
|
|
as_root apt-get update
|
|
as_root apt-get install -y openssh-server
|
|
elif need_cmd dnf; then
|
|
log "Installing OpenSSH server with dnf."
|
|
as_root dnf install -y openssh-server
|
|
elif need_cmd yum; then
|
|
log "Installing OpenSSH server with yum."
|
|
as_root yum install -y openssh-server
|
|
elif need_cmd pacman; then
|
|
log "Installing OpenSSH server with pacman."
|
|
as_root pacman -Sy --noconfirm openssh
|
|
elif need_cmd zypper; then
|
|
log "Installing OpenSSH server with zypper."
|
|
as_root zypper --non-interactive install openssh
|
|
else
|
|
log "No supported package manager found. Install OpenSSH server manually."
|
|
return
|
|
fi
|
|
|
|
if need_cmd systemctl; then
|
|
if systemctl list-unit-files ssh.service >/dev/null 2>&1; then
|
|
as_root systemctl enable --now ssh
|
|
elif systemctl list-unit-files sshd.service >/dev/null 2>&1; then
|
|
as_root systemctl enable --now sshd
|
|
else
|
|
as_root systemctl restart ssh || as_root systemctl restart sshd || true
|
|
fi
|
|
elif need_cmd service; then
|
|
as_root service ssh start || as_root service sshd start || true
|
|
fi
|
|
}
|
|
|
|
install_git() {
|
|
case "$INSTALL_GIT" in
|
|
true|1|yes|y|on) ;;
|
|
*)
|
|
return
|
|
;;
|
|
esac
|
|
|
|
if need_cmd git; then
|
|
log "Git is already installed."
|
|
return
|
|
fi
|
|
|
|
if [ "$(uname -s)" != "Linux" ]; then
|
|
log "Automatic Git installation is supported only on Linux. Skipping."
|
|
return
|
|
fi
|
|
|
|
if need_cmd apt-get; then
|
|
log "Installing Git with apt."
|
|
as_root apt-get update
|
|
as_root apt-get install -y git
|
|
elif need_cmd dnf; then
|
|
log "Installing Git with dnf."
|
|
as_root dnf install -y git
|
|
elif need_cmd yum; then
|
|
log "Installing Git with yum."
|
|
as_root yum install -y git
|
|
elif need_cmd pacman; then
|
|
log "Installing Git with pacman."
|
|
as_root pacman -Sy --noconfirm git
|
|
elif need_cmd zypper; then
|
|
log "Installing Git with zypper."
|
|
as_root zypper --non-interactive install git
|
|
else
|
|
log "No supported package manager found. Install Git manually."
|
|
fi
|
|
}
|
|
|
|
prepare_sources() {
|
|
if [ -n "$REPO_URL" ]; then
|
|
as_root mkdir -p "$INSTALL_DIR"
|
|
if [ -d "$INSTALL_DIR/.git" ]; then
|
|
as_root git -C "$INSTALL_DIR" pull --ff-only
|
|
else
|
|
tmp_dir="$(mktemp -d)"
|
|
git clone "$REPO_URL" "$tmp_dir"
|
|
as_root cp -a "$tmp_dir/." "$INSTALL_DIR/"
|
|
rm -rf "$tmp_dir"
|
|
fi
|
|
else
|
|
current_dir="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)"
|
|
as_root mkdir -p "$INSTALL_DIR"
|
|
as_root cp -a "$current_dir/." "$INSTALL_DIR/"
|
|
fi
|
|
as_root chown -R "$RUN_USER":"$RUN_USER" "$INSTALL_DIR"
|
|
}
|
|
|
|
install_dependencies() {
|
|
(cd "$INSTALL_DIR" && bun install)
|
|
}
|
|
|
|
write_agent_config_from_env() {
|
|
(
|
|
cd "$INSTALL_DIR"
|
|
AGENT_CONFIG_PATH="$INSTALL_DIR/agent.config.json" \
|
|
WORKSPACE_ROOT="$WORKSPACE_ROOT" \
|
|
ROBOT_STACK="$ROBOT_STACK" \
|
|
ROS_DOMAIN_ID="$ROS_DOMAIN_ID" \
|
|
CAMERA_ENABLED="$CAMERA_ENABLED" \
|
|
CAMERA_VIDEO_DEVICE="$CAMERA_VIDEO_DEVICE" \
|
|
CAMERA_SIGNALING_URL="$CAMERA_SIGNALING_URL" \
|
|
KASMVNC_GEOMETRY="$KASMVNC_GEOMETRY" \
|
|
AGENT_GATEWAY_TOKEN="$AGENT_GATEWAY_TOKEN" \
|
|
bun -e '
|
|
const fs = require("fs");
|
|
const path = process.env.AGENT_CONFIG_PATH;
|
|
const cfg = JSON.parse(fs.readFileSync(path, "utf8"));
|
|
cfg.security ??= {};
|
|
cfg.workspace ??= {};
|
|
cfg.ros ??= {};
|
|
cfg.camera ??= {};
|
|
cfg.desktop ??= {};
|
|
cfg.security.gatewayToken = process.env.AGENT_GATEWAY_TOKEN || cfg.security.gatewayToken || "";
|
|
cfg.workspace.root = process.env.WORKSPACE_ROOT || cfg.workspace.root || "~/autonomy-studio/workspaces";
|
|
cfg.ros.stack = process.env.ROBOT_STACK === "nav2" ? "nav2" : "autoware";
|
|
cfg.ros.domainId = /^\d+$/.test(process.env.ROS_DOMAIN_ID || "") ? process.env.ROS_DOMAIN_ID : "0";
|
|
cfg.camera.enabled = /^(true|1|yes|y|on)$/i.test(process.env.CAMERA_ENABLED || "");
|
|
cfg.camera.videoDevice = process.env.CAMERA_VIDEO_DEVICE || cfg.camera.videoDevice || "/dev/video0";
|
|
cfg.camera.signalingUrl = process.env.CAMERA_SIGNALING_URL || cfg.camera.signalingUrl || "ws://127.0.0.1:3000";
|
|
cfg.desktop.geometry = process.env.KASMVNC_GEOMETRY || cfg.desktop.geometry || "1600x900";
|
|
fs.writeFileSync(path, JSON.stringify(cfg, null, 2) + "\n");
|
|
'
|
|
)
|
|
}
|
|
|
|
write_service() {
|
|
service_path="/etc/systemd/system/${SERVICE_NAME}.service"
|
|
tmp_service="$(mktemp)"
|
|
bun_bin="$(command -v bun)"
|
|
cat > "$tmp_service" <<EOF
|
|
[Unit]
|
|
Description=Platform Agent
|
|
After=network-online.target docker.service
|
|
Wants=network-online.target
|
|
|
|
[Service]
|
|
Type=simple
|
|
User=${RUN_USER}
|
|
WorkingDirectory=${INSTALL_DIR}
|
|
ExecStart=${bun_bin} src/server.tsx
|
|
Restart=always
|
|
RestartSec=2
|
|
Environment=HOST=0.0.0.0
|
|
Environment=AGENT_CONFIG=${INSTALL_DIR}/agent.config.json
|
|
Environment=AGENT_SSH_USER=${RUN_USER}
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
EOF
|
|
as_root cp "$tmp_service" "$service_path"
|
|
rm -f "$tmp_service"
|
|
as_root systemctl daemon-reload
|
|
as_root systemctl enable "$SERVICE_NAME"
|
|
as_root systemctl restart "$SERVICE_NAME"
|
|
}
|
|
|
|
print_host_notes() {
|
|
log ""
|
|
log "Install complete: ${INSTALL_DIR}"
|
|
log "Service: systemctl status ${SERVICE_NAME}"
|
|
log "Settings UI: http://<device-ip>:3100"
|
|
if need_cmd tailscale; then
|
|
ts_ip="$(tailscale ip -4 2>/dev/null | head -n 1 || true)"
|
|
if [ -n "$ts_ip" ]; then
|
|
log "Tailscale Settings UI: http://${ts_ip}:3100"
|
|
fi
|
|
fi
|
|
log ""
|
|
log "Automatic Tailscale install example:"
|
|
log "INSTALL_TAILSCALE=true TAILSCALE_AUTH_KEY=tskey-auth-... sh scripts/install.sh"
|
|
log ""
|
|
log "Optional host dependencies:"
|
|
log "- KasmVNC/XFCE: vncserver or kasmvncserver, XFCE"
|
|
log "- Camera: GStreamer, v4l-utils, python3-gi, websockets"
|
|
log "- ROS bridge: ROS2 Humble, FastAPI requirements, Autoware/Nav2 message packages"
|
|
log "- Docker module runtime: docker, nvidia-container-toolkit, or the device-specific GPU runtime"
|
|
}
|
|
|
|
handle_existing_install
|
|
collect_install_config
|
|
run_step "Install Tailscale if requested" install_tailscale
|
|
run_step "Install OpenSSH server if needed" install_openssh_server
|
|
run_step "Install Git if needed" install_git
|
|
run_step "Install Bun if needed" install_bun
|
|
run_step "Prepare sources" prepare_sources
|
|
run_step "Install dependencies" install_dependencies
|
|
run_step "Write agent config" write_agent_config_from_env
|
|
run_step "Install systemd service" write_service
|
|
print_host_notes
|