308 lines
11 KiB
Bash
308 lines
11 KiB
Bash
#!/usr/bin/env bash
|
|
#
|
|
# Installe (ou met à jour) l'agent Stream Control sur une VM Linux.
|
|
#
|
|
# curl -fsSL https://git.jeanbonapp.com/api/packages/jeanbon/generic/stream-control-agent/latest/install-agent.sh \
|
|
# | sudo bash -s -- \
|
|
# --registry https://git.jeanbonapp.com \
|
|
# --server ws://<IP_DU_CONTROLE>:8080/ws/agent \
|
|
# --token <JETON_ENROLEMENT>
|
|
#
|
|
# Réexécuter le script met à jour le binaire sans toucher à l'identité d'un
|
|
# agent déjà enrôlé.
|
|
|
|
set -euo pipefail
|
|
|
|
REGISTRY=""
|
|
OWNER="jeanbon"
|
|
VERSION="latest"
|
|
PACKAGE_NAME="stream-control-agent"
|
|
PACKAGE_TOKEN=""
|
|
|
|
# Vide = option non fournie. Les valeurs par défaut ne s'appliquent qu'à une
|
|
# première installation ; sur une mise à jour, seules les options réellement
|
|
# passées écrasent la configuration existante.
|
|
SERVER_URL=""
|
|
TOKEN=""
|
|
AGENT_NAME=""
|
|
OBS_HOST=""
|
|
OBS_PORT=""
|
|
OBS_PASSWORD=""
|
|
OBS_PASSWORD_SET=false # un mot de passe vide est une valeur légitime
|
|
RESET_IDENTITY=false
|
|
|
|
RUN_USER=""
|
|
INSTALL_DIR="/opt/stream-control-agent"
|
|
SERVICE="stream-control-agent"
|
|
|
|
die() { echo "✗ $*" >&2; exit 1; }
|
|
info() { echo "· $*"; }
|
|
|
|
usage() {
|
|
cat <<'EOF'
|
|
Installe (ou met à jour) l'agent Stream Control sur une VM Linux.
|
|
|
|
curl -fsSL https://git.jeanbonapp.com/api/packages/jeanbon/generic/stream-control-agent/latest/install-agent.sh \
|
|
| sudo bash -s -- \
|
|
--registry https://git.jeanbonapp.com \
|
|
--server ws://<IP_DU_CONTROLE>:8080/ws/agent \
|
|
--token <JETON_ENROLEMENT>
|
|
|
|
Réexécuter le script met à jour le binaire et applique les options fournies.
|
|
L'identité de l'agent (jeton permanent, agentId) est préservée : sans cela il
|
|
réapparaîtrait en double dans le dashboard.
|
|
|
|
Options :
|
|
--registry URL Base de l'instance Gitea (obligatoire)
|
|
--server URL WebSocket du plan de contrôle (obligatoire à la 1re install)
|
|
--token JETON Jeton d'enrôlement (obligatoire à la 1re install)
|
|
--name NOM Nom affiché dans le dashboard (défaut : hostname)
|
|
--user UTILISATEUR Compte exécutant l'agent (défaut : l'utilisateur sudo)
|
|
--obs-host HOTE Défaut 127.0.0.1
|
|
--obs-port PORT Défaut 4455
|
|
--obs-password MDP Mot de passe obs-websocket
|
|
--reset-identity Réenrôler avec le --token fourni (efface l'identité
|
|
existante — crée un nouvel agent dans le dashboard)
|
|
--version VERSION Version du paquet à installer (défaut : latest)
|
|
--owner PROPRIETAIRE Propriétaire du paquet Gitea (défaut : jeanbon)
|
|
--package-token JETON Jeton de lecture si le paquet est privé
|
|
--help
|
|
EOF
|
|
exit 0
|
|
}
|
|
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--registry) REGISTRY="${2:?}"; shift 2 ;;
|
|
--server) SERVER_URL="${2:?}"; shift 2 ;;
|
|
--token) TOKEN="${2:?}"; shift 2 ;;
|
|
--name) AGENT_NAME="${2:?}"; shift 2 ;;
|
|
--user) RUN_USER="${2:?}"; shift 2 ;;
|
|
--obs-host) OBS_HOST="${2:?}"; shift 2 ;;
|
|
--obs-port) OBS_PORT="${2:?}"; shift 2 ;;
|
|
--obs-password) OBS_PASSWORD="${2-}"; OBS_PASSWORD_SET=true; shift 2 ;;
|
|
--reset-identity) RESET_IDENTITY=true; shift ;;
|
|
--version) VERSION="${2:?}"; shift 2 ;;
|
|
--owner) OWNER="${2:?}"; shift 2 ;;
|
|
--package-token) PACKAGE_TOKEN="${2:?}"; shift 2 ;;
|
|
--help|-h) usage ;;
|
|
*) die "Option inconnue : $1 (--help pour l'aide)" ;;
|
|
esac
|
|
done
|
|
|
|
[ "$(id -u)" -eq 0 ] || die "À exécuter en root (sudo)."
|
|
[ -n "$REGISTRY" ] || die "--registry est obligatoire."
|
|
REGISTRY="${REGISTRY%/}"
|
|
|
|
# L'agent doit tourner sous le compte qui ouvre la session graphique d'OBS :
|
|
# c'est ce qui lui donne accès à l'affichage pour le rappel plein écran.
|
|
if [ -z "$RUN_USER" ]; then
|
|
RUN_USER="${SUDO_USER:-root}"
|
|
fi
|
|
id "$RUN_USER" >/dev/null 2>&1 || die "L'utilisateur « $RUN_USER » n'existe pas."
|
|
|
|
CONFIG_FILE="$INSTALL_DIR/agent.config.json"
|
|
IS_UPGRADE=false
|
|
[ -f "$CONFIG_FILE" ] && IS_UPGRADE=true
|
|
|
|
if [ "$IS_UPGRADE" = false ]; then
|
|
[ -n "$SERVER_URL" ] || die "--server est obligatoire pour une première installation."
|
|
[ -n "$TOKEN" ] || die "--token est obligatoire pour une première installation."
|
|
# Les défauts ne s'appliquent qu'ici : sur une mise à jour, une option absente
|
|
# doit laisser la valeur existante intacte, pas la remplacer par un défaut.
|
|
[ -n "$AGENT_NAME" ] || AGENT_NAME="$(hostname)"
|
|
[ -n "$OBS_HOST" ] || OBS_HOST="127.0.0.1"
|
|
[ -n "$OBS_PORT" ] || OBS_PORT="4455"
|
|
fi
|
|
|
|
echo
|
|
echo "Installation de l'agent Stream Control"
|
|
echo " utilisateur : $RUN_USER"
|
|
echo " destination : $INSTALL_DIR"
|
|
echo " version : $VERSION"
|
|
echo
|
|
|
|
# --- Node.js ----------------------------------------------------------------
|
|
NODE_MAJOR=0
|
|
if command -v node >/dev/null 2>&1; then
|
|
NODE_MAJOR="$(node -p 'process.versions.node.split(".")[0]' 2>/dev/null || echo 0)"
|
|
fi
|
|
|
|
if [ "$NODE_MAJOR" -lt 22 ]; then
|
|
info "Node.js 22+ requis (trouvé : ${NODE_MAJOR:-aucun}) — installation depuis NodeSource"
|
|
command -v apt-get >/dev/null 2>&1 \
|
|
|| die "Distribution non gérée automatiquement : installe Node.js 22+ puis relance."
|
|
curl -fsSL https://deb.nodesource.com/setup_24.x | bash -
|
|
apt-get install -y nodejs
|
|
else
|
|
info "Node.js $(node -v) présent"
|
|
fi
|
|
|
|
# --- xdotool (rappel plein écran) -------------------------------------------
|
|
if ! command -v xdotool >/dev/null 2>&1; then
|
|
if command -v apt-get >/dev/null 2>&1; then
|
|
info "Installation de xdotool (rappel plein écran)"
|
|
apt-get install -y xdotool >/dev/null
|
|
else
|
|
echo "! xdotool absent : le rappel plein écran sera indisponible."
|
|
fi
|
|
fi
|
|
|
|
# --- Téléchargement du bundle -----------------------------------------------
|
|
URL="$REGISTRY/api/packages/$OWNER/generic/$PACKAGE_NAME/$VERSION/agent.cjs"
|
|
info "Téléchargement depuis $URL"
|
|
|
|
mkdir -p "$INSTALL_DIR"
|
|
CURL_AUTH=()
|
|
[ -n "$PACKAGE_TOKEN" ] && CURL_AUTH=(--header "Authorization: token $PACKAGE_TOKEN")
|
|
|
|
# Le fichier temporaire doit garder l'extension .cjs : `node --check` déduit le
|
|
# format du module de l'extension et rejette tout ce qu'il ne reconnaît pas
|
|
# (« Unknown file extension .tmp »), quel que soit le contenu.
|
|
TMP_FILE="$INSTALL_DIR/agent-new.cjs"
|
|
|
|
curl -fsSL "${CURL_AUTH[@]}" -o "$TMP_FILE" "$URL" \
|
|
|| die "Téléchargement impossible. Vérifie --registry, --version, et --package-token si le paquet est privé."
|
|
|
|
if ! CHECK_ERR="$(node --check "$TMP_FILE" 2>&1)"; then
|
|
echo "$CHECK_ERR" >&2
|
|
echo " (fichier conservé pour inspection : $TMP_FILE)" >&2
|
|
die "Le fichier téléchargé n'est pas un script Node valide — voir l'erreur ci-dessus."
|
|
fi
|
|
|
|
mv "$TMP_FILE" "$INSTALL_DIR/agent.cjs"
|
|
|
|
# --- Configuration ----------------------------------------------------------
|
|
if [ "$IS_UPGRADE" = true ]; then
|
|
# Fusion, et non conservation en bloc : l'identité (jeton, agentId) est
|
|
# préservée — l'écraser ferait réapparaître l'agent en double dans le
|
|
# dashboard — mais toute option explicitement passée doit s'appliquer.
|
|
RESET_FLAG=0
|
|
[ "$RESET_IDENTITY" = true ] && RESET_FLAG=1
|
|
PW_FLAG=0
|
|
[ "$OBS_PASSWORD_SET" = true ] && PW_FLAG=1
|
|
|
|
node -e '
|
|
const fs = require("node:fs");
|
|
const a = process.argv.slice(1);
|
|
const [file, serverUrl, name, obsHost, obsPort, pwFlag, obsPassword, resetFlag, token, packageUrl] = a;
|
|
|
|
const current = JSON.parse(fs.readFileSync(file, "utf8").replace(/^\uFEFF/, ""));
|
|
const next = { ...current, obs: { ...(current.obs || {}) } };
|
|
const applied = [];
|
|
|
|
// Toujours rafraichie : infrastructure et non identite. Sans elle, un agent
|
|
// ne saurait pas ou chercher sa propre mise a jour.
|
|
// (Pas d apostrophe dans ce bloc : il vit dans une chaine bash entre quotes.)
|
|
if (packageUrl && packageUrl !== current.packageUrl) {
|
|
next.packageUrl = packageUrl;
|
|
applied.push("packageUrl");
|
|
}
|
|
if (serverUrl) { next.serverUrl = serverUrl; applied.push("serverUrl"); }
|
|
if (name) { next.name = name; applied.push("name"); }
|
|
if (obsHost) { next.obs.host = obsHost; applied.push("obs.host"); }
|
|
if (obsPort) { next.obs.port = Number(obsPort); applied.push("obs.port"); }
|
|
if (pwFlag === "1") { next.obs.password = obsPassword; applied.push("obs.password"); }
|
|
|
|
if (resetFlag === "1" && token) {
|
|
next.token = token;
|
|
delete next.agentId;
|
|
applied.push("identité réinitialisée");
|
|
} else if (token && token !== current.token) {
|
|
console.log(" (--token ignoré : identité conservée. Utilise --reset-identity pour forcer.)");
|
|
}
|
|
|
|
fs.writeFileSync(file, JSON.stringify(next, null, 2) + "\n");
|
|
console.log(applied.length
|
|
? " champs mis à jour : " + applied.join(", ")
|
|
: " aucun changement demandé");
|
|
' "$CONFIG_FILE" "$SERVER_URL" "$AGENT_NAME" "$OBS_HOST" "$OBS_PORT" \
|
|
"$PW_FLAG" "$OBS_PASSWORD" "$RESET_FLAG" "$TOKEN" "$URL" \
|
|
|| die "Fusion de la configuration existante impossible ($CONFIG_FILE)."
|
|
|
|
info "Configuration mise à jour, identité de l'agent préservée"
|
|
else
|
|
umask 077
|
|
cat > "$CONFIG_FILE" <<EOF
|
|
{
|
|
"serverUrl": "$SERVER_URL",
|
|
"token": "$TOKEN",
|
|
"name": "$AGENT_NAME",
|
|
"packageUrl": "$URL",
|
|
"obs": {
|
|
"host": "$OBS_HOST",
|
|
"port": $OBS_PORT,
|
|
"password": "$OBS_PASSWORD"
|
|
}
|
|
}
|
|
EOF
|
|
info "Configuration écrite dans $CONFIG_FILE"
|
|
fi
|
|
|
|
chown -R "$RUN_USER" "$INSTALL_DIR"
|
|
chmod 600 "$CONFIG_FILE"
|
|
|
|
# --- Service systemd --------------------------------------------------------
|
|
USER_HOME="$(getent passwd "$RUN_USER" | cut -d: -f6)"
|
|
|
|
cat > "/etc/systemd/system/$SERVICE.service" <<EOF
|
|
[Unit]
|
|
Description=Stream Control — agent OBS
|
|
After=network-online.target
|
|
Wants=network-online.target
|
|
|
|
[Service]
|
|
Type=simple
|
|
User=$RUN_USER
|
|
WorkingDirectory=$INSTALL_DIR
|
|
Environment=NODE_ENV=production
|
|
Environment=AGENT_CONFIG=$CONFIG_FILE
|
|
# Accès à la session graphique, nécessaire au rappel plein écran via xdotool.
|
|
Environment=DISPLAY=:0
|
|
Environment=XAUTHORITY=$USER_HOME/.Xauthority
|
|
ExecStart=$(command -v node) $INSTALL_DIR/agent.cjs
|
|
Restart=always
|
|
RestartSec=5
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
EOF
|
|
|
|
systemctl daemon-reload
|
|
systemctl enable "$SERVICE" >/dev/null 2>&1 || true
|
|
systemctl restart "$SERVICE"
|
|
|
|
# --- Vérification -----------------------------------------------------------
|
|
echo
|
|
# Mêmes variables que l'unité systemd, sinon le diagnostic signale un DISPLAY
|
|
# absent que le service, lui, aura bien.
|
|
CHECK_OK=true
|
|
sudo -u "$RUN_USER" \
|
|
AGENT_CONFIG="$CONFIG_FILE" \
|
|
DISPLAY=":0" \
|
|
XAUTHORITY="$USER_HOME/.Xauthority" \
|
|
"$(command -v node)" "$INSTALL_DIR/agent.cjs" --check || CHECK_OK=false
|
|
|
|
sleep 2
|
|
if systemctl is-active --quiet "$SERVICE"; then
|
|
if [ "$CHECK_OK" = true ]; then
|
|
echo "✓ Agent installé et démarré."
|
|
else
|
|
echo "✓ Agent installé et démarré — mais le diagnostic ci-dessus signale un"
|
|
echo " problème bloquant. L'agent retentera en boucle jusqu'à sa résolution ;"
|
|
echo " il n'apparaîtra pas dans le dashboard d'ici là."
|
|
fi
|
|
else
|
|
echo "✗ Le service n'est pas actif. Journal :"
|
|
journalctl -u "$SERVICE" -n 20 --no-pager || true
|
|
exit 1
|
|
fi
|
|
|
|
cat <<EOF
|
|
|
|
état : systemctl status $SERVICE
|
|
journal: journalctl -u $SERVICE -f
|
|
test : sudo -u $RUN_USER node $INSTALL_DIR/agent.cjs --check
|
|
|
|
EOF
|