El instalador solo necesitaba root por dos razones circunstanciales: el PREFIX por omisión en /opt y el unit en /etc/systemd/system. El agente en sí no lo necesita — su unit corre como el usuario que instala, y las rutas de data_folder las escribe SQL Server vía las cláusulas MOVE del T-SQL, no el agente. --user-service instala donde apunte PREFIX (escribible por el usuario, típicamente bajo su home) y registra el unit en ~/.config/systemd/user/. Para que sobreviva al cierre de sesión intenta habilitar lingering; si el destino no lo permite, cae a una entrada @reboot en el crontab del usuario más un vigilante cada 5 min que cubre lo que en systemd hace Restart=always. Ninguno de los dos mecanismos requiere root. Con esto, el instalador remoto del panel puede actualizar un servidor cuya cuenta SSH no es root ni tiene sudo sin contraseña, sin pasarle nunca la contraseña a sudo. Detalles que costaron una prueba cada uno: - `pgrep -f "$PREFIX/$BIN_NAME"` se auto-detectaba: la línea de cron del vigilante contiene esa ruta, así que el `sh -c` que la ejecuta hacía match consigo mismo y el vigilante nunca rearrancaba. Va anclado con `^`. - El sed de la plantilla sustituía los marcadores dentro de su propio comentario, dejando rutas absolutas en un texto sin sentido. - El bootstrap de 20 s era el único hijo que heredaba el stdin del canal SSH; ahora lleva `</dev/null`, lo que hace estructural que no pueda consumir nada de él. Reinstalar es idempotente: no duplica entradas de cron.
325 lines
14 KiB
Bash
Executable File
325 lines
14 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Instalador Linux de CloudRestoreAS.
|
|
#
|
|
# NO instala ni descarga NADA del sistema: el binario es 100% autocontenido (Qt/xcb,
|
|
# driver ODBC + Kerberos/OpenSSL, y 7-Zip van embebidos). Este script solo coloca el
|
|
# binario, marca permisos, hace el bootstrap de config/, opcionalmente siembra las
|
|
# credenciales del PANEL, y registra el arranque automático (servicio systemd headless
|
|
# o autostart de escritorio).
|
|
#
|
|
# Uso:
|
|
# sudo ./install.sh --service # 24/7 headless vía systemd (recomendado en servidor)
|
|
# ./install.sh --user-service # 24/7 headless SIN root: unit de systemd de usuario
|
|
# ./install.sh --desktop # autostart de escritorio (requiere sesión gráfica)
|
|
# ./install.sh # solo instala; sin arranque automático
|
|
#
|
|
# --user-service no necesita privilegios: instala donde apunte PREFIX (que debe ser escribible
|
|
# por el usuario, típicamente bajo su home), registra el unit en ~/.config/systemd/user/ y lo
|
|
# arranca con `systemctl --user`. Para que sobreviva al cierre de sesión intenta habilitar
|
|
# lingering; si el destino no lo permite, cae a una entrada @reboot en el crontab del usuario
|
|
# más un vigilante que lo rearranca. Es la vía cuando la cuenta SSH no es root ni tiene sudo
|
|
# sin contraseña: al panel le bastan entonces el usuario y la contraseña que ya tiene guardados.
|
|
#
|
|
# Opciones:
|
|
# --panel-env-file <ruta> # archivo KEY=valor con CLOUDRESTORE_PANEL_* que se
|
|
# # fusiona en config/.env y se borra al terminar.
|
|
# # Lo usa el instalador remoto del PANEL para dejar
|
|
# # el servidor configurado sin intervención.
|
|
#
|
|
# Variables:
|
|
# PREFIX=/opt/cloudrestoreas # carpeta destino (default)
|
|
# SERVICE_USER=<usuario> # usuario del servicio systemd (default: quien invoca)
|
|
set -euo pipefail
|
|
|
|
MODE="none"
|
|
PANEL_ENV_FILE=""
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--service) MODE="service"; shift ;;
|
|
--user-service) MODE="user-service"; shift ;;
|
|
--desktop) MODE="desktop"; shift ;;
|
|
--panel-env-file)
|
|
[[ $# -ge 2 ]] || { echo "ERROR: --panel-env-file requiere una ruta" >&2; exit 2; }
|
|
PANEL_ENV_FILE="$2"; shift 2 ;;
|
|
--panel-env-file=*) PANEL_ENV_FILE="${1#*=}"; shift ;;
|
|
-h|--help)
|
|
grep '^#' "$0" | grep -v '^#!' | sed 's/^# \{0,1\}//'; exit 0 ;;
|
|
*) echo "Opción desconocida: $1" >&2; exit 2 ;;
|
|
esac
|
|
done
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
PREFIX="${PREFIX:-/opt/cloudrestoreas}"
|
|
BIN_NAME="CloudRestoreAS"
|
|
UNIT_NAME="cloudrestoreas.service"
|
|
# Patrón para localizar el agente vivo. El ancla ^ NO es opcional: sin ella, la propia
|
|
# línea de cron del vigilante contiene esta ruta, así que el `sh -c` que la ejecuta hace
|
|
# match consigo mismo, el vigilante cree que el agente corre y no lo rearranca nunca.
|
|
PGREP_PAT="^$PREFIX/$BIN_NAME"
|
|
|
|
# Localizar el binario: dist/CloudRestoreAS, dist/CloudRestoreAS-linux, o junto al script.
|
|
SRC=""
|
|
for cand in \
|
|
"$SCRIPT_DIR/dist/$BIN_NAME" \
|
|
"$SCRIPT_DIR/dist/${BIN_NAME}-linux" \
|
|
"$SCRIPT_DIR/$BIN_NAME" \
|
|
"$SCRIPT_DIR/${BIN_NAME}-linux"; do
|
|
if [[ -f "$cand" ]]; then SRC="$cand"; break; fi
|
|
done
|
|
if [[ -z "$SRC" ]]; then
|
|
echo "ERROR: no se encontró el binario ($BIN_NAME). Ejecuta ./build.sh primero" >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo "==============================================="
|
|
echo "CloudRestoreAS - Instalación Linux"
|
|
echo " Binario : $SRC"
|
|
echo " Destino : $PREFIX"
|
|
echo " Modo : $MODE"
|
|
echo "==============================================="
|
|
|
|
# --- Detener el servicio si está corriendo ------------------------------------
|
|
# Un ELF en ejecución no se puede sobrescribir (ETXTBSY), así que una actualización
|
|
# tiene que detenerlo antes de reemplazar el binario. Se recuerda si estaba activo
|
|
# para volver a levantarlo al final incluso en los modos que no tocan systemd.
|
|
WAS_ACTIVE=0
|
|
# En modo usuario el unit vive en la instancia de systemd del propio usuario, que necesita
|
|
# XDG_RUNTIME_DIR: un `exec` de SSH no es una sesión de login y no siempre lo trae.
|
|
export XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}"
|
|
SYSTEMCTL_USER=(systemctl --user)
|
|
if command -v systemctl >/dev/null 2>&1; then
|
|
if [[ "$MODE" == "user-service" ]]; then
|
|
if "${SYSTEMCTL_USER[@]}" is-active --quiet "$UNIT_NAME" 2>/dev/null; then
|
|
WAS_ACTIVE=1
|
|
echo "Deteniendo $UNIT_NAME (usuario) para reemplazar el binario..."
|
|
"${SYSTEMCTL_USER[@]}" stop "$UNIT_NAME" >/dev/null 2>&1 || \
|
|
echo "AVISO: no se pudo detener $UNIT_NAME; la copia puede fallar." >&2
|
|
fi
|
|
# Sin systemd de usuario disponible el arranque pudo haber quedado por cron: el binario
|
|
# sigue vivo y sobrescribirlo daría ETXTBSY, así que se termina igual.
|
|
if [[ "$WAS_ACTIVE" -eq 0 ]] && pgrep -f "$PGREP_PAT" >/dev/null 2>&1; then
|
|
WAS_ACTIVE=1
|
|
echo "Terminando el proceso en curso para reemplazar el binario..."
|
|
pkill -f "$PGREP_PAT" >/dev/null 2>&1 || true
|
|
sleep 2
|
|
fi
|
|
elif systemctl is-active --quiet "$UNIT_NAME" 2>/dev/null; then
|
|
WAS_ACTIVE=1
|
|
echo "Deteniendo $UNIT_NAME para reemplazar el binario..."
|
|
systemctl stop "$UNIT_NAME" >/dev/null 2>&1 || \
|
|
echo "AVISO: no se pudo detener $UNIT_NAME (¿falta sudo?); la copia puede fallar." >&2
|
|
fi
|
|
fi
|
|
|
|
mkdir -p "$PREFIX"
|
|
if ! install -m 0755 "$SRC" "$PREFIX/$BIN_NAME"; then
|
|
echo "ERROR: no se pudo instalar el binario en $PREFIX/$BIN_NAME" >&2
|
|
echo " Si el archivo está en uso, detén el proceso y reintenta:" >&2
|
|
echo " sudo systemctl stop $UNIT_NAME # o mata el proceso de escritorio" >&2
|
|
exit 1
|
|
fi
|
|
echo "Binario instalado en $PREFIX/$BIN_NAME"
|
|
|
|
# Bootstrap de config/: el propio binario crea config/, .env y carpetas de trabajo
|
|
# al inicio de su arranque. Se corre una vez en modo offscreen (sin display) y se
|
|
# corta con timeout; así el usuario ya puede editar config/.env antes de habilitar
|
|
# el servicio.
|
|
echo "Inicializando config/ (bootstrap)..."
|
|
# `</dev/null` no es cosmético: es el único hijo que heredaría el stdin del canal SSH cuando el
|
|
# instalador corre en remoto. Cerrárselo hace estructural —y no accidental— que nada de lo que
|
|
# venga por ese canal pueda ser consumido aquí.
|
|
( cd "$PREFIX" && QT_QPA_PLATFORM=offscreen timeout 20 "$PREFIX/$BIN_NAME" --headless </dev/null >/dev/null 2>&1 || true )
|
|
if [[ -f "$PREFIX/config/.env" ]]; then
|
|
echo "config/.env creado."
|
|
else
|
|
echo "NOTA: config/.env se creará en la primera ejecución."
|
|
fi
|
|
|
|
# --- Siembra de credenciales del PANEL ----------------------------------------
|
|
# Fusión replace-or-append: respeta el resto de config/.env y es idempotente, así que
|
|
# reinstalar no duplica claves ni pierde ajustes locales. Se escribe vía archivo y no
|
|
# por argumentos para que el token no quede visible en `ps` ni en el historial.
|
|
merge_env_keys() {
|
|
local target="$1"; shift
|
|
local -a pairs=("$@")
|
|
local tmp line key pair matched
|
|
|
|
tmp="$(mktemp)"
|
|
if [[ -f "$target" ]]; then
|
|
while IFS= read -r line || [[ -n "$line" ]]; do
|
|
matched=""
|
|
for pair in "${pairs[@]}"; do
|
|
key="${pair%%=*}"
|
|
if [[ "$line" =~ ^[[:space:]]*"$key"[[:space:]]*= ]]; then
|
|
matched="$pair"
|
|
break
|
|
fi
|
|
done
|
|
if [[ -n "$matched" ]]; then
|
|
printf '%s\n' "$matched" >> "$tmp"
|
|
else
|
|
printf '%s\n' "$line" >> "$tmp"
|
|
fi
|
|
done < "$target"
|
|
fi
|
|
|
|
for pair in "${pairs[@]}"; do
|
|
key="${pair%%=*}"
|
|
if ! grep -qE "^[[:space:]]*${key}[[:space:]]*=" "$tmp" 2>/dev/null; then
|
|
printf '%s\n' "$pair" >> "$tmp"
|
|
fi
|
|
done
|
|
|
|
# cat > preserva el inodo y los permisos 0600 del .env; mv los reemplazaría.
|
|
cat "$tmp" > "$target"
|
|
rm -f "$tmp"
|
|
}
|
|
|
|
if [[ -n "$PANEL_ENV_FILE" ]]; then
|
|
if [[ ! -f "$PANEL_ENV_FILE" ]]; then
|
|
echo "ERROR: no existe el archivo indicado en --panel-env-file: $PANEL_ENV_FILE" >&2
|
|
exit 1
|
|
fi
|
|
|
|
ENV_PATH="$PREFIX/config/.env"
|
|
touch "$ENV_PATH"
|
|
chmod 0600 "$ENV_PATH" 2>/dev/null || true
|
|
|
|
# Lista blanca: el archivo llega por la red y no debe poder inyectar otras claves.
|
|
ALLOWED_KEYS="CLOUDRESTORE_PANEL_API_URL CLOUDRESTORE_PANEL_API_TOKEN"
|
|
ALLOWED_KEYS="$ALLOWED_KEYS CLOUDRESTORE_PANEL_INSTANCE_KEY CLOUDRESTORE_PANEL_VERIFY_SSL"
|
|
|
|
PAIRS=()
|
|
while IFS= read -r raw || [[ -n "$raw" ]]; do
|
|
raw="${raw%$'\r'}" # tolera CRLF si vino de Windows
|
|
raw="${raw#"${raw%%[![:space:]]*}"}" # recorta espacios a la izquierda
|
|
if [[ -z "$raw" || "${raw:0:1}" == "#" || "$raw" != *=* ]]; then
|
|
continue
|
|
fi
|
|
key="${raw%%=*}"
|
|
key="${key//[[:space:]]/}"
|
|
value="${raw#*=}"
|
|
for allowed in $ALLOWED_KEYS; do
|
|
if [[ "$key" == "$allowed" ]]; then
|
|
PAIRS+=("${key}=${value}")
|
|
break
|
|
fi
|
|
done
|
|
done < "$PANEL_ENV_FILE"
|
|
|
|
if [[ "${#PAIRS[@]}" -gt 0 ]]; then
|
|
merge_env_keys "$ENV_PATH" "${PAIRS[@]}"
|
|
echo "${#PAIRS[@]} clave(s) del PANEL escritas en config/.env"
|
|
else
|
|
echo "AVISO: --panel-env-file no traía claves CLOUDRESTORE_PANEL_* válidas." >&2
|
|
fi
|
|
|
|
# El archivo trae el token en claro: se borra en cuanto se consumió.
|
|
rm -f "$PANEL_ENV_FILE"
|
|
fi
|
|
|
|
case "$MODE" in
|
|
service)
|
|
SERVICE_USER="${SERVICE_USER:-${SUDO_USER:-$(id -un)}}"
|
|
UNIT_SRC="$SCRIPT_DIR/packaging/linux/$UNIT_NAME"
|
|
UNIT_DST="/etc/systemd/system/$UNIT_NAME"
|
|
if [[ ! -w "$(dirname "$UNIT_DST")" ]]; then
|
|
echo "ERROR: se requiere sudo para instalar el servicio systemd" >&2
|
|
exit 1
|
|
fi
|
|
sed -e "s|__USER__|$SERVICE_USER|g" \
|
|
-e "s|__WORKDIR__|$PREFIX|g" \
|
|
-e "s|__EXEC__|$PREFIX/$BIN_NAME|g" \
|
|
"$UNIT_SRC" > "$UNIT_DST"
|
|
systemctl daemon-reload
|
|
systemctl enable --now "$UNIT_NAME"
|
|
echo "Servicio systemd instalado y arrancado (usuario: $SERVICE_USER)."
|
|
echo " Estado : systemctl status cloudrestoreas"
|
|
echo " Logs : journalctl -u cloudrestoreas -f"
|
|
;;
|
|
user-service)
|
|
# Sin root: unit en el home y arranque con la instancia de systemd del propio usuario.
|
|
# Requisito previo: PREFIX escribible por este usuario (el panel lo apunta a su home).
|
|
UNIT_SRC="$SCRIPT_DIR/packaging/linux/cloudrestoreas-user.service"
|
|
UNIT_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user"
|
|
UNIT_DST="$UNIT_DIR/$UNIT_NAME"
|
|
if [[ ! -f "$UNIT_SRC" ]]; then
|
|
echo "ERROR: falta la plantilla del unit de usuario ($UNIT_SRC)" >&2
|
|
exit 1
|
|
fi
|
|
mkdir -p "$UNIT_DIR"
|
|
# Sin __USER__: un unit de usuario no admite User=, ya corre como su dueño.
|
|
sed -e "s|__WORKDIR__|$PREFIX|g" \
|
|
-e "s|__EXEC__|$PREFIX/$BIN_NAME|g" \
|
|
"$UNIT_SRC" > "$UNIT_DST"
|
|
|
|
STARTED=""
|
|
if "${SYSTEMCTL_USER[@]}" daemon-reload >/dev/null 2>&1 && \
|
|
"${SYSTEMCTL_USER[@]}" enable --now "$UNIT_NAME" >/dev/null 2>&1; then
|
|
STARTED="systemd-usuario"
|
|
# Sin lingering, la instancia de usuario muere al cerrar la última sesión y con ella el
|
|
# restaurador. Muchos polkit permiten habilitarlo para uno mismo; si no, no es fatal:
|
|
# se complementa con cron más abajo.
|
|
if loginctl enable-linger "$(id -un)" >/dev/null 2>&1; then
|
|
echo "Lingering habilitado: el servicio sobrevive al cierre de sesión."
|
|
else
|
|
echo "AVISO: no se pudo habilitar lingering; se agrega respaldo por cron." >&2
|
|
STARTED="systemd-usuario+cron"
|
|
fi
|
|
else
|
|
echo "AVISO: systemd de usuario no disponible; se usará cron." >&2
|
|
fi
|
|
|
|
# Respaldo (o sustituto) por cron: @reboot para el arranque y un vigilante cada 5 min que
|
|
# cubre lo que en systemd hace Restart=always. El crontab del usuario no requiere root.
|
|
if [[ "$STARTED" != "systemd-usuario" ]]; then
|
|
if command -v crontab >/dev/null 2>&1; then
|
|
CRON_CMD="cd $PREFIX && QT_QPA_PLATFORM=offscreen $PREFIX/$BIN_NAME --start-engine --headless >/dev/null 2>&1 &"
|
|
CRON_TAG="# cloudrestoreas-autostart"
|
|
# Se reescribe el crontab completo filtrando las entradas propias anteriores, para que
|
|
# reinstalar no acumule duplicados.
|
|
{
|
|
crontab -l 2>/dev/null | grep -v "$CRON_TAG" || true
|
|
echo "@reboot $CRON_CMD $CRON_TAG"
|
|
echo "*/5 * * * * pgrep -f '$PGREP_PAT' >/dev/null || ($CRON_CMD) $CRON_TAG"
|
|
} | crontab -
|
|
STARTED="${STARTED:+$STARTED+}cron"
|
|
# El vigilante tarda hasta 5 min en actuar; se arranca ya para no dejar el hueco.
|
|
if ! pgrep -f "$PGREP_PAT" >/dev/null 2>&1; then
|
|
( cd "$PREFIX" && QT_QPA_PLATFORM=offscreen "$PREFIX/$BIN_NAME" --start-engine --headless </dev/null >/dev/null 2>&1 & )
|
|
fi
|
|
else
|
|
echo "ERROR: sin systemd de usuario y sin crontab no hay forma de dejarlo arrancado." >&2
|
|
echo " Instala cron, habilita lingering, o usa --service con privilegios." >&2
|
|
exit 1
|
|
fi
|
|
fi
|
|
echo "Servicio de usuario instalado y arrancado (mecanismo: $STARTED)."
|
|
echo " Estado : systemctl --user status cloudrestoreas"
|
|
echo " Logs : journalctl --user -u cloudrestoreas -f"
|
|
;;
|
|
desktop)
|
|
echo "Modo escritorio: la app registra su autostart .desktop al iniciarse."
|
|
echo "Ejecuta '$PREFIX/$BIN_NAME' en tu sesión gráfica."
|
|
;;
|
|
none)
|
|
echo "Instalación sin arranque automático."
|
|
echo "Ejecuta: QT_QPA_PLATFORM=offscreen $PREFIX/$BIN_NAME --start-engine --headless"
|
|
;;
|
|
esac
|
|
|
|
# Si se detuvo un servicio que estaba activo y el modo elegido no lo relevanta, se
|
|
# restaura: una actualización no debe dejar el restaurador apagado en silencio.
|
|
if [[ "$WAS_ACTIVE" -eq 1 && "$MODE" != "service" && "$MODE" != "user-service" ]]; then
|
|
echo "Reiniciando $UNIT_NAME (estaba activo antes de la actualización)..."
|
|
systemctl start "$UNIT_NAME" >/dev/null 2>&1 || \
|
|
echo "AVISO: no se pudo reiniciar $UNIT_NAME; hazlo a mano." >&2
|
|
fi
|
|
|
|
echo ""
|
|
echo "Siguiente paso: edita $PREFIX/config/.env (CLOUDRESTORE_PANEL_*) y reinicia."
|
|
[[ "$MODE" == "service" ]] && echo " Tras editar: sudo systemctl restart cloudrestoreas"
|
|
[[ "$MODE" == "user-service" ]] && echo " Tras editar: systemctl --user restart cloudrestoreas"
|
|
echo "Listo."
|