commit 5dd160472dc6aa596dd82f9356d62b22e2c9fdf9 Author: tecnotel Date: Sat Sep 12 10:37:06 2026 +0200 iride-setup 0.1.0 - bootstrap, install, first_setup, health check diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e232440 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.tar.gz +.DS_Store +__pycache__/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..18e46c3 --- /dev/null +++ b/README.md @@ -0,0 +1,44 @@ +# iride-setup + +Bootstrap e primo install di un'istanza **IRIDE** (Tecnotel Servizi SRL), sullo +schema di `argos-setup`. Il repository applicativo è `iride`; gli aggiornamenti +successivi li fa `iride/scripts/update.sh`. + +## Install su un server Ubuntu LTS pulito + +```text +curl -fsSL -H "Authorization: token " \ + https://repo.argosdefense.io/tecnotel/iride-setup/raw/branch/main/bootstrap.sh \ + | sudo bash -s -- --token +``` + +`` è un token Gitea di **sola lettura dedicato all'istanza** (uno per +cliente, revocabile), mai un token personale. Resta in +`/opt/iride/config/git-credentials` (0600, utente `iride`) e serve a `update.sh`. + +Non interattivo (VM di test, CI): + +```text +... | sudo bash -s -- --token --client "Nome Cliente" --domain iride.cliente.it --admin-password '' +``` + +## Cosa fa + +| Passo | Script | Esito | +|---|---|---| +| pacchetti, utente `iride`, `/opt/iride/*`, credenziali git | `bootstrap.sh` | clona questo repo in `/opt/iride/setup` | +| clone di `iride` in `/opt/iride/app`, venv, dipendenze | `install.sh` | | +| configurazione da `config/*.example` + wizard | `first_setup.py` | `iride.json`, `users.json` (0600) | +| migrazioni, build frontend (se presente), unit systemd, nginx + certificato | `install.sh` | servizi abilitati e avviati | +| verifica | `checks/health.sh` | exit 1 se qualcosa non va | + +Layout risultante: `/opt/iride/{app,config,data,logs,backups,certs,setup}`. + +## Note + +- Il certificato generato è self-signed: **WhatsApp richiede un certificato + valido** (Let's Encrypt o del cliente) in `/opt/iride/certs/{fullchain,privkey}.pem`. +- Il wizard web (porta 8888, stile ARGOS) e la validazione della licenza + all'install arrivano dopo il pilota (B-055 li collauda su VM di test). +- Riesecuzione sicura: `bootstrap.sh` aggiorna il setup e rilancia `install.sh`, + che non tocca configurazione, utenti e certificati già presenti. diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..6e8bf73 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.1.0 diff --git a/bootstrap.sh b/bootstrap.sh new file mode 100755 index 0000000..6ee70a6 --- /dev/null +++ b/bootstrap.sh @@ -0,0 +1,73 @@ +#!/bin/bash +# ══════════════════════════════════════════════════════════════════════════════ +# IRIDE — Bootstrap installer (Tecnotel Servizi SRL) +# +# Uso su un server Ubuntu LTS pulito: +# +# curl -fsSL -H "Authorization: token " \ +# https://repo.argosdefense.io/tecnotel/iride-setup/raw/branch/main/bootstrap.sh \ +# | sudo bash -s -- --token +# +# = token Gitea di SOLA LETTURA dedicato all'istanza (mai personale). +# Opzioni: --gitea-url URL (default https://repo.argosdefense.io) +# --org ORG (default tecnotel) --branch BRANCH (default main) +# --setup-branch BRANCH (default main) +# --client NOME --domain FQDN --admin-password PWD (non interattivo) +# Riesecuzione sicura: aggiorna iride-setup e rilancia install.sh. +# ══════════════════════════════════════════════════════════════════════════════ +set -euo pipefail + +GITEA_URL="https://repo.argosdefense.io" +ORG="tecnotel" +BRANCH="main" +SETUP_BRANCH="main" +TOKEN="" +PASSTHROUGH=() + +while [[ $# -gt 0 ]]; do + case "$1" in + --token) TOKEN="$2"; shift 2 ;; + --gitea-url) GITEA_URL="$2"; shift 2 ;; + --org) ORG="$2"; shift 2 ;; + --branch) BRANCH="$2"; shift 2 ;; + --setup-branch) SETUP_BRANCH="$2"; shift 2 ;; + --client|--domain|--admin-password|--admin-user) PASSTHROUGH+=("$1" "$2"); shift 2 ;; + *) echo "Opzione sconosciuta: $1"; exit 1 ;; + esac +done + +[[ $EUID -eq 0 ]] || { echo "Eseguire con sudo"; exit 1; } +[[ -n "$TOKEN" ]] || { echo "Serve --token "; exit 1; } + +GITEA_HOST="${GITEA_URL#https://}"; GITEA_HOST="${GITEA_HOST#http://}"; GITEA_HOST="${GITEA_HOST%%/*}" +SETUP_URL="$GITEA_URL/$ORG/iride-setup.git" +APP_URL="$GITEA_URL/$ORG/iride.git" + +echo "IRIDE bootstrap — $GITEA_URL/$ORG (app: $BRANCH, setup: $SETUP_BRANCH)" +export DEBIAN_FRONTEND=noninteractive +apt-get update -qq +apt-get install -y -qq git curl ca-certificates >/dev/null + +if ! id iride >/dev/null 2>&1; then + useradd --system --home-dir /opt/iride --shell /usr/sbin/nologin iride +fi +mkdir -p /opt/iride/config /opt/iride/setup +chown iride:iride /opt/iride /opt/iride/config /opt/iride/setup +chmod 700 /opt/iride/config + +CRED="/opt/iride/config/git-credentials" +printf 'https://oauth2:%s@%s\n' "$TOKEN" "$GITEA_HOST" > "$CRED" +chown iride:iride "$CRED"; chmod 600 "$CRED" +sudo -u iride -H git config --global credential.helper "store --file=$CRED" +sudo -u iride -H git config --global safe.directory /opt/iride/setup +sudo -u iride -H git config --global safe.directory /opt/iride/app + +if [[ -d /opt/iride/setup/.git ]]; then + sudo -u iride -H git -C /opt/iride/setup fetch --quiet origin "$SETUP_BRANCH" + sudo -u iride -H git -C /opt/iride/setup checkout --quiet "$SETUP_BRANCH" + sudo -u iride -H git -C /opt/iride/setup merge --ff-only --quiet "origin/$SETUP_BRANCH" +else + sudo -u iride -H git clone --quiet --branch "$SETUP_BRANCH" "$SETUP_URL" /opt/iride/setup +fi + +exec bash /opt/iride/setup/install.sh --app-url "$APP_URL" --branch "$BRANCH" --gitea-host "$GITEA_HOST" "${PASSTHROUGH[@]}" diff --git a/checks/health.sh b/checks/health.sh new file mode 100755 index 0000000..4c40995 --- /dev/null +++ b/checks/health.sh @@ -0,0 +1,34 @@ +#!/bin/bash +# IRIDE — verifica post-install / post-update (Tecnotel Servizi SRL) +# Uso: bash /opt/iride/setup/checks/health.sh (exit 1 se qualcosa non va) +set -uo pipefail +RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m' +ok=0; ko=0 +pass() { echo -e " ${GREEN}✔${NC} $1"; ok=$((ok+1)); } +fail() { echo -e " ${RED}✘${NC} $1"; ko=$((ko+1)); } + +echo "IRIDE health check" +for svc in iride-api iride-worker iride-scheduler; do + systemctl is-active --quiet "$svc" && pass "$svc attivo" || fail "$svc non attivo (journalctl -u $svc -n 50)" +done +systemctl is-active --quiet nginx && pass "nginx attivo" || fail "nginx non attivo" +nginx -t >/dev/null 2>&1 && pass "configurazione nginx valida" || fail "nginx -t fallito" + +for i in 1 2 3 4 5; do + HEALTH="$(curl -fsS http://127.0.0.1:8080/api/health 2>/dev/null)" && break + sleep 2 +done +if [[ -n "${HEALTH:-}" ]]; then + pass "API: $(echo "$HEALTH" | python3 -c 'import json,sys; d=json.load(sys.stdin); print("versione", d["version"], "| schema", d["schema_version"], "| job", d["jobs"])' 2>/dev/null)" +else + fail "API non risponde su http://127.0.0.1:8080/api/health" +fi + +[[ -f /opt/iride/data/iride.db ]] && pass "database presente" || fail "database assente in /opt/iride/data" +PERM="$(stat -c '%a %U' /opt/iride/config/iride.json 2>/dev/null)" +[[ "$PERM" == "600 iride" ]] && pass "config 0600 iride" || fail "permessi config: ${PERM:-assente} (atteso 600 iride)" +FREE_GB=$(df -BG --output=avail /opt/iride | tail -1 | tr -dc '0-9') +[[ "${FREE_GB:-0}" -ge 5 ]] && pass "spazio disco: ${FREE_GB} GB liberi" || echo -e " ${YELLOW}!${NC} spazio disco basso: ${FREE_GB:-?} GB" + +echo " $ok ok, $ko errori" +[[ $ko -eq 0 ]] diff --git a/first_setup.py b/first_setup.py new file mode 100755 index 0000000..6a62e8c --- /dev/null +++ b/first_setup.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python3 +""" +IRIDE — first_setup.py: wizard di prima configurazione (CLI) +Tecnotel Servizi SRL + +Eseguito da install.sh come utente iride con il venv dell'app e +PYTHONPATH=/opt/iride/app/backend. Scrive: + - $IRIDE_CONFIG_DIR/iride.json (cliente, dominio, chiavi di sistema generate) + - $IRIDE_CONFIG_DIR/users.json (utente admin con hash bcrypt) + +Non interattivo con IRIDE_CLIENT_NAME, IRIDE_DOMAIN, IRIDE_ADMIN_USER, +IRIDE_ADMIN_PASSWORD nell'ambiente. Rieseguibile: non sovrascrive chiavi e +utenti già reali senza conferma. Il wizard web (porta 8888, stile ARGOS) +arriva dopo il pilota. +""" +from __future__ import annotations + +import getpass +import json +import os +import re +import secrets +import sys +from pathlib import Path + +CONFIG_DIR = Path(os.environ.get("IRIDE_CONFIG_DIR", "/opt/iride/config")) +PLACEHOLDER = "GENERATO-DA-first_setup" +INTERACTIVE = sys.stdin.isatty() + + +def ask(label: str, default: str = "", env: str = "", secret: bool = False, required: bool = False) -> str: + value = os.environ.get(env, "") if env else "" + if value: + return value + if not INTERACTIVE: + if required and not default: + sys.exit(f"Manca {env or label} (esecuzione non interattiva)") + return default + prompt = f"{label}" + (f" [{default}]" if default and not secret else "") + ": " + while True: + value = getpass.getpass(prompt) if secret else input(prompt) + value = value.strip() or default + if value or not required: + return value + print(" valore obbligatorio") + + +def slugify(s: str) -> str: + return re.sub(r"[^a-z0-9]+", "-", s.lower()).strip("-") or "cliente" + + +def load(path: Path) -> dict: + try: + return json.loads(path.read_text(encoding="utf-8")) + except (OSError, ValueError): + return {} + + +def save(path: Path, data: dict) -> None: + tmp = path.with_suffix(".tmp") + tmp.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") + os.chmod(tmp, 0o600) + tmp.replace(path) + + +def main() -> None: + print("\n── IRIDE: prima configurazione ──") + iride = load(CONFIG_DIR / "iride.json") + users = load(CONFIG_DIR / "users.json") + cliente = iride.setdefault("cliente", {}) + system = iride.setdefault("system", {}) + + already = system.get("secret_key") and system.get("secret_key") != PLACEHOLDER + if already and INTERACTIVE: + keep = ask("Configurazione già presente: mantenerla? (s/n)", "s") + if keep.lower().startswith("s"): + print("Configurazione mantenuta.") + return + + full_name = ask("Nome del cliente", cliente.get("full_name") or "", "IRIDE_CLIENT_NAME", required=True) + cliente["full_name"] = full_name + cliente["name"] = slugify(cliente.get("name") if cliente.get("name") not in ("", "cliente") else full_name) + cliente["domain"] = ask("Dominio pubblico dell'istanza (FQDN)", cliente.get("domain") or "", "IRIDE_DOMAIN") + cliente.setdefault("type", "customer_service") + cliente.setdefault("ai_context", "") + system.setdefault("timezone", "Europe/Rome") + if not already: + system["secret_key"] = secrets.token_urlsafe(48) + system["internal_api_key"] = secrets.token_urlsafe(32) + ai = iride.setdefault("ai", {}) + text = ai.setdefault("text", {"provider": "fake", "options": {}}) + provider = ask("Provider AI per il testo (fake/openai)", text.get("provider", "fake"), "IRIDE_AI_PROVIDER") + text["provider"] = provider if provider in ("fake", "openai", "anthropic") else "fake" + if text["provider"] != "fake": + providers = ai.setdefault("providers", {}) + key = ask(f"API key {text['provider']} (invio per lasciarla vuota)", "", "IRIDE_AI_API_KEY", secret=True) + if key: + providers.setdefault(text["provider"], {})["api_key"] = key + save(CONFIG_DIR / "iride.json", iride) + print(f" iride.json scritto (cliente: {cliente['name']}, dominio: {cliente['domain'] or '-'})") + + from core.auth import hash_password # dal backend dell'app (PYTHONPATH) + admin_user = ask("Utente amministratore", "admin", "IRIDE_ADMIN_USER") + existing = (users.get("users") or {}).get(admin_user, {}) + if existing.get("password_hash", "").startswith("$2") and PLACEHOLDER not in existing.get("password_hash", ""): + print(f" utente {admin_user} già presente: password invariata") + else: + while True: + pwd = ask(f"Password di {admin_user} (min 12 caratteri)", "", "IRIDE_ADMIN_PASSWORD", secret=True, required=True) + if len(pwd) < 12: + print(" troppo corta") + if not INTERACTIVE: + sys.exit("password troppo corta") + continue + if INTERACTIVE and not os.environ.get("IRIDE_ADMIN_PASSWORD"): + if getpass.getpass("Conferma password: ") != pwd: + print(" non coincidono") + continue + break + users.setdefault("users", {})[admin_user] = { + "password_hash": hash_password(pwd), "roles": ["admin"], "totp_secret": "", "enabled": True} + save(CONFIG_DIR / "users.json", users) + print(f" users.json scritto (admin: {admin_user})") + print("── configurazione completata ──\n") + + +if __name__ == "__main__": + main() diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..711ae44 --- /dev/null +++ b/install.sh @@ -0,0 +1,130 @@ +#!/bin/bash +# ══════════════════════════════════════════════════════════════════════════════ +# IRIDE — install.sh: primo install di un'istanza (Tecnotel Servizi SRL) +# Lanciato da bootstrap.sh; rieseguibile (idempotente sui passi già fatti). +# +# sudo bash /opt/iride/setup/install.sh --app-url URL --branch main --gitea-host host +# [--client NOME] [--domain FQDN] [--admin-user admin] [--admin-password PWD] +# +# Passi: repo app → venv → config da esempi → wizard first_setup.py → migrazioni +# → build frontend (se presente) → systemd → nginx + certificato → permessi +# → avvio → health check. Gli aggiornamenti successivi: scripts/update.sh. +# ══════════════════════════════════════════════════════════════════════════════ +set -euo pipefail +SETUP_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=lib/common.sh +source "$SETUP_DIR/lib/common.sh" + +APP_URL=""; BRANCH="main"; GITEA_HOST="" +CLIENT=""; DOMAIN=""; ADMIN_USER="admin"; ADMIN_PASSWORD="" +while [[ $# -gt 0 ]]; do + case "$1" in + --app-url) APP_URL="$2"; shift 2 ;; + --branch) BRANCH="$2"; shift 2 ;; + --gitea-host) GITEA_HOST="$2"; shift 2 ;; + --client) CLIENT="$2"; shift 2 ;; + --domain) DOMAIN="$2"; shift 2 ;; + --admin-user) ADMIN_USER="$2"; shift 2 ;; + --admin-password) ADMIN_PASSWORD="$2"; shift 2 ;; + *) error "Opzione sconosciuta: $1" ;; + esac +done +require_root +[[ -n "$APP_URL" ]] || APP_URL="https://${GITEA_HOST:-repo.argosdefense.io}/tecnotel/iride.git" + +section "1. Sistema" +detect_os +ensure_packages +ensure_user_and_dirs + +section "2. Repository applicativo ($BRANCH)" +clone_or_update "$APP_URL" "$IRIDE_APP" "$BRANCH" +APP_VERSION="$(cat "$IRIDE_APP/VERSION" 2>/dev/null || echo '?')" +info "IRIDE $APP_VERSION" + +section "3. Virtualenv e dipendenze" +if [[ ! -x "$IRIDE_VENV/bin/python" ]]; then + as_iride python3 -m venv "$IRIDE_VENV" +fi +as_iride "$IRIDE_VENV/bin/pip" install --quiet --upgrade pip +as_iride "$IRIDE_VENV/bin/pip" install --quiet -r "$IRIDE_APP/backend/requirements.txt" +success "Dipendenze installate in $IRIDE_VENV" + +section "4. Configurazione" +for example in "$IRIDE_APP"/config/*.example; do + target="$IRIDE_CONFIG/$(basename "${example%.example}")" + if [[ ! -f "$target" ]]; then + cp "$example" "$target" + info "Creato $target da esempio" + fi +done +chown "$IRIDE_USER:$IRIDE_USER" "$IRIDE_CONFIG"/*.json +chmod 600 "$IRIDE_CONFIG"/*.json +export IRIDE_CONFIG_DIR="$IRIDE_CONFIG" IRIDE_DATA_DIR="$IRIDE_DATA" IRIDE_LOGS_DIR="$IRIDE_LOGS" IRIDE_DB="$IRIDE_DATA/iride.db" +export IRIDE_CLIENT_NAME="$CLIENT" IRIDE_DOMAIN="$DOMAIN" IRIDE_ADMIN_USER="$ADMIN_USER" IRIDE_ADMIN_PASSWORD="$ADMIN_PASSWORD" +sudo -u "$IRIDE_USER" -H --preserve-env=IRIDE_CONFIG_DIR,IRIDE_DATA_DIR,IRIDE_LOGS_DIR,IRIDE_DB,IRIDE_CLIENT_NAME,IRIDE_DOMAIN,IRIDE_ADMIN_USER,IRIDE_ADMIN_PASSWORD \ + env PYTHONPATH="$IRIDE_APP/backend" "$IRIDE_VENV/bin/python" "$SETUP_DIR/first_setup.py" +SERVER_NAME="$("$IRIDE_VENV/bin/python" -c "import json; print(json.load(open('$IRIDE_CONFIG/iride.json'))['cliente'].get('domain') or '')")" +[[ -n "$SERVER_NAME" ]] || SERVER_NAME="$(hostname -f 2>/dev/null || hostname)" + +section "5. Migrazioni DB" +cd "$IRIDE_APP/backend" +sudo -u "$IRIDE_USER" -H --preserve-env=IRIDE_CONFIG_DIR,IRIDE_DATA_DIR,IRIDE_LOGS_DIR,IRIDE_DB "$IRIDE_VENV/bin/python" db.py +success "Schema allineato" + +section "6. Frontend" +if [[ -f "$IRIDE_APP/frontend/package.json" ]]; then + command -v npm >/dev/null || error "npm assente: installare Node LTS (NodeSource) e rilanciare" + cd "$IRIDE_APP/frontend" + as_iride npm ci --silent + as_iride npm run build + success "Frontend compilato" +else + warn "Nessun frontend/package.json: nginx servirà solo l'API (B-067)" +fi + +section "7. systemd" +for svc in "${IRIDE_SERVICES[@]}"; do + cp "$IRIDE_APP/deploy/systemd/$svc.service" "/etc/systemd/system/$svc.service" +done +systemctl daemon-reload +for svc in "${IRIDE_SERVICES[@]}"; do systemctl enable --quiet "$svc"; done +cp "$IRIDE_APP/deploy/logrotate/iride" /etc/logrotate.d/iride +cp "$IRIDE_APP/deploy/sudoers/iride-systemctl" /etc/sudoers.d/iride-systemctl +chmod 440 /etc/sudoers.d/iride-systemctl +visudo -cf /etc/sudoers.d/iride-systemctl >/dev/null || error "sudoers non valido" +success "Unit installate e abilitate: ${IRIDE_SERVICES[*]}" + +section "8. nginx e certificato ($SERVER_NAME)" +if [[ ! -f "$IRIDE_CERTS/fullchain.pem" ]]; then + openssl req -x509 -nodes -newkey rsa:2048 -days 825 -subj "/CN=$SERVER_NAME" \ + -keyout "$IRIDE_CERTS/privkey.pem" -out "$IRIDE_CERTS/fullchain.pem" >/dev/null 2>&1 + chown "$IRIDE_USER:$IRIDE_USER" "$IRIDE_CERTS"/*.pem; chmod 600 "$IRIDE_CERTS/privkey.pem" + warn "Certificato self-signed generato: per WhatsApp serve un certificato valido in $IRIDE_CERTS" +fi +sed "s/IRIDE_SERVER_NAME/$SERVER_NAME/g" "$IRIDE_APP/deploy/nginx/iride.conf" > /etc/nginx/sites-available/iride +ln -sf /etc/nginx/sites-available/iride /etc/nginx/sites-enabled/iride +rm -f /etc/nginx/sites-enabled/default +nginx -t >/dev/null 2>&1 || error "Configurazione nginx non valida: nginx -t" +systemctl enable --quiet nginx +systemctl reload nginx || systemctl restart nginx +success "nginx configurato" + +section "9. Permessi" +chown -R "$IRIDE_USER:$IRIDE_USER" "$IRIDE_ROOT" +chmod 700 "$IRIDE_CONFIG" +chmod 600 "$IRIDE_CONFIG"/*.json "$IRIDE_CONFIG/git-credentials" 2>/dev/null || true +success "Proprietario $IRIDE_USER, config 0600" + +section "10. Avvio e verifica" +for svc in "${IRIDE_SERVICES[@]}"; do systemctl restart "$svc"; done +sleep 2 +bash "$SETUP_DIR/checks/health.sh" || error "Health check fallito: vedere $IRIDE_LOGS" + +echo +success "IRIDE $APP_VERSION installato" +echo " Portale: https://$SERVER_NAME/ (API: /api/health, /api/docs)" +echo " Utente admin: $ADMIN_USER" +echo " Prossimi passi: canali in $IRIDE_CONFIG/channels.json e credenziali in credentials.json," +echo " poi: sudo systemctl restart iride-api" +echo " Aggiornamenti: sudo bash $IRIDE_APP/scripts/update.sh" diff --git a/lib/common.sh b/lib/common.sh new file mode 100644 index 0000000..34f36e6 --- /dev/null +++ b/lib/common.sh @@ -0,0 +1,81 @@ +#!/bin/bash +# IRIDE setup — funzioni comuni (sourced da bootstrap.sh e install.sh) +# Tecnotel Servizi SRL + +RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BLUE='\033[0;34m'; CYAN='\033[0;36m'; NC='\033[0m' +info() { echo -e "${CYAN}[INFO]${NC} $1"; } +success() { echo -e "${GREEN}[OK]${NC} $1"; } +warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } +error() { echo -e "${RED}[ERROR]${NC} $1"; exit 1; } +section() { echo -e "\n${BLUE}══════════════════════════════════════${NC}"; echo -e "${BLUE} $1${NC}"; echo -e "${BLUE}══════════════════════════════════════${NC}"; } + +IRIDE_ROOT="/opt/iride" +IRIDE_APP="$IRIDE_ROOT/app" +IRIDE_CONFIG="$IRIDE_ROOT/config" +IRIDE_DATA="$IRIDE_ROOT/data" +IRIDE_LOGS="$IRIDE_ROOT/logs" +IRIDE_BACKUPS="$IRIDE_ROOT/backups" +IRIDE_CERTS="$IRIDE_ROOT/certs" +IRIDE_SETUP="$IRIDE_ROOT/setup" +IRIDE_USER="iride" +IRIDE_VENV="$IRIDE_APP/backend/venv" +IRIDE_SERVICES=(iride-api iride-worker iride-scheduler) + +require_root() { [[ $EUID -eq 0 ]] || error "Eseguire con sudo"; } + +detect_os() { + if [[ -f /etc/os-release ]]; then + . /etc/os-release + case "${ID:-}:${VERSION_ID:-}" in + ubuntu:22.04|ubuntu:24.04) success "Sistema: $PRETTY_NAME" ;; + ubuntu:*) warn "Ubuntu ${VERSION_ID} non collaudato (riferimento: 22.04 / 24.04)" ;; + *) warn "Distribuzione ${ID:-?} non collaudata: il riferimento è Ubuntu Server LTS" ;; + esac + fi +} + +as_iride() { sudo -u "$IRIDE_USER" -H "$@"; } + +ensure_packages() { + export DEBIAN_FRONTEND=noninteractive + apt-get update -qq + apt-get install -y -qq git curl ca-certificates openssl python3 python3-venv python3-pip nginx sqlite3 >/dev/null + success "Pacchetti di base installati" +} + +ensure_user_and_dirs() { + if ! id "$IRIDE_USER" >/dev/null 2>&1; then + useradd --system --home-dir "$IRIDE_ROOT" --shell /usr/sbin/nologin "$IRIDE_USER" + success "Utente di servizio $IRIDE_USER creato" + fi + mkdir -p "$IRIDE_APP" "$IRIDE_CONFIG" "$IRIDE_DATA" "$IRIDE_LOGS" "$IRIDE_BACKUPS" "$IRIDE_CERTS" "$IRIDE_SETUP" + chown "$IRIDE_USER:$IRIDE_USER" "$IRIDE_ROOT" "$IRIDE_APP" "$IRIDE_CONFIG" "$IRIDE_DATA" "$IRIDE_LOGS" "$IRIDE_BACKUPS" "$IRIDE_CERTS" "$IRIDE_SETUP" + chmod 750 "$IRIDE_ROOT" + chmod 700 "$IRIDE_CONFIG" +} + +# Credenziali git di sola lettura per le istanze: file 0600 dell'utente iride, +# usato dal credential helper "store". Mai token personali. +configure_git_credentials() { + local gitea_host="$1" token="$2" + local cred="$IRIDE_CONFIG/git-credentials" + printf 'https://oauth2:%s@%s\n' "$token" "$gitea_host" > "$cred" + chown "$IRIDE_USER:$IRIDE_USER" "$cred"; chmod 600 "$cred" + as_iride git config --global credential.helper "store --file=$cred" + as_iride git config --global safe.directory "$IRIDE_APP" + as_iride git config --global safe.directory "$IRIDE_SETUP" + success "Credenziali git salvate in $cred (0600)" +} + +clone_or_update() { + local url="$1" dest="$2" branch="$3" + if [[ -d "$dest/.git" ]]; then + as_iride git -C "$dest" fetch --quiet origin "$branch" + as_iride git -C "$dest" checkout --quiet "$branch" + as_iride git -C "$dest" merge --ff-only --quiet "origin/$branch" + success "Aggiornato $dest ($branch)" + else + as_iride git clone --quiet --branch "$branch" "$url" "$dest" + success "Clonato $url in $dest" + fi +}