iride-setup 0.2.0 - bootstrap, first-setup, web installer 8888 con licenza, dev-setup runner
This commit is contained in:
parent
5dd160472d
commit
eaac6c22c2
135
README.md
135
README.md
|
|
@ -1,44 +1,121 @@
|
||||||
# iride-setup
|
# IRIDE — Setup & Installer
|
||||||
|
|
||||||
Bootstrap e primo install di un'istanza **IRIDE** (Tecnotel Servizi SRL), sullo
|
Tecnotel Servizi SRL — [www.tecnotelsrl.com](https://www.tecnotelsrl.com)
|
||||||
schema di `argos-setup`. Il repository applicativo è `iride`; gli aggiornamenti
|
|
||||||
successivi li fa `iride/scripts/update.sh`.
|
|
||||||
|
|
||||||
## Install su un server Ubuntu LTS pulito
|
Repository **pubblico** con l'installer di prima fase e il Web Installer di IRIDE,
|
||||||
|
sullo schema di `argos-setup`. Non contiene segreti: solo la chiave pubblica con cui
|
||||||
|
si verifica la licenza. Il software è nel repository privato `tecnotel/iride`,
|
||||||
|
raggiungibile con il token di sola lettura incluso nella licenza.
|
||||||
|
|
||||||
|
## Flusso
|
||||||
|
|
||||||
```text
|
```text
|
||||||
curl -fsSL -H "Authorization: token <TOKEN>" \
|
curl bootstrap.sh | sudo bash
|
||||||
https://repo.argosdefense.io/tecnotel/iride-setup/raw/branch/main/bootstrap.sh \
|
└─ first-setup.sh sistema, Node 22, utente iride, sudoers ristretti,
|
||||||
| sudo bash -s -- --token <TOKEN>
|
/opt/iride/*, UFW, fail2ban, nginx temporaneo
|
||||||
|
└─ Web Installer http://IP:8888 — wizard in 6 step
|
||||||
|
└─ install() licenza → clone iride → venv → iride.json/users.json →
|
||||||
|
migrazioni → (frontend) → SSL → nginx → systemd →
|
||||||
|
licenza in /opt/iride/data → chiude la 8888 e si rimuove
|
||||||
```
|
```
|
||||||
|
|
||||||
`<TOKEN>` è un token Gitea di **sola lettura dedicato all'istanza** (uno per
|
## Prerequisiti
|
||||||
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):
|
- VM **Ubuntu 24.04 o 26.04 LTS** pulita (Python 3.12/3.14), accesso `sudo`.
|
||||||
|
- Sizing minimo: 2 vCPU / 4 GB RAM / 40 GB disco (il DB è SQLite, niente broker).
|
||||||
|
- Uscita verso `repo.argosdefense.io` (codice e aggiornamenti) e `license.argosdefense.io`
|
||||||
|
(licenze, heartbeat); per i canali: Meta, Telegram, provider SMS, OpenAI.
|
||||||
|
- Ingresso: `443` dal pubblico se i canali usano webhook (WhatsApp, Telegram in
|
||||||
|
webhook), `8888` **solo** dal tuo IP durante il wizard, `22` solo dagli IP admin.
|
||||||
|
- Una **licenza IRIDE** emessa da Tecnotel per il `machine_id` del server (lo mostra
|
||||||
|
il wizard al primo step).
|
||||||
|
|
||||||
|
## Installazione
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://repo.argosdefense.io/tecnotel/iride-setup/raw/branch/main/bootstrap.sh | sudo bash
|
||||||
|
```
|
||||||
|
|
||||||
|
Manuale equivalente:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt update && sudo apt install -y git
|
||||||
|
git clone https://repo.argosdefense.io/tecnotel/iride-setup.git /opt/iride-setup-pkg
|
||||||
|
cd /opt/iride-setup-pkg && sudo bash first-setup.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Poi apri `http://<IP>:8888`:
|
||||||
|
|
||||||
|
1. **Licenza** — copia il `machine_id`, invialo a Tecnotel, carica `license.json`.
|
||||||
|
Il wizard verifica firma Ed25519, prodotto (`iride`), `machine_id`, scadenza e
|
||||||
|
presenza del token Gitea.
|
||||||
|
2. **Cliente** — nome breve, nome completo, tipo, contesto per l'assistente, logo.
|
||||||
|
3. **Rete & SSL** — hostname pubblico, alias, certificato:
|
||||||
|
- *Let's Encrypt* (automatico, richiede DNS pubblico già puntato alla VM),
|
||||||
|
- *certificato esistente* (upload `.crt` + `.key`, es. wildcard aziendale),
|
||||||
|
- *autofirmato* (RSA 4096, 10 anni, SAN con hostname, alias e IP: LAN e demo;
|
||||||
|
**non** per WhatsApp, che richiede un certificato valido).
|
||||||
|
4. **Intelligenza artificiale** — provider del modello testuale: `fake` (demo, nessuna
|
||||||
|
chiave), OpenAI o Anthropic con API key. Modificabile dal portale.
|
||||||
|
5. **Utente admin** — primo accesso al portale (password ≥ 12 caratteri, hash bcrypt).
|
||||||
|
6. **Installa** — riepilogo e avvio; il log dell'installazione scorre nella pagina.
|
||||||
|
|
||||||
|
A fine wizard la porta 8888 viene chiusa e `/opt/iride-setup-pkg` rimosso.
|
||||||
|
Aggiornamenti successivi: `sudo bash /opt/iride/app/scripts/update.sh` (usa il token
|
||||||
|
della licenza, mai credenziali personali). Verifica rapida: `iride-health`.
|
||||||
|
|
||||||
|
## Layout risultante
|
||||||
|
|
||||||
```text
|
```text
|
||||||
... | sudo bash -s -- --token <TOKEN> --client "Nome Cliente" --domain iride.cliente.it --admin-password '<min 12 caratteri>'
|
/opt/iride/app repository tecnotel/iride (utente iride)
|
||||||
|
/opt/iride/config iride.json, users.json, channels.json, credentials.json (0600)
|
||||||
|
/opt/iride/data iride.db (WAL), license.json, heartbeat, stato update
|
||||||
|
/opt/iride/logs demoni e nginx (logrotate)
|
||||||
|
/opt/iride/certs certificati TLS
|
||||||
|
/opt/iride/backups snapshot pre-update e giornalieri
|
||||||
```
|
```
|
||||||
|
|
||||||
## Cosa fa
|
Servizi: `iride-api` (8080), `iride-worker`, `iride-scheduler`; `iride-voice` (8081)
|
||||||
|
solo dopo il POC-01. nginx su 80/443.
|
||||||
|
|
||||||
| Passo | Script | Esito |
|
## Hardening applicato (stesso di argos-setup)
|
||||||
|---|---|---|
|
|
||||||
| 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}`.
|
Aggiornamento completo del sistema, UFW con default deny (22, 80, 443; 8888 chiusa a
|
||||||
|
fine setup), fail2ban, utente di servizio senza privilegi con sudoers limitati al
|
||||||
|
restart dei demoni IRIDE e a `update.sh`, configurazione e licenza in file 0600,
|
||||||
|
token Gitea mai in `.git/config` (URL autenticato solo per clone e fetch),
|
||||||
|
Strict-Transport-Security e rate limit sulle API in nginx.
|
||||||
|
|
||||||
## Note
|
## Server di sviluppo (solo Tecnotel)
|
||||||
|
|
||||||
- Il certificato generato è self-signed: **WhatsApp richiede un certificato
|
Lo stesso server può ospitare l'istanza demo, il checkout di sviluppo e il runner CI:
|
||||||
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
|
```bash
|
||||||
all'install arrivano dopo il pilota (B-055 li collauda su VM di test).
|
sudo bash /opt/iride-setup-pkg/dev-setup.sh --token <REGISTRATION_TOKEN>
|
||||||
- Riesecuzione sicura: `bootstrap.sh` aggiorna il setup e rilancia `install.sh`,
|
```
|
||||||
che non tocca configurazione, utenti e certificati già presenti.
|
|
||||||
|
Installa l'ultimo **Gitea Runner** (ex act_runner) in modalità host con label
|
||||||
|
`ubuntu-latest:host`, come utente `gitea-runner`, servizio `gitea-runner.service`.
|
||||||
|
Il token si crea su Gitea in *Impostazioni → Actions → Runners*. Va lanciato
|
||||||
|
**prima** del wizard (che a fine install rimuove `/opt/iride-setup-pkg`).
|
||||||
|
Il checkout di sviluppo è un normale `git clone` nella home dell'utente.
|
||||||
|
|
||||||
|
## Contenuto del repository
|
||||||
|
|
||||||
|
| File | Scopo |
|
||||||
|
|---|---|
|
||||||
|
| `bootstrap.sh` | One-liner: scarica il repo e avvia `first-setup.sh` |
|
||||||
|
| `first-setup.sh` | Installer ambiente base: sistema, utente, firewall, nginx temporaneo, avvia il wizard |
|
||||||
|
| `setup_server.py` | Backend del Web Installer (porta 8888, self-contained) |
|
||||||
|
| `setup.html` | Frontend del Web Installer, wizard in 6 step |
|
||||||
|
| `dev-setup.sh` | Gitea Runner in modalità host per il server di sviluppo |
|
||||||
|
| `checks/health.sh` | Verifica dell'istanza (copiato in `/usr/local/bin/iride-health`) |
|
||||||
|
|
||||||
|
## Repository correlati
|
||||||
|
|
||||||
|
- [`tecnotel/iride`](https://repo.argosdefense.io/tecnotel/iride) — codice di IRIDE
|
||||||
|
(privato, accessibile con licenza).
|
||||||
|
|
||||||
|
## Versioning
|
||||||
|
|
||||||
|
SemVer, tag `vX.Y.Z`, allineato al repository `iride`.
|
||||||
|
|
|
||||||
132
bootstrap.sh
132
bootstrap.sh
|
|
@ -1,73 +1,81 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# ══════════════════════════════════════════════════════════════════════════════
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
# IRIDE — Bootstrap installer (Tecnotel Servizi SRL)
|
# IRIDE — Bootstrap Installer (one-liner)
|
||||||
|
# Tecnotel Servizi SRL — Ubuntu 24.04 / 26.04 LTS
|
||||||
#
|
#
|
||||||
# Uso su un server Ubuntu LTS pulito:
|
# Uso tramite one-liner:
|
||||||
|
# curl -fsSL https://repo.argosdefense.io/tecnotel/iride-setup/raw/branch/main/bootstrap.sh | sudo bash
|
||||||
#
|
#
|
||||||
# curl -fsSL -H "Authorization: token <TOKEN>" \
|
# Oppure manuale (raccomandato per verifica):
|
||||||
# https://repo.argosdefense.io/tecnotel/iride-setup/raw/branch/main/bootstrap.sh \
|
# curl -fsSLo /tmp/iride-bootstrap.sh https://repo.argosdefense.io/tecnotel/iride-setup/raw/branch/main/bootstrap.sh
|
||||||
# | sudo bash -s -- --token <TOKEN>
|
# less /tmp/iride-bootstrap.sh
|
||||||
|
# sudo bash /tmp/iride-bootstrap.sh
|
||||||
#
|
#
|
||||||
# <TOKEN> = token Gitea di SOLA LETTURA dedicato all'istanza (mai personale).
|
# Stesso flusso di argos-setup: scarica questo repository (pubblico, nessun
|
||||||
# Opzioni: --gitea-url URL (default https://repo.argosdefense.io)
|
# segreto), esegue first-setup.sh (sistema, utente, firewall, nginx temporaneo)
|
||||||
# --org ORG (default tecnotel) --branch BRANCH (default main)
|
# e avvia il Web Installer sulla porta 8888, che chiede la licenza IRIDE e
|
||||||
# --setup-branch BRANCH (default main)
|
# clona il repository privato tecnotel/iride con il token della licenza.
|
||||||
# --client NOME --domain FQDN --admin-password PWD (non interattivo)
|
|
||||||
# Riesecuzione sicura: aggiorna iride-setup e rilancia install.sh.
|
|
||||||
# ══════════════════════════════════════════════════════════════════════════════
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
GITEA_URL="https://repo.argosdefense.io"
|
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
|
||||||
ORG="tecnotel"
|
BLUE='\033[0;34m'; CYAN='\033[0;36m'; NC='\033[0m'
|
||||||
BRANCH="main"
|
info() { echo -e "${CYAN}[INFO]${NC} $1"; }
|
||||||
SETUP_BRANCH="main"
|
success() { echo -e "${GREEN}[OK]${NC} $1"; }
|
||||||
TOKEN=""
|
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
||||||
PASSTHROUGH=()
|
error() { echo -e "${RED}[ERROR]${NC} $1" >&2; exit 1; }
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
# ── Check privilegi e OS ──────────────────────────────────────────────────────
|
||||||
case "$1" in
|
[[ $EUID -ne 0 ]] && error "Eseguire come root (sudo)."
|
||||||
--token) TOKEN="$2"; shift 2 ;;
|
[[ -f /etc/os-release ]] || error "OS non riconosciuto: /etc/os-release mancante."
|
||||||
--gitea-url) GITEA_URL="$2"; shift 2 ;;
|
. /etc/os-release
|
||||||
--org) ORG="$2"; shift 2 ;;
|
SUPPORTED_VERSIONS=("24.04" "26.04")
|
||||||
--branch) BRANCH="$2"; shift 2 ;;
|
VERSION_OK=0
|
||||||
--setup-branch) SETUP_BRANCH="$2"; shift 2 ;;
|
for v in "${SUPPORTED_VERSIONS[@]}"; do
|
||||||
--client|--domain|--admin-password|--admin-user) PASSTHROUGH+=("$1" "$2"); shift 2 ;;
|
[[ "$VERSION_ID" == "$v" ]] && VERSION_OK=1
|
||||||
*) echo "Opzione sconosciuta: $1"; exit 1 ;;
|
done
|
||||||
esac
|
if [[ "$ID" != "ubuntu" || $VERSION_OK -eq 0 ]]; then
|
||||||
|
error "Richiesto Ubuntu 24.04 o 26.04 LTS (trovato: $ID $VERSION_ID)."
|
||||||
|
fi
|
||||||
|
info "Sistema rilevato: Ubuntu $VERSION_ID LTS"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${BLUE}╔══════════════════════════════════════════════════╗${NC}"
|
||||||
|
echo -e "${BLUE}║ IRIDE — Bootstrap Installer ║${NC}"
|
||||||
|
echo -e "${BLUE}║ Tecnotel Servizi SRL ║${NC}"
|
||||||
|
echo -e "${BLUE}╚══════════════════════════════════════════════════╝${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
SETUP_REPO_URL="https://repo.argosdefense.io/tecnotel/iride-setup.git"
|
||||||
|
SETUP_DIR="/opt/iride-setup-pkg"
|
||||||
|
|
||||||
|
# ── Install git se mancante ──────────────────────────────────────────────────
|
||||||
|
if ! command -v git >/dev/null 2>&1; then
|
||||||
|
info "Git non installato — installo..."
|
||||||
|
apt-get update -qq
|
||||||
|
apt-get install -y -qq git
|
||||||
|
success "Git installato"
|
||||||
|
else
|
||||||
|
info "Git già presente ($(git --version | awk '{print $3}'))"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Scarica o aggiorna iride-setup ───────────────────────────────────────────
|
||||||
|
if [[ -d "$SETUP_DIR/.git" ]]; then
|
||||||
|
info "iride-setup già presente in $SETUP_DIR — aggiorno..."
|
||||||
|
git -C "$SETUP_DIR" pull --ff-only origin main
|
||||||
|
success "iride-setup aggiornato"
|
||||||
|
else
|
||||||
|
info "Scarico iride-setup da $SETUP_REPO_URL..."
|
||||||
|
rm -rf "$SETUP_DIR"
|
||||||
|
git clone --depth=1 "$SETUP_REPO_URL" "$SETUP_DIR"
|
||||||
|
success "iride-setup scaricato in $SETUP_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
for f in first-setup.sh setup_server.py setup.html; do
|
||||||
|
[[ -f "$SETUP_DIR/$f" ]] || error "File $f mancante in $SETUP_DIR — repo iride-setup incompleto?"
|
||||||
done
|
done
|
||||||
|
|
||||||
[[ $EUID -eq 0 ]] || { echo "Eseguire con sudo"; exit 1; }
|
info "Avvio installer ambiente..."
|
||||||
[[ -n "$TOKEN" ]] || { echo "Serve --token <token Gitea di sola lettura>"; exit 1; }
|
echo ""
|
||||||
|
cd "$SETUP_DIR"
|
||||||
GITEA_HOST="${GITEA_URL#https://}"; GITEA_HOST="${GITEA_HOST#http://}"; GITEA_HOST="${GITEA_HOST%%/*}"
|
exec bash ./first-setup.sh "$@"
|
||||||
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[@]}"
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# IRIDE — verifica post-install / post-update (Tecnotel Servizi SRL)
|
# IRIDE — verifica post-install / post-update (Tecnotel Servizi SRL)
|
||||||
# Uso: bash /opt/iride/setup/checks/health.sh (exit 1 se qualcosa non va)
|
# Uso: iride-health (copiato in /usr/local/bin dal web installer; exit 1 se qualcosa non va)
|
||||||
set -uo pipefail
|
set -uo pipefail
|
||||||
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m'
|
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m'
|
||||||
ok=0; ko=0
|
ok=0; ko=0
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,132 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
# IRIDE — dev-setup.sh: Gitea Runner (CI) sul server di sviluppo
|
||||||
|
# Tecnotel Servizi SRL
|
||||||
|
#
|
||||||
|
# SOLO sul server di sviluppo Tecnotel (mai su un server cliente). Installa il
|
||||||
|
# Gitea Runner ufficiale (ex act_runner) in modalità HOST: i job girano sul
|
||||||
|
# server con il Python e il Node già presenti, senza Docker.
|
||||||
|
#
|
||||||
|
# Uso: sudo bash /opt/iride-setup-pkg/dev-setup.sh --token <REGISTRATION_TOKEN>
|
||||||
|
# [--instance https://repo.argosdefense.io] [--name iride-dev]
|
||||||
|
# [--labels "ubuntu-latest:host,ubuntu-26.04:host"]
|
||||||
|
#
|
||||||
|
# Il token di registrazione si crea su Gitea: Impostazioni utente (tecnotel) →
|
||||||
|
# Actions → Runners → "Crea nuovo runner". Vale una volta sola.
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; 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" >&2; exit 1; }
|
||||||
|
|
||||||
|
INSTANCE="https://repo.argosdefense.io"
|
||||||
|
TOKEN=""
|
||||||
|
NAME="$(hostname -s)"
|
||||||
|
LABELS="ubuntu-latest:host,ubuntu-26.04:host,ubuntu-24.04:host"
|
||||||
|
RUNNER_USER="gitea-runner"
|
||||||
|
RUNNER_HOME="/opt/gitea-runner"
|
||||||
|
BIN="/usr/local/bin/gitea-runner"
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--token) TOKEN="$2"; shift 2 ;;
|
||||||
|
--instance) INSTANCE="$2"; shift 2 ;;
|
||||||
|
--name) NAME="$2"; shift 2 ;;
|
||||||
|
--labels) LABELS="$2"; shift 2 ;;
|
||||||
|
*) error "Opzione sconosciuta: $1" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
[[ $EUID -eq 0 ]] || error "Eseguire con sudo"
|
||||||
|
[[ -n "$TOKEN" ]] || error "Serve --token <registration token> (Gitea → Impostazioni → Actions → Runners)"
|
||||||
|
command -v node >/dev/null || error "Node.js assente: eseguire prima first-setup.sh (serve alle azioni JavaScript in modalità host)"
|
||||||
|
command -v git >/dev/null || error "git assente"
|
||||||
|
|
||||||
|
# ── Utente dedicato ──────────────────────────────────────────────────────────
|
||||||
|
if ! id "$RUNNER_USER" &>/dev/null; then
|
||||||
|
useradd -r -s /bin/bash -m -d "$RUNNER_HOME" "$RUNNER_USER"
|
||||||
|
success "Utente $RUNNER_USER creato"
|
||||||
|
fi
|
||||||
|
mkdir -p "$RUNNER_HOME"
|
||||||
|
chown "$RUNNER_USER:$RUNNER_USER" "$RUNNER_HOME"
|
||||||
|
|
||||||
|
# ── Download dell'ultima release (gitea/runner, ex act_runner) ───────────────
|
||||||
|
info "Cerco l'ultima release di gitea/runner..."
|
||||||
|
ARCH="$(dpkg --print-architecture)" # amd64 | arm64
|
||||||
|
ASSET_URL="$(curl -fsSL https://gitea.com/api/v1/repos/gitea/runner/releases/latest | python3 -c "
|
||||||
|
import json, sys
|
||||||
|
rel = json.load(sys.stdin)
|
||||||
|
arch = sys.argv[1]
|
||||||
|
for a in rel.get('assets', []):
|
||||||
|
n = a.get('name', '')
|
||||||
|
if f'linux-{arch}' in n and not n.endswith(('.sha256', '.asc', '.sig', '.txt')):
|
||||||
|
print(a['browser_download_url']); break
|
||||||
|
print('', end='')
|
||||||
|
" "$ARCH")" || ASSET_URL=""
|
||||||
|
if [[ -z "$ASSET_URL" ]]; then
|
||||||
|
error "Nessun asset linux-$ARCH nell'ultima release di https://gitea.com/gitea/runner/releases: scaricare il binario a mano in $BIN e rilanciare"
|
||||||
|
fi
|
||||||
|
info "Asset: $ASSET_URL"
|
||||||
|
TMP="$(mktemp -d)"
|
||||||
|
curl -fsSL -o "$TMP/runner.bin" "$ASSET_URL"
|
||||||
|
if file "$TMP/runner.bin" | grep -qi "xz compressed"; then
|
||||||
|
xz -d -c "$TMP/runner.bin" > "$TMP/runner"
|
||||||
|
else
|
||||||
|
mv "$TMP/runner.bin" "$TMP/runner"
|
||||||
|
fi
|
||||||
|
install -m 0755 "$TMP/runner" "$BIN"
|
||||||
|
rm -rf "$TMP"
|
||||||
|
success "Runner installato: $($BIN --version 2>/dev/null | head -1 || echo "$BIN")"
|
||||||
|
|
||||||
|
# ── Registrazione (il token vale una volta) ──────────────────────────────────
|
||||||
|
if [[ -f "$RUNNER_HOME/.runner" ]]; then
|
||||||
|
warn "Runner già registrato ($RUNNER_HOME/.runner presente): salto la registrazione"
|
||||||
|
else
|
||||||
|
info "Registro il runner '$NAME' su $INSTANCE con label $LABELS"
|
||||||
|
sudo -u "$RUNNER_USER" -H bash -c "cd '$RUNNER_HOME' && '$BIN' register --no-interactive --instance '$INSTANCE' --token '$TOKEN' --name '$NAME' --labels '$LABELS'" \
|
||||||
|
|| error "Registrazione fallita: verificare token, URL dell'istanza e che Actions sia abilitato su Gitea (Site Administration → Actions)"
|
||||||
|
success "Runner registrato"
|
||||||
|
fi
|
||||||
|
if [[ ! -f "$RUNNER_HOME/config.yaml" ]]; then
|
||||||
|
# gitea-runner >= 1.0: "config init" scrive una config minima (act_runner usava generate-config)
|
||||||
|
sudo -u "$RUNNER_USER" -H bash -c "cd '$RUNNER_HOME' && ('$BIN' config init >/dev/null 2>&1 || '$BIN' generate-config > config.yaml)" \
|
||||||
|
&& success "config.yaml scritto in $RUNNER_HOME" || warn "config non generata: il runner userà i default"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Servizio systemd ─────────────────────────────────────────────────────────
|
||||||
|
CONFIG_ARG=""
|
||||||
|
[[ -f "$RUNNER_HOME/config.yaml" ]] && CONFIG_ARG=" --config $RUNNER_HOME/config.yaml"
|
||||||
|
cat > /etc/systemd/system/gitea-runner.service << UNITEOF
|
||||||
|
[Unit]
|
||||||
|
Description=Gitea Runner (IRIDE CI, modalità host)
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=$RUNNER_USER
|
||||||
|
Group=$RUNNER_USER
|
||||||
|
WorkingDirectory=$RUNNER_HOME
|
||||||
|
ExecStart=$BIN daemon$CONFIG_ARG
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
SyslogIdentifier=gitea-runner
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
UNITEOF
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable --now gitea-runner
|
||||||
|
sleep 2
|
||||||
|
if systemctl is-active --quiet gitea-runner; then
|
||||||
|
success "gitea-runner attivo: journalctl -u gitea-runner -f per i job"
|
||||||
|
else
|
||||||
|
error "gitea-runner non parte: journalctl -u gitea-runner -n 50"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
echo " Verifica su Gitea: Impostazioni → Actions → Runners deve mostrare '$NAME' online."
|
||||||
|
echo " Il workflow .gitea/workflows/ci.yml del repo iride usa runs-on: ubuntu-latest → label ubuntu-latest:host."
|
||||||
|
|
@ -0,0 +1,196 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
# IRIDE — Installer ambiente base
|
||||||
|
# Tecnotel Servizi SRL — Ubuntu 24.04 / 26.04 LTS
|
||||||
|
# Uso: sudo bash first-setup.sh (lanciato da bootstrap.sh)
|
||||||
|
#
|
||||||
|
# Stesso hardening di argos-setup: aggiornamento del sistema, UFW con default
|
||||||
|
# deny, fail2ban, utente di servizio senza privilegi con sudoers a scope
|
||||||
|
# ristretto, nginx temporaneo, Web Installer su 8888 chiuso a fine wizard.
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
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}"; }
|
||||||
|
|
||||||
|
[[ $EUID -ne 0 ]] && error "Eseguire come root: sudo bash first-setup.sh"
|
||||||
|
. /etc/os-release
|
||||||
|
SUPPORTED_VERSIONS=("24.04" "26.04")
|
||||||
|
VERSION_OK=0
|
||||||
|
for v in "${SUPPORTED_VERSIONS[@]}"; do
|
||||||
|
[[ "$VERSION_ID" == "$v" ]] && VERSION_OK=1
|
||||||
|
done
|
||||||
|
[[ "$ID" != "ubuntu" || $VERSION_OK -eq 0 ]] && \
|
||||||
|
error "Richiesto Ubuntu 24.04 o 26.04 LTS (rilevato: $ID $VERSION_ID)"
|
||||||
|
info "Sistema rilevato: Ubuntu $VERSION_ID LTS"
|
||||||
|
|
||||||
|
SETUP_PKG="/opt/iride-setup-pkg"
|
||||||
|
APP_USER="iride"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${BLUE}╔══════════════════════════════════════════╗${NC}"
|
||||||
|
echo -e "${BLUE}║ IRIDE — Setup ambiente base ║${NC}"
|
||||||
|
echo -e "${BLUE}║ Tecnotel Servizi SRL ║${NC}"
|
||||||
|
echo -e "${BLUE}╚══════════════════════════════════════════╝${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
section "1. Sistema base"
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
timedatectl set-timezone Europe/Rome
|
||||||
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
|
apt-get update -qq
|
||||||
|
apt-get upgrade -y -qq
|
||||||
|
apt-get install -y -qq \
|
||||||
|
curl wget git vim htop unzip jq \
|
||||||
|
python3 python3-pip python3-venv python3-cryptography python3-dev \
|
||||||
|
nginx certbot python3-certbot-nginx \
|
||||||
|
ufw fail2ban \
|
||||||
|
build-essential libssl-dev libffi-dev \
|
||||||
|
sqlite3 net-tools dnsutils lsof \
|
||||||
|
ca-certificates gnupg apt-transport-https openssl
|
||||||
|
success "Pacchetti sistema installati"
|
||||||
|
|
||||||
|
PY_VER="$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')"
|
||||||
|
python3 -c 'import sys; sys.exit(0 if sys.version_info >= (3, 12) else 1)' \
|
||||||
|
|| error "Python >= 3.12 richiesto (trovato $PY_VER)"
|
||||||
|
success "Python $PY_VER"
|
||||||
|
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
section "2. Node.js 22 LTS"
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
# Serve alla build del frontend (solo sul server) e alle azioni JavaScript del
|
||||||
|
# Gitea Runner in modalità host (dev-setup.sh).
|
||||||
|
if ! node --version 2>/dev/null | grep -q "v2[2-9]"; then
|
||||||
|
curl -fsSL https://deb.nodesource.com/setup_22.x | bash - >/dev/null 2>&1
|
||||||
|
apt-get install -y -qq nodejs
|
||||||
|
fi
|
||||||
|
success "Node.js $(node --version) installato"
|
||||||
|
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
section "3. Utente applicazione"
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
if ! id "$APP_USER" &>/dev/null; then
|
||||||
|
useradd -r -s /bin/bash -m -d /home/$APP_USER $APP_USER
|
||||||
|
success "Utente $APP_USER creato"
|
||||||
|
else
|
||||||
|
warn "Utente $APP_USER già esistente"
|
||||||
|
fi
|
||||||
|
usermod -aG systemd-journal $APP_USER
|
||||||
|
success "Utente $APP_USER nel gruppo systemd-journal (lettura log dal portale)"
|
||||||
|
|
||||||
|
# Restart dei soli demoni IRIDE dal portale (nessun altro comando)
|
||||||
|
cat > /etc/sudoers.d/iride-systemctl <<'SUDOEOF'
|
||||||
|
iride ALL=(ALL) NOPASSWD: /bin/systemctl restart iride-api
|
||||||
|
iride ALL=(ALL) NOPASSWD: /bin/systemctl restart iride-worker
|
||||||
|
iride ALL=(ALL) NOPASSWD: /bin/systemctl restart iride-scheduler
|
||||||
|
iride ALL=(ALL) NOPASSWD: /bin/systemctl restart iride-voice
|
||||||
|
SUDOEOF
|
||||||
|
chmod 440 /etc/sudoers.d/iride-systemctl
|
||||||
|
visudo -cf /etc/sudoers.d/iride-systemctl > /dev/null
|
||||||
|
success "Sudoers per restart servizi configurato"
|
||||||
|
|
||||||
|
# update.sh dal portale: SOLO quello script, path esatto (symlink /usr/bin/bash e /bin/bash)
|
||||||
|
cat > /etc/sudoers.d/iride-update <<'SUDOEOF'
|
||||||
|
iride ALL=(root) NOPASSWD: /usr/bin/bash /opt/iride/app/scripts/update.sh
|
||||||
|
iride ALL=(root) NOPASSWD: /bin/bash /opt/iride/app/scripts/update.sh
|
||||||
|
SUDOEOF
|
||||||
|
chmod 440 /etc/sudoers.d/iride-update
|
||||||
|
visudo -cf /etc/sudoers.d/iride-update > /dev/null
|
||||||
|
success "Sudoers per update.sh dal portale configurato"
|
||||||
|
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
section "4. Struttura cartelle"
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
mkdir -p /opt/iride/{app,config,data,logs,certs,backups}
|
||||||
|
mkdir -p /opt/iride/config/assets
|
||||||
|
chown -R $APP_USER:$APP_USER /opt/iride
|
||||||
|
chown -R $APP_USER:$APP_USER "$SETUP_PKG"
|
||||||
|
chmod -R 750 /opt/iride
|
||||||
|
chmod 700 /opt/iride/config
|
||||||
|
success "Struttura /opt/iride/ creata"
|
||||||
|
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
section "5. Firewall UFW"
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
ufw --force reset >/dev/null
|
||||||
|
ufw default deny incoming >/dev/null
|
||||||
|
ufw default allow outgoing >/dev/null
|
||||||
|
ufw allow 22/tcp comment 'SSH' >/dev/null
|
||||||
|
ufw allow 80/tcp comment 'HTTP' >/dev/null
|
||||||
|
ufw allow 443/tcp comment 'HTTPS' >/dev/null
|
||||||
|
ufw allow 8888/tcp comment 'IRIDE Web Installer (temporaneo)' >/dev/null
|
||||||
|
ufw --force enable >/dev/null
|
||||||
|
success "Firewall UFW configurato (22, 80, 443, 8888 temporanea)"
|
||||||
|
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
section "6. Fail2ban"
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
systemctl enable --now fail2ban >/dev/null 2>&1
|
||||||
|
success "Fail2ban attivo"
|
||||||
|
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
section "7. Nginx temporaneo"
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
rm -f /etc/nginx/sites-enabled/default
|
||||||
|
cat > /etc/nginx/sites-available/iride-setup << 'NGINX'
|
||||||
|
server {
|
||||||
|
listen 80 default_server;
|
||||||
|
server_name _;
|
||||||
|
return 200 'IRIDE Setup in corso — vai a http://IP:8888';
|
||||||
|
add_header Content-Type text/plain;
|
||||||
|
}
|
||||||
|
NGINX
|
||||||
|
ln -sf /etc/nginx/sites-available/iride-setup /etc/nginx/sites-enabled/
|
||||||
|
nginx -t && systemctl restart nginx
|
||||||
|
success "Nginx temporaneo configurato"
|
||||||
|
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
section "8. Web Installer"
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
# Gira da /opt/iride-setup-pkg (clone del bootstrap); a fine install la
|
||||||
|
# cartella viene rimossa e la porta 8888 chiusa.
|
||||||
|
cat > /etc/systemd/system/iride-setup.service << 'UNITEOF'
|
||||||
|
[Unit]
|
||||||
|
Description=IRIDE Web Installer
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=root
|
||||||
|
WorkingDirectory=/opt/iride-setup-pkg
|
||||||
|
ExecStart=/usr/bin/python3 /opt/iride-setup-pkg/setup_server.py
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=3
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
SyslogIdentifier=iride-setup
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
|
||||||
|
UNITEOF
|
||||||
|
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable --now iride-setup
|
||||||
|
success "Web installer avviato"
|
||||||
|
|
||||||
|
SERVER_IP=$(hostname -I | awk '{print $1}')
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${GREEN}╔══════════════════════════════════════════════════════╗${NC}"
|
||||||
|
echo -e "${GREEN}║ Ambiente pronto! ║${NC}"
|
||||||
|
echo -e "${GREEN}╚══════════════════════════════════════════════════════╝${NC}"
|
||||||
|
echo ""
|
||||||
|
echo -e " ${CYAN}Completa la configurazione aprendo nel browser:${NC}"
|
||||||
|
echo -e " ${YELLOW}→ http://${SERVER_IP}:8888${NC}"
|
||||||
|
echo ""
|
||||||
|
echo -e " ${YELLOW}NOTA:${NC} La porta 8888 verrà chiusa automaticamente"
|
||||||
|
echo -e " al termine dell'installazione."
|
||||||
|
echo -e " Server di sviluppo con runner CI: sudo bash ${SETUP_PKG}/dev-setup.sh --token <token>"
|
||||||
|
echo ""
|
||||||
128
first_setup.py
128
first_setup.py
|
|
@ -1,128 +0,0 @@
|
||||||
#!/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()
|
|
||||||
130
install.sh
130
install.sh
|
|
@ -1,130 +0,0 @@
|
||||||
#!/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"
|
|
||||||
|
|
@ -1,81 +0,0 @@
|
||||||
#!/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
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,499 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="it">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>IRIDE — Installazione</title>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600;700&family=Barlow:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||||||
|
:root{
|
||||||
|
--bg:#080a0e;--bg2:#0e1117;--bg3:#161b24;--bg4:#1c2333;
|
||||||
|
--border:#1e2840;--border2:#2a3a58;
|
||||||
|
--text:#e2e8f4;--text2:#8a9ab8;--text3:#445068;
|
||||||
|
--brand:#8A7CFF;--brand2:#6F5FE6;--brand-dim:#8A7CFF18;
|
||||||
|
--ok:#10b981;--ok-dim:#10b98118;--err:#ef4444;--err-dim:#ef444418;--warn:#f59e0b;
|
||||||
|
--mono:'JetBrains Mono',monospace;--sans:'Barlow',sans-serif;--r:10px;
|
||||||
|
}
|
||||||
|
html,body{height:100%;background:var(--bg);color:var(--text);font-family:var(--sans);font-size:14px;line-height:1.6}
|
||||||
|
.shell{min-height:100vh;display:grid;grid-template-rows:auto 1fr auto}
|
||||||
|
.hdr{background:var(--bg2);border-bottom:1px solid var(--border);padding:16px 32px;display:flex;align-items:center;gap:16px}
|
||||||
|
.hdr-logo{font-size:22px;font-weight:800;color:var(--brand);letter-spacing:3px}
|
||||||
|
.hdr-sep{width:1px;height:24px;background:var(--border2)}
|
||||||
|
.hdr-sub{font-size:12px;color:var(--text3);font-family:var(--mono);text-transform:uppercase;letter-spacing:1.5px}
|
||||||
|
.hdr-badge{margin-left:auto;font-size:10px;font-family:var(--mono);background:var(--brand-dim);color:var(--brand);padding:4px 10px;border-radius:20px;border:1px solid #8A7CFF33}
|
||||||
|
.main{display:flex;max-width:1000px;margin:0 auto;width:100%;padding:32px 24px;gap:28px}
|
||||||
|
.sidebar{width:200px;flex-shrink:0}
|
||||||
|
.sidebar-title{font-size:10px;font-weight:700;color:var(--text3);text-transform:uppercase;letter-spacing:1.5px;padding:0 12px 12px;font-family:var(--mono)}
|
||||||
|
.tab-item{display:flex;align-items:center;gap:10px;padding:10px 12px;border-radius:var(--r);cursor:pointer;transition:all .15s;color:var(--text2);font-size:13px;font-weight:500;margin-bottom:2px;border:1px solid transparent}
|
||||||
|
.tab-item:hover{background:var(--bg3);color:var(--text)}
|
||||||
|
.tab-item.active{background:var(--bg3);color:var(--brand);border-color:var(--border2)}
|
||||||
|
.tab-item.done .tab-num{background:var(--ok);color:#000}
|
||||||
|
.tab-item.active .tab-num{background:var(--brand);color:#000}
|
||||||
|
.tab-num{width:22px;height:22px;border-radius:50%;background:var(--bg4);color:var(--text3);font-size:10px;font-weight:700;font-family:var(--mono);display:flex;align-items:center;justify-content:center;flex-shrink:0;transition:all .2s}
|
||||||
|
.content{flex:1;min-width:0}
|
||||||
|
.tab-panel{display:none}
|
||||||
|
.tab-panel.active{display:block}
|
||||||
|
.panel-title{font-size:22px;font-weight:700;margin-bottom:4px}
|
||||||
|
.panel-sub{font-size:13px;color:var(--text2);margin-bottom:24px}
|
||||||
|
.progress{display:flex;gap:6px;margin-bottom:28px}
|
||||||
|
.prog-step{flex:1;height:3px;border-radius:2px;background:var(--border2);transition:background .3s}
|
||||||
|
.prog-step.done{background:var(--ok)}
|
||||||
|
.prog-step.active{background:var(--brand)}
|
||||||
|
.form-grid{display:grid;grid-template-columns:1fr 1fr;gap:16px}
|
||||||
|
.form-full{grid-column:1/-1}
|
||||||
|
.form-label{display:block;font-size:11px;font-weight:700;color:var(--text2);text-transform:uppercase;letter-spacing:.8px;margin-bottom:6px}
|
||||||
|
.form-label span{color:var(--err);margin-left:2px}
|
||||||
|
.form-input,.form-select{width:100%;background:var(--bg3);border:1px solid var(--border2);border-radius:var(--r);padding:10px 14px;color:var(--text);font-family:var(--sans);font-size:13px;outline:none;transition:border-color .15s,box-shadow .15s}
|
||||||
|
.form-input:focus,.form-select:focus{border-color:var(--brand);box-shadow:0 0 0 3px var(--brand-dim)}
|
||||||
|
.form-input::placeholder{color:var(--text3)}
|
||||||
|
.mono-input{font-family:var(--mono);font-size:12px}
|
||||||
|
.form-hint{font-size:11px;color:var(--text3);margin-top:5px}
|
||||||
|
.sec-div{grid-column:1/-1;font-size:10px;font-weight:700;color:var(--text3);text-transform:uppercase;letter-spacing:1.5px;font-family:var(--mono);border-top:1px solid var(--border);padding-top:18px;margin-top:4px}
|
||||||
|
.sec-div:first-child{border-top:none;padding-top:0;margin-top:0}
|
||||||
|
.ssl-toggle{grid-column:1/-1;display:flex;gap:8px}
|
||||||
|
.ssl-btn{flex:1;padding:12px 16px;background:var(--bg3);border:1px solid var(--border2);border-radius:var(--r);cursor:pointer;text-align:center;font-size:13px;font-weight:600;color:var(--text2);transition:all .15s}
|
||||||
|
.ssl-btn.active{background:var(--brand-dim);border-color:var(--brand);color:var(--brand)}
|
||||||
|
.ssl-btn small{display:block;font-weight:400;font-size:11px;opacity:.7;margin-top:2px}
|
||||||
|
.upload-area{grid-column:1/-1;border:2px dashed var(--border2);border-radius:var(--r);padding:20px;text-align:center;cursor:pointer;transition:all .15s}
|
||||||
|
.upload-area:hover,.upload-area.drag{border-color:var(--brand);background:var(--brand-dim)}
|
||||||
|
.upload-preview{display:none;align-items:center;justify-content:center;gap:10px;margin-top:10px}
|
||||||
|
.upload-preview img{max-height:50px;max-width:180px;border-radius:6px}
|
||||||
|
.alert{padding:12px 16px;border-radius:var(--r);font-size:13px;margin-bottom:0}
|
||||||
|
.alert-err{background:var(--err-dim);border:1px solid #ef444433;color:var(--err)}
|
||||||
|
.alert-ok{background:var(--ok-dim);border:1px solid #10b98133;color:var(--ok)}
|
||||||
|
.alert-warn{background:#f59e0b15;border:1px solid #f59e0b33;color:var(--warn)}
|
||||||
|
.nav-row{display:flex;justify-content:space-between;align-items:center;margin-top:28px;padding-top:20px;border-top:1px solid var(--border)}
|
||||||
|
.btn{padding:10px 22px;border-radius:var(--r);border:none;font-family:var(--sans);font-size:13px;font-weight:700;cursor:pointer;transition:all .15s;display:inline-flex;align-items:center;gap:8px}
|
||||||
|
.btn-brand{background:var(--brand);color:#000}
|
||||||
|
.btn-brand:hover{background:var(--brand2)}
|
||||||
|
.btn-ghost{background:transparent;color:var(--text2);border:1px solid var(--border2)}
|
||||||
|
.btn-ghost:hover{background:var(--bg3);color:var(--text)}
|
||||||
|
.btn-ok{background:var(--ok);color:#000;font-size:15px;padding:14px 36px}
|
||||||
|
.btn-ok:hover{filter:brightness(1.1)}
|
||||||
|
.btn:disabled{opacity:.4;cursor:not-allowed}
|
||||||
|
.summary-table{background:var(--bg3);border:1px solid var(--border);border-radius:var(--r);overflow:hidden;margin-bottom:16px}
|
||||||
|
.summary-section{font-size:10px;font-weight:700;color:var(--text3);text-transform:uppercase;letter-spacing:1px;padding:10px 14px;font-family:var(--mono);background:var(--bg4);border-bottom:1px solid var(--border)}
|
||||||
|
.summary-row{display:flex;border-bottom:1px solid #1e284044}
|
||||||
|
.summary-row:last-child{border-bottom:none}
|
||||||
|
.summary-label{width:160px;padding:8px 14px;font-size:12px;color:var(--text3);flex-shrink:0}
|
||||||
|
.summary-value{padding:8px 14px;font-size:12px;color:var(--text);font-family:var(--mono);flex:1}
|
||||||
|
.summary-value.empty{color:var(--text3);font-style:italic;font-family:var(--sans)}
|
||||||
|
.footer{background:var(--bg2);border-top:1px solid var(--border);padding:12px 32px;text-align:center;font-size:11px;color:var(--text3);font-family:var(--mono)}
|
||||||
|
@keyframes spin{to{transform:rotate(360deg)}}
|
||||||
|
@keyframes pulse{0%,100%{opacity:.3;transform:scale(.8)}50%{opacity:1;transform:scale(1.2)}}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="shell">
|
||||||
|
<header class="hdr">
|
||||||
|
<div class="hdr-logo">IRIDE</div>
|
||||||
|
<div class="hdr-sep"></div>
|
||||||
|
<div class="hdr-sub">Setup Wizard — Tecnotel Servizi SRL</div>
|
||||||
|
<div class="hdr-badge">v0.2.0</div>
|
||||||
|
</header>
|
||||||
|
<div class="main">
|
||||||
|
<aside class="sidebar">
|
||||||
|
<div class="sidebar-title">Configurazione</div>
|
||||||
|
<div class="tab-item active" onclick="goTab(0)" id="tab-0"><div class="tab-num">1</div> Licenza</div>
|
||||||
|
<div class="tab-item" onclick="goTab(1)" id="tab-1"><div class="tab-num">2</div> Cliente</div>
|
||||||
|
<div class="tab-item" onclick="goTab(2)" id="tab-2"><div class="tab-num">3</div> Rete & SSL</div>
|
||||||
|
<div class="tab-item" onclick="goTab(3)" id="tab-3"><div class="tab-num">4</div> Intelligenza artificiale</div>
|
||||||
|
<div class="tab-item" onclick="goTab(4)" id="tab-4"><div class="tab-num">5</div> Utente admin</div>
|
||||||
|
<div class="tab-item" onclick="goTab(5)" id="tab-5"><div class="tab-num">6</div> Installa</div>
|
||||||
|
</aside>
|
||||||
|
<div class="content">
|
||||||
|
<div class="progress">
|
||||||
|
<div class="prog-step active" id="ps-0"></div>
|
||||||
|
<div class="prog-step" id="ps-1"></div>
|
||||||
|
<div class="prog-step" id="ps-2"></div>
|
||||||
|
<div class="prog-step" id="ps-3"></div>
|
||||||
|
<div class="prog-step" id="ps-4"></div>
|
||||||
|
<div class="prog-step" id="ps-5"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 1: Licenza -->
|
||||||
|
<div class="tab-panel active" id="panel-0">
|
||||||
|
<div class="panel-title">🔑 Licenza IRIDE</div>
|
||||||
|
<div class="panel-sub">IRIDE richiede una licenza valida emessa da Tecnotel Servizi SRL, vincolata
|
||||||
|
all'identificativo hardware di questo server. La licenza contiene anche le credenziali di sola
|
||||||
|
lettura per scaricare il software e i suoi aggiornamenti.</div>
|
||||||
|
<div class="form-grid">
|
||||||
|
<div class="form-full">
|
||||||
|
<label class="form-label">Machine ID di questo server</label>
|
||||||
|
<div style="display:flex;gap:8px">
|
||||||
|
<input class="form-input mono-input" id="machine-id-display" value="Calcolo in corso..." readonly style="flex:1">
|
||||||
|
<button class="btn btn-ghost" onclick="copyMachineId()" id="btn-copy-mid" style="padding:10px 16px">📋 Copia</button>
|
||||||
|
</div>
|
||||||
|
<div class="form-hint">Invia questo identificativo a Tecnotel per ricevere il file <code>license.json</code>.</div>
|
||||||
|
</div>
|
||||||
|
<div class="upload-area form-full" id="lic-drop"
|
||||||
|
ondragover="event.preventDefault();this.classList.add('drag')" ondragleave="this.classList.remove('drag')"
|
||||||
|
ondrop="handleLicenseDrop(event)" onclick="document.getElementById('lic-input').click()">
|
||||||
|
<input type="file" id="lic-input" accept=".json,application/json" style="display:none" onchange="handleLicenseFile(this)">
|
||||||
|
<div style="font-size:28px;margin-bottom:6px">📄</div>
|
||||||
|
<div style="font-weight:600">Trascina qui <code>license.json</code> oppure clicca per selezionarlo</div>
|
||||||
|
<div class="form-hint">Firma Ed25519 verificata sul server prima di procedere</div>
|
||||||
|
</div>
|
||||||
|
<div id="lic-error" class="form-full alert alert-err" style="display:none"></div>
|
||||||
|
<div id="lic-summary" class="form-full" style="display:none;background:var(--ok-dim);border:1px solid var(--ok);border-radius:var(--r);padding:16px 20px">
|
||||||
|
<div style="font-weight:700;color:var(--ok);margin-bottom:10px">✓ Licenza valida</div>
|
||||||
|
<div style="display:grid;grid-template-columns:120px 1fr;gap:6px 12px;font-size:12px;font-family:var(--mono)">
|
||||||
|
<div style="color:var(--text3)">Cliente:</div> <div id="lic-customer"></div>
|
||||||
|
<div style="color:var(--text3)">Prodotto:</div> <div id="lic-product"></div>
|
||||||
|
<div style="color:var(--text3)">Tier:</div> <div id="lic-tier"></div>
|
||||||
|
<div style="color:var(--text3)">Hostname:</div> <div id="lic-hostname"></div>
|
||||||
|
<div style="color:var(--text3)">Emessa:</div> <div id="lic-issued"></div>
|
||||||
|
<div style="color:var(--text3)">Scadenza:</div> <div id="lic-expires"></div>
|
||||||
|
<div style="color:var(--text3)">Token Gitea:</div><div id="lic-gitea"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="nav-row">
|
||||||
|
<div></div>
|
||||||
|
<button class="btn btn-brand" id="btn-after-license" onclick="goTab(1)" disabled style="opacity:.4;cursor:not-allowed">Avanti →</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 2: Cliente -->
|
||||||
|
<div class="tab-panel" id="panel-1">
|
||||||
|
<div class="panel-title">🏢 Informazioni cliente</div>
|
||||||
|
<div class="panel-sub">Identità dell'istanza: compare nel portale, nei messaggi dell'assistente e nei report.</div>
|
||||||
|
<div class="form-grid">
|
||||||
|
<div><label class="form-label">Nome breve <span>*</span></label><input class="form-input" id="cliente_name" placeholder="es. poliambulatorio-rossi"><div class="form-hint">Identificativo tecnico, minuscolo senza spazi</div></div>
|
||||||
|
<div><label class="form-label">Tipo organizzazione</label><select class="form-select" id="cliente_type"><option value="healthcare">Sanità / Healthcare</option><option value="customer_service">Customer service</option><option value="hospitality">Hospitality</option><option value="pa">Pubblica Amministrazione</option><option value="enterprise">Azienda</option></select></div>
|
||||||
|
<div class="form-full"><label class="form-label">Nome completo <span>*</span></label><input class="form-input" id="cliente_full" placeholder="es. Poliambulatorio Rossi S.r.l."></div>
|
||||||
|
<div><label class="form-label">Dominio email</label><input class="form-input" id="cliente_domain" placeholder="es. poliambulatoriorossi.it"><div class="form-hint">Dominio degli utenti del portale (opzionale)</div></div>
|
||||||
|
<div class="form-full"><label class="form-label">Contesto per l'assistente <span style="font-weight:400">(opzionale)</span></label><textarea class="form-input" id="ai_context" rows="3" placeholder="es. Poliambulatorio con 3 sedi, prenotazioni e ritiro referti, orari 8-20 nei giorni feriali."></textarea><div class="form-hint">Poche frasi sul cliente: entrano nelle istruzioni dell'assistente predefinito, modificabili dal portale.</div></div>
|
||||||
|
<div class="sec-div">Logo (opzionale)</div>
|
||||||
|
<div class="upload-area form-full" id="logo-drop" ondragover="event.preventDefault();this.classList.add('drag')" ondragleave="this.classList.remove('drag')" ondrop="handleLogoDrop(event)" onclick="document.getElementById('logo-input').click()">
|
||||||
|
<input type="file" id="logo-input" accept="image/*" style="display:none" onchange="handleLogoFile(this)">
|
||||||
|
<div style="font-size:24px;margin-bottom:4px">🖼️</div>
|
||||||
|
<div style="font-weight:600">Trascina il logo del cliente oppure clicca</div>
|
||||||
|
<div class="form-hint">PNG o SVG, max 2 MB — usato nel portale e nel widget</div>
|
||||||
|
<div class="upload-preview" id="logo-preview"><img id="logo-img" src=""><div style="font-size:11px;color:var(--ok);font-family:var(--mono)" id="logo-fname"></div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="nav-row"><button class="btn btn-ghost" onclick="goTab(0)">← Indietro</button><button class="btn btn-brand" onclick="goTab(2)">Avanti →</button></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 3: Rete & SSL -->
|
||||||
|
<div class="tab-panel" id="panel-2">
|
||||||
|
<div class="panel-title">🌐 Rete & SSL</div>
|
||||||
|
<div class="panel-sub">Nome pubblico dell'istanza e certificato. Per WhatsApp e per i webhook dei canali serve HTTPS pubblico con certificato valido.</div>
|
||||||
|
<div class="form-grid">
|
||||||
|
<div class="form-full"><label class="form-label">Hostname principale <span>*</span></label><input class="form-input mono-input" id="domain" placeholder="es. iride.cliente.it"></div>
|
||||||
|
<div class="form-full"><label class="form-label">Nomi alternativi</label><input class="form-input mono-input" id="aliases" placeholder="es. assistente.cliente.it"><div class="form-hint">Separati da spazio — opzionale</div></div>
|
||||||
|
<div class="sec-div">Certificato SSL</div>
|
||||||
|
<div class="ssl-toggle">
|
||||||
|
<div class="ssl-btn active" id="ssl-le" onclick="setSsl('letsencrypt')">🔒 Let's Encrypt<small>Automatico, richiede DNS pubblico</small></div>
|
||||||
|
<div class="ssl-btn" id="ssl-manual" onclick="setSsl('manual')">📄 Certificato esistente<small>Carica .crt e .key</small></div>
|
||||||
|
<div class="ssl-btn" id="ssl-selfsigned" onclick="setSsl('selfsigned')">🔐 Autofirmato<small>LAN / demo — 10 anni</small></div>
|
||||||
|
</div>
|
||||||
|
<div id="le-block" class="form-full"><label class="form-label">Email amministratore <span>*</span></label><input class="form-input" id="admin_email" placeholder="admin@tecnotelsrl.com"><div class="form-hint">Per le notifiche di scadenza del certificato</div></div>
|
||||||
|
<div id="manual-block" class="form-full" style="display:none">
|
||||||
|
<div class="form-grid">
|
||||||
|
<div><label class="form-label">File .crt <span>*</span></label><input type="file" class="form-input" accept=".crt,.pem,.cer" onchange="uploadSsl(this,'cert')"><div class="form-hint" id="crt-status">Nessun file selezionato</div></div>
|
||||||
|
<div><label class="form-label">File .key <span>*</span></label><input type="file" class="form-input" accept=".key,.pem" onchange="uploadSsl(this,'key')"><div class="form-hint" id="key-status">Nessun file selezionato</div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="selfsigned-block" class="form-full" style="display:none">
|
||||||
|
<div class="alert alert-warn">Certificato RSA 4096 autofirmato, validità 10 anni, con hostname, alias e IP del server nel SAN. I browser lo segnaleranno come non attendibile: va bene per LAN e demo, non per WhatsApp.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="nav-row"><button class="btn btn-ghost" onclick="goTab(1)">← Indietro</button><button class="btn btn-brand" onclick="goTab(3)">Avanti →</button></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 4: AI -->
|
||||||
|
<div class="tab-panel" id="panel-3">
|
||||||
|
<div class="panel-title">🧠 Intelligenza artificiale</div>
|
||||||
|
<div class="panel-sub">Provider del modello testuale. "Fake" è deterministico e senza costi: va bene per demo e collaudo; la chiave si può inserire o cambiare dopo dal portale.</div>
|
||||||
|
<div class="form-grid">
|
||||||
|
<div><label class="form-label">Provider</label><select class="form-select" id="ai_provider" onchange="onProviderChange()"><option value="fake">Fake (demo, nessuna chiave)</option><option value="openai">OpenAI</option><option value="anthropic">Anthropic (disponibile nella slice 2)</option></select></div>
|
||||||
|
<div><label class="form-label">Modello</label><input class="form-input mono-input" id="ai_model" placeholder="gpt-4.1-mini" value="gpt-4.1-mini"></div>
|
||||||
|
<div class="form-full" id="ai-key-block" style="display:none"><label class="form-label">API key</label><input class="form-input mono-input" type="password" id="ai_api_key" placeholder="sk-..."><div class="form-hint">Salvata in /opt/iride/config/iride.json (0600). Prima di dati reali: DPA e regione di elaborazione verificati (B-063).</div></div>
|
||||||
|
</div>
|
||||||
|
<div class="nav-row"><button class="btn btn-ghost" onclick="goTab(2)">← Indietro</button><button class="btn btn-brand" onclick="goTab(4)">Avanti →</button></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 5: Utente admin -->
|
||||||
|
<div class="tab-panel" id="panel-4">
|
||||||
|
<div class="panel-title">👤 Utente amministratore</div>
|
||||||
|
<div class="panel-sub">Primo accesso al portale IRIDE. Gli altri utenti e i ruoli si creano dal portale.</div>
|
||||||
|
<div class="form-grid">
|
||||||
|
<div><label class="form-label">Username <span>*</span></label><input class="form-input" id="admin_username" placeholder="admin" value="admin"></div>
|
||||||
|
<div><label class="form-label">Email</label><input class="form-input" id="admin_email_user" placeholder="admin@cliente.it"></div>
|
||||||
|
<div><label class="form-label">Password <span>*</span></label><input class="form-input" type="password" id="admin_password" placeholder="Min. 12 caratteri"></div>
|
||||||
|
<div><label class="form-label">Conferma password <span>*</span></label><input class="form-input" type="password" id="admin_password2" placeholder="Ripeti la password"></div>
|
||||||
|
<div class="form-full" id="pw-error" style="display:none"><div class="alert alert-err">Le password non corrispondono o sono più corte di 12 caratteri</div></div>
|
||||||
|
</div>
|
||||||
|
<div class="nav-row"><button class="btn btn-ghost" onclick="goTab(3)">← Indietro</button><button class="btn btn-brand" onclick="goToInstall()">Riepilogo →</button></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 6: Installa -->
|
||||||
|
<div class="tab-panel" id="panel-5">
|
||||||
|
<div class="panel-title">🚀 Riepilogo e installazione</div>
|
||||||
|
<div class="panel-sub">Controlla i dati. L'installazione clona il repository, crea l'ambiente Python, configura SSL, nginx e i servizi systemd, poi chiude la porta 8888.</div>
|
||||||
|
<div id="summary-box"></div>
|
||||||
|
<div id="errors-box" style="display:none"></div>
|
||||||
|
<div id="spinner-box" style="display:none;text-align:center;padding:40px 20px">
|
||||||
|
<div style="width:48px;height:48px;border:4px solid var(--border2);border-top-color:var(--brand);border-radius:50%;margin:0 auto 20px;animation:spin 1s linear infinite"></div>
|
||||||
|
<div style="font-size:15px;font-weight:600;margin-bottom:6px">Installazione in corso</div>
|
||||||
|
<div style="font-size:12px;color:var(--text3);font-family:var(--mono)" id="spinner-status">Avvio installazione...</div>
|
||||||
|
<pre id="log-box" style="text-align:left;margin-top:20px;max-height:260px;overflow:auto;background:var(--bg3);border:1px solid var(--border);border-radius:var(--r);padding:12px;font-family:var(--mono);font-size:11px;color:var(--text2)"></pre>
|
||||||
|
</div>
|
||||||
|
<div id="success-box" style="display:none;text-align:center;padding:40px 20px">
|
||||||
|
<div style="font-size:48px;margin-bottom:12px">✅</div>
|
||||||
|
<div style="font-size:18px;font-weight:700;margin-bottom:6px">IRIDE installata</div>
|
||||||
|
<div style="font-size:13px;color:var(--text2);margin-bottom:24px">La porta 8888 è stata chiusa. Configura i canali in <code>/opt/iride/config/channels.json</code> e <code>credentials.json</code>.</div>
|
||||||
|
<button class="btn btn-ok" id="btn-open" onclick="openIride()">Apri IRIDE →</button>
|
||||||
|
</div>
|
||||||
|
<div class="nav-row" id="install-nav">
|
||||||
|
<button class="btn btn-ghost" onclick="goTab(4)">← Indietro</button>
|
||||||
|
<button class="btn btn-ok" id="btn-install" onclick="avviaInstallazione()">🚀 Avvia installazione</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<footer class="footer">IRIDE — Tecnotel Servizi SRL · Web Installer v0.2.0 · La porta 8888 verrà chiusa al termine</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let currentTab = 0;
|
||||||
|
let sslMode = 'letsencrypt';
|
||||||
|
let certUploaded = false;
|
||||||
|
let keyUploaded = false;
|
||||||
|
let installing = false;
|
||||||
|
let installDomain = '';
|
||||||
|
let licenseValid = false;
|
||||||
|
|
||||||
|
async function loadMachineId() {
|
||||||
|
const el = document.getElementById('machine-id-display');
|
||||||
|
try {
|
||||||
|
const r = await fetch('/api/machine-id');
|
||||||
|
const d = await r.json();
|
||||||
|
el.value = (d && d.machine_id) ? d.machine_id : 'Errore caricamento';
|
||||||
|
} catch (e) { el.value = 'Errore: ' + e.message; }
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyMachineId() {
|
||||||
|
const el = document.getElementById('machine-id-display');
|
||||||
|
el.select();
|
||||||
|
navigator.clipboard.writeText(el.value).then(() => {
|
||||||
|
const btn = document.getElementById('btn-copy-mid');
|
||||||
|
const orig = btn.textContent;
|
||||||
|
btn.textContent = '✅ Copiato';
|
||||||
|
setTimeout(() => btn.textContent = orig, 1500);
|
||||||
|
}).catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleLicenseDrop(ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.currentTarget.classList.remove('drag');
|
||||||
|
if (ev.dataTransfer.files.length) uploadLicense(ev.dataTransfer.files[0]);
|
||||||
|
}
|
||||||
|
function handleLicenseFile(inp) { if (inp.files.length) uploadLicense(inp.files[0]); }
|
||||||
|
|
||||||
|
async function uploadLicense(file) {
|
||||||
|
const errEl = document.getElementById('lic-error');
|
||||||
|
const sumEl = document.getElementById('lic-summary');
|
||||||
|
const btnNext = document.getElementById('btn-after-license');
|
||||||
|
errEl.style.display = 'none'; sumEl.style.display = 'none';
|
||||||
|
try {
|
||||||
|
const text = await file.text();
|
||||||
|
const r = await fetch('/api/license/upload', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: text });
|
||||||
|
const d = await r.json();
|
||||||
|
if (!d.ok) {
|
||||||
|
errEl.textContent = 'Licenza non valida: ' + (d.error || 'errore sconosciuto');
|
||||||
|
errEl.style.display = 'block';
|
||||||
|
licenseValid = false;
|
||||||
|
btnNext.disabled = true; btnNext.style.opacity = '.4'; btnNext.style.cursor = 'not-allowed';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const s = d.summary || {};
|
||||||
|
document.getElementById('lic-customer').textContent = s.customer || '—';
|
||||||
|
document.getElementById('lic-product').textContent = s.product || '—';
|
||||||
|
document.getElementById('lic-tier').textContent = (s.tier || '—').toUpperCase();
|
||||||
|
document.getElementById('lic-hostname').textContent = s.issued_to || '—';
|
||||||
|
document.getElementById('lic-issued').textContent = s.issued_at || '—';
|
||||||
|
document.getElementById('lic-expires').textContent = s.expires_at || '—';
|
||||||
|
document.getElementById('lic-gitea').textContent = s.has_gitea ? '✓ Incluso' : '— Non incluso';
|
||||||
|
sumEl.style.display = 'block';
|
||||||
|
licenseValid = true;
|
||||||
|
btnNext.disabled = false; btnNext.style.opacity = '1'; btnNext.style.cursor = 'pointer';
|
||||||
|
if (s.issued_to && !g('domain')) document.getElementById('domain').value = s.issued_to;
|
||||||
|
} catch (e) {
|
||||||
|
errEl.textContent = 'Errore upload: ' + e.message;
|
||||||
|
errEl.style.display = 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.addEventListener('DOMContentLoaded', loadMachineId);
|
||||||
|
|
||||||
|
function goTab(n) {
|
||||||
|
if (n > 0 && !licenseValid) return;
|
||||||
|
if (n > currentTab) document.getElementById('tab-' + currentTab).classList.add('done');
|
||||||
|
document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
|
||||||
|
document.querySelectorAll('.tab-item').forEach(t => t.classList.remove('active'));
|
||||||
|
document.querySelectorAll('.prog-step').forEach((s,i) => {
|
||||||
|
s.classList.remove('active','done');
|
||||||
|
if (i < n) s.classList.add('done'); else if (i === n) s.classList.add('active');
|
||||||
|
});
|
||||||
|
document.getElementById('panel-' + n).classList.add('active');
|
||||||
|
document.getElementById('tab-' + n).classList.add('active');
|
||||||
|
currentTab = n;
|
||||||
|
}
|
||||||
|
|
||||||
|
function goToInstall() {
|
||||||
|
const pw1 = g('admin_password'), pw2 = g('admin_password2');
|
||||||
|
const err = document.getElementById('pw-error');
|
||||||
|
if (pw1 !== pw2 || pw1.length < 12) { err.style.display = 'block'; return; }
|
||||||
|
err.style.display = 'none';
|
||||||
|
goTab(5);
|
||||||
|
buildSummary();
|
||||||
|
}
|
||||||
|
|
||||||
|
function setSsl(mode) {
|
||||||
|
sslMode = mode;
|
||||||
|
document.getElementById('ssl-le').classList.toggle('active', mode === 'letsencrypt');
|
||||||
|
document.getElementById('ssl-manual').classList.toggle('active', mode === 'manual');
|
||||||
|
document.getElementById('ssl-selfsigned').classList.toggle('active', mode === 'selfsigned');
|
||||||
|
document.getElementById('le-block').style.display = mode === 'letsencrypt' ? 'block' : 'none';
|
||||||
|
document.getElementById('manual-block').style.display = mode === 'manual' ? 'block' : 'none';
|
||||||
|
document.getElementById('selfsigned-block').style.display = mode === 'selfsigned' ? 'block' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
function onProviderChange() {
|
||||||
|
const p = g('ai_provider');
|
||||||
|
document.getElementById('ai-key-block').style.display = p === 'fake' ? 'none' : 'block';
|
||||||
|
const m = document.getElementById('ai_model');
|
||||||
|
if (p === 'openai' && (!m.value || m.value.startsWith('claude'))) m.value = 'gpt-4.1-mini';
|
||||||
|
if (p === 'anthropic' && (!m.value || m.value.startsWith('gpt'))) m.value = 'claude-sonnet-4-6';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadSsl(input, type) {
|
||||||
|
const file = input.files[0]; if (!file) return;
|
||||||
|
const sid = type === 'cert' ? 'crt-status' : 'key-status';
|
||||||
|
try {
|
||||||
|
const r = await fetch('/api/upload/' + type, { method: 'POST', body: await file.arrayBuffer() });
|
||||||
|
const d = await r.json();
|
||||||
|
if (d.ok) {
|
||||||
|
document.getElementById(sid).textContent = '✓ ' + file.name;
|
||||||
|
document.getElementById(sid).style.color = 'var(--ok)';
|
||||||
|
if (type === 'cert') certUploaded = true; else keyUploaded = true;
|
||||||
|
}
|
||||||
|
} catch(e) { document.getElementById(sid).textContent = 'Errore: ' + e.message; }
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleLogoDrop(e) { e.preventDefault(); document.getElementById('logo-drop').classList.remove('drag'); processLogo(e.dataTransfer.files[0]); }
|
||||||
|
function handleLogoFile(input) { processLogo(input.files[0]); }
|
||||||
|
async function processLogo(file) {
|
||||||
|
if (!file || file.size > 2*1024*1024) return;
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = e => {
|
||||||
|
document.getElementById('logo-img').src = e.target.result;
|
||||||
|
document.getElementById('logo-fname').textContent = file.name;
|
||||||
|
document.getElementById('logo-preview').style.display = 'flex';
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
fetch('/api/upload/logo', { method: 'POST', body: await file.arrayBuffer() });
|
||||||
|
}
|
||||||
|
|
||||||
|
function g(id) { return (document.getElementById(id)?.value || '').trim(); }
|
||||||
|
|
||||||
|
function collectData() {
|
||||||
|
return {
|
||||||
|
cliente_name: g('cliente_name').toLowerCase().replace(/[^a-z0-9-]+/g, '-'), cliente_full: g('cliente_full'),
|
||||||
|
cliente_domain: g('cliente_domain'), cliente_type: g('cliente_type'), ai_context: g('ai_context'),
|
||||||
|
domain: g('domain'), aliases: g('aliases'), ssl_mode: sslMode, admin_email: g('admin_email'),
|
||||||
|
ai_provider: g('ai_provider') || 'fake', ai_model: g('ai_model'), ai_api_key: g('ai_api_key'),
|
||||||
|
admin_username: g('admin_username') || 'admin', admin_email_user: g('admin_email_user'),
|
||||||
|
admin_password: g('admin_password'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildSummary() {
|
||||||
|
const d = collectData();
|
||||||
|
const errors = [];
|
||||||
|
if (!d.cliente_name) errors.push('Nome breve del cliente obbligatorio');
|
||||||
|
if (!d.cliente_full) errors.push('Nome completo del cliente obbligatorio');
|
||||||
|
if (!d.domain) errors.push('Hostname obbligatorio');
|
||||||
|
if (!d.admin_password) errors.push('Password admin obbligatoria');
|
||||||
|
if (d.ai_provider !== 'fake' && !d.ai_api_key) errors.push('API key del provider AI obbligatoria (oppure scegli Fake)');
|
||||||
|
if (sslMode === 'letsencrypt' && !d.admin_email) errors.push("Email per Let's Encrypt obbligatoria");
|
||||||
|
if (sslMode === 'manual' && (!certUploaded || !keyUploaded)) errors.push('Certificato SSL (.crt e .key) non caricato');
|
||||||
|
|
||||||
|
const row = (l, v) => '<div class="summary-row"><div class="summary-label">' + l + '</div><div class="summary-value ' + (v ? '' : 'empty') + '">' + (v || 'non configurato') + '</div></div>';
|
||||||
|
const sslLabel = sslMode === 'letsencrypt' ? "Let's Encrypt (automatico)" : sslMode === 'manual' ? "Certificato esistente caricato" : "Autofirmato (RSA 4096, 10 anni)";
|
||||||
|
document.getElementById('summary-box').innerHTML = '<div class="summary-table">' +
|
||||||
|
'<div class="summary-section">Cliente</div>' +
|
||||||
|
row('Nome breve', d.cliente_name) + row('Nome completo', d.cliente_full) + row('Tipo', d.cliente_type) + row('Dominio email', d.cliente_domain) +
|
||||||
|
(d.ai_context ? row('Contesto', d.ai_context.substring(0, 80) + (d.ai_context.length > 80 ? '…' : '')) : '') +
|
||||||
|
'<div class="summary-section">Rete & SSL</div>' + row('Hostname', d.domain) + row('Alias', d.aliases) + row('SSL', sslLabel) +
|
||||||
|
'<div class="summary-section">Intelligenza artificiale</div>' + row('Provider', d.ai_provider) + row('Modello', d.ai_provider === 'fake' ? '—' : d.ai_model) + row('API key', d.ai_provider === 'fake' ? '—' : (d.ai_api_key ? '••••' + d.ai_api_key.slice(-4) : '')) +
|
||||||
|
'<div class="summary-section">Utente admin</div>' + row('Username', d.admin_username) + row('Email', d.admin_email_user) +
|
||||||
|
'</div>';
|
||||||
|
const errBox = document.getElementById('errors-box');
|
||||||
|
if (errors.length) {
|
||||||
|
errBox.style.display = 'block';
|
||||||
|
errBox.innerHTML = '<div class="alert alert-err" style="margin-bottom:16px">⚠️ <strong>Correggere prima di procedere:</strong><ul style="margin-top:8px;padding-left:16px">' + errors.map(e => '<li style="margin-top:4px">' + e + '</li>').join('') + '</ul></div>';
|
||||||
|
document.getElementById('btn-install').disabled = true;
|
||||||
|
} else {
|
||||||
|
errBox.style.display = 'none';
|
||||||
|
document.getElementById('btn-install').disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function avviaInstallazione() {
|
||||||
|
if (installing) return;
|
||||||
|
installing = true;
|
||||||
|
const data = collectData();
|
||||||
|
installDomain = data.domain;
|
||||||
|
document.getElementById('summary-box').style.display = 'none';
|
||||||
|
document.getElementById('errors-box').style.display = 'none';
|
||||||
|
document.getElementById('install-nav').style.display = 'none';
|
||||||
|
document.getElementById('spinner-box').style.display = 'block';
|
||||||
|
try {
|
||||||
|
const r = await fetch('/api/install', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) });
|
||||||
|
const res = await r.json();
|
||||||
|
if (!res.ok) throw new Error(res.error || 'Errore sconosciuto');
|
||||||
|
pollStatus();
|
||||||
|
} catch(e) {
|
||||||
|
document.getElementById('spinner-status').textContent = 'ERRORE: ' + e.message;
|
||||||
|
document.getElementById('spinner-status').style.color = 'var(--err)';
|
||||||
|
installing = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function pollStatus() {
|
||||||
|
let errors = 0;
|
||||||
|
const statusEl = document.getElementById('spinner-status');
|
||||||
|
const logEl = document.getElementById('log-box');
|
||||||
|
const iv = setInterval(async () => {
|
||||||
|
try {
|
||||||
|
const r = await fetch('/api/status');
|
||||||
|
const d = await r.json();
|
||||||
|
errors = 0;
|
||||||
|
if (d.log && d.log.length) {
|
||||||
|
logEl.textContent = d.log.join('\n');
|
||||||
|
logEl.scrollTop = logEl.scrollHeight;
|
||||||
|
const last = d.log[d.log.length-1];
|
||||||
|
if (last.startsWith('__ERROR__')) statusEl.textContent = last.replace('__ERROR__: ', 'Errore: ');
|
||||||
|
else if (last.includes('──')) statusEl.textContent = last.replace(/^\[[^\]]+\]\s*/, '').replace(/─/g, '').trim();
|
||||||
|
}
|
||||||
|
if (d.done) { clearInterval(iv); showSuccess(); }
|
||||||
|
if (d.error) { clearInterval(iv); statusEl.style.color = 'var(--err)'; }
|
||||||
|
} catch(e) {
|
||||||
|
errors++;
|
||||||
|
if (errors >= 6) { clearInterval(iv); showSuccess(); }
|
||||||
|
}
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showSuccess() {
|
||||||
|
document.getElementById('spinner-box').style.display = 'none';
|
||||||
|
document.getElementById('success-box').style.display = 'block';
|
||||||
|
}
|
||||||
|
function openIride() { window.location.href = 'https://' + installDomain; }
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,679 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
IRIDE — Web Installer Server (porta 8888)
|
||||||
|
Tecnotel Servizi SRL
|
||||||
|
|
||||||
|
Porting di argos-setup/setup_server.py. Self-contained: solo stdlib +
|
||||||
|
python3-cryptography (pacchetto di sistema) per la verifica della licenza.
|
||||||
|
|
||||||
|
Flusso: licenza (Ed25519 + machine_id + product) → clone di tecnotel/iride
|
||||||
|
con il token della licenza → virtualenv → iride.json / users.json → config
|
||||||
|
da template → migrazioni → build frontend (se presente) → SSL (Let's Encrypt,
|
||||||
|
certificato caricato o autofirmato) → nginx → unit systemd → licenza in
|
||||||
|
/opt/iride/data → chiusura della 8888 e rimozione di /opt/iride-setup-pkg.
|
||||||
|
"""
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import secrets
|
||||||
|
import shutil
|
||||||
|
import signal
|
||||||
|
import subprocess
|
||||||
|
import threading
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||||
|
from pathlib import Path
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
APP_DIR = Path("/opt/iride/app")
|
||||||
|
CONFIG_DIR = Path("/opt/iride/config")
|
||||||
|
DATA_DIR = Path("/opt/iride/data")
|
||||||
|
LOGS_DIR = Path("/opt/iride/logs")
|
||||||
|
CERTS_DIR = Path("/opt/iride/certs")
|
||||||
|
BACKUP_DIR = Path("/opt/iride/backups")
|
||||||
|
SETUP_DIR = Path("/opt/iride/setup")
|
||||||
|
APP_USER = "iride"
|
||||||
|
PORT = 8888
|
||||||
|
PRODUCT = "iride"
|
||||||
|
|
||||||
|
# ── Licenza — chiave pubblica Ed25519 del vendor (raw 32 byte, base64) ────────
|
||||||
|
# Stessa costante di iride/backend/core/license.py e del portale Tecnotel.
|
||||||
|
_LICENSE_PUBLIC_KEY_B64 = "GMRsZMoxOlCBiJU66EsQcj0ZO0gVd0GHB5LelEo/hns="
|
||||||
|
|
||||||
|
# ── Clone: username Basic Auth del bot Gitea al quale appartiene il token ─────
|
||||||
|
GITEA_BOT_USER = "argos-portal-bot"
|
||||||
|
GITEA_REPO_PATH = "/tecnotel/iride.git"
|
||||||
|
|
||||||
|
# ── Heartbeat verso il portale vendor (stessa chiave di registrazione ARGOS) ──
|
||||||
|
VENDOR_HEARTBEAT_URL = "https://license.argosdefense.io"
|
||||||
|
VENDOR_INSTALL_KEY = "5b1ab5c872383f686d3a25a5e123adca"
|
||||||
|
|
||||||
|
SERVICES = ["iride-api", "iride-worker", "iride-scheduler"]
|
||||||
|
|
||||||
|
install_log = []
|
||||||
|
install_done = False
|
||||||
|
install_error = False
|
||||||
|
|
||||||
|
|
||||||
|
def get_machine_id() -> str:
|
||||||
|
"""Fingerprint univoco del server: SHA256 hex di
|
||||||
|
/etc/machine-id | hostname | MAC prima interfaccia fisica.
|
||||||
|
IDENTICO a iride/backend/core/license.py e a ARGOS core.get_machine_id()."""
|
||||||
|
import socket as _sock
|
||||||
|
parts = []
|
||||||
|
try:
|
||||||
|
with open("/etc/machine-id") as f:
|
||||||
|
parts.append(f.read().strip())
|
||||||
|
except Exception:
|
||||||
|
parts.append("")
|
||||||
|
try:
|
||||||
|
parts.append(_sock.gethostname())
|
||||||
|
except Exception:
|
||||||
|
parts.append("")
|
||||||
|
try:
|
||||||
|
r = subprocess.run(["cat", "/sys/class/net/eth0/address"], capture_output=True, text=True, timeout=2)
|
||||||
|
mac = r.stdout.strip()
|
||||||
|
if not mac or mac == "00:00:00:00:00:00":
|
||||||
|
r = subprocess.run(["ip", "-o", "link", "show"], capture_output=True, text=True, timeout=2)
|
||||||
|
for line in r.stdout.splitlines():
|
||||||
|
if "link/ether" in line and "00:00:00:00:00:00" not in line:
|
||||||
|
if "docker" in line or "br-" in line or "veth" in line:
|
||||||
|
continue
|
||||||
|
mac = line.split("link/ether")[1].split()[0].strip()
|
||||||
|
break
|
||||||
|
parts.append(mac or "")
|
||||||
|
except Exception:
|
||||||
|
parts.append("")
|
||||||
|
return hashlib.sha256("|".join(parts).encode()).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def verify_license(raw_bytes):
|
||||||
|
"""Firma Ed25519 + product + machine_id + scadenza + credenziali Gitea.
|
||||||
|
Ritorna (ok, license_dict, errore)."""
|
||||||
|
try:
|
||||||
|
raw = json.loads(raw_bytes)
|
||||||
|
except Exception as e:
|
||||||
|
return (False, None, f"File non è JSON valido: {e}")
|
||||||
|
if not isinstance(raw, dict):
|
||||||
|
return (False, None, "Formato licenza non riconosciuto")
|
||||||
|
try:
|
||||||
|
import base64
|
||||||
|
from cryptography.exceptions import InvalidSignature
|
||||||
|
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
|
||||||
|
sig = raw.pop("signature", "")
|
||||||
|
if not sig:
|
||||||
|
return (False, None, "Licenza senza firma (campo 'signature' mancante)")
|
||||||
|
payload = json.dumps(raw, sort_keys=True, separators=(",", ":"))
|
||||||
|
raw["signature"] = sig
|
||||||
|
pub = Ed25519PublicKey.from_public_bytes(base64.b64decode(_LICENSE_PUBLIC_KEY_B64))
|
||||||
|
try:
|
||||||
|
pub.verify(base64.b64decode(sig), payload.encode())
|
||||||
|
except InvalidSignature:
|
||||||
|
return (False, None, "Firma non valida: licenza manomessa o emessa da un altro vendor.")
|
||||||
|
except ImportError:
|
||||||
|
return (False, None, "Libreria 'cryptography' non disponibile: apt install python3-cryptography")
|
||||||
|
except Exception as e:
|
||||||
|
return (False, None, f"Errore verifica firma: {e}")
|
||||||
|
|
||||||
|
product = raw.get("product")
|
||||||
|
if product is not None and product != PRODUCT:
|
||||||
|
return (False, None, f"Licenza per il prodotto '{product}', non per IRIDE.")
|
||||||
|
lic_machine = raw.get("machine_id", "")
|
||||||
|
if not lic_machine:
|
||||||
|
return (False, None, "Licenza senza machine_id: formato non supportato")
|
||||||
|
cur_machine = get_machine_id()
|
||||||
|
if lic_machine != cur_machine:
|
||||||
|
return (False, None, f"Machine ID non corrisponde: licenza per {lic_machine[:12]}..., "
|
||||||
|
f"questo server è {cur_machine[:12]}... La licenza non vale per questa macchina.")
|
||||||
|
expires = raw.get("expires_at", "")
|
||||||
|
if expires and expires < datetime.now().strftime("%Y-%m-%d"):
|
||||||
|
return (False, None, f"Licenza scaduta il {expires}")
|
||||||
|
if not raw.get("gitea_url") or not raw.get("gitea_token"):
|
||||||
|
return (False, None, "Licenza priva di credenziali Gitea: contattare Tecnotel per riemetterla.")
|
||||||
|
return (True, raw, "")
|
||||||
|
|
||||||
|
|
||||||
|
def log(msg):
|
||||||
|
line = f"[{datetime.now().strftime('%H:%M:%S')}] {msg}"
|
||||||
|
install_log.append(line)
|
||||||
|
print(line, flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
def run(cmd, check=True):
|
||||||
|
log(f"$ {cmd}")
|
||||||
|
result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
|
||||||
|
if result.stdout.strip(): log(result.stdout.strip()[-2000:])
|
||||||
|
if result.stderr.strip(): log(result.stderr.strip()[-2000:])
|
||||||
|
if check and result.returncode != 0:
|
||||||
|
raise RuntimeError(f"Comando fallito (exit {result.returncode}): {cmd}")
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def chown(path):
|
||||||
|
run(f"chown -R {APP_USER}:{APP_USER} {path}", check=False)
|
||||||
|
|
||||||
|
|
||||||
|
def venv_python() -> str:
|
||||||
|
return str(APP_DIR / "backend/venv/bin/python")
|
||||||
|
|
||||||
|
|
||||||
|
# ── Generazione configurazione ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def generate_iride_json(data):
|
||||||
|
hostname = data.get("domain", "").strip().lower()
|
||||||
|
aliases = [a.strip().lower() for a in data.get("aliases", "").split() if a.strip()]
|
||||||
|
provider = data.get("ai_provider", "fake") or "fake"
|
||||||
|
if provider not in ("fake", "openai", "anthropic"):
|
||||||
|
provider = "fake"
|
||||||
|
api_key = data.get("ai_api_key", "").strip()
|
||||||
|
cfg = {
|
||||||
|
"_version": "1.0",
|
||||||
|
"_installed": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
||||||
|
"cliente": {
|
||||||
|
"name": data.get("cliente_name", "").strip(),
|
||||||
|
"full_name": data.get("cliente_full", "").strip() or data.get("cliente_name", "").strip(),
|
||||||
|
"domain": hostname,
|
||||||
|
"email_domain": data.get("cliente_domain", "").strip().lower(),
|
||||||
|
"type": data.get("cliente_type", "customer_service"),
|
||||||
|
"ai_context": data.get("ai_context", "").strip(),
|
||||||
|
},
|
||||||
|
"network": {"hostname": hostname, "aliases": aliases},
|
||||||
|
"system": {
|
||||||
|
"secret_key": secrets.token_hex(32),
|
||||||
|
"internal_api_key": secrets.token_hex(24),
|
||||||
|
"timezone": "Europe/Rome",
|
||||||
|
"vendor_heartbeat": {
|
||||||
|
"enabled": True,
|
||||||
|
"url": VENDOR_HEARTBEAT_URL,
|
||||||
|
"install_key": VENDOR_INSTALL_KEY,
|
||||||
|
"interval_h": 6,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"ai": {
|
||||||
|
"text": {"provider": provider,
|
||||||
|
"options": {"model": data.get("ai_model", "").strip() or "gpt-4.1-mini", "temperature": 0.3}},
|
||||||
|
"realtime": {"provider": "openai", "options": {"model": ""}},
|
||||||
|
"providers": {
|
||||||
|
"openai": {"api_key": api_key if provider == "openai" else "", "base_url": "https://api.openai.com/v1"},
|
||||||
|
"anthropic": {"api_key": api_key if provider == "anthropic" else ""},
|
||||||
|
},
|
||||||
|
"limits": {"max_ai_turns": 20, "daily_budget_micros": 5000000},
|
||||||
|
},
|
||||||
|
"ports": {"api": 8080, "voice": 8081},
|
||||||
|
"paths": {"config_dir": str(CONFIG_DIR), "data_dir": str(DATA_DIR), "logs_dir": str(LOGS_DIR),
|
||||||
|
"backups": str(BACKUP_DIR)},
|
||||||
|
"backup": {"keep_snapshots": 14},
|
||||||
|
"license": {"file": str(DATA_DIR / "license.json")},
|
||||||
|
}
|
||||||
|
return cfg
|
||||||
|
|
||||||
|
|
||||||
|
def create_admin_user(data):
|
||||||
|
"""users.json con hash bcrypt calcolato dal backend IRIDE (stesso modulo
|
||||||
|
che lo verifica al login). La password passa via stdin, mai in argv."""
|
||||||
|
username = (data.get("admin_username") or "admin").strip().lower()
|
||||||
|
password = data.get("admin_password", "")
|
||||||
|
if not username or not password:
|
||||||
|
log("WARN: credenziali admin mancanti — skip creazione utente")
|
||||||
|
return
|
||||||
|
proc = subprocess.run(
|
||||||
|
[venv_python(), "-c", "import sys; from core.auth import hash_password; print(hash_password(sys.stdin.read()))"],
|
||||||
|
input=password, capture_output=True, text=True, cwd=str(APP_DIR / "backend"),
|
||||||
|
env={**os.environ, "PYTHONPATH": str(APP_DIR / "backend"), "IRIDE_CONFIG_DIR": str(CONFIG_DIR),
|
||||||
|
"IRIDE_DATA_DIR": str(DATA_DIR)})
|
||||||
|
if proc.returncode != 0:
|
||||||
|
raise RuntimeError(f"hash della password fallito: {proc.stderr.strip()[-300:]}")
|
||||||
|
pw_hash = proc.stdout.strip()
|
||||||
|
users_file = CONFIG_DIR / "users.json"
|
||||||
|
try:
|
||||||
|
users = json.loads(users_file.read_text())
|
||||||
|
except Exception:
|
||||||
|
users = {"users": {}}
|
||||||
|
users.setdefault("users", {})[username] = {
|
||||||
|
"password_hash": pw_hash, "roles": ["admin"], "totp_secret": "",
|
||||||
|
"email": (data.get("admin_email_user") or "").strip(), "enabled": True,
|
||||||
|
}
|
||||||
|
users_file.write_text(json.dumps(users, indent=2, ensure_ascii=False) + "\n")
|
||||||
|
os.chmod(users_file, 0o600)
|
||||||
|
chown(users_file)
|
||||||
|
log(f"Utente admin '{username}' creato in users.json")
|
||||||
|
|
||||||
|
|
||||||
|
# ── Installazione ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def install(data):
|
||||||
|
global install_done, install_error
|
||||||
|
try:
|
||||||
|
log("=== AVVIO INSTALLAZIONE IRIDE ===")
|
||||||
|
|
||||||
|
# 0. Licenza (già validata da /api/license/upload)
|
||||||
|
log("── Verifica licenza IRIDE ──")
|
||||||
|
lic_path = SETUP_DIR / "license.json"
|
||||||
|
if not lic_path.exists():
|
||||||
|
raise RuntimeError("license.json non trovata in /opt/iride/setup/: caricare una licenza valida.")
|
||||||
|
ok, lic, err = verify_license(lic_path.read_bytes())
|
||||||
|
if not ok:
|
||||||
|
raise RuntimeError(f"Licenza non valida: {err}")
|
||||||
|
gitea_url = lic.get("gitea_url", "").rstrip("/")
|
||||||
|
gitea_token = lic.get("gitea_token", "")
|
||||||
|
gitea_host = gitea_url[:-len("/api/v1")] if gitea_url.endswith("/api/v1") else gitea_url
|
||||||
|
log(f"Licenza OK: {lic.get('customer')} / {lic.get('tier')} / exp {lic.get('expires_at')}")
|
||||||
|
|
||||||
|
# 1. Clone (URL autenticato temporaneo: il token NON resta in .git/config)
|
||||||
|
log("── Clone repository IRIDE ──")
|
||||||
|
if (APP_DIR / ".git").exists():
|
||||||
|
log("Repository già presente — skip clone")
|
||||||
|
else:
|
||||||
|
auth_url = f"https://{GITEA_BOT_USER}:{gitea_token}@{gitea_host[len('https://'):]}{GITEA_REPO_PATH}"
|
||||||
|
APP_DIR.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
run(f"git config --global --add safe.directory {APP_DIR}")
|
||||||
|
run(f"git clone {auth_url} {APP_DIR}")
|
||||||
|
run(f"git -C {APP_DIR} remote set-url origin {gitea_host}{GITEA_REPO_PATH}")
|
||||||
|
chown(APP_DIR)
|
||||||
|
log(f"Repository IRIDE pronto ({(APP_DIR / 'VERSION').read_text().strip() if (APP_DIR / 'VERSION').exists() else '?'})")
|
||||||
|
|
||||||
|
# 2. Virtualenv
|
||||||
|
log("── Virtualenv Python ──")
|
||||||
|
venv_dir = APP_DIR / "backend/venv"
|
||||||
|
if not venv_dir.exists():
|
||||||
|
run(f"python3 -m venv {venv_dir}")
|
||||||
|
run(f"{venv_dir}/bin/pip install --upgrade pip -q")
|
||||||
|
run(f"{venv_dir}/bin/pip install -r {APP_DIR}/backend/requirements.txt -q")
|
||||||
|
chown(venv_dir)
|
||||||
|
log(f"Virtualenv pronto ({run(f'{venv_dir}/bin/python --version', check=False).stdout.strip()})")
|
||||||
|
|
||||||
|
# 3. iride.json
|
||||||
|
log("── Generazione iride.json ──")
|
||||||
|
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
cfg_path = CONFIG_DIR / "iride.json"
|
||||||
|
cfg_path.write_text(json.dumps(generate_iride_json(data), indent=2, ensure_ascii=False) + "\n")
|
||||||
|
os.chmod(cfg_path, 0o600)
|
||||||
|
chown(CONFIG_DIR)
|
||||||
|
log("iride.json creato")
|
||||||
|
|
||||||
|
# 4. Config da template .example (glob, idempotente: mai clobber)
|
||||||
|
_GENERATED = {"iride.json", "users.json"}
|
||||||
|
for src in sorted((APP_DIR / "config").glob("*.json.example")):
|
||||||
|
name = src.name[:-len(".example")]
|
||||||
|
if name in _GENERATED:
|
||||||
|
continue
|
||||||
|
dst = CONFIG_DIR / name
|
||||||
|
if dst.exists():
|
||||||
|
continue
|
||||||
|
shutil.copy(src, dst)
|
||||||
|
os.chmod(dst, 0o600)
|
||||||
|
chown(dst)
|
||||||
|
log(f"{name} copiato da template")
|
||||||
|
|
||||||
|
# 5. Logo cliente
|
||||||
|
logo_src = SETUP_DIR / "logo_cliente.png"
|
||||||
|
if logo_src.exists():
|
||||||
|
(CONFIG_DIR / "assets").mkdir(parents=True, exist_ok=True)
|
||||||
|
shutil.copy(logo_src, CONFIG_DIR / "assets" / "logo_cliente.png")
|
||||||
|
chown(CONFIG_DIR / "assets")
|
||||||
|
log("Logo cliente copiato")
|
||||||
|
|
||||||
|
# 6. Utente admin (bcrypt dal backend)
|
||||||
|
log("── Creazione utente admin ──")
|
||||||
|
create_admin_user(data)
|
||||||
|
|
||||||
|
# 7. Migrazioni DB
|
||||||
|
log("── Migrazioni database ──")
|
||||||
|
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
chown(DATA_DIR)
|
||||||
|
run(f"cd {APP_DIR}/backend && sudo -u {APP_USER} env IRIDE_CONFIG_DIR={CONFIG_DIR} IRIDE_DATA_DIR={DATA_DIR} "
|
||||||
|
f"IRIDE_LOGS_DIR={LOGS_DIR} IRIDE_DB={DATA_DIR}/iride.db {venv_dir}/bin/python db.py")
|
||||||
|
log("Schema allineato")
|
||||||
|
|
||||||
|
# 8. Frontend (solo se presente: B-067)
|
||||||
|
if (APP_DIR / "frontend/package.json").exists():
|
||||||
|
log("── Build frontend ──")
|
||||||
|
run(f"cd {APP_DIR}/frontend && npm ci --silent")
|
||||||
|
run(f"cd {APP_DIR}/frontend && npm run build")
|
||||||
|
chown(APP_DIR / "frontend")
|
||||||
|
run(f"chmod 755 /opt/iride /opt/iride/app /opt/iride/app/frontend")
|
||||||
|
run(f"chmod -R 755 {APP_DIR}/frontend/dist/", check=False)
|
||||||
|
log("Frontend compilato")
|
||||||
|
else:
|
||||||
|
log("Nessun frontend/package.json: nginx serve la sola API (B-067)")
|
||||||
|
|
||||||
|
# 9. SSL — stesse tre modalità di ARGOS
|
||||||
|
log("── Configurazione SSL ──")
|
||||||
|
domain = data.get("domain", "").strip()
|
||||||
|
aliases = data.get("aliases", "").strip()
|
||||||
|
ssl_mode = data.get("ssl_mode", "letsencrypt")
|
||||||
|
all_names = (domain + " " + aliases).strip()
|
||||||
|
CERTS_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
if ssl_mode == "manual":
|
||||||
|
crt_src, key_src = SETUP_DIR / "uploaded.crt", SETUP_DIR / "uploaded.key"
|
||||||
|
if not crt_src.exists() or not key_src.exists():
|
||||||
|
raise RuntimeError("File SSL .crt o .key non trovati in /opt/iride/setup/")
|
||||||
|
shutil.copy(crt_src, CERTS_DIR / "fullchain.pem")
|
||||||
|
shutil.copy(key_src, CERTS_DIR / "privkey.pem")
|
||||||
|
os.chmod(CERTS_DIR / "privkey.pem", 0o600)
|
||||||
|
ssl_crt, ssl_key = str(CERTS_DIR / "fullchain.pem"), str(CERTS_DIR / "privkey.pem")
|
||||||
|
log("Certificato SSL caricato dal wizard")
|
||||||
|
elif ssl_mode == "selfsigned":
|
||||||
|
log("Generazione certificato autofirmato (RSA 4096, validità 10 anni)")
|
||||||
|
crt_path, key_path, cnf_path = CERTS_DIR / "fullchain.pem", CERTS_DIR / "privkey.pem", CERTS_DIR / "openssl-selfsigned.cnf"
|
||||||
|
san_dns = [n for n in all_names.split() if n]
|
||||||
|
try:
|
||||||
|
server_ip = subprocess.check_output(["hostname", "-I"], text=True).strip().split()[0]
|
||||||
|
except Exception:
|
||||||
|
server_ip = ""
|
||||||
|
san_lines = "\n".join(f"DNS.{i+1} = {n}" for i, n in enumerate(san_dns)) or "DNS.1 = iride.local"
|
||||||
|
if server_ip:
|
||||||
|
san_lines += f"\nIP.1 = {server_ip}"
|
||||||
|
client_full = data.get("cliente_full") or data.get("cliente_name") or "IRIDE"
|
||||||
|
cn = domain or "iride.local"
|
||||||
|
cnf_path.write_text(f"""[req]
|
||||||
|
default_bits = 4096
|
||||||
|
prompt = no
|
||||||
|
default_md = sha256
|
||||||
|
distinguished_name = dn
|
||||||
|
req_extensions = req_ext
|
||||||
|
x509_extensions = v3_ext
|
||||||
|
|
||||||
|
[dn]
|
||||||
|
C = IT
|
||||||
|
O = {client_full}
|
||||||
|
OU = IRIDE
|
||||||
|
CN = {cn}
|
||||||
|
|
||||||
|
[req_ext]
|
||||||
|
subjectAltName = @alt_names
|
||||||
|
|
||||||
|
[v3_ext]
|
||||||
|
subjectAltName = @alt_names
|
||||||
|
basicConstraints = critical, CA:FALSE
|
||||||
|
keyUsage = critical, digitalSignature, keyEncipherment
|
||||||
|
extendedKeyUsage = serverAuth
|
||||||
|
|
||||||
|
[alt_names]
|
||||||
|
{san_lines}
|
||||||
|
""")
|
||||||
|
run(f"openssl req -x509 -nodes -days 3650 -newkey rsa:4096 -keyout {key_path} -out {crt_path} -config {cnf_path}")
|
||||||
|
os.chmod(key_path, 0o600)
|
||||||
|
os.chmod(crt_path, 0o644)
|
||||||
|
ssl_crt, ssl_key = str(crt_path), str(key_path)
|
||||||
|
log(f"Certificato autofirmato generato (CN={cn}, SAN: {len(san_dns)} DNS{' + 1 IP' if server_ip else ''})")
|
||||||
|
log("ATTENZIONE: i browser lo segnaleranno come non attendibile; per WhatsApp serve un certificato valido.")
|
||||||
|
else:
|
||||||
|
_write_nginx_http(all_names)
|
||||||
|
run("nginx -t && systemctl restart nginx")
|
||||||
|
certbot_d = " ".join(f"-d {n}" for n in all_names.split())
|
||||||
|
email = data.get("admin_email", "admin@tecnotelsrl.com")
|
||||||
|
run(f"certbot --nginx {certbot_d} --non-interactive --agree-tos -m {email}")
|
||||||
|
ssl_crt = f"/etc/letsencrypt/live/{domain}/fullchain.pem"
|
||||||
|
ssl_key = f"/etc/letsencrypt/live/{domain}/privkey.pem"
|
||||||
|
log("Certificato Let's Encrypt ottenuto")
|
||||||
|
chown(CERTS_DIR)
|
||||||
|
|
||||||
|
# 10. Nginx finale
|
||||||
|
log("── Nginx configurazione finale ──")
|
||||||
|
_write_nginx_final(all_names or "_", ssl_crt, ssl_key)
|
||||||
|
run("nginx -t && systemctl restart nginx")
|
||||||
|
log("Nginx configurato")
|
||||||
|
|
||||||
|
# 11. Servizi systemd (unit dal repo: update.sh le tiene allineate)
|
||||||
|
log("── Creazione e avvio servizi ──")
|
||||||
|
for svc in SERVICES:
|
||||||
|
shutil.copy(APP_DIR / "deploy/systemd" / f"{svc}.service", f"/etc/systemd/system/{svc}.service")
|
||||||
|
if (APP_DIR / "deploy/logrotate/iride").exists():
|
||||||
|
shutil.copy(APP_DIR / "deploy/logrotate/iride", "/etc/logrotate.d/iride")
|
||||||
|
run("systemctl daemon-reload")
|
||||||
|
LOGS_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
chown(LOGS_DIR)
|
||||||
|
for svc in SERVICES:
|
||||||
|
run(f"systemctl enable --now {svc}")
|
||||||
|
log(f"{svc} avviato")
|
||||||
|
|
||||||
|
# 12. Licenza in posizione finale
|
||||||
|
log("── Copia licenza in posizione finale ──")
|
||||||
|
final_lic = DATA_DIR / "license.json"
|
||||||
|
shutil.copy(lic_path, final_lic)
|
||||||
|
os.chmod(final_lic, 0o600)
|
||||||
|
chown(final_lic)
|
||||||
|
log(f"Licenza copiata in {final_lic}")
|
||||||
|
|
||||||
|
# 13. Health check e strumento di verifica permanente
|
||||||
|
health = Path(__file__).parent / "checks" / "health.sh"
|
||||||
|
if health.exists():
|
||||||
|
shutil.copy(health, "/usr/local/bin/iride-health")
|
||||||
|
os.chmod("/usr/local/bin/iride-health", 0o755)
|
||||||
|
res = run("bash /usr/local/bin/iride-health", check=False)
|
||||||
|
if res.returncode != 0:
|
||||||
|
log("ATTENZIONE: health check con errori (vedi sopra): verificare i log in /opt/iride/logs")
|
||||||
|
|
||||||
|
# 14. Chiusura web installer
|
||||||
|
log("── Chiusura web installer ──")
|
||||||
|
run("systemctl disable --now iride-setup", check=False)
|
||||||
|
run("ufw delete allow 8888/tcp", check=False)
|
||||||
|
log("Porta 8888 chiusa — web installer disabilitato")
|
||||||
|
|
||||||
|
log("=== INSTALLAZIONE COMPLETATA ===")
|
||||||
|
_schedule_cleanup()
|
||||||
|
install_done = True
|
||||||
|
|
||||||
|
def shutdown():
|
||||||
|
import time
|
||||||
|
time.sleep(15)
|
||||||
|
os.kill(os.getpid(), signal.SIGTERM)
|
||||||
|
threading.Thread(target=shutdown, daemon=True).start()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
log(f"ERRORE: {e}")
|
||||||
|
install_log.append(f"__ERROR__: {e}")
|
||||||
|
install_error = True
|
||||||
|
|
||||||
|
|
||||||
|
# ── Nginx ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _write_nginx_http(all_names):
|
||||||
|
_write_nginx_conf(f"""server {{
|
||||||
|
listen 80;
|
||||||
|
server_name {all_names};
|
||||||
|
location /.well-known/acme-challenge/ {{ root /var/www/html; }}
|
||||||
|
location / {{ return 301 https://$host$request_uri; }}
|
||||||
|
}}
|
||||||
|
""")
|
||||||
|
|
||||||
|
|
||||||
|
def _write_nginx_final(all_names, ssl_crt, ssl_key):
|
||||||
|
has_frontend = (APP_DIR / "frontend/dist/index.html").exists()
|
||||||
|
root_block = (f""" location / {{
|
||||||
|
root {APP_DIR}/frontend/dist;
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
expires 1h;
|
||||||
|
}}""" if has_frontend else
|
||||||
|
""" location = / {
|
||||||
|
return 200 'IRIDE installata. Portale in arrivo (B-067). API: /api/health';
|
||||||
|
add_header Content-Type text/plain;
|
||||||
|
}""")
|
||||||
|
conf = f"""limit_req_zone $binary_remote_addr zone=iride:10m rate=20r/s;
|
||||||
|
|
||||||
|
server {{
|
||||||
|
listen 80;
|
||||||
|
server_name {all_names};
|
||||||
|
location / {{ return 301 https://$host$request_uri; }}
|
||||||
|
location /.well-known/acme-challenge/ {{ root /var/www/html; }}
|
||||||
|
}}
|
||||||
|
|
||||||
|
server {{
|
||||||
|
listen 443 ssl http2;
|
||||||
|
server_name {all_names};
|
||||||
|
|
||||||
|
ssl_certificate {ssl_crt};
|
||||||
|
ssl_certificate_key {ssl_key};
|
||||||
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||||
|
|
||||||
|
add_header X-Frame-Options SAMEORIGIN;
|
||||||
|
add_header X-Content-Type-Options nosniff;
|
||||||
|
add_header Strict-Transport-Security "max-age=31536000" always;
|
||||||
|
|
||||||
|
client_max_body_size 25m;
|
||||||
|
|
||||||
|
# Webhook dei canali: nessun rate limit per IP (Meta e Telegram arrivano da pochi IP)
|
||||||
|
location /api/v1/webhooks/ {{
|
||||||
|
proxy_pass http://127.0.0.1:8080;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto https;
|
||||||
|
proxy_read_timeout 30s;
|
||||||
|
}}
|
||||||
|
location /api/ {{
|
||||||
|
limit_req zone=iride burst=40 nodelay;
|
||||||
|
proxy_pass http://127.0.0.1:8080;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto https;
|
||||||
|
proxy_connect_timeout 30s;
|
||||||
|
proxy_send_timeout 120s;
|
||||||
|
proxy_read_timeout 120s;
|
||||||
|
}}
|
||||||
|
# WebSocket dell'inbox operatori (B-016)
|
||||||
|
location /ws/ {{
|
||||||
|
proxy_pass http://127.0.0.1:8080;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_read_timeout 3600s;
|
||||||
|
}}
|
||||||
|
# Relay vocale (POC-01): attivare solo con iride-voice installato
|
||||||
|
# location /v1/voice/ {{
|
||||||
|
# proxy_pass http://127.0.0.1:8081;
|
||||||
|
# proxy_http_version 1.1;
|
||||||
|
# proxy_set_header Upgrade $http_upgrade;
|
||||||
|
# proxy_set_header Connection "upgrade";
|
||||||
|
# proxy_read_timeout 3600s;
|
||||||
|
# proxy_send_timeout 3600s;
|
||||||
|
# }}
|
||||||
|
location /widget/ {{
|
||||||
|
alias {APP_DIR}/frontend/packages/widget/dist/;
|
||||||
|
add_header Cache-Control "public, max-age=3600";
|
||||||
|
}}
|
||||||
|
{root_block}
|
||||||
|
|
||||||
|
access_log {LOGS_DIR}/nginx-access.log;
|
||||||
|
error_log {LOGS_DIR}/nginx-error.log;
|
||||||
|
}}
|
||||||
|
"""
|
||||||
|
_write_nginx_conf(conf)
|
||||||
|
|
||||||
|
|
||||||
|
def _write_nginx_conf(conf):
|
||||||
|
Path("/etc/nginx/sites-available/iride").write_text(conf)
|
||||||
|
p = Path("/etc/nginx/sites-enabled/iride")
|
||||||
|
if not p.exists():
|
||||||
|
p.symlink_to("/etc/nginx/sites-available/iride")
|
||||||
|
for f in ["/etc/nginx/sites-enabled/default", "/etc/nginx/sites-enabled/iride-setup"]:
|
||||||
|
if Path(f).exists():
|
||||||
|
Path(f).unlink()
|
||||||
|
|
||||||
|
|
||||||
|
# ── HTTP ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
class SetupHandler(BaseHTTPRequestHandler):
|
||||||
|
def log_message(self, *args):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def do_GET(self):
|
||||||
|
path = urlparse(self.path).path
|
||||||
|
if path in ("/", "/setup"):
|
||||||
|
html_path = Path(__file__).parent / "setup.html"
|
||||||
|
if not html_path.exists():
|
||||||
|
self.send_response(404); self.end_headers(); return
|
||||||
|
html = html_path.read_bytes()
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header("Content-Type", "text/html; charset=utf-8")
|
||||||
|
self.send_header("Content-Length", len(html))
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(html)
|
||||||
|
elif path == "/api/status":
|
||||||
|
self._json({"done": install_done, "error": install_error, "log": install_log[-60:]})
|
||||||
|
elif path == "/api/machine-id":
|
||||||
|
self._json({"machine_id": get_machine_id()})
|
||||||
|
else:
|
||||||
|
self.send_response(404); self.end_headers()
|
||||||
|
|
||||||
|
def do_POST(self):
|
||||||
|
path = urlparse(self.path).path
|
||||||
|
length = int(self.headers.get("Content-Length", 0))
|
||||||
|
body = self.rfile.read(length)
|
||||||
|
if path == "/api/install":
|
||||||
|
try:
|
||||||
|
data = json.loads(body)
|
||||||
|
threading.Thread(target=install, args=(data,), daemon=True).start()
|
||||||
|
self._json({"ok": True})
|
||||||
|
except Exception as e:
|
||||||
|
self._json({"ok": False, "error": str(e)}, 400)
|
||||||
|
elif path in ("/api/upload/cert", "/api/upload/key", "/api/upload/logo"):
|
||||||
|
SETUP_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
name = {"cert": "uploaded.crt", "key": "uploaded.key", "logo": "logo_cliente.png"}[path.rsplit("/", 1)[1]]
|
||||||
|
(SETUP_DIR / name).write_bytes(body)
|
||||||
|
os.chmod(SETUP_DIR / name, 0o600)
|
||||||
|
self._json({"ok": True})
|
||||||
|
elif path == "/api/license/upload":
|
||||||
|
ok, lic, err = verify_license(body)
|
||||||
|
if not ok:
|
||||||
|
self._json({"ok": False, "error": err}, 400)
|
||||||
|
return
|
||||||
|
SETUP_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
lic_path = SETUP_DIR / "license.json"
|
||||||
|
lic_path.write_bytes(body)
|
||||||
|
os.chmod(lic_path, 0o600)
|
||||||
|
self._json({"ok": True, "summary": {
|
||||||
|
"customer": lic.get("customer", ""),
|
||||||
|
"tier": lic.get("tier", ""),
|
||||||
|
"issued_to": lic.get("issued_to", ""),
|
||||||
|
"issued_at": lic.get("issued_at", ""),
|
||||||
|
"expires_at": lic.get("expires_at", ""),
|
||||||
|
"product": lic.get("product", "") or "(non indicato)",
|
||||||
|
"has_gitea": bool(lic.get("gitea_token")),
|
||||||
|
}})
|
||||||
|
else:
|
||||||
|
self.send_response(404); self.end_headers()
|
||||||
|
|
||||||
|
def _json(self, data, code=200):
|
||||||
|
body = json.dumps(data).encode()
|
||||||
|
self.send_response(code)
|
||||||
|
self.send_header("Content-Type", "application/json")
|
||||||
|
self.send_header("Content-Length", len(body))
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(body)
|
||||||
|
|
||||||
|
|
||||||
|
def _schedule_cleanup():
|
||||||
|
"""Disabilita iride-setup e rimuove /opt/iride-setup-pkg via transient
|
||||||
|
unit systemd-run, indipendente dal processo padre (che sta per morire)."""
|
||||||
|
script = r"""#!/bin/bash
|
||||||
|
sleep 5
|
||||||
|
systemctl stop iride-setup.service 2>/dev/null || true
|
||||||
|
systemctl disable iride-setup.service 2>/dev/null || true
|
||||||
|
rm -f /etc/systemd/system/iride-setup.service
|
||||||
|
systemctl daemon-reload
|
||||||
|
ufw delete allow 8888/tcp 2>/dev/null || true
|
||||||
|
rm -rf /opt/iride-setup-pkg
|
||||||
|
echo "iride-setup cleanup completato $(date -Iseconds)" >> /var/log/iride-setup-cleanup.log
|
||||||
|
"""
|
||||||
|
script_path = "/tmp/iride-setup-cleanup.sh"
|
||||||
|
try:
|
||||||
|
import time
|
||||||
|
Path(script_path).write_text(script)
|
||||||
|
os.chmod(script_path, 0o755)
|
||||||
|
unit_name = f"iride-setup-cleanup-{int(time.time())}.service"
|
||||||
|
subprocess.Popen(["systemd-run", "--no-block", "--unit", unit_name, "/bin/bash", script_path],
|
||||||
|
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
||||||
|
start_new_session=True)
|
||||||
|
log(f"Cleanup schedulato via systemd-run come {unit_name} (delay 5s)")
|
||||||
|
except Exception as e:
|
||||||
|
log(f"Errore schedulazione cleanup: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print(f"\n{'='*55}\n IRIDE — Web Installer\n Tecnotel Servizi SRL\n Apri: http://<IP_SERVER>:{PORT}\n{'='*55}\n")
|
||||||
|
HTTPServer(("0.0.0.0", PORT), SetupHandler).serve_forever()
|
||||||
Loading…
Reference in New Issue